Hola etherians!

En esta oportunidad veremos como crear un contrato inteligente muy sencillo para la venta de entradas para un evento cualquiera, pero antes repasemos un poco. En el artículo sobre como programar un Chat / Libro de visitas descentralizado en la Blockchain de Ethereum aprendimos que:

  1. La variable msg.sender toma el valor del emisor de la transacción
  2. Si una función lleva el mismo nombre del contrato, entonces se ejecuta sólo cuando éste se crea.
  3. Las funciones y variables pueden ser privadas o públicas, dependiendo de la sensibilidad de la información.
  4. Qué es una estructura y las distintas formas de utilizarlas.
  5. Qué es el block.timestamp (now) en Solidity.
  6. Cómo compilar y publicar un contrato inteligente en la red de pruebas Rinkeby.
  7. Cómo invocar un contrato inteligente con sus variables y funciones (públicas) desde un navegador mediante Javascript y Metamask.

Nota importante: En este código utilizaremos msg.value, que es una variable tipo entero sin signo/unsigned integer (uint) que toma como valor el monto enviado a través de la transacción (pero en WEI)
Ahora, utilizando las mismas herramientas, crearemos un Contrato Inteligente que nos servirá para vender de entradas para un evento cualquiera.

Necesitaremos…

  1. El plugin Metamask.
  2. IDE online Solidity Remix – Solidity IDE.
  3. ETHER en la red Rinkeby de Ethereum.
  4. Entusiasmo y ganas de aprender 😀

¡A programar!

Antes de tocar una línea de código, explicaré como lo haremos en esta ocasión, ya que en el pasado artículo se publicó el código del contrato y luego se explicó parte por parte, ahora creo que sería bueno hacerlo a la inversa. Vamos a ir armando el código según las cosas que vayamos necesitando hacer.

Trabajaremos desde la siguiente plantilla:

¿Qué información necesita guardar el Contrato Inteligente?

Como este ejemplo se trata de crear un Contrato Inteligente que nos sirva para vender entradas para un evento recibiendo ETHER como medio de pago, a mi juicio las variables que necesitaríamos son las siguientes:

  1. creadorEvento (address): Será quién reciba el dinero en ETHER por la compra de las entradas.
  2. valorEntrada (uint): Como bien lo describe su nombre, esta variable guardará el valor de la entrada. El monto debe estar definido en WEI a no ser que se especifique una unidad diferente como Finney o Szabo. El conversor de unidades Ethereum nos ayudará bastante para esto.
  3. numeroEntradas (uint): Esta variable indicará cuantas entradas hay a la venta.
  4. contadorEntradas (uint): Nos ayudará a identificar cada entrada con un ID único.
  5. nombreEvento (string || bytes32): No es una variable necesaria, pero si se quisiera tener toda la información sobre el evento en un contrato, almacenar el nombre del evento es una buena idea.
  6. estadoVenta (bool): Con esta variable el administrador del evento podría determinar si la venta de entradas sigue activa o no. Por defecto la dejaremos en true, pero crearemos una función que permita cambiar este estado fácilmente 😉

También necesitaremos crear una estructura para guardar la información de cada entrada. Por ejemplo para poder identificar al dueño de cada entrada y saber cuantas entradas compró, utilizaría una estructura tan sencilla como esta

Esto ya nos quiere decir que para utilizar la estructura Entrada tendremos que recurrir nuevamente un mapping() para crear un diccionario de estructuras, cuyos índices sean única y exclusivamente números enteros sin signo/unsigned integer (uint) (contadorMensajes) y que cada índice apunte a una estructura Entrada que contiene la información del comprador de la entrada.

Con este mapping ya podríamos utilizar la variable entradas como un diccionario de estructuras, con el que podríamos añadir entradas manualmente haciendo

Con esto tendríamos 2 entradas con distintos ID ( 1 y 2 ):

  1. Entrada #1 – Rut: 12.345.678-9 (entradas[1].rut) | Numero de entradas: 2 (entradas[1].entradasCompradas) | Cuenta: 0xc8612033ccc8c1616bf66f88d31a7af0fff61a83 (entradas[1].addressComprador)
  2. Entrada #2 – Rut: 8.765.432-1 (entradas[2].rut) | Numero de entradas: 1 (entradas[2].entradasCompradas) | Cuenta: 0x785c5cac1a72255f41821eca69dc77138a54d230 (entradas[2].addressComprador)

Funcionaría para añadir solo 2 entradas, pero quedémonos con la idea de como asignamos los valores… Hasta el momento nuestro código debería lucir exactamente así:

Con este código ya tenemos prácticamente la mitad del trabajo hecho, ahora solo nos queda pensar en cuales son las funciones que necesitamos para registrar la compra de cada entrada y cuales son las condiciones que se deben cumplir.

Funciones y Modificador

