paint-brush
Cách xây dựng một Web3.0 DAO huy hoàng với React, Solidity và CometChattừ tác giả@daltonic
3,528 lượt đọc
3,528 lượt đọc

Cách xây dựng một Web3.0 DAO huy hoàng với React, Solidity và CometChat

từ tác giả Darlington Gospel
Darlington Gospel  HackerNoon profile picture

Darlington Gospel

@daltonic

Blockchain Developer | YouTuber | Author | Educator

2022/06/22
Read on Terminal Reader
Read this story in a terminal
Print this story
Read this story w/o Javascript
Read this story w/o Javascript

dài quá đọc không nổi

Darlington Gospel là một Dapp Mentor trợ giúp các nhà phát triển từ Web 2.0 đến Web 3.0.0. Trong hướng dẫn này, bạn sẽ học từng bước cách triển khai một tổ chức tự trị phi tập trung (DAO) với các tính năng trò chuyện ẩn danh. Trong hướng dẫn dưới đây, sao chép Dự án Web3 Starter bên dưới. Điều này sẽ đảm bảo rằng tất cả chúng ta đều ở trên cùng một trang và đang sử dụng các gói giống nhau. Mã trên thiết bị đầu cuối để xác nhận nó được cài đặt trên toàn cầu để cài đặt các gói thiết yếu này trên toàn cầu.

Companies Mentioned

Mention Thumbnail
Dapp
Mention Thumbnail
Initialized

Coins Mentioned

Mention Thumbnail
Ethereum
Mention Thumbnail
Stacks
featured image - Cách xây dựng một Web3.0 DAO huy hoàng với React, Solidity và CometChat
Darlington Gospel  HackerNoon profile picture
Darlington Gospel

Darlington Gospel

@daltonic

Blockchain Developer | YouTuber | Author | Educator

Xem bản demogit repo tại đây để biết những gì bạn sẽ xây dựng!


Dominion DAO Demo

Dominion DAO Demo


Giao diện trò chuyện

Giao diện trò chuyện

Giới thiệu

Tôi rất vui khi phát hành bản dựng web3.0 này cho bạn, tôi biết bạn đang tìm kiếm một ví dụ tuyệt vời để giúp bạn bắt đầu phát triển các ứng dụng phi tập trung.


Nếu bạn là người mới ở đây, tôi là Darlington Mừng, một Cố vấn của Dapp đang giúp các nhà phát triển chuyển đổi như bạn từ Web 2.0 sang Web 3.0.


Trong hướng dẫn này, bạn sẽ học từng bước cách triển khai một tổ chức tự trị phi tập trung (DAO) với các tính năng trò chuyện ẩn danh.


Nếu bạn thấy hứng thú với bản dựng này, hãy chuyển sang phần hướng dẫn…

Điều kiện tiên quyết

Bạn sẽ cần cài đặt các công cụ sau để phá vỡ thành công bản dựng này:

  • Nút
  • Ganache-Cli
  • Truffle
  • Phản ứng
  • Vô niệu
  • CSS Tailwind
  • SDK CometChat
  • Metamask
  • Sợi

Cài đặt phụ thuộc

Cài đặt NodeJs Đảm bảo rằng bạn đã cài đặt NodeJs trên máy tính của mình. Tiếp theo, chạy mã trên thiết bị đầu cuối để xác nhận rằng nó đã được cài đặt.


Đã cài đặt nút

Đã cài đặt nút

Cài đặt Yarn, Ganache-cli và Truffle Chạy các mã sau trên thiết bị đầu cuối của bạn để cài đặt các gói thiết yếu này trên toàn cầu.


 npm i -g yarn npm i -g truffle npm i -g ganache-cli


Sao chép dự án khởi động Web3 Sử dụng các lệnh bên dưới, sao chép dự án khởi động web 3.0 bên dưới. Điều này sẽ đảm bảo rằng tất cả chúng ta đều ở trên cùng một trang và đang sử dụng các gói giống nhau.


 git clone https://github.com/Daltonic/dominionDAO


Tuyệt vời, hãy để chúng tôi thay thế tệp package.json bằng tệp bên dưới:


 { "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" ] } }


