Programando un Smart Contract en WAX Blockchain

Marcos DK
eosbarcelona
Published in
10 min readOct 20, 2020

Si seguiste el artículo anterior “Cómo preparar el entorno de desarrollo para crear Smart Contracts en WAX Blockchain” ya estarás preparado para comenzar a crear tu primer smart contract. De lo contrario, te recomiendo que sigas el enlace y repases el artículo.

Objetivo

Vamos a desarrollar un smart contract muy sencillo y quizás no muy práctico en la vida real pero que incluye algunas de las características más importantes en la gestión de la información que va a manejar cualquier smart contract.

Nuestra intención es simular un sencillo libro de entradas en el que vamos a ir apuntando las aportaciones, o donaciones, que los usuarios realizan a nuestra cuenta (sin detenernos en analizar el motivo de para qué o por qué lo hacen).

Tendremos una tabla de datos en la que guardaremos el nombre del usuario que realiza la aportación y el saldo de estas aportaciones. Así mismo, para gestionar esta tabla crearemos 3 acciones:

  • Registro de entradas
  • Borrado de registro de usuario
  • Envío de notificaciones

Cada vez que se requiera apuntar una entrada el código comprobará si el usuario ya tiene un registro asociado. De ser así, se incrementará su saldo con la cantidad aportada. Si el usuario es nuevo, se creará su registro. También podremos eliminar por completo el registro de un usuario. Con estas acciones tan elementales dejamos cubierto el sistema de CRUD básico de gestión de bases de datos (crear, leer, modificar y borrar).

He querido añadir una acción muy sencilla que permite guardar un apunte en la blockchain a modo de notificación y que será recibida tanto por el smart contract como por el usuario involucrado en la acción.

Creación del proyecto

Tal y como vimos en el artículo de preparación del entorno de desarrollo, nos aseguraremos de tener la imagen de Docker “waxdev” en funcionamiento antes de abrir una consola de sistema y conectarnos a ella con el comando:

docker attach waxdev

A continuación, crearemos un proyecto nuevo llamado “donaciones

eosio-init -project donaciones

Y abrimos la carpeta “donaciones” con nuestro editor de código para empezar a trabajar.

Declaración de tablas y acciones

En el archivo include/donaciones.hpp incluiremos las declaraciones de nuestras acciones y de las tablas que utilizará el smart contract.

Al editar el archivo, lo primero que vemos son estas definiciones:

#include <eosio/eosio.hpp>
using namespace eosio;
using namespace std;

con las que estamos diciendo al compilador que debe incluir la librería eosio/eosio.hpp con nuestro código y que vamos a utilizar los espacios de nombres “eosio” y “std”. Esto último no es obligatorio pero nos ahorrará trabajo al escribir código ya que a la hora de referirnos a miembros de esas clases, como std::double o eosio::name, podremos escribir, simplemente double y name.

La herramienta eosio-init nos crea una acción de ejemplo llamada “hi” que podemos borrar dejando el código del archivo donaciones.hpp de esta forma:

#include <eosio/eosio.hpp>
using namespace eosio;
using namespace std;
CONTRACT donaciones : public contract {
public:
using contract::contract;
};

La clase contract tendrá unas declaraciones públicas y otras privadas. En la sección public añadimos las declaraciones de nuestras 3 acciones:

ACTION entrada( name patreon, double cantidad);
ACTION borrar( name patreon);
ACTION notifica(name patreon, string memo);

La acción entrada recibirá 2 datos de entrada; el nombre del usuario y la cantidad a aportar. La acción borrar solo requiere el nombre de usuario cuyo registro queremos eliminar. La acción notifica recibirá el nombre del usuario que recibirá la copia de la notificación y una cadena de texto para el mensaje a enviar.

A continuación declaramos la sección private y definimos la tabla de datos:

private:
TABLE tabla_saldos
{
name patreon; // Cuenta del donante
double balance; // Cantidad de crypto donada
uint64_t primary_key() const { return patreon.value; }
};
typedef multi_index<”saldos”_n, tabla_saldos> t_saldos;

La definición de las tablas es muy similar a la definición de una struct en C++

Después de declarar los dos campos de la tabla le indicamos el índice primario de la tabla. Las tablas en EOSIO y en WAX pueden tener múltiples índices para facilitar la búsqueda de datos en ellas. Por ahora nos centraremos en el índice primario el cual devolverá un puntero al registro indicado por el valor del campo patreon (nombre del donante).

Para poder acceder a la tabla definimos un tipo multi_index llamado t_saldos. El nombre “saldos”_n será el nombre público que la tabla mostrará en los exploradores de bloques y el que utilizarían otros smart contracts o aplicaciones para acceder a la misma.