A simple vista podemos observar que apenas necesitaremos 4 funciones: Comprar entradas, Consultarlas, Activar/Desactivar venta, Retirar ETH. Además de un modificador llamado soloCreador que nos dará la posibilidad de crear una especie de permiso para ciertas funciones que solo el creador del evento podrá invocar.

El modificador soloCreador debería lucir así

Lo que permitirá que existan funciones que solamente puedan ser ejecutadas por el creador del evento y en caso de que alguien diferente intente hacerlo, rechace la transacción y revierta los cambios que intentaba realizar.

Hasta el momento, nos va quedando así

Cuando programamos cualquier función en Solidity, por más que le enviemos dinero, no retendrá lo enviado si no se lo decimos de manera explícita. Para esto debemos añadir la palabra payable (pagable) luego de la visibilidad de la función (public en este caso).

Como ya sabemos que para que una función reciba ETHER debemos añadir la palabra payable, vamos a ver que funciones construiremos:

  • comprarEntrada(uint numeroEntradasComprar, bytes32 rut) public payable : Función que registrará la compra de entradas. Recibe ETHER y los parámetros numeroEntradasComprar (número de entradas que se comprarán) y rut (a que RUT estarán asociadas). Para que la entrada se compre correctamente, la función debe cumplir un par de condiciones. Si no se cumple alguna de ellas, rechazará la compra y devolverá el dinero al comprador, además revertirá los cambios que se pudieran haber realizado (revert) en el Contrato Inteligente:
    1. El dinero que se está enviando (msg.value) debe ser exactamente igual (==) a valorEntrada * numeroEntradas, por lo que si un usuario quiere comprar 2 entradas y cada entrada cuesta 10 Finney (0.01 ETH), el costo será de 0.02 ETH (10 finney * 2 ó 0.01 ETH * 2).
    2. El rut debe tener una longitud mínima. (Opcional)
    3. Deben existir entradas disponibles para la venta (numeroEntradas >= numeroEntradasComprar).
  • verEntrada(uint idEntrada) public : Es una función que retorna información, por lo que utilizarla es gratis y no conlleva un gasto en ETHER. Retornará la información asociada al id de la entrada consultada. Es pública.
  • cambiarEstado() public : Función que permite desactivar o reanudar la venta de entradas. Solo puede ser activada por el creador del contrato, gracias al modificador que creamos llamado soloCreador. Es pública.
  • retirarETH() public : Es una función que le permitirá al creador del evento solicitar el retiro del ETHER recaudado. Veremos el caso de la función que envía todo el balance del contrato y el caso de que el creador del evento quiera solicitar retiro con un monto “personalizado”. Es pública y solo el creador puede ejecutar (soloCreador)

Recordemos que entradas es un diccionario cuyas claves son números enteros sin signo (uint) y que cada clave estará apuntando a una estructura Entrada, por lo que cada clave que ingresemos en este diccionario, tendrá los atributos de Entrada:

  1. addressComprador.
  2. entradasCompradas.
  3. rut.

Según lo dicho anteriormente, nuestra función comprarEntrada debería lucir más o menos así:

Cuando llamemos desde Javascript a la función comprarEntrada del Smart Contract pasará que:

  1. Verificará que el número de entradas a comprar sea mayor que 0 y que el dinero enviado corresponda al equivalente entre valorEntradas * numeroEntradas. Además deben existir entradas disponibles, así que verifica que numeroEntradas no sea 0 y que numeroEntradas sea mayor o igual a numeroEntradasComprar. Finalmente verifica que el estado de la venta de tickets sea verdadero (estadoVenta), lo que indica que quedan entradas para la venta. Si no se cumple alguna de las condiciones, debe rechazar la compra, devolver el dinero al comprador y revertir los cambios que pudo haber realizado en el Contrato.
  2. Si se cumplen las condiciones anteriores, seguirá con el código y procederá a registrar la compra

Imaginemos la compra de la primera entrada con la siguiente configuración:

  • comprador = 0x785c5cac1a72255f41821eca69dc77138a54d230; (msg.sender)
  • valorEntradas = 10 finney; (uint)
  • rut = “12.345.678-9” (bytes32)
  • Pagamos 20 finney por las 2 entradas (msg.value = 0.02 ETH)
  • Se cumplen las 3 condiciones necesarias para efectuar el registro de la compra

Inicialmente a contadorEntradas le asignamos el valor de 1337, por lo que la primera entrada tendrá este ID. En la primera ejecución ocurriría esto (Si se cumplen las 3 condiciones):

Con esto le estamos diciendo al Contrato que añada la clave contadorEntradas (1337) al diccionario de estructuras Entrada llamado entradas y que dicha clave tenga la información del comprador. Luego de realizar el registro de la clave contadorEntradas (1337) en el diccionario entradas, debemos decirle al contrato que vendimos numeroEntradasComprar (2) y que lo reste a numeroEntradas  numeroEntradas = numeroEntradas – numeroEntradasComprar;