Tuyệt vời, hãy thay thế tệp package.json của bạn bằng mã trên và sau đó chạy yarn install trên thiết bị đầu cuối của bạn.


Với tất cả những thứ đó đã được cài đặt, hãy bắt đầu với việc viết hợp đồng thông minh Dominion DAO.

Định cấu hình SDK CometChat

Để định cấu hình SDK CometChat , hãy làm theo các bước bên dưới, ở phần cuối, bạn cần lưu trữ các khóa này dưới dạng biến môi trường.


BƯỚC 1: Truy cập Bảng điều khiển CometChat và tạo tài khoản.


Đăng ký tài khoản CometChat mới nếu bạn chưa có

Đăng ký tài khoản CometChat mới nếu bạn chưa có


BƯỚC 2: Đăng nhập vào bảng điều khiển CometChat , chỉ sau khi đăng ký.


Đăng nhập vào Bảng điều khiển CometChat bằng tài khoản đã tạo của bạn

Đăng nhập vào Bảng điều khiển CometChat bằng tài khoản đã tạo của bạn


BƯỚC 3: Từ bảng điều khiển, thêm một ứng dụng mới có tên dominionDAO.


Tạo ứng dụng CometChat mới - Bước 1

Tạo ứng dụng CometChat mới - Bước 1

Tạo ứng dụng CometChat mới - Bước 2

Tạo ứng dụng CometChat mới - Bước 2


BƯỚC 4: Chọn ứng dụng bạn vừa tạo từ danh sách.


Chọn ứng dụng đã tạo của bạn

Chọn ứng dụng đã tạo của bạn


BƯỚC 5: Từ Bắt đầu nhanh, sao chép APP_ID , REGIONAUTH_KEY vào tệp .env của bạn. Xem hình ảnh và đoạn mã.


Sao chép APP_ID, REGION và AUTH_KEY

Sao chép APP_ID, REGION và AUTH_KEY


Thay thế các khóa chỗ REACT_COMET_CHAT bằng các giá trị thích hợp của chúng.


 REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Cấu hình ứng dụng Infuria

BƯỚC 1: Đi đến Infuria và tạo một tài khoản.


Đăng nhập vào tài khoản infuria của bạn

Đăng nhập vào tài khoản infuria của bạn


BƯỚC 2: Từ bảng điều khiển, tạo một dự án mới.


Tạo một dự án mới: bước 1

Tạo một dự án mới: bước 1

Tạo một dự án mới: bước 2

Tạo một dự án mới: bước 2


BƯỚC 3: Sao chép URL điểm cuối WebSocket của mạng thử nghiệm Rinkeby vào tệp .env của bạn.


Phím Rinkeby Testnet

Phím Rinkeby Testnet


Tiếp theo, thêm cụm từ bí mật Metamask và khóa cá nhân tài khoản ưa thích của bạn. Nếu bạn đã làm những điều đó một cách chính xác, các biến môi trường của bạn bây giờ sẽ trông như thế này.


 ENDPOINT_URL=*************************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************


Nếu bạn không biết cách truy cập khóa cá nhân của mình, hãy xem phần bên dưới.

Truy cập khóa cá nhân Metamask của bạn

BƯỚC 1: Nhấp vào tiện ích mở rộng trình duyệt Metamask của bạn và đảm bảo Rinkeby được chọn làm mạng thử nghiệm. Tiếp theo, trên tài khoản ưa thích, nhấp vào đường chấm dọc và chọn chi tiết tài khoản. Xem hình ảnh bên dưới.


Bước một

Bước một


BƯỚC 2: Nhập mật khẩu của bạn vào trường được cung cấp và nhấp vào nút xác nhận, thao tác này sẽ cho phép bạn truy cập vào khóa cá nhân tài khoản của mình.


Bước hai

Bước hai


BƯỚC 3: Nhấp vào "xuất khóa cá nhân" để xem khóa cá nhân của bạn. Đảm bảo rằng bạn không bao giờ để lộ khóa của mình trên một trang công khai như Github . Đó là lý do tại sao chúng tôi thêm nó như một biến môi trường.


Bước thứ ba

Bước thứ ba


BƯỚC 4: Sao chép khóa cá nhân của bạn vào tệp .env của bạn. Xem hình ảnh và đoạn mã bên dưới:


