En este artículo de Chema Garabito para El lado del mal se hace una introducción práctica a Smart Contracts creando un contrato básico que maneje las URLs de los artículos de un blog como ejemplo. Para ellos vamos a utilizar Solidity que es el lenguaje de Smart Contracts que se usa en la Blockchain de Ethereum, una de las Blockchain que alberga el mayor número de estos contratos inteligentes. Esto es porque es uno de los lenguajes que más características ofrece a nivel de desarrollo y además de que se puede usar en la Blockchain de Polygon y Solana con algunos ajustes.
Primer SmartContract con Solidity
Como herramienta de desarrollo vamos a utilizar Remix, un IDE diseñado para Solidity y que además corre sobre la web, con lo que no hace falta instalarlo, también ofrece numerosas utilidades para testear los contratos y “subirlos” a la Blockchain.
Para compilar el código utilizaremos la EVM(Ethereum virtual machine) de Ethereum que viene instalado por defecto en la app de Remix. Me gustaría aclarar que en esta introducción asumiré que sabéis programación básica y si además conocéis conceptos sobre OOP (Programación Orientada a Objetos) será muy fácil que sigáis esta explicación.
Los archivos en Solidity deben tener la extensión .sol, pero lejos de eso su máquina compiladora o EVM no requiere que tus proyectos tengan una estructura exacta, en este caso se parece a lenguajes como JavaScript. Pero por comodidad vamos a seguir las convenciones establecidas por la comunidad. Un proyecto cualquiera de Web3 que requiera Smart Contracts se suele estructurar como se ve en la Figura 3. Como podéis ver los contratos que se usan en esta webapp se guardan bajo el directorio de contratos. No se suelen usar más porque es muy raro que para una aplicación se utilicen más de 3 o 4 contratos.
Primer paso: versión de Solidity y definir el contrato
La unidad mínima de ejecución de código en Solidity es un contrato, al igual que en Java si no tienes una clase Main no puedes ejecutar código. Para crear este contrato crearemos un archivo llamado FirstContract.sol. Lo primero que tenemos que hacer es definir la versión de Solidity que utilizaremos, en nuestro caso usaremos la última versión la 0.8.x que ha traído numerosas funcionalidades al lenguaje. La versión se define así:
Después definiremos contrato y le daremos nombre:Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3;
En nuestro ejemplo queremos que el contrato tenga algunos datos del dueño del blog como su edad, su nombre, su dirección de eth (muy importante) y los datos del blog propiamente, que será una lista con todas las URLs de sus posts. Para ello tenemos que conocer las variables y tipos de datos básicos que podemos utilizar.Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract {}
Variables y tipos de datos básicos
Una cosa muy importante que debemos tener en cuenta es que todo lo que se guarde como estado se guardará para siempre en la Blockchain y eso tiene un coste. En Solidity existen los siguientes tipos de datos básicos:
- Bool: Son expresiones booleanas.
- Strings: Listas de caracteres, palabras, frases. Es decir, cadenas de texto.
- Integers: números enteros que se pueden definir de diferentes maneras en función de lo que necesitemos.
- Address: que es un tipo de dato muy especial en el que se guardan direcciones de ethereum ya sean cuentas de wallets o direcciones de otros contratos.
- Arrays: En Solidity los arrays pueden ser de cualquier tipo pero solo pueden contener elementos de un mismo tipo. En versiones anteriores de Solidity solo se permitía usar arrays con una longitud fija, lo que cambiaba el paradigma un poco, pero a partir de la versión 0.8.x se pueden utilizar arrays dinámicos.
En el código se definirían escribiendo primero la palabra clave del tipo de dato después el nombre y la asignación. En Solidity el punto y coma es obligatorio así que no lo olvidéis.
Propiedades y métodosCódigo:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // a boolean is defined like bool isAdult = ownerAge <= 18 address public owner; // In the form 0x923c89… string public ownerName = "Chiquito de la Calzada"; uint256 public ownerAge = 23; // An array that will contain all the posts urls string[] public posts; }
También queremos unas cuantas funciones para actualizar el dueño del contrato, añadir o eliminar Posts, obtenerlos. Los contratos en Solidity funcionan de manera muy parecida a las clases en programación orientada a objetos. Tienen propiedades y métodos pero una vez subidos a la Blockchain solo se pueden acceder a los métodos. Al igual que en las clases de OOP creamos un constructor que servirá para inicializar el objeto con unas ciertas propiedades. Los métodos (también llamados funciones) y el constructor se definen de la siguiente manera:
Como veis en el código he introducido muchas cosas nuevas, vamos a explicarlas una a una:Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // code that goes before … //Contract contstructor constructor(address _firstOwner){ owner = _firstOwner; } //Definition of contract methods function setOwnerState( address _newOwner, string memory _ownerName, uint256 _age ) public { require(msg.sender == owner, "Error, you are not the owner"); owner = _newOwner; ownerName = _ownerName; ownerAge = _age; } }
Para declarar la función se utiliza la palabra reservada function y como en la mayoría de los lenguajes se definen los parámetros como si fueran variables pero sin asignación. Os habréis dado cuenta de que usó la palabra memory al declarar el string _ownerName, eso lo veremos más adelante.
Acto seguido podemos ver que está escrita la palabra reservada “public”. Esta palabra es un modificador de acceso, estos se pueden establecer en las variables globales si queremos pero en las funciones son obligatorias. Por defecto en Solidity existen los siguientes:
- Public: Establece la función como pública y a está podrá acceder cualquier persona que posea el contrato y también podrá ser usada en los contratos que hereden de este.
- Private: La función solo puede ser llamada por el mismo contrato pero no será heredable.
- Internal: Es como public solo que deja de ser accesible por las personas.
- External: La función será accesible por otros contratos, pero ni se heredará ni será accesible por el contrato actual.
Figura 6: Require
Es una función en la que evaluamos condiciones y si no se cumplen rompen el flujo de ejecución del Smart Contract y devuelve al cliente un error con el string que pasemos. En este caso accedemos a quién llama la función y si esta persona no es el dueño la función no se ejecutará.
Toda función que pueda ser llamada desde fuera tiene una variable accesible llamada “msg”. Esta tendría las siguientes propiedades:
- msg.sender: La direccion de eth de quién llamo la funcion.
- msg.value: La cantidad de eth que se ha enviado con esa función.
- Otras más que por ahora no voy a nombrar que no son importantes en este momento.
Las Naming Conventions son simplemente las convenciones establecidas a la hora de nombrar variables, funciones… En Solidity son las siguientes:
- Uso de barra baja para variables locales, sirve para distinguir fácilmente el ámbito en el que puede actuar una variable, si la variable es global pues no hay que hacer nada, pero si por el contrario la variable tiene un ámbito local en una función por ejemplo se utiliza la barra baja para nombrarla “_variable”.
- CamelCase: La convención para nombrar variables, funciones, objetos… Es la misma que en JavaScript, el uso de la nomenclatura CamelCase. La cual simplemente es “esMayorDeEdad”.
Condicionales y bucles
Ahora podemos cambiar el dueño del contrato y sus propiedades siempre que queramos. Vamos a implementar unas funciones que nos permitan añadir posts, eliminarlos y leerlos utilizando condicionales y bucles.
- Condicionales: En Solidity funcionan igual que en otros lenguajes pero las expresiones de evaluación no, además como todo tiene que correr dentro de un nodo de la Blockchain que solo maneja números y direcciones no existen como tal strings puros, entonces manejar strings se vuelve peliagudo a veces como cuando hay que compararlos. Lo que debemos hacer es hashearlos mediante la función keccka256 y entonces compararlos.
- Bucles: Funcionan igual que en lenguajes como Java solo que en Solidity solo tenemos la versión normal del “for” en la que hay que hacer i++ y demás.
Vamos a crear ahora unas cuantas funciones para añadir, eliminar posts.
● _existsUrl: Esta función se va a encargar de decirnos si ya se guardó antes una URL.
Primero el parámetro que se le pasa es un string memory porque no queremos que eth guarde esos datos en la Blockchain. Después le asignamos el modificador private dado que no queremos que la función sea accedida desde fuera de nuestro contrato. Os habréis fijado que tenemos unos cuantos modificadores más, eso lo explicaré en la continuación de este artículo.Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // code that goes before … //Contract contstructor constructor(address _firstOwner){ owner = _firstOwner; } function addPostUrl(string memory _url) public { require(msg.sender == owner, "Error, you are not the owner"); require(!_existsUrl(_url), "The url already exists"); posts.push(_url); } }
[I][I][I]Posteriormente le decimos que retorne un valor booleano. Iniciamos un bucle tal y como vemos en el código para iterar los posts que están dentro del array. Dentro del bucle evaluamos que si existe retorna falso. Puede parecer un poco raro como se hace la comparación entre “_url” y “posts” pero Solidity requiere que los strings se comparen de esa forma. Finalmente si ningún valor ha coincidido le retornamos false.
● addPost: Esta función se va a encargar de añadir las URLs de los post a la Blockchain.
Definimos la función con el parámetro de la URL que será de tipo string y además con el modificador memory para asegurarnos que eth no guarda este valor en la Blockchain. Como modificador de acceso le ponemos publicidad puesto que queremos poder llamarla desde fuera del contrato. Evaluamos si quien envía la petición es el dueño del contrato, si no lo es rompemos el flujo de ejecución. De lo contrario evaluamos que no exista ninguna url como la que nos pasa. Por último hacemos un push al array de los posts. Cabe destacar que el método push solo está disponible en los arrays de tipo memory.Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // code that goes before … //Contract contstructor constructor(address _firstOwner){ owner = _firstOwner; } //Definition of contract methods function addPostUrl(string memory _url) public { require(msg.sender == owner, "Error, you are not the owner"); require(!_existsUrl(_url), "The url already exists"); posts.push(_url); } }
● getPosts: Esta función retorna todos los posts almacenados en la Blockchain.
Primero definimos la función y le ponemos un modificador de acceso “público”, después le añadimos “view” un modificador que simplemente le dice a Solidity que la función no realiza escritura en la base de datos con lo que no nos cobrara fees. Finalmente le decimos que retorne un array de strings.Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // code that goes before … //Contract contstructor constructor(address _firstOwner){ owner = _firstOwner; } function getPosts() public view returns(string[] memory){ return posts; } }
● deletePost: Esta función se va a encargar de eliminar la url proveída de la Blockchain.
Código:// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.3; contract FirstContract { // code that goes before … //Contract contstructor constructor(address _firstOwner){ owner = _firstOwner; } function deletePostUrl(string memory _url) public { require(_existsUrl(_url), "The provided url does not exist" ); for(uint i=0; i<posts.length; i++){ if( keccak256(abi.encodePacked(_url))== keccak256(abi.encodePacked(posts)) ){ // as we do not care about the orde delete posts; } } } }
Primero definimos la función y el parámetro que va a recibir, que va a ser igual que en la anterior función. Le añadimos el modificador de acceso público y como no retorna nada pues no hace falta poner el “returns”. Después nos aseguramos de que existe la URL que hay que borrar. Por último creamos el array y cuando encontramos el elemento simplemente lo eliminamos con la palabra reservada delete. Cabe destacar que solo habrá que eliminar una vez la URL dado que por el diseño que hemos implementado no puede existir una misma URL dos veces.
Ahora tenemos un Smart Contract funcional. Como este artículo ha quedado muy larga continuaremos desarrollando el contrato en el siguiente artículo de la serie de Blockchain & Smart Contracts en la que veremos conceptos más avanzados de Solidity y testearmos y desplegaremos nuestro contrato a la “TestNet” de Ethreum. Os dejo el código en este repositorio de GitHub.
¡Nos vemos en próximos artículos de esta serie!
Autor: Chema Garabito. Desarrollador Full-Stack. Ideas Locas Telefónica CDO, en https://www.elladodelmal.com/2021/12/blockchain-smartcontrats-primer-smart.html
Marcadores