Inline Actions

Las acciones de un smart contract pueden ser llamadas desde otro smart contract pero también podemos llamar desde dentro de nuestro smart contract otras acciones definidas en el mismo. Este será el caso de la acción “notifica”, a la cual llamaremos cada vez que se ejecuten las acciones “entrada” o “borrar”.

Para llamar a una acción de nuestro smart contracta o de otro smart contract se utiliza al función eosio::action y su definición es la siguiente:

eosio::action(
permission_level{<cuenta_de_autoridad>, <permiso>},
<smart_contract>,
<acción>,
<datos>
);

En nuestro ejercicio queremos llamar a la acción “notifica” de nuestro propio smart contract y pasarle como parámetros un nombre de usuario y un mensaje:

action(
permission_level{get_self(), name(“active”)},
get_self(),
name(“notifica”),
make_tuple(patreon, memo)
).send();

action() prepara la acción y send() realiza la llamada a la acción.

El código de la llamada a la acción lo introduciremos en el cuerpo de una función privada a la que llamaremos “envia_nota” y que recibirá los parámetros “patreon” y “memo”.

donaciones.hpp

El código final de nuestro archivo donaciones.hpp quedará de esta forma:

Pasamos ahora a la codificación del cuerpo del smart contract. Para ello editamos el archivo “src/donaciones.cpp” y borramos todo su contenido excepto la inclusión del archivo de cabecera “donaciones.hpp

Lectura, creación y modificación de datos en las tablas del smart contract

Comenzamos con la definición de la acción “entrada

