Глубокое погружение в смарт-контракты

В публикации «Moving From Full-Stack Developer To Web3 Pioneer» был представлен обзор высокого уровня для того, чтобы дать разработчикам полного стека представление о мире разработки Web3. Если у вас еще не было возможности ознакомиться с этой статьей, посмотрите ее, поскольку там также дается хорошее введение в Web3.

Конечный результат моей первоначальной статьи демонстрировал, как ассоциация домовладельцев (HOA) может использовать технологию Web3 для размещения избирательного бюллетеня. Проблема первоначального проекта заключалась в том, что базовый смарт-контракт допускал только один ответ «да» или «нет». Это было сделано специально, чтобы сохранить простоту смарт-контракта, но в то же время представить другие концепции, необходимые для создания избирательного бюллетеня ТСЖ с использованием технологий Web3.

Целью данной публикации является более глубокое изучение смарт-контрактов для создания приложения, которое не только отражает реалистичные потребности и функции для голосования в ТСЖ, но и разрабатывает такое приложение, которое можно использовать повторно от одних выборов к другим.

Об умных контрактах

Прежде чем мы начнем, давайте дадим определение смарт-контракту:

«Умный контракт — это программа, которая запускается по адресу на Ethereum. Они состоят из данных и функций, которые могут выполняться при получении транзакции». Вот обзор того, из чего состоит смарт-контракт».
источник ethereum.org

Gumball Machine

Хотите верьте, хотите нет, но простой пример смарт-контракта можно найти в простом автомате по продаже жевательной резинки:

Люди легко понимают стоимость, связанную с покупкой с помощью автомата с жевательной резинкой. Обычно это четвертак (США). Здесь важно отметить, что потребитель является анонимным, так как автомат не требует знать, кем является человек, прежде чем выдать ему кусочек жевательной резинки.

Анонимный потребитель кладет валюту в автомат и поворачивает циферблат, чтобы принять условия договора. Этот шаг важен, потому что сделка прозрачна и является одноранговой: между вами и автоматом. Сделка также является безопасной, поскольку для использования автомата вы должны предоставить ожидаемую валюту.

Как только валюта попадает в автомат, условия контракта принимаются, и шарик скатывается в нижнюю часть автомата, позволяя клиенту получить свою покупку. В этот момент контракт полностью выполнен.

Клиент должен принять то, что ему предоставлено, то есть он не может вернуть жевательный шарик или изменить направление вращения циферблата, чтобы получить свою валюту обратно. Точно так же смарт-контракты обычно необратимы и неизменяемы.

Примеры использования смарт-контрактов

Помимо примеров, связанных с финансовой деятельностью, ниже приведены некоторые сценарии, в которых могут быть реализованы анонимные, доверительные, децентрализованные и прозрачные взаимодействия, которые являются необратимыми и неизменяемыми:

  • Клинические испытания — результаты независимых тестов
  • Выборы — голоса, поданные участниками
  • Идентификация — позволить людям определять, с кем они делятся своей личностью
  • Страховые полисы — индивидуальные полисы и условия& Отслеживание продукции и поставок — отслеживание состояния производства и поставок
  • Недвижимость и земля — документы, связанные с недвижимостью и землей, которые могут быть использованы для определения текущего владельца в любой момент времени
  • Recording Information — официальные записи и стенограммы (например, Геттисбергское обращение).

В каждом случае содержимое смарт-контракта можно вызывать и просматривать как можно чаще, не имея возможности изменить или модифицировать результаты. В каждом приведенном выше примере использования смарт-контракт выступает в качестве системы записи информации, лежащей в его основе.

Чем не является смарт-контракт

На данный момент смарт-контракты не являются юридически обязывающими соглашениями, за исключением некоторых исключений. Это означает, что если вы не удовлетворены результатом смарт-контракта, то обратиться к судье в какой-либо судебной системе не представляется возможным.

Есть несколько исключений, например, в штате Аризона, где смарт-контракты считаются юридически обязывающими. Кроме того, если вы находитесь в штате Калифорния и ваше разрешение на брак содержится в смарт-контракте, это соглашение также является юридически обязательным. Ожидается, что в будущем все больше правительств будут признавать смарт-контракты как юридически обязывающие соглашения.

Пример использования: Создание реалистичного бюллетеня для голосования в ТСЖ

Основываясь на простом бинарном (да/нет) смарт-контракте из публикации «Moving From Full-Stack Developer To Web3 Pioneer», давайте сделаем еще один шаг вперед и предположим, что существует следующее требование для голосования в ТСЖ для района, в котором есть одна должность, которую необходимо заполнить:

  • Выбрать председателя ТСЖ

В идеале, цель должна состоять в том, чтобы один смарт-контракт использовался каждый раз, когда проводятся выборы в ТСЖ. Ожидается, что кандидаты на должность председателя будут меняться от одних выборов к другим.

Итак, давайте начнем создавать смарт-контракт для наших нужд.

Определение нашего нового смарт-контракта