Bước bốn

Bước bốn


 ENDPOINT_URL=*************************** SECRET_KEY=****************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************


Đối với SECRET_KEY của bạn, bạn được yêu cầu dán cụm từ bí mật Metamask của mình vào khoảng trống được cung cấp trong tệp môi trường.

Hợp đồng thông minh Dominion DAO

Đây là mã đầy đủ cho hợp đồng thông minh, tôi sẽ giải thích tất cả các chức năng và biến lần lượt.


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


Trong dự án bạn vừa nhân bản, hãy truy cập thư mục src >> contract và tạo một tệp có tên là DominionDAO.sol , sau đó dán các mã trên vào bên trong nó.


Giải trình:


 // SPDX-License-Identifier: MIT pragma solidity ^0.8.7;


Solidity yêu cầu một mã định danh cấp phép để biên dịch mã của bạn, nếu không nó sẽ đưa ra cảnh báo yêu cầu bạn chỉ định một mã. Ngoài ra, Solidity yêu cầu bạn chỉ định phiên bản trình biên dịch cho hợp đồng thông minh của mình. Đó là những gì từ pragma đại diện.


 import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol";


Trong khối mã trên, chúng tôi đang sử dụng hai hợp đồng thông minh openzeppelin's để chỉ định vai trò và bảo vệ hợp đồng thông minh của chúng tôi trước các cuộc tấn công lần đầu.


 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;


Chúng tôi thiết lập một số biến trạng thái cho vai trò của các bên liên quan và cộng tác viên và chỉ định thời lượng bỏ phiếu tối thiểu là một tuần. Và chúng tôi cũng khởi tạo bộ đếm tổng đề xuất và một biến số để lưu giữ hồ sơ về số dư khả dụng của chúng tôi.


 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;


raisedProposals đề xuất được nâng lên theo dõi tất cả các đề xuất được gửi đến hợp đồng thông minh của chúng tôi. Các phiếu bầu như tên gọi của nó ngụ ý theo dõi các phiếu bầu của các stakeholderVotes liên quan. votedOn theo dõi tất cả các phiếu bầu được liên kết với một đề xuất. Trong khi những người đóng góp theo dõi bất kỳ ai đã đóng góp cho nền tảng của chúng tôi, thì mặt khác, các bên liên quan sẽ theo dõi những người đã đóng góp tới 1 ether .


 struct ProposalStruct { uint256 id; uint256 amount; uint256 duration; uint256 upvotes; uint256 downvotes; string title; string description; bool passed; bool paid; address payable beneficiary; address proposer; address executor; } struct VotedStruct { address voter; uint256 timestamp; bool choosen; }


proposalStruct xuấtStruct mô tả nội dung của mỗi đề xuất trong khi được votedStruct mô tả nội dung của mỗi phiếu bầu.


 event Action( address indexed initiator, bytes32 role, string message, address indexed beneficiary, uint256 amount );


Đây là một sự kiện động được gọi là Hành động. Điều này sẽ giúp chúng tôi làm phong phú thêm thông tin đăng xuất trên mỗi giao dịch.


 modifier stakeholderOnly(string memory message) { require(hasRole(STAKEHOLDER_ROLE, msg.sender), message); _; } modifier contributorOnly(string memory message) { require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message); _; }


Các công cụ sửa đổi ở trên giúp chúng tôi xác định người dùng theo vai trò và cũng ngăn họ truy cập một số tài nguyên trái phép.


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


Hàm trên lấy tiêu đề, mô tả, số tiền và địa chỉ ví của người thụ hưởng và tạo một đề xuất. Chức năng này chỉ cho phép các bên liên quan tạo đề xuất. Các bên liên quan là những người dùng đã đóng góp ít nhất 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 ); }


Hàm này chấp nhận hai đối số, một Id đề xuất và một lựa chọn ưu tiên được biểu thị bằng giá trị Boolean. Đúng nghĩa là bạn đã chấp nhận phiếu bầu và Sai thể hiện sự từ chối.


 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"); } }


Chức năng này thực hiện bỏ phiếu thực tế bao gồm kiểm tra xem người dùng có phải là một bên liên quan và đủ điều kiện để bỏ phiếu hay không.


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


