Vamos a ensuciarnos las manos.
Cree una carpeta debajo de su directorio raíz y nómbrela contracts
.
Cree un archivo en la carpeta contracts
y nómbrelo coffee.sol
.
Usarás Solidity para escribir el contrato inteligente. Los archivos de Solidity se nombran con la extensión
.sol
porque es la extensión de archivo estándar para el código fuente de Solidity.
Agregue el siguiente código fuente al 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
: Este identificador de licencia indica que el código está licenciado bajo la Licencia .
pragma solidity >=0.8.0 <0.9.0;
: Especifica que el código está escrito para versiones de Solidity entre 0.8.0 (inclusive) y 0.9.0 (exclusiva). uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived;
coffeePrice
: se establece como un valor constante de 0.0002 ether
.totalCoffeesSold
: realiza un seguimiento del número de cafés vendidos.totalEtherReceived
: rastrea el total de Ether recibido por el contrato.Los errores personalizados en Solidity son mensajes de error que se adaptan a un caso de uso específico, en lugar de los mensajes de error predeterminados que proporciona el lenguaje de programación . Pueden ayudar a mejorar la experiencia del usuario y también pueden ayudar con la depuración y el mantenimiento de contratos inteligentes.
error
: Esta palabra clave se utiliza para definir un error personalizado error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: garantiza que la cantidad sea mayor que cero.InsufficientEtherSent(uint256 required, uint256 sent)
: garantiza que el Ether enviado sea suficiente.DirectEtherTransferNotAllowed()
: evita transferencias directas de Ether al contrato. event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost)
: Registra las compras de café.Las funciones son módulos de código autónomos que realizan una tarea específica. Eliminan la redundancia de tener que reescribir el mismo fragmento de código. En cambio, los desarrolladores pueden llamar a una función en el programa cuando sea necesario.
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
: Gestiona las compras de café y realiza las siguientes operaciones:receive() external payable
: revierte las transferencias directas de Ether en caso de que alguien envíe fondos directamente a la dirección del contrato.getTotalCoffeesSold() external view returns (uint256)
: Devuelve el total de cafés vendidos.getTotalEtherReceived() external view returns (uint256)
: Devuelve el Ether total recibido.
Instale Hardhat utilizando el siguiente símbolo del sistema.
npm install --save-dev hardhat
En el mismo directorio donde inicializas hardhat usando este símbolo del sistema:
npx hardhat init
Seleccione Create a Javascript project
utilizando el botón de flecha hacia abajo y presione Enter.
Presione enter para instalar en la carpeta raíz
Acepte todas las indicaciones utilizando la y
en su teclado, incluidas las dependencias @nomicfoundation/hardhat-toolbox
A continuación, verá esta respuesta que muestra que ha inicializado correctamente
Notarás que se agregaron algunas carpetas y archivos nuevos a tu proyecto, por ejemplo,
Lock.sol
,iginition/modules
,test/Lock.js
yhardhat.config.cjs
. No te preocupes por ellos.
Los únicos que son útiles son
iginition/modules
yhardhat.config.cjs
. Más adelante sabrás para qué se usan. Puedes eliminarLock.sol
de la carpetacontracts
yLock.js
de la carpetaiginition/modules
.
Compile el contrato utilizando el siguiente símbolo del sistema:
npx hardhat compile
Coffee.json
está el código ABI en formato JSON que llamarás cuando interactúes con el contrato inteligente. { "_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": {} }
En la carpeta test
, cree un nuevo archivo y nómbrelo Coffee.
. Dentro del archivo, pegue el código que aparece a continuación:
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: "", }); 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"); }); }); });
Este código prueba la funcionalidad del contrato inteligente Coffee. Incluye pruebas de implementación, compra de café y manejo de transferencias directas de Ether al contrato.
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: "", }); 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
y totalEtherReceived
estén configurados en cero después de la implementación. 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
y totalEtherReceived
se actualicen correctamente después de una compra. 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()
desde su código de Solidity. Para usarlo, debe importar hardhat/console.sol
en su código de contrato de la siguiente manera: //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "hardhat/console.sol"; contract Coffee { //... }
Para probar el contrato, ejecute el siguiente comando en su terminal:
npx hardhat test
Deberías tener un resultado como el que se muestra a continuación:
Si ejecuta
npx hardhat test
compilará y probará automáticamente el contrato inteligente. Puede probarlo y decírmelo en la sección de comentarios.
Instale el paquete dotenv y estas dependencias.
npm install dotenv npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
Esto agregará Web3.Js y Dotenv a su proyecto al incluirlo en la carpeta 'node_modules'.
Importarlos a su archivo 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", } };
Crea un archivo .env
en tu carpeta raíz.
Obtén la clave privada de tu cuenta desde tu billetera MetaMask y la clave API dRPC.
Guárdelos en su archivo .env
.
DRPC_API_KEY=your_drpc_api_key PRIVATE_KEY=your_wallet_private_key
Actualice el archivo hardhat.config.cjs
para incluir la configuración de 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: `//lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`, accounts: [`0x${PRIVATE_KEY}`], } } };
Cree un nuevo archivo de secuencia de comandos en la carpeta ignition/module
y nómbrelo deploy.cjs
. Agregue el siguiente código para implementar su contrato inteligente:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); const CoffeeModule = buildModule("CoffeeModule", (m) => { const coffee = m.contract("Coffee"); return { coffee }; }); module.exports = CoffeeModule;
Implemente el contrato inteligente ejecutando el siguiente comando en su terminal:
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
Después de ejecutar el símbolo del sistema, se le solicitará que Confirm deploy to network sepolia (11155111)? (y/n)
, escriba y
. Debería ver la dirección de su contrato inteligente implementado en la terminal luego de una implementación exitosa.
También puede acceder a la dirección del contrato en el archivo deployed_addresses.json
.
Felicitaciones, has implementado exitosamente tu contrato inteligente en Sepolia Testnet. 🎉