Используя Solidity, я работал с Полом Макэвини, который создал наш смарт-контракт для голосования в ТСЖ, как показано ниже:

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

}
Перейдите в полноэкранный режим Выход из полноэкранного режима

Вот некоторые ключевые моменты, связанные с дизайном смарт-контракта:

  • По умолчанию в бюллетене нет кандидатов.
  • Кандидаты могут быть добавлены (только владельцем смарт-контракта) с помощью функции addCandidate().
  • Аналогично, кандидаты могут быть удалены (только владельцем смарт-контракта) с помощью функции removeCandidate().
  • При голосовании используется функция getCandidates(), которая может быть использована в соответствующем Dapp для вызова функции addVoteByName().
  • Тот же метод getCandidates() может быть вызван для определения текущего количества голосов.
  • Контракт Ownable от OpenZeppelin позволяет владеть контрактом, а также передавать право собственности на другой адрес.

Теперь давайте подготовим смарт-контракт к использованию.

Подготовка к использованию смарт-контракта

Чтобы иметь возможность использовать наш смарт-контракт, мы создадим простой проект Truffle и развернем контракт в тестовой сети Ropsten. Для этого нам понадобится последняя версия Truffle. Установив NPM, выполните команду:

npm install -g truffle
Войти в полноэкранный режим Выйти из полноэкранного режима

Установка последней версии даст нам доступ к Truffle Dashboard, что сделает развертывание нашего смарт-контракта намного проще и значительно безопаснее, поскольку нам не придется делиться нашими приватными ключами кошелька или мнемоническими фразами. Впрочем, к этому мы вернемся чуть позже.

Далее создайте новую директорию и инициализируйте новый проект Truffle.

mkdir hoa-ballot-contract && cd hoa-ballot-contract
truffle init
Вход в полноэкранный режим Выйти из полноэкранного режима

Это создаст базовый проект смарт-контракта, который мы сможем наполнить по своему усмотрению. Итак, откройте проект в вашем любимом редакторе кода и приступайте к делу!

Для того чтобы использовать OpenZeppelin, в папке проекта необходимо выполнить следующую команду:

npm install @openzeppelin/contracts
Войти в полноэкранный режим Выйти из полноэкранного режима

Откройте файл truffle-config.js, и мы добавим Truffle Dashboard внутри объекта networks. За исключением всего закомментированного шаблона, наш объект теперь должен выглядеть следующим образом:

networks: {
  dashboard: {
    port: 24012,
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

На следующем этапе мы создадим новый файл смарт-контракта. В папке contracts создайте новый файл и назовите его HOABallot.sol. Отсюда мы просто вставим приведенный выше смарт-контракт.

Последнее, что нам нужно сделать, прежде чем мы сможем развернуть этот контракт, — это настроить сценарий развертывания. Используя приведенное ниже содержимое, нам нужно создать новый файл в папке migrations под названием 2_hoaballot_migration.js.

const HOABallot = artifacts.require("HOABallot");

Module.exports = function (deployer) {
  deployer.deploy(HOABallot);
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

Теперь мы готовы развернуть наш контракт в тестовой сети Ropsten. В новом окне терминала введите следующую команду, чтобы запустить приборную панель:

truffle dashboard
Войти в полноэкранный режим Выйти из полноэкранного режима

После запуска в нашем браузере должен появиться интерфейс, предлагающий подключить наш кошелек. Если он не появляется, перейдите по адресу localhost:24012.

Однократное нажатие на кнопку METAMASK запустит MetaMask через плагин для браузера. Если у вас не установлено расширение для браузера кошелька, вы можете получить его на сайте metamask.io. Выполните шаги по созданию учетной записи, а затем вернитесь в Truffle Dashboard для подключения:

После ввода действительного пароля и использования кнопки Unlock, панель Truffle Dashboard подтверждает сеть для использования:

После нажатия кнопки CONFIRM приборная панель Truffle теперь прослушивает запросы:

Для выполнения развертывания нам понадобится Ropsten Eth. Если у вас его нет, вы можете запросить его в этом кране.

Теперь нам остается только развернуть контракт. В исходном окне терминала убедитесь, что вы находитесь в папке проекта, и введите команду:

truffle migrate --network dashboard
Войти в полноэкранный режим Выйти из полноэкранного режима

Truffle автоматически скомпилирует наш смарт-контракт, а затем направит запрос через приборную панель. Каждый запрос будет следовать одному и тому же потоку, приведенному ниже.

Сначала приборная панель Truffle запрашивает подтверждение для обработки запроса:

После нажатия кнопки PROCESS плагин MetaMask также запросит подтверждение:

Кнопка подтверждения позволит снять средства с данного ассоциированного кошелька для обработки каждого запроса.

Когда процесс будет завершен, в окне терминала, используемого для выполнения команды truffle migrate, появится следующая информация:

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
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, используя значение адреса контракта, мы можем проверить смарт-контракт, используя следующий URL:

https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2

Теперь мы можем переключиться и начать создание Dapp.

Создание Dapp для голосования ТСЖ с помощью React

Я создам приложение React под названием hoa-ballot-client с помощью React CLI:

npx create-react-app hoa-ballot-client
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем я сменил директории во вновь созданной папке и выполнил следующие действия для установки зависимостей web3 и OpenZepplin в приложение React:

cd hoa-ballot-client
npm install web3
npm install @openzeppelin/contracts —save
Войти в полноэкранный режим Выход из полноэкранного режима

Основываясь на содержимом файла смарт-контракта HOABallot.sol, я перешел в папку build/contracts и открыл файл HOBallot.json, затем использовал значения свойства «abi» для константы hoaBallot файла abi.js, как показано ниже:

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
  }
];
Вход в полноэкранный режим Выход из полноэкранного режима

Этот файл был помещен во вновь созданную папку abi внутри папки src приложения React.

Теперь нам нужно обновить файл React Apps.js. Начнем с верхней части файла, которая должна быть настроена, как показано ниже:

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);
Войти в полноэкранный режим Выйти из полноэкранного режима

