WEB3DEV Español

Cover image for Escribe un Contrato Inteligente para un Ticket de Evento en Solidity
Hector
Hector

Posted on

Escribe un Contrato Inteligente para un Ticket de Evento en Solidity

Image description

Hoy, vamos a hablar sobre otro lenguaje: el lenguaje de Solidity. Antes de empezar a estudiar este lenguaje por primera vez tenía un poco de nervio pero cuando lo hice, empecé a divertirme.

Su sistema de array es un poco distinto que el de otros lenguajes. En el array de otros lenguajes (como PHP y JavaScript) puedes añadir muchos elementos en el array pero Solidity tiene algunas barreras.

No te preocupes, hoy no hablaremos sobre el Array. Haremos un Contrato para un Ticket de Evento. Hablemos sobre la historia del Ticket y luego trabajamos.

En este Contrato, cualquier usuario puede crear cuantos Tickets quiera. Cuando el usuario crea un Ticket, el dueño del Contrato obtiene una comisión. Supón que Jone crea un Ticket y si el Límite del Ticket es 15 y el precio de cada Ticket es de 2 ETH; si él vende 15 tickets entonces él dueño tendrá un ingreso de 15*2= 30 Eth. Pero aquí añadiremos alguna complejidad como cuando otro usuario compra este ticket y entonces el dueño del contrato obtendrá una comisión.

Nota: no estamos haciendo toda la app. Sólo estamos creando un contrato.

Este es el código Completo del Contrato. Hablemos sobre esta función, paso a paso:

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

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Ticket is ERC721URIStorage, Ownable {
   using Counters for Counters.Counter;
   Counters.Counter private tokenIdCounter;

   struct TicketInfo {
       uint256 tokenId;
       uint256 totalTickets;
       uint256 ticketsSold;
       uint256 ticketPrice;
       uint256 ticketStartDate;
       uint256 ticketEndDate;
       address creator;
       bool ticketSold;
   }

   struct PurchaseInfo {
       address buyer;
       uint256 ticketsBought;
       uint256 totalPrice;
       uint256 ticketId;
       uint256 purchaseId;
       uint256 purchaseTimestamp;
   }

   uint256 public creationFeePercentage;  // Porcentaje de la tasa por crear un ticket
   uint256 public purchaseFeePercentage;  // Porcentaje de la tasa por comprar un ticket

   mapping(uint256 => TicketInfo) public tickets;
   mapping(address => uint256[]) public userTickets;
   mapping(uint256 => PurchaseInfo[]) public ticketPurchases;  // Mapeo para almacenar la información de compra por cada ticket

   event TicketCreated(
       uint256 indexed tokenId,
       uint256 totalTickets,
       uint256 ticketPrice,
       uint256 ticketStartDate,
       uint256 ticketEndDate
   );

   event TicketPurchased(
       uint256 indexed tokenId,
       address buyer,
       uint256 ticketsBought
   );

   constructor(uint256 _creationFeePercentage, uint256 _purchaseFeePercentage) ERC721("Ticket", "TICKET") {
       creationFeePercentage = _creationFeePercentage;
       purchaseFeePercentage = _purchaseFeePercentage;
   }

   function createTicket(
       string calldata tokenURI,
       uint256 _totalTickets,
       uint256 _ticketPrice,
       uint256 _ticketEndDate
   ) external payable {
       require(_totalTickets > 0, "Total tickets must be greater than 0");
       require(_ticketPrice > 0, "Ticket price must be greater than 0");
       require(_ticketEndDate > block.timestamp, "Ticket end date must be in the future");

       uint256 currentID = tokenIdCounter.current();
       tokenIdCounter.increment();

       _safeMint(msg.sender, currentID);
       _setTokenURI(currentID, tokenURI);

       uint256 ticketStartDate = block.timestamp;

       tickets[currentID] = TicketInfo({
           tokenId: currentID,
           totalTickets: _totalTickets,
           ticketsSold: 0,
           ticketPrice: _ticketPrice,
           ticketStartDate: ticketStartDate,
           ticketEndDate: _ticketEndDate,
           creator: msg.sender,
           ticketSold: false
       });

       // Calcula la tasa de creación y lo transfiere al dueño del contrato
       uint256 creationFee = creationFeePercentage;
       require(msg.value == creationFee, "Incorrect creation fee sent");

       // Transfiere la tasa de la creación al contrato del dueño
       payable(owner()).transfer(creationFee);

       emit TicketCreated(currentID, _totalTickets, _ticketPrice, ticketStartDate, _ticketEndDate);
   }

   function purchaseTicket(uint256 tokenID, uint256 ticketsToBuy) external payable {
       TicketInfo storage ticket = tickets[tokenID];
       require(!ticket.ticketSold, "Ticket has already been sold");
       require(ticketsToBuy > 0 && ticketsToBuy <= ticket.totalTickets - ticket.ticketsSold, "Invalid number of tickets");

       uint256 totalPrice = ticket.ticketPrice * ticketsToBuy;
       uint256 purchaseFee = purchaseFeePercentage;
       uint256 totalPriceWithFee = totalPrice + purchaseFee;

       require(msg.value == totalPriceWithFee, "Incorrect amount sent");

       // Transfiere directamente el precio del ticket al creador del ticket
       payable(ticket.creator).transfer(totalPrice);

       // Transfiere la tasa de compra al dueño del contrato
       payable(owner()).transfer(purchaseFee);

       // Acuña tickets y los registros de las compras
       for (uint256 i = 0; i < ticketsToBuy; i++) {
           uint256 newTokenId = tokenIdCounter.current();
           tokenIdCounter.increment();
           _safeMint(msg.sender, newTokenId);
           _setTokenURI(newTokenId, tokenURI(tokenID));

           // Almacena el ticket comprado para el usuario
           userTickets[msg.sender].push(newTokenId);

           // Almacena la información de compra para el ticket
           ticketPurchases[newTokenId].push(PurchaseInfo({
               buyer: msg.sender,
               ticketsBought: 1,
               totalPrice: ticket.ticketPrice,
               ticketId:tokenID,
               purchaseId:newTokenId,
               purchaseTimestamp: block.timestamp
           }));

           ticket.ticketsSold++;

           emit TicketPurchased(newTokenId, msg.sender, ticketsToBuy);
       }

       // Marca el ticket como vendido cuando todos los tickets son vendidos 
       if (ticket.ticketsSold == ticket.totalTickets) {
           ticket.ticketSold = true;
       }
   }

   function getUserTickets(address user) external view returns (uint256[] memory) {
       return userTickets[user];
   }

   function getTicketInfo(uint256 tokenID) external view returns (TicketInfo memory) {
       return tickets[tokenID];
   }

   function getPurchaseInfo(uint256 tokenID) external view returns (PurchaseInfo[] memory) {
       return ticketPurchases[tokenID];
   }

   function updateCreationFeePercentage(uint256 _creationFeePercentage) external onlyOwner {
       creationFeePercentage = _creationFeePercentage;
   }

   function updatePurchaseFeePercentage(uint256 _purchaseFeePercentage) external onlyOwner {
       purchaseFeePercentage = _purchaseFeePercentage;
   }

   function getCreationFeePercentage() external view returns (uint256) {
       return creationFeePercentage;
   }

   function getPurchaseFeePercentage() external view returns (uint256) {
       return purchaseFeePercentage;
   }
}
Enter fullscreen mode Exit fullscreen mode