Chức năng này chịu trách nhiệm thanh toán cho người thụ hưởng kèm theo một đề xuất dựa trên các tiêu chí nhất định.


  • Một, người thụ hưởng phải chưa được thanh toán.
  • Hai, thời hạn đề xuất phải hết hạn.
  • Ba, số dư khả dụng phải có khả năng thanh toán cho người thụ hưởng.
  • Bốn, không được có sự ràng buộc về số phiếu bầu.


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


Chức năng này chịu trách nhiệm thu thập các khoản đóng góp từ các nhà tài trợ và những người quan tâm đến việc trở thành các bên liên quan.


 function getProposals() external view returns (ProposalStruct[] memory props) { props = new ProposalStruct[](totalProposals); for (uint256 i = 0; i < totalProposals; i++) { props[i] = raisedProposals[i]; } }


Chức năng này truy xuất một loạt các đề xuất được ghi lại trên hợp đồng thông minh này.


 function getProposal(uint256 proposalId) external view returns (ProposalStruct memory) { return raisedProposals[proposalId]; }


Hàm này truy xuất một đề xuất cụ thể theo Id.


 function getVotesOf(uint256 proposalId) external view returns (VotedStruct[] memory) { return votedOn[proposalId]; }


Điều này trả về một danh sách các phiếu bầu được liên kết với một đề xuất cụ thể.


 function getStakeholderVotes() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256[] memory) { return stakeholderVotes[msg.sender]; }


Thao tác này trả về danh sách các bên liên quan trên hợp đồng thông minh và chỉ một bên liên quan mới có thể gọi chức năng này.


 function getStakeholderBalance() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256) { return stakeholders[msg.sender]; }


Điều này trả lại số tiền mà các bên liên quan đã đóng góp.


 function isStakeholder() external view returns (bool) { return stakeholders[msg.sender] > 0; }


Trả về Đúng hoặc Sai nếu người dùng là một bên liên quan.


 function getContributorBalance() external view contributorOnly("Denied: User is not a contributor") returns (uint256) { return contributors[msg.sender]; }


Điều này trả lại số dư của một người đóng góp và chỉ người đóng góp mới có thể truy cập được.


 function isContributor() external view returns (bool) { return contributors[msg.sender] > 0; }


Điều này kiểm tra xem người dùng có phải là người đóng góp hay không và nó được thể hiện bằng Đúng hay Sai.


 function getBalance() external view returns (uint256) { return contributors[msg.sender]; }


Trả về số dư của người dùng đang gọi bất kể vai trò của anh ta.


 function payTo( address to, uint256 amount ) internal returns (bool) { (bool success,) = payable(to).call{value: amount}(""); require(success, "Payment failed"); return true; }


Chức năng này thực hiện thanh toán với cả số tiền và tài khoản được chỉ định.

Định cấu hình Tập lệnh Triển khai

Một điều nữa cần làm với hợp đồng thông minh là định cấu hình tập lệnh triển khai.

Trên đầu dự án, hãy chuyển đến thư mục di chuyển , >> 2_deploy_contracts.js và cập nhật nó bằng đoạn mã bên dưới.


 const DominionDAO = artifacts.require('DominionDAO') module.exports = async function (deployer) { await deployer.deploy(DominionDAO) }


Tuyệt vời, chúng tôi vừa hoàn thành hợp đồng thông minh cho ứng dụng của mình, đã đến lúc bắt đầu xây dựng giao diện Dapp.

Phát triển giao diện người dùng

Giao diện người dùng bao gồm nhiều thành phần và bộ phận. Chúng tôi sẽ tạo tất cả các thành phần, khung nhìn và phần còn lại của các thiết bị ngoại vi.


Thành phần tiêu đề

Chế độ tối

Chế độ tối

Chế độ sáng

Chế độ sáng


Thành phần này nắm bắt thông tin về người dùng hiện tại và có nút chuyển đổi chủ đề cho các chế độ sáng và tối. Và nếu bạn tự hỏi làm thế nào tôi đã làm điều đó, đó là thông qua Tailwind CSS, hãy xem đoạn mã bên dưới.


 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


Thành phần biểu ngữ