ContractAddress может быть найден несколькими способами. В данном случае я использовал результаты команды truffle — migrate CLI. Другой вариант — использовать сайт Etherscan.

Теперь осталось только создать стандартный код React для выполнения следующих действий:

  • Добавить кандидата в президенты ТСЖ
  • Удалить кандидата в президенты ТСЖ
  • Получить список кандидатов в президенты ТСЖ
  • Проголосовать за кандидата в президенты ТСЖ
  • Определить председателя ТСЖ

В своей публикации «Moving From Full-Stack Developer To Web3 Pioneer» я также добавил компонент Nav, чтобы адрес избирателя отображался для удобства.

Обновленное React-приложение теперь выглядит следующим образом:

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 }) => ({

    backgroundColor: theme.palette.common.black,
    color: theme.palette.common.white,
    fontSize: 14,
    fontWeight: 'bold'
  },

    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> &nbsp;
                                {(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;
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы запустить Dapp на основе React, можно использовать Yarn CLI:

yarn start
Войти в полноэкранный режим Выйти из полноэкранного режима

После компиляции и проверки приложение появится на экране, как показано ниже:

Во время видео:

  • Я подтвердил, что являюсь владельцем контракта, поскольку значение «Ваш подключенный адрес» полностью совпадает со значением «Этот бюллетень принадлежит» и отображается раздел «Действия владельца бюллетеня».
  • Как владелец контракта, я смог увидеть и использовать кнопку ADD NEW CANDIDATE, чтобы создать кандидатов для выборов. В данном примере я использовал имена Дэйв Браун и Стив Смит.
  • Как владелец контракта, я мог также использовать кнопку УДАЛИТЬ КАНДИДАТА.
  • После создания обоих кандидатов я проголосовал за одного из них, используя кнопку VOTE в той же строке, что и нужный кандидат. Я проголосовал за Дэйва Брауна.
  • Текущий победитель выборов отображается под таблицей кандидатов. В данном случае это Дэйв Браун.

После развертывания смарт-контракта любой желающий может просмотреть всю историю выборов по следующему URL:

https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2

Заключение

С 2021 года я стараюсь жить в соответствии со следующей миссией, которая, как мне кажется, может быть применима к любому специалисту в области технологий:

«Сосредоточьте свое время на предоставлении возможностей/функциональности, которые увеличивают ценность вашей интеллектуальной собственности. Для всего остального используйте фреймворки, продукты и сервисы».
J. Вестер

Интеллектуальные контракты позволяют двум сторонам заключить соглашение, результат которого становится официальной записью сделки. Принятие смарт-контракта соответствует моему личному заявлению о миссии в том, что лежащая в его основе структура позволяет не изобретать велосипед, когда возникает необходимость в таком контракте.

В то же время сама конструкция смарт-контракта идет на шаг дальше и соответствует моему заявлению о миссии с точки зрения возможности повторного использования. В этом примере можно использовать один и тот же смарт-контракт HOA, несмотря на разных кандидатов, участвующих в текущих выборах. Здесь мы используем возможности смарт-контракта, чтобы не создавать новый смарт-контракт каждый раз, когда проходят выборы.

При использовании Etherscan для поиска стоимости конвертации одной из транзакций с помощью конвертера ETH в USD от Google, стоимость одной транзакции составила 0,24 (USD) за 0,0001348975 ETH. По иронии судьбы, именно столько стоила скромная жевательная резинка из жевательного автомата, когда я был ребенком.

Если вы хотите узнать больше о смарт-контрактах, команда ConsenSys предоставила отличные ресурсы, которые помогут вам прототипировать ваши идеи, чтобы понять, является ли принятие смарт-контрактов обоснованным вариантом использования.

Если вас интересует исходный код этой статьи, вы можете найти его по следующим адресам:

https://github.com/paul-mcaviney/smart-contract-deep-dive/blob/main/HOABallot.sol

https://gitlab.com/johnjvester/hoa-ballot-contract

https://gitlab.com/johnjvester/hoa-ballot-client

Хорошего дня!

Оцените статью
devanswers.ru
Добавить комментарий