Paso 1:

Primero, importamos las Otras Clases de Contrato necesarias o útiles.

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
Enter fullscreen mode Exit fullscreen mode

Paso 2:

Luego declaramos Our Contract, nombramos su Ticket y usamos otros Contratos de importación.

Paso 3:

Luego definimos Counters y tokenIdCounter:

using Counters for Counters.Counter;
Counters.Counter private tokenIdCounter;
Enter fullscreen mode Exit fullscreen mode

Paso 4:

Declara el Struct u Object:

struct TicketInfo {
       uint256 tokenId;
       uint256 totalTickets;
       uint256 ticketsSold;
       uint256 ticketPrice;
       uint256 ticketStartDate;
       uint256 ticketEndDate;
       address creator;
       bool ticketSold;
   }

   struct PurchaseInfo {
       address buyer;
       uint256 ticketsBought;
       uint256 totalPrice;
       uint256 ticketId;
       uint256 purchaseId;
       uint256 purchaseTimestamp;
   }
Enter fullscreen mode Exit fullscreen mode

Paso 5:

Luego llamamos al mapeo para que lea este Struct u Object:

mapping(uint256 => TicketInfo) public tickets;
mapping(uint256 => PurchaseInfo[]) public ticketPurchases;
mapping(address => uint256[]) public userTickets;
Enter fullscreen mode Exit fullscreen mode

Aquí puedes ver que estamos usando 3 mapeos. Las primeras dos lecturas para el Struct de TicketInfo y PurchaseInfo y el tercer mapeo puede sea un poco difícil, como cuando el usuario compra este Ticket y luego empujamos la dirección de este usuario en este mapeo. En el futuro, podemos encontrar fácilmente esta compra del usuario y cuántos tickets ha comprado.

