Въведение От разбирането на технологичния стек за разработка на Web3 DApp трябва да сте научили основния технологичен стек за разработка на web3 dApp, ролята на RPC в разработката на dApp и как да използвате dRPC за създаване на акаунт, генериране на API ключ, крайни точки, анализ на крайни точки , добавете средства към вашия dRPC акаунт и проверете баланса си. Ролята на dRPC при внедряването на интелигентни договори е да опрости процеса на настройка на възел на Ethereum, което улеснява разработчиците да взаимодействат и внедряват само с един ред код. В тази статия ще напишете, компилирате, тествате и внедрите интелигентен договор за плащане на кафе към Ethereum Sepolia Testnet, като използвате dRPC крайна точка и API ключ. Характеристиките включват: Плащане за кафе Преглед на цената на кафето Извличане на общ брой продадени кафета и обща сума на спечелените пари Хайде да си изцапаме ръцете. Предпоставки Вече финансирайте акаунта си със Sepolia Faucet. Имате портфейл, напр. Metamask. Редактор на кодове. Вече инсталирани всички Js библиотеки или рамки по ваш избор (напр. React.js, Next.js и т.н.). Буркан с вода. Необходими технологии и инструменти Солидност. React.js с помощта на Vite.js(Typescript) каска. Web3.js. Дотенв. dRPC API ключ и крайна точка. Частен ключ на вашия акаунт. MetaMask Писане на интелигентния договор за плащане на кафе Създайте папка във вашата основна директория и я наименувайте . contracts Създайте файл в папката и го наименувайте . contracts coffee.sol ще използвате солидността, за да напишете интелигентния договор. Файловете на Solidity са именувани с разширението , защото това е стандартното файлово разширение за изходния код на Solidity. .sol Добавете следния изходен код към : 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) : Указва, че кодът е написан за версии на Solidity между 0.8.0 (включително) и 0.9.0 (изключително). pragma 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() : Гарантира, че изпратеният Ether е достатъчен. InsufficientEtherSent(uint256 required, uint256 sent) : Предотвратява директни прехвърляния на Ether към договора. DirectEtherTransferNotAllowed() събития Събитието е част от договора, която съхранява аргументите, предадени в регистрационните файлове на транзакциите, когато бъдат излъчени. Събитията обикновено се използват за информиране на извикващото приложение за текущото състояние на договора, като се използва функцията за регистриране на 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 Проверете дали количеството е валидно. Изчислява общата цена. Гарантира изпращането на достатъчно етер. Актуализира променливите на състоянието. Излъчва събитието за покупка. Възстановява излишния етер. : Връща директните Ether трансфери, в случай че някой изпрати средства директно на адреса на договора. receive() external payable : Връща общия брой продадени кафета. 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), като гарантира, че вашият интелигентен договор работи според очакванията, преди да го внедрите в мрежата на живо. cjs. Във файла поставете този код по-долу: Под test папка създайте нов файл и го наречете Coffee. 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 към договора. Ето разбивка: Функция на Fixture: 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"); }); }); : Гарантира, че изпращането на Ether директно към договора (без извикване на функция) връща транзакцията. Връщане при директен трансфер на Ether Тестване на интелигентния договор След като напишете тестовия скрипт, ще : Когато изпълнявате своите договори и тестове в Hardhat Network, можете да отпечатвате съобщения за регистриране и променливи на договора, извиквайки 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 той автоматично компилира и тества интелигентния договор. Можете да го изпробвате и да ме уведомите в секцията за коментари. Внедряване на интелигентния договор Тук ще внедрите вашия интелигентен договор в Testnet ви позволява да тествате интелигентния си договор в среда, която имитира основната мрежа на Ethereum, без да поемате значителни разходи. Ако се справяте добре с функцията на dApp, можете да преразпределите към Ethereum Mainnet. Sepolia Testnet. Инсталирайте пакета 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. Този потребителски интерфейс ще се състои от: Поле за въвеждане на брой закупени кафета. Бутон, който задейства платежна транзакция и я удържа от сметката ви. Показване на общо закупено кафе и получената сума в етер и USD Цената на самото кафе както в етер, така и в щатски долари. справка Отвъд съобщенията по подразбиране: Овладяване на персонализирани грешки в Solidity Овладяване на персонализирани грешки в Solidity: Издигнете интелигентните си договори отвъд съобщенията по подразбиране Какво представляват функциите за солидност? Какво представляват събитията в Solidity? ← Предишна статия Следваща статия →