Como el contrato ya registró el ID de la primera compra asociado a contadorEntradas (1337), ahora que quién compre la siguiente entrada tendrá el ID 1338, ya que luego de registrar la compra anterior, hicimos contadorEntradas++;

Habiendo realizado todo esto, llega el momento en que la cantidad de ETHER pagado por la cantidad de entradas compradas, debe ser enviado al creador del evento, para esto utilizamos el método transfer() que funciona solo con el tipo de dato address creadorEvento.transfer(msg.value); . Con esto el contrato transfiere el valor de la compra al creador del evento, por lo que el contrato solo es un intermediario.


Leyendo la información de una entrada

Para leer la información de una entrada mediante su ID, crearemos una función llamada verEntrada que recibe como parámetro un número entero sin signo (uint) llamado id. La función nos devolverá un array con el rut (bytes32) y número de entradas compradas (uint) que se asocian al ID consultado.
En este caso la función sólo consultará información del contrato, por lo que podemos decir que es una función constante y no altera el estado del contrato. Es gratis consultar los contratos! Entonces cómo hago que una función sea gratis de consultar? Primero que todo debes asegurarte que la función realmente no alterará de ninguna forma el estado del contrato. Teniendo esto en cuenta, solo debemos añadir constant returns (tipoDato) a la función, siendo tipoDato cualquier tipo de dato de Solidity y que tenga congruencia con el return que estamos haciendo.

Si bien todos los programadores siempre solucionaremos los problemas de distintas formas, para mi la función verEntrada quedaría así


Cambiando el estado de la venta

El creador del evento puede pausar o reanudar la compra de tickets, por lo que necesitaremos crear una función llamada cambiarEstado. Como solo cambiaremos un true por un false y viceversa, no recibiremos parámetros ni retornaremos nada. Añadiremos el modificador soloCreador para asegurarnos de que solo el creador del evento sea quién pueda pausar o reanudar la venta.


Solicitar retiro

Solo el creador del evento puede solicitar retirar ETHER del contrato. Además, el retiro siempre se realizará al creador del contrato.

Podríamos definir que el contrato envíe todo el balance 

function retirarETH() public soloCreador {
    creadorEvento.transfer(this.balance);
}

O que el creador del contrato pueda definir el monto que desea retirar

Contrato Inteligente terminado


Compilar y publicar el contrato inteligente

Para compilar y publicar el contrato dEvents debemos seguir los mismos pasos que con el dMessenger

  1. Ir a Remix – Solidity IDE
  2. Creamos un archivo nuevo dando clic en el “+” de la esquina superior izquierda. Escribimos dEvents.sol y damos clic en OK.
  3. Pegamos el código de dEvents.sol y el IDE compilará automáticamente.
  4. Damos clic en RUN y seleccionamos el Entorno donde ejecutaremos el contrato: Injected Web3, porque estamos usando Metamask.
  5. Clic en Create.
  6. Metamask se abrirá y nos pedirá firmar la transacción para publicar el contrato. Damos clic en Submit y esperamos que el contrato sea minado por la red.

Cuando la transacción ya fue minada, veremos que nos aparecerá la interfaz para interactuar con las funciones y variables públicas del contrato. Podríamos comprar y consultar entradas con esta interfaz, pero no es muy amigable así que haremos una más sencilla con algo de Javascript


Creando el archivo main.js

Primero que todo debemos llamar al contrato y sus variables/funciones públicas. Esto se logra teniendo solo 2 cosas del contrato: El ABI y la dirección (address).

Para encontrar el ABI de nuestro contrato en Remix, vamos a la pestaña Compile, luego veremos un botón Details al que daremos clic y abrirá una ventana flotante. Buscamos donde dice INTERFACE – ABI

Para obtener la dirección (address) de nuestro contrato debemos ir a la pestaña Run y dar clic en el ícono de Copiar

Teniendo esta información, la utilizaremos para invocar al contrato inteligente desde Javascript

Acá solo tendríamos que modificar la variable direcciónContrato por la dirección que les aparece en Remix:

Teniendo esto en el main.js, nuestra variable funcionesContrato ahora es un objeto que contiene como atributos toda la información pública del contrato, como su variables y funciones.

Ahora podemos comprar entradas en dEvents desde Javascript:

funcionesContrato es un objeto que tiene un atributo comprarEntrada que apunta a una función que vive en el contrato inteligente.
Si quisieramos consultar la entrada n°1337 tendríamos que hacer lo siguiente:

Incluso podemos consultar entradas sin tener que recurrir a la función verEntrada, accediendo directamente al mapping entradas

Obtener el número de entradas disponibles:

Obtener el nombre del evento:

Cambiar el estado del evento:

Solicitar retiro en ETHER (solo creador del contrato)

Teniendo estas funciones Javascript podemos crear un HTML con el que podamos mostrar la información del evento y un par de formularios para comprar/consultar entradas.

Ante cualquier duda por favor déjala en los comentarios 🙂

¡ Saludos comunidad 😀 !