Paso 6:

Luego declaramos, crearemos la Tasa del Ticket y la Compra de la Tasa del Ticket:

uint256 public creationFeePercentage; 
uint256 public purchaseFeePercentage;
Enter fullscreen mode Exit fullscreen mode

Paso 7:

Luego declaramos el evento para el registro en la blockchain:

event TicketCreated(
       uint256 indexed tokenId,
       uint256 totalTickets,
       uint256 ticketPrice,
       uint256 ticketStartDate,
       uint256 ticketEndDate
   );

   event TicketPurchased(
       uint256 indexed tokenId,
       address buyer,
       uint256 ticketsBought
   );
Enter fullscreen mode Exit fullscreen mode

Paso 8:

Aquí aplicamos el método del constructor; Cuando el Contrato se despliega, este Contrato tiene dos parámetros:

constructor(uint256 _creationFeePercentage, uint256 _purchaseFeePercentage) ERC721("Ticket", "TICKET") {
       creationFeePercentage = _creationFeePercentage;
       purchaseFeePercentage = _purchaseFeePercentage;
}
Enter fullscreen mode Exit fullscreen mode

Paso 9:

Esta vez trabajaremos en el método createTicket:

function createTicket(
       string calldata tokenURI,
       uint256 _totalTickets,
       uint256 _ticketPrice,
       uint256 _ticketEndDate
   ) external payable {
       require(_totalTickets > 0, "Total tickets must be greater than 0");
       require(_ticketPrice > 0, "Ticket price must be greater than 0");
       require(_ticketEndDate > block.timestamp, "Ticket end date must be in the future");

       uint256 currentID = tokenIdCounter.current();
       tokenIdCounter.increment();

       _safeMint(msg.sender, currentID);
       _setTokenURI(currentID, tokenURI);

       uint256 ticketStartDate = block.timestamp;

       tickets[currentID] = TicketInfo({
           tokenId: currentID,
           totalTickets: _totalTickets,
           ticketsSold: 0,
           ticketPrice: _ticketPrice,
           ticketStartDate: ticketStartDate,
           ticketEndDate: _ticketEndDate,
           creator: msg.sender,
           ticketSold: false
       });

       // Calcula la tasa de la creación y lo transfiere el dueño del contrato
       uint256 creationFee = creationFeePercentage;
       require(msg.value == creationFee, "Incorrect creation fee sent");

       // Transfiere la tasa de la creación para el dueño del contrato
       payable(owner()).transfer(creationFee);

       emit TicketCreated(currentID, _totalTickets, _ticketPrice, ticketStartDate, _ticketEndDate);
   }
Enter fullscreen mode Exit fullscreen mode

Este método necesita tres parámetros:

  1. tokenURI
  2. _totalTickets
  3. _ticketPrice
  4. _ticketEndDate

Aquí, tokenURI es el url del link. Usualmente cuando usamos los metadatos como el nombre del ticket, la categoría, etc, y la imagen. Esa vez usamos IPFS y otros parámetros que ya sabemos.

En esta función, hacemos algunas condiciones antes de crear un Ticket:

require(_totalTickets > 0, "Total tickets must be greater than 0");
require(_ticketPrice > 0, "Ticket price must be greater than 0");
require(_ticketEndDate > block.timestamp, "Ticket end date must be in the future");
Enter fullscreen mode Exit fullscreen mode

Luego recolectamos el currentID e incrementamos este currentID. Luego de eso, haremos la acuñación así que usamos _safeMint y _setTokenURI y luego todos los datos pasan al mapeo de los tickets:

uint256 currentID = tokenIdCounter.current();
       tokenIdCounter.increment();

       _safeMint(msg.sender, currentID);
       _setTokenURI(currentID, tokenURI);

       uint256 ticketStartDate = block.timestamp;

       tickets[currentID] = TicketInfo({
           tokenId: currentID,
           totalTickets: _totalTickets,
           ticketsSold: 0,
           ticketPrice: _ticketPrice,
           ticketStartDate: ticketStartDate,
           ticketEndDate: _ticketEndDate,
           creator: msg.sender,
           ticketSold: false
});
Enter fullscreen mode Exit fullscreen mode

Luego enviamos una pequeña cantidad al dueño del contrato como un cargo. Al final, creamos un evento para el registro de la blockchain:

// Calcula la tasa de la creación y la transfiere al dueño del contrato
uint256 creationFee = creationFeePercentage;
require(msg.value == creationFee, "Incorrect creation fee sent");