Thành phần biểu ngữ

Thành phần biểu ngữ


Thành phần này chứa thông tin về trạng thái hiện tại của DAO, chẳng hạn như tổng số dư và số lượng đề xuất mở.


Thành phần này cũng bao gồm khả năng sử dụng hàm đóng góp để tạo đề xuất mới. Nhìn vào đoạn mã dưới đây.


 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


Thành phần đề xuất

Các thành phần đề xuất

Các thành phần đề xuất

Thành phần này chứa danh sách các đề xuất trong hợp đồng thông minh của chúng tôi. Ngoài ra, cho phép bạn lọc giữa các đề xuất đóng và mở. Khi đề xuất hết hạn, một nút thanh toán sẽ có sẵn để cung cấp cho bên liên quan tùy chọn thanh toán số tiền liên quan đến đề xuất. Xem mã bên dưới.


 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


Thành phần Chi tiết Đề xuất

Chi tiết Đề xuất

Chi tiết Đề xuất


Thành phần này hiển thị thông tin về đề xuất hiện tại, bao gồm cả chi phí. Thành phần này cho phép các bên liên quan chấp nhận hoặc từ chối một đề xuất.


Người đề xuất có thể thành lập một nhóm và người dùng nền tảng khác có thể tham gia vào trò chuyện ẩn danh kiểu web3.0.


Thành phần này cũng bao gồm một biểu đồ thanh cho phép bạn xem tỷ lệ người chấp nhận và người bị từ chối. Nhìn vào đoạn mã dưới đây.


 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


Thành phần cử tri

Thành phần cử tri

Thành phần cử tri


Thành phần này chỉ đơn giản là liệt kê các bên liên quan đã bỏ phiếu cho một đề xuất. Thành phần này cũng cung cấp cho người dùng cơ hội để lọc giữa người bị từ chối và người được chấp nhận. Xem mã bên dưới.


 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


Thành phần tin nhắn

Thành phần Tin nhắn

Thành phần Tin nhắn


Với sức mạnh của SDK CometChat kết hợp với thành phần này, người dùng có thể tham gia vào cuộc trò chuyện một-nhiều người một cách ẩn danh. Những người đóng góp và các bên liên quan có thể thảo luận thêm về một đề xuất trong quá trình ra quyết định của họ tại đây. Tất cả người dùng duy trì tình trạng ẩn danh của họ và được đại diện bởi Identicons của họ.


 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


Tạo thành phần đề xuất

Tạo thành phần đề xuất

Tạo thành phần đề xuất

Thành phần này chỉ cho phép bạn đưa ra đề xuất bằng cách cung cấp thông tin về các trường được thấy trong hình trên. Xem mã bên dưới.


 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


Thành phần xác thực

Thành phần xác thực trò chuyện

Thành phần xác thực trò chuyện


Thành phần này giúp bạn tham gia vào các tính năng trò chuyện. Bạn cần tạo một tài khoản hoặc đăng nhập nếu bạn đã đăng ký. Bằng cách đăng nhập, bạn có thể tham gia trò chuyện nhóm và nói chuyện ẩn danh với những người tham gia khác trong một đề xuất theo kiểu web3.0. Xem mã bên dưới.


 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


Tuyệt vời, hãy đảm bảo rằng các chế độ xem được thể hiện tốt…


Xem nhà

Xem nhà

Xem nhà


Chế độ xem này bao gồm các thành header , bannerproposals để cung cấp trải nghiệm người dùng DAO đặc biệt. Chúng tôi cũng đã sử dụng sức mạnh của Tailwind CSS để đạt được giao diện này. Nhìn vào đoạn mã dưới đây.


 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


Chế độ xem Đề xuất

Chế độ xem Đề xuất

Chế độ xem Đề xuất


Chế độ xem này kết hợp tiêu đề, chi tiết đề xuất và thành phần cử tri lại với nhau để hiển thị một bản trình bày mượt mà của một thành phần số ít. Xem mã bên dưới.


 import Header from '../components/Header' import ProposalDetails from '../components/ProposalDetails' import Voters from '../components/Voters' const Proposal = () => { return ( <> <Header /> <ProposalDetails /> <Voters /> </> ) } export default Proposal


