Từ việc hiểu về công nghệ phát triển DApp Web3, bạn hẳn đã học được công nghệ cốt lõi để phát triển dApp web3, vai trò của RPC trong phát triển dApp và cách sử dụng dRPC để tạo tài khoản, tạo khóa API, điểm cuối, phân tích điểm cuối, thêm tiền vào Tài khoản dRPC và kiểm tra số dư của bạn.
Vai trò của dRPC trong việc triển khai hợp đồng thông minh là đơn giản hóa quy trình thiết lập một nút Ethereum, giúp các nhà phát triển dễ dàng tương tác và triển khai chỉ bằng một dòng mã.
Trong bài viết này, bạn sẽ viết, biên dịch, kiểm tra và triển khai hợp đồng thông minh thanh toán cà phê lên Ethereum Sepolia Testnet bằng điểm cuối dRPC và khóa API.
Các tính năng bao gồm:
Hãy cùng bắt tay vào làm nhé.
Tạo một Thư mục trong thư mục gốc của bạn và đặt tên là contracts
.
Tạo một File trong thư mục contracts
và đặt tên là coffee.sol
.
bạn sẽ sử dụng solidity để viết hợp đồng thông minh. Các tệp Solidity được đặt tên với phần mở rộng
.sol
vì đây là phần mở rộng tệp chuẩn cho mã nguồn Solidity.
Thêm mã nguồn sau vào coffee.sol
:
// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; contract Coffee { uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived; // Custom error definitions error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed(); // Event to log coffee purchases event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost); // Function to buy coffee function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } // Fallback function to handle Ether sent directly to the contract receive() external payable { revert DirectEtherTransferNotAllowed(); } // Public view functions to get totals function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; } }
//SPDX-License-Identifier: MIT
: Mã định danh giấy phép này cho biết mã được cấp phép theo Giấy phép của Viện Công nghệ Massachusetts (MIT) .
pragma solidity >=0.8.0 <0.9.0;
: Chỉ định rằng mã được viết cho các phiên bản Solidity từ 0.8.0 (bao gồm) đến 0.9.0 (không bao gồm). uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived;
coffeePrice
: Đặt giá trị hằng số là 0.0002 ether
.totalCoffeesSold
: Theo dõi số lượng cà phê đã bán.totalEtherReceived
: Theo dõi tổng số Ether nhận được theo hợp đồng.Lỗi tùy chỉnh trong Solidity là thông báo lỗi được điều chỉnh theo trường hợp sử dụng cụ thể, thay vì thông báo lỗi mặc định do ngôn ngữ lập trình cung cấp . Chúng có thể giúp cải thiện trải nghiệm của người dùng và cũng có thể giúp gỡ lỗi và duy trì hợp đồng thông minh.
Để xác định lỗi tùy chỉnh trong Solidity, bạn có thể sử dụng cú pháp sau:
error
: Từ khóa này được sử dụng để xác định lỗi tùy chỉnh error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: Đảm bảo số lượng lớn hơn không.InsufficientEtherSent(uint256 required, uint256 sent)
: Đảm bảo Ether được gửi là đủ.DirectEtherTransferNotAllowed()
: Ngăn chặn việc chuyển Ether trực tiếp vào hợp đồng.Sự kiện là một phần của hợp đồng lưu trữ các đối số được truyền trong nhật ký giao dịch khi được phát ra. Sự kiện thường được sử dụng để thông báo cho ứng dụng gọi về trạng thái hiện tại của hợp đồng bằng cách sử dụng tính năng ghi nhật ký của EVM. Chúng thông báo cho ứng dụng về những thay đổi được thực hiện đối với hợp đồng, sau đó có thể được sử dụng để chạy logic liên quan.
event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost)
: Ghi lại nhật ký mua cà phê.Hàm là các mô-đun mã độc lập thực hiện một nhiệm vụ cụ thể. Chúng loại bỏ sự dư thừa của việc viết lại cùng một đoạn mã. Thay vào đó, các nhà phát triển có thể gọi một hàm trong chương trình khi cần thiết.
function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } receive() external payable { revert DirectEtherTransferNotAllowed(); } function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; }
buyCoffee(uint256 quantity) external payable
: Xử lý việc mua cà phê và thực hiện các hoạt động sau:receive() external payable
: Hoàn lại các khoản chuyển Ether trực tiếp trong trường hợp có người gửi tiền trực tiếp đến địa chỉ hợp đồng.getTotalCoffeesSold() external view returns (uint256)
: Trả về tổng số cà phê đã bán.getTotalEtherReceived() external view returns (uint256)
: Trả về tổng số Ether đã nhận.Ở đây, bạn sẽ sử dụng Hardhat để biên dịch hợp đồng thông minh.
Cài đặt Hardhat bằng dấu nhắc lệnh sau.
npm install --save-dev hardhat
Bạn sẽ nhận được phản hồi bên dưới sau khi cài đặt thành công.
Trong cùng thư mục mà bạn khởi tạo hardhat bằng dấu nhắc lệnh này:
npx hardhat init
Chọn Create a Javascript project
bằng nút mũi tên xuống và nhấn Enter.
Nhấn enter để cài đặt vào thư mục gốc
Chấp nhận tất cả các lời nhắc bằng cách sử dụng phím y
trên bàn phím của bạn bao gồm cả các phụ thuộc @nomicfoundation/hardhat-toolbox
Bạn thấy phản hồi bên dưới cho thấy bạn đã khởi tạo thành công
Bạn sẽ thấy một số thư mục và tệp mới đã được thêm vào dự án của bạn. Ví dụ:
Lock.sol
,iginition/modules
,test/Lock.js
vàhardhat.config.cjs
. Đừng lo lắng về chúng.
Những cái hữu ích duy nhất là
iginition/modules
vàhardhat.config.cjs
. Bạn sẽ biết chúng được sử dụng để làm gì sau này. Hãy thoải mái xóaLock.sol
trong thư mụccontracts
vàLock.js
trong thư mụciginition/modules
.
Biên dịch hợp đồng bằng cách sử dụng dấu nhắc lệnh sau:
npx hardhat compile
Coffee.json
là mã ABI ở định dạng JSON mà bạn sẽ gọi khi tương tác với hợp đồng thông minh. { "_format": "hh-sol-artifact-1", "contractName": "Coffee", "sourceName": "contracts/coffee.sol", "abi": [ { "inputs": [], "name": "DirectEtherTransferNotAllowed", "type": "error" }, { "inputs": [ { "internalType": "uint256", "name": "required", "type": "uint256" }, { "internalType": "uint256", "name": "sent", "type": "uint256" } ], "name": "InsufficientEtherSent", "type": "error" }, { "inputs": [], "name": "QuantityMustBeGreaterThanZero", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "buyer", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "quantity", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "totalCost", "type": "uint256" } ], "name": "CoffeePurchased", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "quantity", "type": "uint256" } ], "name": "buyCoffee", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "coffeePrice", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "stateMutability": "payable", "type": "receive" } ], "bytecode": "", "deployedBytecode": "", "linkReferences": {}, "deployedLinkReferences": {} }
Viết một tập lệnh kiểm tra tự động trong khi xây dựng hợp đồng thông minh của bạn là rất quan trọng và được khuyến khích. Nó hoạt động như xác thực hai yếu tố (2FA), đảm bảo hợp đồng thông minh của bạn hoạt động như mong đợi trước khi triển khai nó vào mạng trực tiếp.
Trong thư mục test
, tạo một tệp mới và đặt tên là Coffee.
. Bên trong tệp, dán mã này bên dưới:
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers.js"); const { expect } = require("chai"); const pkg = require("hardhat"); const ABI = require('../artifacts/contracts/coffee.sol/Coffee.json'); const { web3 } = pkg; describe("Coffee Contract", function () { // Fixture to deploy the Coffee contract async function deployCoffeeFixture() { const coffeeContract = new web3.eth.Contract(ABI.abi); coffeeContract.handleRevert = true; const [deployer, buyer] = await web3.eth.getAccounts(); const rawContract = coffeeContract.deploy({ data: ABI.bytecode, }); // Estimate gas for the deployment const estimateGas = await rawContract.estimateGas({ from: deployer }); // Deploy the contract const coffee = await rawContract.send({ from: deployer, gas: estimateGas.toString(), gasPrice: "10000000000", }); console.log("Coffee contract deployed to: ", coffee.options.address); return { coffee, deployer, buyer, rawContract }; } describe("Deployment", function () { // Test to check initial values after deployment it("Should set the initial values correctly", async function () { const { coffee } = await loadFixture(deployCoffeeFixture); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal("0"); expect(totalEtherReceived).to.equal("0"); }); }); describe("Buying Coffee", function () { // Test to check coffee purchase and event emission it("Should purchase coffee and emit an event", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 3; const totalCost = web3.utils.toWei("0.0006", "ether"); // Buyer purchases coffee const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); // Check event const event = receipt.events.CoffeePurchased; expect(event).to.exist; expect(event.returnValues.buyer).to.equal(buyer); expect(event.returnValues.quantity).to.equal(String(quantity)); expect(event.returnValues.totalCost).to.equal(totalCost); }); // Test to check revert when quantity is zero it("Should revert if the quantity is zero", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") }) ).to.be.revertedWith("QuantityMustBeGreaterThanZero"); }); // Test to check if totalCoffeesSold and totalEtherReceived are updated correctly it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 5; const totalCost = web3.utils.toWei("0.001", "ether"); await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal(String(quantity)); expect(totalEtherReceived).to.equal(totalCost); }); }); describe("Fallback function", function () { // Test to check revert when ether is sent directly to the contract it("Should revert if ether is sent directly to the contract", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( web3.eth.sendTransaction({ from: buyer, to: coffee.options.address, value: web3.utils.toWei("0.001", "ether"), }) ).to.be.revertedWith("DirectEtherTransferNotAllowed"); }); }); });
Mã này kiểm tra chức năng của hợp đồng thông minh Coffee. Nó bao gồm các bài kiểm tra về triển khai, mua cà phê và xử lý chuyển Ether trực tiếp vào hợp đồng.
Sau đây là bảng phân tích:
deployCoffeeFixture
async function deployCoffeeFixture() { const coffeeContract = new web3.eth.Contract(ABI.abi); coffeeContract.handleRevert = true; const [deployer, buyer] = await web3.eth.getAccounts(); const rawContract = coffeeContract.deploy({ data: ABI.bytecode, }); const estimateGas = await rawContract.estimateGas({ from: deployer }); const coffee = await rawContract.send({ from: deployer, gas: estimateGas.toString(), gasPrice: "10000000000", }); console.log("Coffee contract deployed to: ", coffee.options.address); return { coffee, deployer, buyer, rawContract }; }
describe("Deployment", function () { it("Should set the initial values correctly", async function () { const { coffee } = await loadFixture(deployCoffeeFixture); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal("0"); expect(totalEtherReceived).to.equal("0"); }); });
totalCoffeesSold
và totalEtherReceived
được đặt thành 0 sau khi triển khai. describe("Buying Coffee", function () { it("Should purchase coffee and emit an event", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 3; const totalCost = web3.utils.toWei("0.0006", "ether"); const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const event = receipt.events.CoffeePurchased; expect(event).to.exist; expect(event.returnValues.buyer).to.equal(buyer); expect(event.returnValues.quantity).to.equal(String(quantity)); expect(event.returnValues.totalCost).to.equal(totalCost); }); it("Should revert if the quantity is zero", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") }) ).to.be.revertedWith("QuantityMustBeGreaterThanZero"); }); it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 5; const totalCost = web3.utils.toWei("0.001", "ether"); await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal(String(quantity)); expect(totalEtherReceived).to.equal(totalCost); }); });
CoffeePurchased
hay không.totalCoffeesSold
và totalEtherReceived
được cập nhật chính xác sau khi mua hàng. describe("Fallback function", function () { it("Should revert if ether is sent directly to the contract", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( web3.eth.sendTransaction({ from: buyer, to: coffee.options.address, value: web3.utils.toWei("0.001", "ether"), }) ).to.be.revertedWith("DirectEtherTransferNotAllowed"); }); });
Sau khi bạn đã viết xong kịch bản kiểm tra, bạn sẽ :
console.log()
từ mã Solidity của bạn. Để sử dụng nó, bạn phải nhập hardhat/console.sol
vào mã hợp đồng của bạn như thế này: //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "hardhat/console.sol"; contract Coffee { //... }
Để kiểm tra hợp đồng, hãy chạy lệnh sau trong terminal của bạn:
npx hardhat test
Bạn sẽ có kết quả đầu ra như bên dưới:
Điều này cho thấy hợp đồng thông minh của bạn hoạt động theo đúng mong đợi.
Nếu bạn chạy
npx hardhat test
nó sẽ tự động biên dịch và kiểm tra hợp đồng thông minh. Bạn có thể dùng thử và cho tôi biết trong phần bình luận.
Tại đây, bạn sẽ triển khai hợp đồng thông minh của mình lên Sepolia Testnet. Testnet cho phép bạn kiểm tra hợp đồng thông minh của mình trong môi trường mô phỏng mạng chính Ethereum mà không phải chịu chi phí đáng kể. Nếu bạn giỏi về chức năng của dApp, sau đó bạn có thể triển khai lại lên Ethereum Mainnet.
Cài đặt gói dotenv và các gói phụ thuộc này.
npm install dotenv npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
Thao tác này sẽ thêm Web3.Js và Dotenv vào dự án của bạn bằng cách đưa nó vào thư mục 'node_modules'.
nhập chúng vào tệp hardhat.config.cjs
của bạn
require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); module.exports = { solidity: "0.8.24", } };
Tạo một tệp .env
trong thư mục gốc của bạn.
Nhận khóa riêng tư của tài khoản từ ví MetaMask và khóa API dRPC.
Lưu trữ chúng trong tệp .env
của bạn.
DRPC_API_KEY=your_drpc_api_key PRIVATE_KEY=your_wallet_private_key
Cập nhật tệp hardhat.config.cjs
để bao gồm Cấu hình Sepolia Testnet:
require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); const dRPC_API_KEY = process.env.VITE_dRPC_API_KEY; const PRIVATE_KEY = process.env.VITE_PRIVATE_KEY; module.exports = { solidity: "0.8.24", networks: { sepolia: { url: `https://lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`, accounts: [`0x${PRIVATE_KEY}`], } } };
Tạo một tệp tập lệnh mới trong thư mục ignition/module
và đặt tên là deploy.cjs
. Thêm mã sau để triển khai hợp đồng thông minh của bạn:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); const CoffeeModule = buildModule("CoffeeModule", (m) => { const coffee = m.contract("Coffee"); return { coffee }; }); module.exports = CoffeeModule;
Triển khai hợp đồng thông minh bằng cách chạy lệnh sau trong thiết bị đầu cuối của bạn:
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
Sau khi chạy dấu nhắc lệnh, bạn sẽ được yêu cầu Confirm deploy to network sepolia (11155111)? (y/n)
, nhập y
. Bạn sẽ thấy địa chỉ hợp đồng thông minh đã triển khai của mình trong thiết bị đầu cuối sau khi triển khai thành công.
Bạn cũng có thể truy cập địa chỉ hợp đồng trong tệp deployed_addresses.json
.
Xin chúc mừng, bạn đã triển khai thành công hợp đồng thông minh của mình lên Sepolia Testnet. 🎉
Bài viết này hướng dẫn bạn cách viết hợp đồng thanh toán thông minh, kiểm tra, biên dịch và triển khai hợp đồng thông minh bằng cách sử dụng hardhat CLI.
Trong bài viết tiếp theo, bạn sẽ học cách xây dựng front-end cho dApp này. Giao diện người dùng này sẽ bao gồm:
Vượt ra ngoài các thông báo mặc định: Làm chủ các lỗi tùy chỉnh trong Solidity