От разбирането на технологичния стек за разработка на Web3 DApp трябва да сте научили основния технологичен стек за разработка на web3 dApp, ролята на RPC в разработката на dApp и как да използвате dRPC за създаване на акаунт, генериране на API ключ, крайни точки, анализ на крайни точки , добавете средства към вашия dRPC акаунт и проверете баланса си.
Ролята на dRPC при внедряването на интелигентни договори е да опрости процеса на настройка на възел на Ethereum, което улеснява разработчиците да взаимодействат и внедряват само с един ред код.
В тази статия ще напишете, компилирате, тествате и внедрите интелигентен договор за плащане на кафе към Ethereum Sepolia Testnet, като използвате dRPC крайна точка и API ключ.
Характеристиките включват:
Хайде да си изцапаме ръцете.
Създайте папка във вашата основна директория и я наименувайте contracts
.
Създайте файл в папката contracts
и го наименувайте coffee.sol
.
ще използвате солидността, за да напишете интелигентния договор. Файловете на Solidity са именувани с разширението
.sol
, защото това е стандартното файлово разширение за изходния код на Solidity.
Добавете следния изходен код към 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
: Този идентификатор на лиценза показва, че кодът е лицензиран съгласно лиценза на Масачузетския технологичен институт (MIT) .
pragma solidity >=0.8.0 <0.9.0;
: Указва, че кодът е написан за версии на Solidity между 0.8.0 (включително) и 0.9.0 (изключително). uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived;
coffeePrice
: Задайте като постоянна стойност от 0.0002 ether
.totalCoffeesSold
: Проследява броя на продадените кафета.totalEtherReceived
: Проследява общия етер, получен от договора.Персонализираните грешки в Solidity са съобщения за грешка, които са пригодени за конкретен случай на употреба, а не съобщенията за грешка по подразбиране, които се предоставят от езика за програмиране . Те могат да помогнат за подобряване на потребителското изживяване и също могат да помогнат при отстраняване на грешки и поддържане на интелигентни договори.
За да дефинирате персонализирана грешка в Solidity, можете да използвате следния синтаксис:
error
: Тази ключова дума се използва за дефиниране на персонализирана грешка error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: Гарантира, че количеството е по-голямо от нула.InsufficientEtherSent(uint256 required, uint256 sent)
: Гарантира, че изпратеният Ether е достатъчен.DirectEtherTransferNotAllowed()
: Предотвратява директни прехвърляния на Ether към договора.Събитието е част от договора, която съхранява аргументите, предадени в регистрационните файлове на транзакциите, когато бъдат излъчени. Събитията обикновено се използват за информиране на извикващото приложение за текущото състояние на договора, като се използва функцията за регистриране на EVM. Те уведомяват приложенията за промени, направени в договорите, които след това могат да се използват за изпълнение на свързана логика.
event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost)
: Регистрира покупките на кафе.Функциите са самостоятелни модули от код, които изпълняват конкретна задача. Те елиминират излишъка от пренаписване на една и съща част от кода. Вместо това, разработчиците могат да извикат функция в програмата, когато е необходимо.
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
: Обработва покупките на кафе и извършва следните операции:receive() external payable
: Връща директните Ether трансфери, в случай че някой изпрати средства директно на адреса на договора.getTotalCoffeesSold() external view returns (uint256)
: Връща общия брой продадени кафета.getTotalEtherReceived() external view returns (uint256)
: Връща общия получен етер.Тук ще използвате Hardhat за компилиране на интелигентния договор.
Инсталирайте Hardhat, като използвате следния команден ред.
npm install --save-dev hardhat
Ще получите отговора по-долу след успешна инсталация.
В същата директория, където инициализирате hardhat с помощта на този команден ред:
npx hardhat init
Изберете Create a Javascript project
като използвате бутона със стрелка надолу и натиснете enter.
Натиснете Enter, за да инсталирате в основната папка
Приемете всички подкани, като използвате y
на клавиатурата, включително зависимостите @nomicfoundation/hardhat-toolbox
Виждате този отговор по-долу, показващ, че сте инициализирали успешно
Ще забележите, че някои нови папки и файлове са добавени към вашия проект. напр.
Lock.sol
,iginition/modules
,test/Lock.js
иhardhat.config.cjs
. Не се тревожете за тях.
Единственият полезен са
iginition/modules
иhardhat.config.cjs
. По-късно ще разберете за какво се използват. Чувствайте се свободни да изтриетеLock.sol
в папкатаcontracts
иLock.js
в папкатаiginition/modules
.
Компилирайте договора, като използвате следния команден ред:
npx hardhat compile
Coffee.json
е ABI кодът във формат JSON, който ще извикате, когато взаимодействате с интелигентния договор. { "_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": {} }
Писането на автоматизиран тестов скрипт, докато изграждате своя интелигентен договор, е от решаващо значение и силно се препоръчва. Той действа като двуфакторно удостоверяване (2FA), като гарантира, че вашият интелигентен договор работи според очакванията, преди да го внедрите в мрежата на живо.
Под test
папка създайте нов файл и го наречете Coffee.
cjs. Във файла поставете този код по-долу:
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"); }); }); });
Този код тества функционалността на интелигентния договор за кафе. Той включва тестове за внедряване, закупуване на кафе и обработка на директни преводи на Ether към договора.
Ето разбивка:
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
и totalEtherReceived
са зададени на нула след внедряването. 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
.totalCoffeesSold
и totalEtherReceived
се актуализират правилно след покупка. 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"); }); });
След като напишете тестовия скрипт, ще :
console.log()
от вашия код на Solidity. За да го използвате, трябва да импортирате hardhat/console.sol
в кода на вашия договор по следния начин: //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "hardhat/console.sol"; contract Coffee { //... }
За да тествате договора, изпълнете следната команда във вашия терминал:
npx hardhat test
Трябва да имате резултат като този по-долу:
Това показва, че вашият интелигентен договор функционира по начина, по който се очаква.
Ако стартирате
npx hardhat test
той автоматично компилира и тества интелигентния договор. Можете да го изпробвате и да ме уведомите в секцията за коментари.
Тук ще внедрите вашия интелигентен договор в Sepolia Testnet. Testnet ви позволява да тествате интелигентния си договор в среда, която имитира основната мрежа на Ethereum, без да поемате значителни разходи. Ако се справяте добре с функцията на dApp, можете да преразпределите към Ethereum Mainnet.
Инсталирайте пакета dotenv и тези зависимости.
npm install dotenv npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
Това ще добави Web3.Js и Dotenv към вашия проект, като го включи в папката 'node_modules'.
импортирайте ги във вашия файл hardhat.config.cjs
require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); module.exports = { solidity: "0.8.24", } };
Създайте .env
файл в основната папка.
Вземете частния ключ на вашия акаунт от вашия портфейл MetaMask и dRPC API ключ.
Съхранявайте ги във вашия .env
файл.
DRPC_API_KEY=your_drpc_api_key PRIVATE_KEY=your_wallet_private_key
Актуализирайте файла hardhat.config.cjs
за да включите конфигурацията на 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}`], } } };
Създайте нов скриптов файл в папката ignition/module
и го наречете deploy.cjs
. Добавете следния код, за да внедрите вашия интелигентен договор:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); const CoffeeModule = buildModule("CoffeeModule", (m) => { const coffee = m.contract("Coffee"); return { coffee }; }); module.exports = CoffeeModule;
Разположете интелигентния договор, като изпълните следната команда във вашия терминал:
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
След като изпълните командния ред, ще бъдете помолени да Confirm deploy to network sepolia (11155111)? (y/n)
, въведете y
. Трябва да видите адреса на вашия внедрен интелигентен договор в терминала при успешно внедряване.
Можете също да получите достъп до адреса на договора във файла deployed_addresses.json
.
Поздравления, успешно внедрихте своя интелигентен договор в Sepolia Testnet. 🎉
Тази статия ви научи как да пишете интелигентен договор за плащане, да тествате, компилирате и разгръщате интелигентен договор с помощта на hardhat CLI.
В следващата статия ще се научите да създавате предния край за това dApp. Този потребителски интерфейс ще се състои от:
Отвъд съобщенията по подразбиране: Овладяване на персонализирани грешки в Solidity
Какво представляват функциите за солидност?
Какво представляват събитията в Solidity?