Chế độ xem trò chuyện

Chế độ xem trò chuyện

Chế độ xem trò chuyện


Cuối cùng, chế độ xem trò chuyện kết hợp thành phần tiêu đề và tin nhắn để hiển thị giao diện trò chuyện chất lượng. Xem mã bên dưới.


 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


Tuyệt vời, đừng quên cập nhật tệp App.jsx .


Thành phần ứng dụng Thay thế thành phần ứng dụng bằng mã bên dưới.


 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


Trên thư mục src, >> dán các mã sau vào các tệp tương ứng của chúng.


Tệp 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') ) })


Tệp 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, }

Khởi động Môi trường Phát triển

BƯỚC 1: Quay một số tài khoản thử nghiệm với ganache-cli bằng lệnh dưới đây:


 ganache-cli -a


Điều này sẽ tạo ra một số tài khoản thử nghiệm với 100 ete giả được nạp vào mỗi tài khoản, tất nhiên, những tài khoản này chỉ dành cho mục đích thử nghiệm. Xem hình ảnh bên dưới:


Khóa riêng đã tạo

Khóa riêng đã tạo

BƯỚC 2: Thêm một mạng thử nghiệm cục bộ với Metamask như trong hình bên dưới.


Mạng máy chủ cục bộ

Mạng máy chủ cục bộ

BƯỚC 3: Nhấp vào biểu tượng tài khoản và chọn tài khoản nhập.


Bước một

Bước một

Sao chép khoảng năm trong số các khóa cá nhân và thêm chúng lần lượt vào mạng thử nghiệm cục bộ của bạn. Xem hình ảnh bên dưới.


Nhập Khóa riêng từ ganache cli

Nhập Khóa riêng từ ganache cli


Quan sát tài khoản mới được thêm vào mạng thử nghiệm cục bộ của bạn với 100 ETH được tải trước. Đảm bảo bạn thêm khoảng năm tài khoản để có thể thực hiện một bài kiểm tra tối đa. Xem hình ảnh bên dưới.


Thử nghiệm miễn phí Ethers

Thử nghiệm miễn phí Ethers

Triển khai Hợp đồng Thông minh

Bây giờ, hãy mở một thiết bị đầu cuối mới và chạy lệnh bên dưới.


 truffle migrate # or truffle migrate --network rinkeby


Lệnh trên sẽ triển khai hợp đồng thông minh của bạn tới mạng thử nghiệm rinkeby cục bộ của bạn hoặc Infuria.


Tiếp theo, mở một thiết bị đầu cuối khác và khởi động ứng dụng phản ứng với yarn start .

Sự kết luận

Hurray, chúng tôi vừa hoàn thành một hướng dẫn tuyệt vời để phát triển một tổ chức tự trị phi tập trung.


Nếu bạn thích hướng dẫn này và muốn có tôi làm cố vấn riêng cho bạn, vui lòng đăng ký lớp học của bạn với tôi .


Cho đến lần sau, tất cả tốt nhất.

Thông tin về các Tác giả

Mừng Darlington là một nhà phát triển blockchain đầy đủ với hơn 6+ năm kinh nghiệm trong ngành phát triển phần mềm.


Bằng cách kết hợp Phát triển phần mềm, viết và giảng dạy, anh ấy trình bày cách xây dựng các ứng dụng phi tập trung trên các mạng blockchain tương thích với EVM.


Các ngăn xếp của anh ấy bao gồm JavaScript , React , Vue , Angular , Node , React Native , NextJs , Solidity , v.v.


Để biết thêm thông tin về anh ấy, vui lòng truy cập và theo dõi trang của anh ấy trên Twitter , Github , LinkedIn hoặc trên trang web của anh ấy.

L O A D I N G
. . . comments & more!

About Author

Darlington Gospel  HackerNoon profile picture
Darlington Gospel @daltonic
Blockchain Developer | YouTuber | Author | Educator

chuyên mục

BÀI VIẾT NÀY CŨNG CÓ MẶT TẠI...

Permanent on Arweave
Read on Terminal Reader
Read this story in a terminal
 Terminal
Read this story w/o Javascript
Read this story w/o Javascript
 Lite

Mentioned in this story