// Transfiere la tasa de la creación al dueño del contrato
payable(owner()).transfer(creationFee);

emit TicketCreated(currentID, _totalTickets, _ticketPrice, ticketStartDate, _ticketEndDate);
Enter fullscreen mode Exit fullscreen mode

Paso 10:

Ahora estamos trabajando en el método purchaseTicket:

function purchaseTicket(uint256 tokenID, uint256 ticketsToBuy) external payable {
       TicketInfo storage ticket = tickets[tokenID];
       require(!ticket.ticketSold, "Ticket has already been sold");
       require(ticketsToBuy > 0 && ticketsToBuy <= ticket.totalTickets - ticket.ticketsSold, "Invalid number of tickets");

       uint256 totalPrice = ticket.ticketPrice * ticketsToBuy;
       uint256 purchaseFee = purchaseFeePercentage;
       uint256 totalPriceWithFee = totalPrice + purchaseFee;

       require(msg.value == totalPriceWithFee, "Incorrect amount sent");

       // Transfiere directamente el precio del ticket directamente al creador del ticket
       payable(ticket.creator).transfer(totalPrice);

       // Transfiere la tasa de la compra al dueño del contrato
       payable(owner()).transfer(purchaseFee);

       // Acuña tickets y los registros de la compra
       for (uint256 i = 0; i < ticketsToBuy; i++) {
           uint256 newTokenId = tokenIdCounter.current();
           tokenIdCounter.increment();
           _safeMint(msg.sender, newTokenId);
           _setTokenURI(newTokenId, tokenURI(tokenID));

           // Almacena los tickets comprados para el usuario
           userTickets[msg.sender].push(newTokenId);

           // Almacena la información Store the purchase information for the ticket
           ticketPurchases[newTokenId].push(PurchaseInfo({
               buyer: msg.sender,
               ticketsBought: 1,
               totalPrice: ticket.ticketPrice,
               ticketId:tokenID,
               purchaseId:newTokenId,
               purchaseTimestamp: block.timestamp
           }));

           ticket.ticketsSold++;

           emit TicketPurchased(newTokenId, msg.sender, ticketsToBuy);
       }

       // Maraca el ticket como vendido cuando todos los tickets son vendidos
       if (ticket.ticketsSold == ticket.totalTickets) {
           ticket.ticketSold = true;
       }
   }
Enter fullscreen mode Exit fullscreen mode

Este método acepta dos parámetros:

  1. tokenID

  2. ticketsToBuy (cuántos tickets)

Primero, encuentra la información y el almacenaje del ticket como TicketInfo. Luego, haremos algunas condiciones antes de comprar los Tickets.

TicketInfo storage ticket = tickets[tokenID];
require(!ticket.ticketSold, "Ticket has already been sold");
require(ticketsToBuy > 0 && ticketsToBuy <= ticket.totalTickets - ticket.ticketsSold, "Invalid number of tickets");
Enter fullscreen mode Exit fullscreen mode

Luego, haremos algunos cálculos y de nuevo, la condición:

uint256 totalPrice = ticket.ticketPrice * ticketsToBuy;
uint256 purchaseFee = purchaseFeePercentage;
uint256 totalPriceWithFee = totalPrice + purchaseFee;

require(msg.value == totalPriceWithFee, "Incorrect amount sent");
Enter fullscreen mode Exit fullscreen mode

Luego, el dueño del ticket y el dueño del contrato obtiene una tasa por comprar los tickets:

// Transfiere directamente el precio del ticket al creador del ticket
 payable(ticket.creator).transfer(totalPrice);

// Transfiere la compra de la tasa al dueño del contrato
payable(owner()).transfer(purchaseFee);
Enter fullscreen mode Exit fullscreen mode

Luego acuñamos este ticket de compra y el registro de todas las cosas necesarias:

// Acuña los tickets y los registros de compra
for (uint256 i = 0; i < ticketsToBuy; i++) {
   uint256 newTokenId = tokenIdCounter.current();
   tokenIdCounter.increment();
   _safeMint(msg.sender, newTokenId);
   _setTokenURI(newTokenId, tokenURI(tokenID));

   // Almacena los tickets comprados por el usuario
   userTickets[msg.sender].push(newTokenId);

   // Almacena la información de compra para el ticket
   ticketPurchases[newTokenId].push(PurchaseInfo({
       buyer: msg.sender,
       ticketsBought: 1,
       totalPrice: ticket.ticketPrice,
       ticketId:tokenID,
       purchaseId:newTokenId,
       purchaseTimestamp: block.timestamp
   }));

   ticket.ticketsSold++;

   emit TicketPurchased(newTokenId, msg.sender, ticketsToBuy);
}