ACTION donaciones::entrada( name patreon, double cantidad {    // cuerpo de la acción}

Es normal que las acciones de un smart contract estén asociadas a la autoridad de alguna cuenta para evitar que cualquiera pueda ejecutar una acción que pueda afectar a datos de terceros. En nuestro caso, solo el propio smart contract tendrá autorización para llamar a sus acciones por lo que la primera línea de nuestra acción será

require_auth( get_self() );

get_self() devolverá la cuenta del propio smart contract. También podíamos indicar la cuenta directamente

require_auth( “donaciones”_n );

pero si luego publicamos nuestro smart contract en una cuenta con diferente nombre, por ejemplo en la cuenta “arpegiator11” que creamos en el artículo anterior o en la que cree cada lector de este artículo, tendremos problemas. Aunque nuestro proyecto se llama “donaciones”, terminará llamándose igual que la cuenta de la blockchain en la que lo publiquemos.

Con la función require_auth nos aseguramos de que solo el propio smart contract podrá llamar a esta acción.

Antes de acceder a la tabla realizaremos los chequeos pertinentes para descartar errores. Por ejemplo, vamos a comprobar que se introduce una cantidad mayor que 0 y, en caso contrario, abortaremos la ejecución de la acción y haremos que el smart contract devuelva un mensaje de error

check(cantidad > 0, “No se permiten entradas sin valor!”);

Si la condición se cumple el programa continuará ejecutándose, pero, de no cumplirse, la acción abortará con el mensaje de error indicado en el segundo parámetro y el resto del código ya no se ejecutará.

Ahora que ya podemos continuar, crearemos un enlace a nuestra tabla y trataremos de localizar un registro cuya clave de indexación principal (nombre de usuario) coincida con el nombre indicado en el parámetro “patreon”

t_saldos saldos(_self, _self.value);
auto ptrDonaciones = saldos.find(patreon.value);

El tipo de datos “name” es una estructura que contiene información sobre una cuenta de usuario en la WAX blockchain. Si queremos referirnos únicamente al nombre, deberemos hacer referencia al miembro “value”.

ptrDonaciones es un puntero que recorrerá la tabla hasta encontrar el campo clave o, si no lo encuentra, llegará hasta el final de la tabla saldos. Aprovecharemos esta posible circunstancia para saber si hemos encontrado al usuario o no

if(ptrDonaciones == saldos.end()){
// No se ha encontrado -> Crear
} else {
// Sí se ha encontrado -> Actualizar
}

Añadir registros a la tabla del smart contract

Para añadir un registro a la tabla haremos uso de la función “emplace”, la cual recibe como argumentos la autoridad que va a realizar la acción y un callback que utilizará una función lamba para crear una referencia al contenido de la tabla.

saldos.emplace(_self, [&](auto &rec){
rec.patreon = patreon;
rec.balance = cantidad;
});

Actualizar contenido de la tabla del smart contract

Para poder actualizar el saldo de un usuario que ya tiene un registro en la tabla necesitaremos, primero, conocer cuál es su saldo actual antes de realizar la actualización. Nuestro primer paso será almacenar en una variable el contenido del campo “cantidad” del registro señalado por nuestro puntero de búsqueda y, a continuación, incrementar su valor con la cantidad aportada

double saldo = ptrDonaciones->balance;
saldo += cantidad;

Ahora ya podemos realizar la actualización del registro haciendo uso de la función “modify”. Los parámetros que recibe son, además de los mismos que la función “emplace”, un puntero al registro a modificar

saldos.modify(ptrDonaciones, _self, [&](auto &rec){
rec.balance = saldo;
});

Antes de dar por cerrada la definición de nuestra acción “entrada” haremos una llamada a la función “envia_nota” para que esta, a su vez, haga la llamada inline a la acción del contract “notifica

envia_nota(patreon, “Apunte para el mecenas “ + name{patreon}.to_string() + “ realizado con éxito.”);

Eliminar registros de la tabla en el smart contract

La acción “borrar” tan solo recibe como parámetro el nombre del usuario cuyo registro queremos eliminar. Las primeras instrucciones de la definición de la acción serán muy similares a la acción anterior

require_auth(get_self());
t_saldos saldos(_self, _self.value);
auto ptrDonaciones = saldos.find(patreon.value);

Si al realizar la búsqueda el puntero ha llegado al finar de la tabla querrá decir que el usuario no existe así que la acción podrá terminar ya que no tiene nada que borrar

check(ptrDonaciones != saldos.end(), “No se ha encontrado al usuario!”);

Por el contrario, si el puntero está señalando un registro con el nombre que buscamos podremos borrarlo directamente

saldos.erase(ptrDonaciones);

y finalizar enviando una notificación

envia_nota(patreon, “El registro del mecenas “ + name{patreon}.to_string() + “ ha sido borrado.”);

Enviando las notificaciones

La acción “notifica” será una acción bastante curiosa ya que, en realidad, no hará nada. Esta acción recibirá dos datos como parámetros; un nombre de usuario y un mensaje, pero no realizará nada con esos datos. El objetivo de esta acción es, simplemente, que quede constancia de su llamada en la blockchain con un registro en el cual podrán verse los datos que recibió como parámetros.

El cuerpo de la función solo tendrá dos instrucciones

require_auth(get_self());

solo el smart contract podrá realizar notificaciones

require_recipient(patreon);

El usuario “patreon” recibirá una copia de la transacción. “require_recipient” es como el campo CC de un e-mail. Podemos utilizarlo tantas veces como cuentas queramos que reciban copia.

donaciones.cpp

Nuestro código final tendrá este aspecto

Compilar y publicar el smart contract

Para compilar el smart contract nos aseguraremos en un terminal de que estamos en la carpeta “build” y ejecutaremos el comando “cmake ..”

A continuación, compilamos con el comando “make”. Si todo ha ido bien veremos algo similar a esto

Para publicar el smart contract en la testnet debemos, primero, desbloquear el wallet que creamos en el artículo anterior.

cleos wallet unlock — password PW5*******tu_clave_de_desbloqueo*********

y luego publicar el smart contract

cleos -u http://wax-api-testnet.eosiomadrid.io set contract arpegiator11 ./donaciones -p arpegiator11@active

Con esta instrucción estoy indicando que quiero publicar el código “donaciones” como smart contract en la cuenta “arpegiator11” (aquí deberás indicar el nombre de la cuenta con la que publicarás tu ejercicio) y que requiere la clave “active” para la autorización.

Veremos algo como esto

Y ya tendremos el smart contract publicado. ¡Enhorabuena!

Probando nuestro smart contract

Navegaremos hasta el explorador de bloques de la testnet (https://wax-test.bloks.io)y localizaremos la cuenta de nuestro smart contract. En la sección de “actions” llamaremos a la acción “entrada” con algunos datos de ejemplo

Y podremos comprobar como se ha creado la tabla con el primer apunte

Si repetimos la misma operación pero cambiando la cantidad a 250 unidades, la tabla debería de modificarse aumentando el balance del usuario.

Si tratamos de introducir una cantidad inválida también veremos que el smart contract aborta con elmensaje de error que nosotros configuramos

Por último, podemos borrar la entrada de este usuario y comprobar que realmente así ha sido

Ahora sí, para finalizar, podemos localizar la cuenta del usuario de pruebas que utilizamos como donante y comprobar en los movimientos que muestra el explorador de bloques que ha recibido una notificación por cada una de estas acciones

Espero que este artículo haya sido de tu interés y te ayude a introducirte en el apasionante mundo de la programación de smart contracts en WAX Blockchain.

--

--

Marcos DK
eosbarcelona

Programador y creador de contenidos digitales. Profesor de informática, game dev y líder de 3DK Render, WAX Guild.