「
私の最初の記事の最終結果は、住宅所有者協会 (HOA) が Web3 テクノロジーを使用して選挙投票用紙をホストする方法を示しました。元の設計の問題は、基礎となるスマート コントラクトが 1 つの「はい」または「いいえ」の回答しか許可されていなかったことです。これは、Web3 テクノロジを使用して HOA 投票を作成するために必要な他の概念を導入しながら、スマート コントラクトをシンプルに保つための設計によるものです。
この出版物の目的は、スマート コントラクトを深く掘り下げて、HOA 投票の現実的なニーズと機能を捉えるだけでなく、選挙ごとに再利用できるものを設計するアプリケーションを構築することです。
始める前に、スマート コントラクトを定義しましょう。
「スマートコントラクトは、イーサリアム上のアドレスで実行されるプログラムです。それらは、トランザクションの受信時に実行できるデータと機能で構成されています。これがスマートコントラクトを構成するものの概要です。」
ソース
ethereum.org
信じられないかもしれませんが、スマート コントラクトの簡単な例として、単純なガムボール マシンがあります。
ガムボール マシンの購入にかかるコストを簡単に理解できます。通常、これは (米国) 四半期です。ここで重要なことは、顧客が匿名であることです。なぜなら、ガムボール マシンは、香ばしいガムを顧客に渡す前に、顧客が誰であるかを知る必要がないからです。
匿名の消費者は、通貨をガムボール マシンに入れ、ダイヤルを回して契約条件を受け入れます。トランザクションは透過的でピアツーピアであるため、この手順は重要です。つまり、ユーザーとマシンの間です。ガムボール マシンを使用するには、予想される通貨を提供する必要があるため、トランザクションも保護されます。
通貨がガムボール マシン内に落ちると、契約条件が受け入れられ、ガムボールがマシンの底に向かって転がり、顧客は購入品を受け取ることができます。この時点で、契約は完全に実行されます。
つまり、ガムボールを返却したり、ダイヤルを逆にして通貨を取り戻すことはできません。同様に、スマート コントラクトは通常、元に戻すことも変更することもできません。
金融主導の例は別として、元に戻すことも変更することもできない、匿名、トラストレス、分散型、透明性のあるやり取りを実装できるいくつかのシナリオを以下に示します。
いずれの場合も、スマート コントラクトの内容は、結果を変更または修正することなく、できるだけ頻繁に呼び出して確認できます。上記の各ユース ケースは、基になる情報の記録システムとしてスマート コントラクトを提供します。
現時点では、いくつかの例外を除いて、スマート コントラクトは法的拘束力のある契約ではありません。これは、スマート コントラクトの結果に満足できない場合、法廷システムの裁判官に問題を提起することは不可能であることを意味します。
スマート コントラクトが法的拘束力があると見なされるアリゾナ州のように、いくつかの例外があります。さらに、あなたがカリフォルニア州にいて、マリッジ ライセンスがスマート コントラクトに含まれている場合、そのコントラクトにも法的拘束力があります。将来的には、より多くの政府がスマート コントラクトを法的拘束力のある契約として認識するようになると予想されます。
「Moving From Full-Stack Developer To Web3 Pioneer」出版物からの単純なバイナリ (はい/いいえ) スマート コントラクトに基づいて、一歩前進し、単一のコミュニティの HOA 投票に次の要件が存在すると仮定します。埋める位置:
理想的には、HOA の選択が行われるたびに単一のスマート コントラクトが使用されるようにすることが目標です。大統領職に立候補する者は、選挙ごとに変わることが予想されます。
それでは、ニーズを処理するスマート コントラクトの作成を始めましょう。
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; } }
スマート コントラクトの設計に関連するいくつかの重要な項目を次に示します。
それでは、スマート コントラクトを使用する準備をしましょう。
スマート コントラクトを使用できるようにするために、単純な Truffle プロジェクトを構築し、コントラクトを Ropsten テストネットにデプロイします。これを行うには、まず最新バージョンの Truffle が必要です。と
npm install -g truffle
最新バージョンをインストールすると、
次に、新しいディレクトリを作成し、新しい Truffle プロジェクトを初期化します。
mkdir hoa-ballot-contract && cd hoa-ballot-contract truffle init
これにより、必要に応じて入力できるベアボーン スマート コントラクト プロジェクトが作成されます。お気に入りのコード エディターでプロジェクトを開き、プロジェクトに取り掛かりましょう。
OpenZeppelin を利用するには、プロジェクト フォルダーでも次のコマンドを実行する必要があります。
npm install @openzeppelin/contracts
truffle-config.jsファイルを開き、 networks
オブジェクト内に Truffle ダッシュボードを追加します。コメントアウトされたすべてのボイラープレートを除けば、オブジェクトは次のようになります。
networks: { dashboard: { port: 24012, } }
次のステップでは、新しいスマート コントラクト ファイルを作成します。契約フォルダー内で、新しいファイルを作成し、 HOABallot.solという名前を付けます。ここからは、上記のスマート コントラクトを貼り付けます。
このコントラクトをデプロイする前に最後に行う必要があるのは、デプロイ スクリプトのセットアップです。以下の内容を使用して、移行フォルダーに2_hoaballot_migration.jsという新しいファイルを作成する必要があります。
const HOABallot = artifacts.require("HOABallot"); Module.exports = function (deployer) { deployer.deploy(HOABallot); }
これで、コントラクトを Ropsten テストネットにデプロイする準備が整いました。新しいターミナル ウィンドウで、次のコマンドを入力してダッシュボードを開始します。
truffle dashboard
実行すると、ブラウザがポップアップし、ウォレットを接続するように求めるインターフェイスが表示されます。これが表示されない場合は、 localhost:24012
に移動します。
METAMASKボタンを 1 回クリックすると、ブラウザ プラグインを介して MetaMask が起動します。ウォレット ブラウザ拡張機能がインストールされていない場合は、次の URL で入手できます。
有効なパスワードを入力して [ロック解除] ボタンを使用すると、Truffle ダッシュボードは使用するネットワークを確認します。
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 を使用してスマート コントラクトを検証できます。
これで切り替えて、Dapp の構築を開始できます。
React CLI を使用して、 hoa-ballot-client
という React アプリケーションを作成します。
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 フォルダーに移動し、 abi.js
ファイルを開き、 HOBallot.json
ファイルのhoaBallot
定数の "abi" プロパティの値を次のように使用しました。下に示された:
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 } ];
このファイルは、React アプリケーションの src フォルダー内に新しく作成された abi フォルダーに配置されました。
ここで、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 コードを作成するだけです。
私の出版物「フルスタック開発者から Web3 パイオニアへの移行」では、投票者のアドレスが簡単に参照できるように 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 }) => ({ [`&.${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;
React ベースの Dapp を開始するには、Yarn CLI を使用できます。
yarn start
コンパイルと検証が完了すると、次のようにアプリケーションが画面に表示されます。
ビデオ中:
スマート コントラクトをデプロイして以来、誰でも次の URL で完全な履歴を表示できます。
https://ropsten.etherscan.io/address/0x2981d347e288E2A4040a3C17c7e5985422e3cAf2
2021 年以来、私は次のミッション ステートメントに従って生きようとしてきました。これは、あらゆるテクノロジー プロフェッショナルに当てはまると思います。
「知的財産の価値を拡張する機能を提供することに時間を集中してください。他のすべてのフレームワーク、製品、およびサービスを活用してください。」
J.ベスター
スマート コントラクトは、2 つの当事者が契約の結果がトランザクションの確定的な公式記録となる契約を締結できるようにする機能を提供します。スマート コントラクトの採用は、そのようなコントラクトの必要性が生じたときに、基礎となるフレームワークが車輪の再発明を回避するという点で、私の個人的なミッション ステートメントに準拠しています。
同時に、スマート コントラクトの設計自体はさらに一歩進んでおり、再利用性という点で私のミッション ステートメントを満たしています。この例では、現在の選挙で異なる候補者が出馬しているにもかかわらず、同じ HOA スマート コントラクトを使用できます。ここでは、スマート コントラクトの力を利用して、選挙のたびに新しいスマート コントラクトを作成することを回避します。
Etherscan を使用して、Google の ETH から USD へのコンバーターを使用してトランザクションの 1 つのコンバージョン値を検索すると、トランザクションあたりのコストは 0.0001348975 ETH で 0.24 (USD) でした。皮肉なことに、それは私が子供の頃、ガムボール マシンから出てきた控えめなガムボールのコストでした。
スマート コントラクトについて詳しく知りたい場合は、 ConsenSysのチームが優れたリソースを提供しており、アイデアのプロトタイプを作成して、スマート コントラクトの採用が有効なユース ケースであるかどうかを確認するのに役立ちます。
この記事のソース コードに興味がある場合は、次の URL で見つけることができます。
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
本当に素晴らしい一日を!