¡Vea una y un aquí para lo que construirá! demostración repositorio git Introducción Estoy muy emocionado de presentarles esta versión web 3.0, sé que han estado buscando un gran ejemplo para comenzar a desarrollar aplicaciones descentralizadas. Si es nuevo aquí, soy Darlington Gospel, un mentor de Dapp que ayuda a los desarrolladores como usted en la transición de la Web 2.0 a la Web 3.0. En este tutorial, aprenderá paso a paso cómo implementar una organización autónoma descentralizada (DAO) con funciones de chat anónimo. Si está entusiasmado con esta compilación, pasemos al tutorial... Requisito previo Necesitará las siguientes herramientas instaladas para aplastar con éxito esta compilación: Nodo Ganache-Cli Trufa Reaccionar infuria CSS viento de cola Kit de desarrollo de software CometChat Metamáscara Hilo Instalación de dependencias Asegúrese de tener NodeJs instalado en su máquina. A continuación, ejecute el código en el terminal para confirmar que está instalado. Instalación de NodeJs Ejecute los siguientes códigos en su terminal para instalar estos paquetes esenciales globalmente. Instalación de Yarn, Ganache-cli y Truffle npm i -g yarn npm i -g truffle npm i -g ganache-cli Usando los comandos a continuación, clone el proyecto de inicio web 3.0 a continuación. Esto asegurará que todos estemos en la misma página y que estemos usando los mismos paquetes. Clonación del proyecto de inicio Web3 git clone https://github.com/Daltonic/dominionDAO Fantástico, reemplacemos el archivo con el siguiente: 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", "@truffle/hdwallet-provider": "^2.0.4", "assert": "^2.0.0", "autoprefixer": "10.4.2", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-preset-stage-3": "^6.24.1", "babel-register": "^6.26.0", "buffer": "^6.0.3", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "crypto-browserify": "^3.12.0", "dotenv": "^16.0.0", "https-browserify": "^1.0.0", "mnemonics": "^1.1.3", "os-browserify": "^0.3.0", "postcss": "8.4.5", "process": "^0.11.10", "react-app-rewired": "^2.1.11", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "tailwindcss": "3.0.18", "url": "^0.11.0" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } Genial, reemplace su archivo con el código anterior y luego ejecute en su terminal. package.json yarn install Con todo eso instalado, comencemos escribiendo el contrato inteligente Dominion DAO. Configuración del SDK de CometChat Para configurar el , siga los pasos a continuación, al final, debe almacenar estas claves como una variable de entorno. SDK de CometChat Dirígete al Tablero de y crea una cuenta. PASO 1: CometChat Inicie sesión en el panel de , solo después de registrarse. PASO 2: CometChat desde el tablero, agregue una nueva aplicación llamada PASO 3: dominionDAO. Seleccione la aplicación que acaba de crear de la lista. PASO 4: Desde el inicio rápido, copie , y en su archivo . Vea la imagen y el fragmento de código. PASO 5: APP_ID REGION AUTH_KEY .env Reemplace las claves de los marcadores de posición con sus valores apropiados. REACT_COMET_CHAT REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=****************************** Configuración de la aplicación Infuria Dirígete a y crea una cuenta. PASO 1: Infuria Desde el tablero crea un nuevo proyecto. PASO 2: Copie la URL del extremo del WebSocket de la red de prueba de en su archivo . PASO 3: Rinkeby .env A continuación, agregue su frase secreta de Metamask y la clave privada de su cuenta preferida. Si lo ha hecho correctamente, sus variables de entorno ahora deberían verse así. ENDPOINT_URL=*************************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=****************************** Si no sabe cómo acceder a su clave privada, consulte la sección a continuación. Acceso a su clave privada de Metamask Haga clic en la extensión de su navegador y asegúrese de que esté seleccionado como la red de prueba. A continuación, en la cuenta preferida, haga clic en la línea punteada vertical y seleccione los detalles de la cuenta. Vea la imagen a continuación. PASO 1: Metamask Rinkeby Ingrese su contraseña en el campo proporcionado y haga clic en el botón de confirmación, esto le permitirá acceder a la clave privada de su cuenta. PASO 2: Haga clic en "exportar clave privada" para ver su clave privada. Asegúrese de nunca exponer sus claves en una página pública como . Es por eso que lo agregamos como una variable de entorno. PASO 3: Github Copie su clave privada en su archivo .env. Vea la imagen y el fragmento de código a continuación: PASO 4: ENDPOINT_URL=*************************** SECRET_KEY=****************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=****************************** En cuanto a su , debe pegar su frase secreta de en el espacio proporcionado en el archivo de entorno. SECRET_KEY Metamask El contrato inteligente Dominion DAO Aquí está el código completo para el contrato inteligente, explicaré todas las funciones y variables una tras otra. // 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; } } En el proyecto que acabas de clonar, dirígete al directorio y crea un archivo llamado , luego pega los códigos anteriores dentro de él. src >> contract DominionDAO.sol Explicación: // SPDX-License-Identifier: MIT pragma solidity ^0.8.7; Solidity requiere un identificador de licencia para compilar su código; de lo contrario, generará una advertencia que le pedirá que especifique uno. Además, Solidity requiere que especifique la versión del compilador para su contrato inteligente. Eso es lo que representa la palabra . pragma import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; En el bloque de código anterior, estamos utilizando dos contratos inteligentes para especificar roles y proteger nuestro contrato inteligente contra ataques de reingreso. openzeppelin's 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 algunas variables de estado para los roles de las partes interesadas y los contribuyentes y especificamos que la duración mínima de la votación sea de una semana. Y también inicializamos el contador de propuestas totales y una variable para llevar un registro de nuestro saldo disponible. 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; un seguimiento de todas las propuestas enviadas a nuestro contrato inteligente. como su nombre implica hacer un seguimiento de los votos realizados por las partes interesadas. realiza un seguimiento de todos los votos asociados con una propuesta. Mientras que los contribuyentes realizan un seguimiento de cualquier persona que haya donado a nuestra plataforma, las partes interesadas, por otro lado, realizan un seguimiento de las personas que han contribuido hasta . raisedProposals stakeholderVotes votedOn 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; } describe el contenido de cada propuesta mientras que describe el contenido de cada voto. proposalStruct votedStruct event Action( address indexed initiator, bytes32 role, string message, address indexed beneficiary, uint256 amount ); Este es un evento dinámico llamado Acción. Esto nos ayudará a enriquecer la información desconectada por transacción. modifier stakeholderOnly(string memory message) { require(hasRole(STAKEHOLDER_ROLE, msg.sender), message); _; } modifier contributorOnly(string memory message) { require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message); _; } Los modificadores anteriores nos ayudan a identificar a los usuarios por función y también evitan que accedan a algunos recursos no 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 ); } La función anterior toma el título, la descripción, el monto y la dirección de la billetera del beneficiario de una propuesta y crea una propuesta. La función solo permite a las partes interesadas crear propuestas. Las partes interesadas son usuarios que han realizado al menos una contribución 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 función acepta dos argumentos, un Id. de propuesta y una opción preferida representada por un valor booleano. Verdadero significa que aceptaste el voto y Falso representa un rechazo. 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 función realiza la votación real, incluida la verificación de si un usuario es una parte interesada y está calificado 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 función se encarga de pagar al beneficiario adjunto a una propuesta en base a ciertos criterios. Uno, el beneficiario no debe estar ya pagado. Dos, la duración de la propuesta debe haber expirado. Tres, el saldo disponible debe poder pagar el beneficiario. Cuarto, no debe haber empate en el 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 función es responsable de recolectar las contribuciones de los donantes y de aquellos interesados en convertirse en partes interesadas. function getProposals() external view returns (ProposalStruct[] memory props) { props = new ProposalStruct[](totalProposals); for (uint256 i = 0; i < totalProposals; i++) { props[i] = raisedProposals[i]; } } Esta función recupera una serie de propuestas registradas en este contrato inteligente. function getProposal(uint256 proposalId) external view returns (ProposalStruct memory) { return raisedProposals[proposalId]; } Esta función recupera una propuesta particular por Id. function getVotesOf(uint256 proposalId) external view returns (VotedStruct[] memory) { return votedOn[proposalId]; } Esto devuelve una lista de votos asociados con una propuesta en particular. function getStakeholderVotes() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256[] memory) { return stakeholderVotes[msg.sender]; } Esto devuelve la lista de partes interesadas en el contrato inteligente y solo una parte interesada puede llamar a esta función. function getStakeholderBalance() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256) { return stakeholders[msg.sender]; } Esto devuelve la cantidad de dinero aportado por las partes interesadas. function isStakeholder() external view returns (bool) { return stakeholders[msg.sender] > 0; } Devuelve verdadero o falso si un usuario es una parte interesada. function getContributorBalance() external view contributorOnly("Denied: User is not a contributor") returns (uint256) { return contributors[msg.sender]; } Esto devuelve el saldo de un contribuyente y solo es accesible para el contribuyente. function isContributor() external view returns (bool) { return contributors[msg.sender] > 0; } Esto verifica si un usuario es un colaborador o no y se representa con Verdadero o Falso. function getBalance() external view returns (uint256) { return contributors[msg.sender]; } Devuelve el saldo del usuario llamante independientemente de su rol. function payTo( address to, uint256 amount ) internal returns (bool) { (bool success,) = payable(to).call{value: amount}(""); require(success, "Payment failed"); return true; } Esta función realiza un pago con una cantidad y una cuenta específicas. Configuración del script de implementación Una cosa más que hacer con el contrato inteligente es configurar el script de implementación. En el proyecto, diríjase a la carpeta de , >> y actualícelo con el fragmento de código a continuación. migraciones 2_deploy_contracts.js, const DominionDAO = artifacts.require('DominionDAO') module.exports = async function (deployer) { await deployer.deploy(DominionDAO) } Fantástico, acabamos de terminar con el contrato inteligente para nuestra aplicación, es hora de comenzar a construir la interfaz Dapp. Desarrollando la interfaz La parte delantera consta de muchos componentes y piezas. Estaremos creando todos los componentes, vistas y el resto de los periféricos. Componente de encabezado Este componente captura información sobre el usuario actual y lleva un botón de cambio de tema para los modos claro y oscuro. Y si te preguntaste cómo lo hice, fue a través de Tailwind CSS, mira el código a continuación. import { useState, useEffect } from 'react' import { FaUserSecret } from 'react-icons/fa' import { MdLightMode } from 'react-icons/md' import { FaMoon } from 'react-icons/fa' import { Link } from 'react-router-dom' import { connectWallet } from '../Dominion' import { useGlobalState, truncate } from '../store' const Header = () => { const [theme, setTheme] = useState(localStorage.theme) const themeColor = theme === 'dark' ? 'light' : 'dark' const darken = theme === 'dark' ? true : false const [connectedAccount] = useGlobalState('connectedAccount') useEffect(() => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) }, [themeColor, theme]) const toggleLight = () => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) setTheme(themeColor) } return ( <header className="sticky top-0 z-50 dark:text-blue-500"> <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]"> <div className="px-6 w-full flex flex-wrap items-center justify-between"> <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2"> <Link to={'/'} className="flex flex-row justify-start items-center space-x-3" > <FaUserSecret className="cursor-pointer" size={25} /> <span className="invisible md:visible dark:text-gray-300"> Dominion </span> </Link> <div className="flex flex-row justify-center items-center space-x-5"> {darken ? ( <MdLightMode className="cursor-pointer" size={25} onClick={toggleLight} /> ) : ( <FaMoon className="cursor-pointer" size={25} onClick={toggleLight} /> )} {connectedAccount ? ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" > {truncate(connectedAccount, 4, 4, 11)} </button> ) : ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" onClick={connectWallet} > Connect Wallet </button> )} </div> </div> </div> </nav> </header> ) } export default Header Componente de pancarta Este componente contiene información sobre el estado actual de la DAO, como el saldo total y el número de propuestas abiertas. Este componente también incluye la capacidad de utilizar la función de contribución para generar una nueva propuesta. Mira el código de abajo. import { useState } from 'react' import { setGlobalState, useGlobalState } from '../store' import { performContribute } from '../Dominion' import { toast } from 'react-toastify' const Banner = () => { const [isStakeholder] = useGlobalState('isStakeholder') const [proposals] = useGlobalState('proposals') const [connectedAccount] = useGlobalState('connectedAccount') const [currentUser] = useGlobalState('currentUser') const [balance] = useGlobalState('balance') const [mybalance] = useGlobalState('mybalance') const [amount, setAmount] = useState('') const onPropose = () => { if (!isStakeholder) return setGlobalState('createModal', 'scale-100') } const onContribute = () => { if (!!!amount || amount == '') return toast.info('Contribution in progress...') performContribute(amount).then((bal) => { if (!!!bal.message) { setGlobalState('balance', Number(balance) + Number(bal)) setGlobalState('mybalance', Number(mybalance) + Number(bal)) setAmount('') toast.success('Contribution received') } }) } const opened = () => proposals.filter( (proposal) => new Date().getTime() < Number(proposal.duration + '000') ).length return ( <div className="p-8"> <h2 className="font-semibold text-3xl mb-5"> {opened()} Proposal{opened() == 1 ? '' : 's'} Currenly Opened </h2> <p> Current DAO Balance: <strong>{balance} Eth</strong> <br /> Your contributions:{' '} <span> <strong>{mybalance} Eth</strong> {isStakeholder ? ', and you are now a stakeholder 😊' : null} </span> </p> <hr className="my-6 border-gray-300 dark:border-gray-500" /> <p> {isStakeholder ? 'You can now raise proposals on this platform 😆' : 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'} </p> <div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4"> <input type="number" className="form-control block w-full px-3 py-1.5 text-base font-normaltext-gray-700 bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 shadow-md focus:text-gray-500 focus:outline-none dark:border-gray-500 dark:bg-transparent" placeholder="eg 2.5 Eth" onChange={(e) => setAmount(e.target.value)} value={amount} required /> </div> <div className="flex flex-row justify-start items-center space-x-3 mt-4" role="group" > <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onContribute} > Contribute </button> {isStakeholder ? ( <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onPropose} > Propose </button> ) : null} {currentUser && currentUser.uid == connectedAccount.toLowerCase() ? null : ( <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:border dark:border-blue-500`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => setGlobalState('loginModal', 'scale-100')} > Login Chat </button> )} </div> </div> ) } export default Banner Componente Propuestas Este componente contiene una lista de propuestas en nuestro contrato inteligente. Además, le permite filtrar entre propuestas cerradas y abiertas. Al vencimiento de una propuesta, aparece un botón de pago que le da a la parte interesada la opción de pagar el monto asociado con la propuesta. Vea el código a continuación. 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 El componente de detalles de la propuesta Este componente muestra información sobre la propuesta actual, incluido el costo. Este componente permite a las partes interesadas aceptar o rechazar una propuesta. El proponente puede formar un grupo y otros usuarios de la plataforma pueden participar en un chat anónimo al estilo web3.0. Este componente también incluye un gráfico de barras que le permite ver la proporción de aceptados y rechazados. Mira el código de abajo. import moment from 'moment' import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { getGroup, createNewGroup, joinGroup } from '../CometChat' import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Legend, Tooltip, } from 'recharts' import { getProposal, voteOnProposal } from '../Dominion' import { useGlobalState } from '../store' const ProposalDetails = () => { const { id } = useParams() const navigator = useNavigate() const [proposal, setProposal] = useState(null) const [group, setGroup] = useState(null) const [data, setData] = useState([]) const [isStakeholder] = useGlobalState('isStakeholder') const [connectedAccount] = useGlobalState('connectedAccount') const [currentUser] = useGlobalState('currentUser') useEffect(() => { retrieveProposal() getGroup(`pid_${id}`).then((group) => { if (!!!group.code) setGroup(group) console.log(group) }) }, [id]) const retrieveProposal = () => { getProposal(id).then((res) => { setProposal(res) setData([ { name: 'Voters', Acceptees: res?.upvotes, Rejectees: res?.downvotes, }, ]) }) } const onVote = (choice) => { if (new Date().getTime() > Number(proposal.duration + '000')) { toast.warning('Proposal expired!') return } voteOnProposal(id, choice).then((res) => { if (!!!res.code) { toast.success('Voted successfully!') window.location.reload() } }) } const daysRemaining = (days) => { const todaysdate = moment() days = Number((days + '000').slice(0)) days = moment(days).format('YYYY-MM-DD') days = moment(days) days = days.diff(todaysdate, 'days') return days == 1 ? '1 day' : days + ' days' } const onEnterChat = () => { if (group.hasJoined) { navigator(`/chat/${`pid_${id}`}`) } else { joinGroup(`pid_${id}`).then((res) => { if (!!res) { navigator(`/chat/${`pid_${id}`}`) console.log('Success joining: ', res) } else { console.log('Error Joining Group: ', res) } }) } } const onCreateGroup = () => { createNewGroup(`pid_${id}`, proposal.title).then((group) => { if (!!!group.code) { toast.success('Group created successfully!') setGroup(group) } else { console.log('Error Creating Group: ', group) } }) } return ( <div className="p-8"> <h2 className="font-semibold text-3xl mb-5">{proposal?.title}</h2> <p> This proposal is to payout <strong>{proposal?.amount} Eth</strong> and currently have{' '} <strong>{proposal?.upvotes + proposal?.downvotes} votes</strong> and will expire in <strong>{daysRemaining(proposal?.duration)}</strong> </p> <hr className="my-6 border-gray-300" /> <p>{proposal?.description}</p> <div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto"> <BarChart width={730} height={250} data={data}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="name" /> <YAxis /> <Tooltip /> <Legend /> <Bar dataKey="Acceptees" fill="#2563eb" /> <Bar dataKey="Rejectees" fill="#dc2626" /> </BarChart> </div> <div className="flex flex-row justify-start items-center space-x-3 mt-4" role="group" > {isStakeholder ? ( <> <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-gray-300 dark:border dark:border-gray-500 dark:bg-transparent" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => onVote(true)} > Accept </button> <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:border dark:border-gray-500 dark:bg-transparent" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => onVote(false)} > Reject </button> {currentUser && currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() && !group ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:border dark:border-blue-500" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onCreateGroup} > Create Group </button> ) : null} </> ) : null} {currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:border dark:border-blue-500" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onEnterChat} > Chat </button> ) : null} {proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() && !!!group ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 dark:bg-transparent text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:border-blue-700 hover:shadow-lg focus:border-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 disabled:bg-blue-300" data-mdb-ripple="true" data-mdb-ripple-color="light" disabled > Group N/A </button> ) : null} </div> </div> ) } export default ProposalDetails Componente de votantes Este componente simplemente enumera las partes interesadas que han votado sobre una propuesta. El componente también ofrece al usuario la oportunidad de filtrar entre rechazados y aceptados. Vea el código a continuación. import Identicon from 'react-identicons' import moment from 'moment' import { useState, useEffect } from 'react' import { useParams } from 'react-router-dom' import { truncate } from '../store' import { listVoters } from '../Dominion' const Voters = () => { const [voters, setVoters] = useState([]) const [data, setData] = useState([]) const { id } = useParams() const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow() const deactive = `bg-transparent text-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-600 transition duration-150 ease-in-out overflow-hidden border border-blue-600 hover:text-white focus:text-white` const active = `bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out overflow-hidden border border-blue-600` useEffect(() => { listVoters(id).then((res) => { setVoters(res) setData(res) }) }, [id]) const getAll = () => setVoters(data) const getAccepted = () => setVoters(data.filter((vote) => vote.choosen)) const getRejected = () => setVoters(data.filter((vote) => !vote.choosen)) return ( <div className="flex flex-col p-8"> <div className="flex flex-row justify-center items-center" role="group"> <button aria-current="page" className={`rounded-l-full px-6 py-2.5 ${active}`} onClick={getAll} > All </button> <button aria-current="page" className={`px-6 py-2.5 ${deactive}`} onClick={getAccepted} > Acceptees </button> <button aria-current="page" className={`rounded-r-full px-6 py-2.5 ${deactive}`} onClick={getRejected} > Rejectees </button> </div> <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8"> <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md"> <table className="min-w-full"> <thead className="border-b dark:border-gray-500"> <tr> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Voter </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Voted </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Vote </th> </tr> </thead> <tbody> {voters.map((voter, i) => ( <tr key={i} className="border-b dark:border-gray-500 transition duration-300 ease-in-out" > <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> <div className="flex flex-row justify-start items-center space-x-3"> <Identicon string={voter.voter.toLowerCase()} size={25} className="h-10 w-10 object-contain rounded-full mr-3" /> <span>{truncate(voter.voter, 4, 4, 11)}</span> </div> </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {timeAgo(voter.timestamp)} </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {voter.choosen ? ( <button className="border-2 rounded-full px-6 py-2.5 border-blue-600 text-blue-600 font-medium text-xs leading-tight uppercase hover:border-blue-700 focus:border-blue-700 focus:outline-none focus:ring-0 active:border-blue-800 transition duration-150 ease-in-out" > Accepted </button> ) : ( <button className="border-2 rounded-full px-6 py-2.5 border-red-600 text-red-600 font-medium text-xs leading-tight uppercase hover:border-red-700 focus:border-red-700 focus:outline-none focus:ring-0 active:border-red-800 transition duration-150 ease-in-out" > Rejected </button> )} </td> </tr> ))} </tbody> </table> </div> </div> </div> <div className="mt-4 text-center"> {voters.length >= 10 ? ( <button aria-current="page" className="rounded-full px-6 py-2.5 bg-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out dark:text-gray-300 dark:border dark:border-gray-500 dark:bg-transparent" > Load More </button> ) : null} </div> </div> ) } export default Voters Componente de mensajes Con el poder de CometChat SDK combinado con este componente, los usuarios pueden participar en un chat de uno a muchos de forma anónima. Los contribuyentes y las partes interesadas pueden discutir una propuesta más a fondo en su proceso de toma de decisiones aquí. Todos los usuarios mantienen su anonimato y están representados por sus 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 Crear componente de propuesta Este componente simplemente le permite plantear una propuesta proporcionando información sobre los campos que se ven en la imagen de arriba. Vea el código a continuación. import { useState } from 'react' import { FaTimes } from 'react-icons/fa' import { raiseProposal } from '../Dominion' import { setGlobalState, useGlobalState } from '../store' import { toast } from 'react-toastify' const CreateProposal = () => { const [createModal] = useGlobalState('createModal') const [title, setTitle] = useState('') const [amount, setAmount] = useState('') const [beneficiary, setBeneficiary] = useState('') const [description, setDescription] = useState('') const handleSubmit = (e) => { e.preventDefault() if (!title || !description || !beneficiary || !amount) return const proposal = { title, description, beneficiary, amount } raiseProposal(proposal).then((proposed) => { if (proposed) { toast.success('Proposal created, reloading in progress...') closeModal() window.location.reload() } }) } const closeModal = () => { setGlobalState('createModal', 'scale-0') resetForm() } const resetForm = () => { setTitle('') setAmount('') setBeneficiary('') setDescription('') } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${createModal}`} > <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> <form className="flex flex-col"> <div className="flex flex-row justify-between items-center"> <p className="font-semibold">Raise Proposal</p> <button type="button" onClick={closeModal} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="title" placeholder="Title" onChange={(e) => setTitle(e.target.value)} value={title} required /> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="amount" placeholder="eg 2.5 Eth" onChange={(e) => setAmount(e.target.value)} value={amount} required /> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="beneficiary" placeholder="Beneficiary Address" onChange={(e) => setBeneficiary(e.target.value)} value={beneficiary} required /> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <textarea className="block w-full text-sm resize-none bg-transparent border-0 focus:outline-none focus:ring-0 h-20" type="text" name="description" placeholder="Description" onChange={(e) => setDescription(e.target.value)} value={description} required ></textarea> </div> <button className="rounded-lg px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5" onClick={handleSubmit} > Submit Proposal </button> </form> </div> </div> ) } export default CreateProposal Componente de autenticación Este componente lo ayuda a participar en las funciones de chat. Debe crear una cuenta o iniciar sesión si ya se ha registrado. Al iniciar sesión, puede participar en un chat grupal y tener una conversación anónima con otros participantes en una propuesta en un estilo web3.0. Vea el código a continuación. import { FaTimes } from 'react-icons/fa' import { loginWithCometChat, signInWithCometChat } from '../CometChat' import { setGlobalState, useGlobalState } from '../store' import { toast } from 'react-toastify' const ChatLogin = () => { const [loginModal] = useGlobalState('loginModal') const [connectedAccount] = useGlobalState('connectedAccount') const handleSignUp = () => { signInWithCometChat(connectedAccount, connectedAccount).then((user) => { if (!!!user.code) { toast.success('Account created, now click the login button.') } else { toast.error(user.message) } }) } const handleLogin = () => { loginWithCometChat(connectedAccount).then((user) => { if (!!!user.code) { setGlobalState('currentUser', user) toast.success('Logged in successful!') closeModal() } else { toast.error(user.message) } }) } const closeModal = () => { setGlobalState('loginModal', 'scale-0') } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${loginModal}`} > <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> <div className="flex flex-col"> <div className="flex flex-row justify-between items-center"> <p className="font-semibold">Authenticate</p> <button type="button" onClick={closeModal} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div className="my-2 font-light"> <span> Once you login, you will be enabled to chat with other stakeholders to make a well-informed voting. </span> </div> <div className="flex flex-row justify-between items-center mt-2" role="group" > <button className="rounded-lg px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5" onClick={handleLogin} > Login </button> <button className="rounded-lg px-6 py-2.5 bg-transparent text-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 hover:text-white focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5 border-blue-600" onClick={handleSignUp} > Create Account </button> </div> </div> </div> </div> ) } export default ChatLogin Fantástico, asegurémonos de que las vistas estén bien representadas... La vista de inicio Esta vista incluye los componentes de , y para proporcionar una experiencia de usuario de DAO excepcional. También usamos el poder de Tailwind CSS para lograr este aspecto. Mira el código de abajo. header banner proposals 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 La vista de propuesta Esta vista combina el encabezado, los detalles de la propuesta y el componente de votantes para ofrecer una presentación fluida de un componente singular. Vea el código a continuación. import Header from '../components/Header' import ProposalDetails from '../components/ProposalDetails' import Voters from '../components/Voters' const Proposal = () => { return ( <> <Header /> <ProposalDetails /> <Voters /> </> ) } export default Proposal La vista de chat Por último, la vista de chat incorpora el componente de encabezado y mensajes para representar una interfaz de chat de calidad. Vea el código a continuación. 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 Increíble, no olvides actualizar también el archivo . App.jsx de la aplicación Reemplace el componente de la aplicación con el siguiente código. El componente 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 En el pegue los siguientes códigos en sus respectivos archivos. directorio src, >> Archivo Index.jsx import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App' import { initCometChat } from './CometChat' initCometChat().then(() => { ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ) }) Archivo 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, } Puesta en marcha del entorno de desarrollo Active una cuenta de prueba con usando el siguiente comando: PASO 1: ganache-cli ganache-cli -a Esto creará algunas cuentas de prueba con 100 éteres falsos cargados en cada cuenta, por supuesto, estos son solo para fines de prueba. Vea la imagen a continuación: agregue una red de prueba local con Metamask como se ve en la imagen a continuación. PASO 2: Haga clic en el icono de la cuenta y seleccione importar cuenta. PASO 3: Copie aproximadamente cinco de las claves privadas y agréguelas una tras otra a su red de prueba local. Vea la imagen a continuación. Observe la nueva cuenta agregada a su red de prueba local con 100 ETH precargados. Asegúrese de agregar unas cinco cuentas para que pueda hacer una prueba máxima. Vea la imagen a continuación. Implementación de contrato inteligente Ahora abra una nueva terminal y ejecute el siguiente comando. truffle migrate # or truffle migrate --network rinkeby El comando anterior implementará su contrato inteligente en su red de prueba local o Infuria rinkeby. A continuación, abra otra terminal y active la aplicación de reacción con el . yarn start Conclusión Hurra, acabamos de completar un increíble tutorial para desarrollar una organización autónoma descentralizada. Si disfrutó de este tutorial y le gustaría tenerme como su mentor privado, por favor . reserve sus clases conmigo Hasta la próxima, todo lo mejor. Sobre el Autor Gospel Darlington es un desarrollador de blockchain de pila completa con más de años de experiencia en la industria del desarrollo de software. 6+ Al combinar el desarrollo de software, la escritura y la enseñanza, demuestra cómo crear aplicaciones descentralizadas en redes de cadena de bloques compatibles con EVM. Sus pilas incluyen , , , , , , , y más. JavaScript React Vue Angular Node React Native NextJs Solidity Para obtener más información sobre él, visite y siga su página en , , o en su . Twitter Github LinkedIn sitio web