Los "
El resultado final de mi artículo original demostró cómo una asociación de propietarios (HOA) podría usar la tecnología Web3 para alojar su boleta electoral. El problema con el diseño original es que el contrato inteligente subyacente solo permitía una sola respuesta de sí o no. Esto fue por diseño para mantener el contrato inteligente simple mientras se introducen otros conceptos necesarios para crear una boleta HOA usando tecnologías Web3.
El propósito de esta publicación es profundizar en los contratos inteligentes para crear una aplicación que no solo capture necesidades y funciones realistas para una boleta de HOA, sino que diseñe una que pueda reutilizarse de una elección a la siguiente.
Antes de comenzar, definamos un contrato inteligente:
“Un contrato inteligente es un programa que se ejecuta en una dirección en Ethereum. Están compuestos por datos y funciones que pueden ejecutarse al recibir una transacción. Aquí hay una descripción general de lo que constituye un contrato inteligente”.
fuente
ethereum.org
Lo crea o no, se puede encontrar un ejemplo fácil de un contrato inteligente en una simple máquina de chicles:
La gente entiende fácilmente el costo relacionado con la compra de la máquina de chicles. Normalmente, se trata de una cuarta parte (EE. UU.). Es importante señalar aquí que el cliente es anónimo, ya que la máquina de chicles no requiere saber quién es una persona antes de darle un sabroso chicle.
El consumidor anónimo coloca dinero en la máquina de chicles y gira el dial para aceptar los términos del contrato. Este paso es importante porque la transacción es transparente y de igual a igual: entre usted y la máquina. La transacción también está asegurada ya que debe proporcionar la moneda esperada para usar la máquina de chicles.
Una vez que el dinero cae dentro de la máquina de chicles, se aceptan los términos del contrato y un chicle rueda hacia la parte inferior de la máquina, lo que permite que el cliente reciba su compra. En este punto, el contrato está completamente ejecutado.
El cliente debe aceptar lo que se le proporciona, lo que significa que no puede devolver el chicle o invertir el dial para recuperar su moneda. De la misma manera, los contratos inteligentes suelen ser irreversibles e inmodificables.
Aparte de los ejemplos impulsados financieramente, a continuación se indican algunos escenarios en los que se podrían implementar interacciones anónimas, sin confianza, descentralizadas y transparentes que son irreversibles e inmodificables:
En todos los casos, los contenidos del contrato inteligente se pueden recuperar y revisar con la mayor frecuencia posible, sin la posibilidad de cambiar o modificar los resultados. Cada caso de uso anterior proporciona el contrato inteligente como el sistema de registro de la información subyacente.
En este momento, los contratos inteligentes no son acuerdos legalmente vinculantes, excepto por algunos casos atípicos. Esto significa que si no está satisfecho con el resultado de su contrato inteligente, no es posible llevar su problema ante un juez en algún sistema judicial.
Hay algunas excepciones, como en el estado de Arizona, donde los contratos inteligentes se consideran legalmente vinculantes. Además, si se encuentra en el estado de California y su licencia de matrimonio está incluida en un contrato inteligente, ese acuerdo también es legalmente vinculante. La expectativa es que más gobiernos reconozcan los contratos inteligentes como acuerdos legalmente vinculantes en el futuro.
Sobre la base del contrato inteligente binario simple (sí/no) de la publicación "Moving From Full-Stack Developer To Web3 Pioneer", demos un paso adelante y supongamos que existe el siguiente requisito para una boleta HOA para un vecindario que tiene un solo puesto a cubrir:
Idealmente, el objetivo sería utilizar un solo contrato inteligente cada vez que haya una elección de HOA. Se espera que los que se postulan para el cargo de presidente cambien de una elección a la siguiente.
Ahora, comencemos a hacer un contrato inteligente para manejar nuestras necesidades.
Usando Solidity, trabajé con
// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /********************************************************/ /* For learning purposes ONLY. Do not use in production */ /********************************************************/ // Download into project folder with `npm install @openzeppelin/contracts` import "@openzeppelin/contracts/access/Ownable.sol"; // Inherits the Ownable contract so we can use its functions and modifiers contract HOABallot is Ownable { // Custom type to describe a Presidential Candidate and hold votes struct Candidate { string name; uint256 votes; } // Array of Presidential Candidates Candidate[] public candidates; // Add a President Candidate - onlyOwner function addCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "addCandidate Error: Please enter a name"); candidates.push(Candidate({name: _name, votes: 0})); } // Remove a Candidate - onlyOwner function removeCandidate(string memory _name) public onlyOwner { require(bytes(_name).length > 0, "removeCandidate Error: Please enter a name"); bool foundCandidate = false; uint256 index; bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); // Set index number for specific candidate for (uint256 i = 0; i < candidates.length; i++) { if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { index = i; foundCandidate = true; } } // Make sure a candidate was found require(foundCandidate, "removeCandidate Error: Candidate not found"); // shift candidate to be removed to the end of the array and the rest forward for (uint256 i = index; i < candidates.length - 1; i++) { candidates[i] = candidates[i + 1]; } // remove last item from array candidates.pop(); } // Reset the President Vote Counts - onlyOwner function resetVoteCount() public onlyOwner { for (uint256 p = 0; p < candidates.length; p++) { candidates[p].votes = 0; } } // Add a vote to a candidate by name function addVoteByName(string memory _name) public { require(bytes(_name).length > 0, "addVoteByName Error: Please enter a name"); // Encode name so only need to do once bytes32 nameEncoded = keccak256(abi.encodePacked(_name)); for (uint256 i = 0; i < candidates.length; i++) { // solidity can't compare strings directly, need to compare hash if (keccak256(abi.encodePacked(candidates[i].name)) == nameEncoded) { candidates[i].votes += 1; } } } // Returns all the Presidential Candidates and their vote counts function getCandidates() public view returns (Candidate[] memory) { return candidates; } function getWinner() public view returns (Candidate memory winner) { uint256 winningVoteCount = 0; for (uint256 i = 0; i < candidates.length; i++) { if (candidates[i].votes > winningVoteCount) { winningVoteCount = candidates[i].votes; winner = candidates[i]; } } return winner; } }
Aquí hay algunos elementos clave relacionados con el diseño del contrato inteligente:
Ahora, preparemos el contrato inteligente para usar.
Para poder utilizar nuestro contrato inteligente, construiremos un proyecto Truffle simple e implementaremos el contrato en la red de prueba de Ropsten. Para hacer esto, primero necesitaremos la versión más reciente de Truffle. Con
npm install -g truffle
Instalar la última versión nos dará acceso a la
A continuación, cree un nuevo directorio e inicialice un nuevo proyecto Truffle.
mkdir hoa-ballot-contract && cd hoa-ballot-contract truffle init
Esto creará un proyecto de contrato inteligente barebones que podemos completar como mejor nos parezca. Así que abra el proyecto en su editor de código favorito y ¡manos a la obra!
Para aprovechar OpenZeppelin, también se debe ejecutar el siguiente comando en la carpeta del proyecto:
npm install @openzeppelin/contracts
Abra el archivo truffle-config.js y agregaremos el Tablero de Truffle dentro del objeto de networks
. Aparte de todo el texto repetitivo comentado, nuestro objeto ahora debería verse así:
networks: { dashboard: { port: 24012, } }
Para el siguiente paso, crearemos un nuevo archivo de contrato inteligente. Dentro de la carpeta de contratos , cree un nuevo archivo y asígnele el nombre HOABallot.sol . A partir de aquí, simplemente pegaremos el contrato inteligente de arriba.
Lo último que debemos hacer antes de poder implementar este contrato es configurar el script de implementación. Usando los contenidos a continuación, necesitamos crear un nuevo archivo en la carpeta de migraciones llamado 2_hoaballot_migration.js .
const HOABallot = artifacts.require("HOABallot"); Module.exports = function (deployer) { deployer.deploy(HOABallot); }
Ahora estamos listos para implementar nuestro contrato en la red de prueba de Ropsten. En una nueva ventana de terminal, escriba el siguiente comando para iniciar el tablero:
truffle dashboard
Una vez que se está ejecutando, nuestro navegador debería aparecer con una interfaz que nos pide que conectemos nuestra billetera. Si esto no aparece para usted, vaya a localhost:24012
.
Al hacer un solo clic en el botón METAMASK , se iniciará MetaMask a través del complemento del navegador. Si no tiene instalada una extensión de navegador de billetera, puede obtener una en
Después de ingresar una contraseña válida y usar el botón Desbloquear , Truffle Dashboard confirma la red que se utilizará:
Después de hacer clic en el botón CONFIRMAR , Truffle Dashboard ahora está escuchando solicitudes:
Necesitaremos Ropsten Eth para llevar a cabo el despliegue. Si no tienes ninguno, puedes
Todo lo que tenemos que hacer ahora es implementar el contrato. En la ventana de su terminal original, asegúrese de estar en la carpeta del proyecto y escriba el comando:
truffle migrate --network dashboard
Truffle compilará automáticamente nuestro contrato inteligente y luego enrutará la solicitud a través del tablero. Cada solicitud seguirá el mismo flujo que se detalla a continuación.
Primero, el Tablero de Truffle solicita confirmación para procesar la solicitud:
Al presionar el botón PROCESAR, el complemento MetaMask también solicitará confirmación:
El botón Confirmación permitirá que se eliminen fondos de esta billetera asociada para procesar cada solicitud.
Cuando se complete el proceso, aparecerá la siguiente información en la ventana del terminal utilizada para emitir el comando de migración de truffle:
2_hoaballot_migration.js ======================== Deploying 'HOABallot' --------------------- > transaction hash: 0x5370b6f9ee1f69e92cc6289f9cb0880386f15bff389b54ab09a966c5d144f59esage. > Blocks: 0 Seconds: 32 > contract address: 0x2981d347e288E2A4040a3C17c7e5985422e3cAf2 > block number: 12479257 > block timestamp: 1656386400 > account: 0x7fC3EF335D16C0Fd4905d2C44f49b29BdC233C94 > balance: 41.088173901232893417 > gas used: 1639525 (0x190465) > gas price: 2.50000001 gwei > value sent: 0 ETH > total cost: 0.00409881251639525 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00409881251639525 ETH Summary ======= > Total deployments: 1 > Final cost: 0.00409881251639525 ETH
Ahora, usando el valor de la dirección del contrato , podemos validar el contrato inteligente usando la siguiente URL:
Ahora podemos cambiar y comenzar a construir el Dapp.
Crearé una aplicación React llamada hoa-ballot-client
usando React CLI:
npx create-react-app hoa-ballot-client
A continuación, cambié los directorios a la carpeta recién creada y ejecuté lo siguiente para instalar las dependencias web3 y OpenZepplin en la aplicación React:
cd hoa-ballot-client npm install web3 npm install @openzeppelin/contracts —save
Basado en el contenido del archivo de contrato inteligente HOABallot.sol
, navegué a la carpeta build/contracts y abrí el archivo HOBallot.json
, luego usé los valores para la propiedad "abi" para la constante hoaBallot
del archivo abi.js
como mostrado a continuación:
export const hoaBallot = [ { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "candidates", "outputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "removeCandidate", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "resetVoteCount", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "addVoteByName", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "getCandidates", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate[]", "name": "", "type": "tuple[]" } ], "stateMutability": "view", "type": "function", "constant": true }, { "inputs": [], "name": "getWinner", "outputs": [ { "components": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "uint256", "name": "votes", "type": "uint256" } ], "internalType": "struct HOABallot.Candidate", "name": "winner", "type": "tuple" } ], "stateMutability": "view", "type": "function", "constant": true } ];
Este archivo se colocó en una carpeta abi recién creada dentro de la carpeta src de la aplicación React.
Ahora, necesitamos actualizar el archivo React Apps.js. Comencemos primero con la parte superior del archivo, que debe configurarse como se muestra a continuación:
import React, { useState } from "react"; import { hoaBallot } from "./abi/abi"; import Web3 from "web3"; import "./App.css"; const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress);
La dirección del contrato se puede encontrar de varias maneras. En este caso, utilicé los resultados en el comando trufa — migrar CLI. Otra opción es usar el sitio de Etherscan.
Ahora, todo lo que queda es crear un código React estándar para lograr lo siguiente:
En mi publicación "Moving From Full-Stack Developer to Web3 Pioneer", también agregué el componente Nav, de modo que la dirección del votante se muestre para una fácil referencia.
La aplicación React actualizada ahora aparece de la siguiente manera:
const web3 = new Web3(Web3.givenProvider); const contractAddress = "0x2981d347e288E2A4040a3C17c7e5985422e3cAf2"; const storageContract = new web3.eth.Contract(hoaBallot, contractAddress); const gasMultiplier = 1.5; const useStyles = makeStyles((theme) => ({ root: { "& > *": { margin: theme.spacing(1), }, }, })); const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white, fontSize: 14, fontWeight: 'bold' }, [`&.${tableCellClasses.body}`]: { fontSize: 14 }, })); function App() { const classes = useStyles(); const [newCandidateName, setNewCandidateName] = useState(""); const [account, setAccount] = useState(""); const [owner, setOwner] = useState(""); const [candidates, updateCandidates] = useState([]); const [winner, setWinner] = useState("unknown candidate"); const [waiting, setWaiting] = useState(false); const loadAccount = async(useSpinner) => { if (useSpinner) { setWaiting(true); } const web3 = new Web3(Web3.givenProvider || "http://localhost:8080"); const accounts = await web3.eth.getAccounts(); setAccount(accounts[0]); if (useSpinner) { setWaiting(false); } } const getOwner = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const owner = await storageContract.methods.owner().call(); setOwner(owner); if (useSpinner) { setWaiting(false); } }; const getCandidates = async (useSpinner) => { if (useSpinner) { setWaiting(true); } const candidates = await storageContract.methods.getCandidates().call(); updateCandidates(candidates); await determineWinner(); if (useSpinner) { setWaiting(false); } }; const determineWinner = async () => { const winner = await storageContract.methods.getWinner().call(); if (winner && winner.name) { setWinner(winner.name); } else { setWinner("<unknown candidate>") } } const vote = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.addVoteByName(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addVoteByName(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const removeCandidate = async (candidate) => { setWaiting(true); const gas = (await storageContract.methods.removeCandidate(candidate).estimateGas({ data: candidate, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.removeCandidate(candidate).send({ from: account, data: candidate, gasAsInt, }); await getCandidates(false); setWaiting(false); } const addCandidate = async () => { setWaiting(true); const gas = (await storageContract.methods.addCandidate(newCandidateName).estimateGas({ data: newCandidateName, from: account })) * gasMultiplier; let gasAsInt = gas.toFixed(0); await storageContract.methods.addCandidate(newCandidateName).send({ from: account, data: newCandidateName, gasAsInt, }); await getCandidates(false); setWaiting(false); } React.useEffect(() => { setWaiting(true); getOwner(false).then(r => { loadAccount(false).then(r => { getCandidates(false).then(r => { setWaiting(false); }); }); }); // eslint-disable-next-line react-hooks/exhaustive-deps },[]); return ( <div className={classes.root}> <Nav /> <div className="main"> <div className="card"> <Typography variant="h3"> HOABallot </Typography> {(owner && owner.length > 0) && ( <div className="paddingBelow"> <Typography variant="caption" > This ballot is owned by: {owner} </Typography> </div> )} {waiting && ( <div className="spinnerArea" > <CircularProgress /> <Typography gutterBottom> Processing Request ... please wait </Typography> </div> )} {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && ( <div className="ownerActions generalPadding"> <Grid container spacing={3}> <Grid item xs={12}> <Typography variant="h6" gutterBottom> Ballot Owner Actions </Typography> </Grid> <Grid item xs={6} sm={6}> <TextField id="newCandidateName" value={newCandidateName} label="Candidate Name" variant="outlined" onChange={event => { const { value } = event.target; setNewCandidateName(value); }} /> </Grid> <Grid item xs={6} sm={6}> <Button id="addCandidateButton" className="button" variant="contained" color="primary" type="button" size="large" onClick={addCandidate}>Add New Candidate</Button> </Grid> </Grid> </div> )} <Typography variant="h5" gutterBottom className="generalPadding"> Candidates </Typography> {(!candidates || candidates.length === 0) && ( <div> <div className="paddingBelow"> <Typography variant="normal"> No candidates current exist. </Typography> </div> <div> <Typography variant="normal" gutterBottom> Ballot owner must use the <strong>ADD NEW CANDIDATE</strong> button to add candidates. </Typography> </div> </div> )} {(candidates && candidates.length > 0) && ( <div> <TableContainer component={Paper}> <Table sx={{ minWidth: 650 }} aria-label="customized table"> <TableHead> <TableRow> <StyledTableCell>Candidate Name</StyledTableCell> <StyledTableCell align="right">Votes</StyledTableCell> <StyledTableCell align="center">Actions</StyledTableCell> </TableRow> </TableHead> <TableBody> {candidates.map((row) => ( <TableRow key={row.name} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > <TableCell component="th" scope="row"> {row.name} </TableCell> <TableCell align="right">{row.votes}</TableCell> <TableCell align="center"> <Button color="success" variant="contained" onClick={() => { vote(row.name); }} > Vote </Button> {(owner && owner.length > 0 && account && account.length > 0 && owner === account) && <Button color="error" variant="contained" onClick={() => { removeCandidate(row.name); }} > Remove Candidate </Button> } </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> <div className="generalPadding"> <Typography variant="normal" gutterBottom> {winner} is winning the election. </Typography> </div> </div> )} </div> </div> </div> ); } export default App;
Para iniciar la Dapp basada en React, se puede usar la CLI de Yarn:
yarn start
Una vez compilada y validada, la aplicación aparecerá en pantalla, como se muestra a continuación:
Durante el vídeo:
Desde la implementación del contrato inteligente, cualquiera puede ver el historial completo en la siguiente URL:
https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2
Desde 2021, he estado tratando de vivir de acuerdo con la siguiente declaración de misión, que creo que se puede aplicar a cualquier profesional de la tecnología:
“Concentre su tiempo en ofrecer características/funcionalidades que amplíen el valor de su propiedad intelectual. Aproveche los marcos, productos y servicios para todo lo demás”.
J. Vester
Los contratos inteligentes brindan la capacidad de permitir que dos partes celebren un acuerdo en el que el resultado del contrato se convierte en un registro oficial grabado en piedra de la transacción. La adopción de un contrato inteligente se adhiere a mi declaración de misión personal en el sentido de que el marco subyacente evita reinventar la rueda cuando surge la necesidad de dicho contrato.
Al mismo tiempo, el diseño del contrato inteligente en sí mismo va un paso más allá y cumple con mi declaración de misión desde un factor de reutilización. En este ejemplo, se puede usar el mismo contrato inteligente de HOA, a pesar de que diferentes candidatos se presenten en las elecciones actuales. Aquí aprovechamos el poder del contrato inteligente para evitar crear un nuevo contrato inteligente cada vez que hay una elección.
Al usar Etherscan para buscar el valor de conversión de una de las transacciones que usan el convertidor de ETH a USD de Google, el costo por transacción fue de 0,24 (USD) por 0,0001348975 ETH. Irónicamente, ese era el costo de un chicle modesto de una máquina de chicles cuando era niño.
Si desea obtener más información sobre los contratos inteligentes, el equipo de ConsenSys ha proporcionado excelentes recursos para ayudarlo a crear prototipos de sus ideas para ver si la adopción de contratos inteligentes es un caso de uso válido.
Si está interesado en el código fuente de este artículo, puede encontrarlo en las siguientes URL:
https://github.com/paul-mcaviney/smart-contract-deep-dive/blob/main/HOABallot.sol
https://gitlab.com/johnjvester/hoa-ballot-contrato
https://gitlab.com/johnjvester/hoa-ballot-cliente
¡Que tengas un gran día!