// Marca el ticket como vendido con Mark the ticket as sold when all tickets are sold
if (ticket.ticketsSold == ticket.totalTickets) {
   ticket.ticketSold = true;
}
Enter fullscreen mode Exit fullscreen mode

Paso 11:

Haz una función getter

function getUserTickets(address user) external view returns (uint256[] memory) {
     return userTickets[user];
 }

function getTicketInfo(uint256 tokenID) external view returns (TicketInfo memory) {
   return tickets[tokenID];
}

function getPurchaseInfo(uint256 tokenID) external view returns (PurchaseInfo[] memory) {
   return ticketPurchases[tokenID];
}
function getCreationFeePercentage() external view returns (uint256) {
     return creationFeePercentage;
 }

function getPurchaseFeePercentage() external view returns (uint256) {
   return purchaseFeePercentage;
}
Enter fullscreen mode Exit fullscreen mode

Paso 12:

Ahora hemos hecho algunas funciones para futuras actualizaciones de cargos.

function getCreationFeePercentage() external view returns (uint256) {
     return creationFeePercentage;
 }

function getPurchaseFeePercentage() external view returns (uint256) {
   return purchaseFeePercentage;
}
Enter fullscreen mode Exit fullscreen mode

Felicidades, hemos terminado nuestro simple contrato. Ahora haremos algunas pruebas. Para probar, usaremos el editor online de remix. Aquí está el link.

Paso 13

Probando

Image description

En esta imagen, el rectángulo rojo pequeño, a la izquierda, es el botón de despliegue. Cuando haces click en ese botón ves esto. Ahora haz click en el gran rectángulo rojo dentro del botón del puntero.

Image description

Luego ponemos algunos cambios. Aquí puse 1000000000000000000 Wei.

Nota: 1000000000000000000 Wei = 1 Eth.

Luego haz click en el botón de la flecha de nuevo. Ahora podemos ver el botón Deploy. Haz click en este botón para desplegar el contrato.

Image description

Cuando despliegas el contrato, vemos algunos métodos para probar como:

Image description

Ahora cambiamos el usuario

Image description

Aquí vemos 15 usuarios para probar el contrato. El primer usuario es el dueño del contrato. Cuando el contrato es desplegado, esas veces el sistema obtiene gas. Por eso el primer balance del usuario no es de 100 ether sino de otros usuarios que tienen 100 ether. Ahora cambiamos al segundo usuario, quien crea un ticket para su evento.

Image description

Aquí ves el botón createTicket (la segunda caja roja). Este botón, en el lado derecho, puedes ver el botón de la fecha. Haz click en este botón y luego verás el campo de entrada. Llena este campo y luego llena el valor del campo (la primera caja roja) y cambia el tipo de la divisa. El valor de este campo es un campo de cargo. Cuando el usuario crea un ticket, el dueño del contrato obtiene una comisión. Ahora haz click en el botón createTicket.

Image description

Image description

Hemos creado exitosamente un ticket. Nuestro primer ticket, tokenId es 0.

Image description

Ya te has dado cuenta que el balance del segundo usuario se disminuye por crear tickets y el primer balance del usuario se incrementa por obtener una comisión.

Ahora vamos a cambiar el usuario. Aquí, el tercer usuario está comprando un ticket. Hagámoslo:

Image description

Aquí la tercera caja es el bóton del método purchaseTicket. Con este botón en el lado derecho ves una flecha, haz click en esta flecha y luego puedes ver dos campos de entrada. Llena este campo de entrada. Aquí comprados 2 Tickets y el tokenID es 0.

En la segunda caja, establecemos un valor de 3 porque compramos dos tickets y cada precio del ticket es de 1eth y la comisión es de 1 eth, así que el precio total es de 3 eth. Ahora haz click en el botón purchaseTicket.

Image description

Image description

Ahora, podemos ver la información de la compra del ticket:

Image description

Aquí, el primer usuario es el dueño del contrato, el segundo usuario es el dueño del ticket y el tercer usuario está comprando un ticket. Cuando un ticket es creado o vendido el primer usuario obtiene una comisión. El segundo usuario es el dueño del Evento, cuando un ticket se vende, el segundo usuario obtiene el precio de venta.

¡Ahora es tiempo para desplegarlo en la red en vivo de Ethereum o en la red de Pruebas y compartirlo con el mundo!

¡Feliz Codeo!🚀

Este artículo es una traducción de syed kamruzzaman, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)