Capítulo 4. Nuestro primer contrato inteligente

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Ahora que lo tenemos todo instalado, es hora de construir nuestro primer contrato. Siguiendo la tradición de los libros de introducción a la programación, nuestro primer programa nos saludará con un "¡Hola, mundo!".

Al desarrollar este programa, vamos a aprender a utilizar las herramientas que nos proporciona Truffle para crear y probar nuestra aplicación. También empezaremos a explorar el lenguaje Solidity, incluyendo un vistazo a las funciones y variables de estado.

Nuestro objetivo en este capítulo es encontrar un ritmo en la forma de construir nuestra aplicación y descubrir si vamos por buen camino. Para ayudarnos en ello, adoptaremos el desarrollo dirigido por pruebas (TDD) para ese bucle de retroalimentación instantánea.

Empecemos por configurar nuestro proyecto.

Configurar

Mientras nos instalamos, necesitaremos un directorio para alojar nuestra nueva aplicación. Creemos primero un directorio llamado greeter y cambiemos a nuestro nuevo directorio. Abre tu terminal y utiliza los siguientes comandos:

$ mkdir greeter
$ cd greeter

Ahora vamos a inicializar un nuevo proyecto Truffle de la siguiente manera:

$ truffle init

Este comando generará la siguiente salida:

✔ Preparing to download
✔ Downloading
✔ Cleaning up temporary files
✔ Setting up box

Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

Nuestro directorio greeter debería incluir ahora los siguientes archivos:

greeter
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

Observa que los comandos especificados en la salida se alinean bien con la estructura de directorios generada al inicializar nuestra aplicación. truffle compile compilará todos los contratos en el directorio contracts, truffle migrate desplegará nuestros contratos compilados ejecutando los scripts en nuestro directorio migrations y, por último, truffle test ejecutará las pruebas en nuestro directorio test.

El último elemento creado para ti es el archivo truffle-config.js. Aquí es donde colocaremos las configuraciones específicas de nuestra aplicación.

Ahora que tenemos nuestra estructura inicial, estamos listos para empezar el desarrollo.

Nuestra primera prueba

A medida que implementemos funciones para nuestros contratos, utilizaremos TDD para aprovechar el breve bucle de retroalimentación que proporciona. Si no estás familiarizado con TDD, es una forma de escribir software en la que primero empezamos con una prueba que falla y luego escribimos el código necesario para que la prueba pase. Una vez que todo funciona, podemos refactorizar el código para hacerlo más fácil de mantener.

El soporte de pruebas que proporciona Truffle es una de las áreas en las que el cinturón de herramientas brilla de verdad. Ofrece soporte de pruebas tanto en JavaScript como en Solidity; para nuestros ejemplos, utilizaremos JavaScript, ya que su adopción está mucho más extendida, lo que facilita la búsqueda de recursos adicionales en caso de que te quedes atascado. Si quieres explorar la escritura de pruebas en Solidity, puedes consultar la documentación de pruebas de Truffle.

Nuestra primera prueba en el Ejemplo 4-1 va a consistir en asegurarnos de que nuestro contrato vacío puede desplegarse correctamente. Esto puede parecer innecesario, pero los errores que experimentaremos al conseguir que esto pase proporcionan una gran manera de ver algunos de los errores que probablemente nos encontremos en nuestra carrera.

En el directorio de pruebas, crea un archivo llamado greeter_test.js:

$ touch test/greeter_test.js

A continuación, añade el código de prueba, como en el Ejemplo 4-1.

Ejemplo 4-1. Prueba de que nuestro contrato puede desplegarse
const GreeterContract = artifacts.require("Greeter"); 1

contract("Greeter", () => {                           2
  it("has been deployed successfully", async () => {  3
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract was not deployed");    4
  });
});
1

Truffle proporciona una forma de cargar e interactuar con contratos que han sido compilados a través de la función artifacts.require. Aquí, pasarás el nombre del contrato, no el nombre del archivo, ya que un archivo puede contener varias declaraciones de contrato.

2

Las pruebas de Truffle utilizan Mocha, pero con un giro. La función contract actuará de forma similar a la incorporada en describe, pero con la ventaja añadida de utilizar la función de sala limpia de Truffle. Esta función significa que los contratos nuevos se implementarán antes de que se ejecuten las pruebas anidadas en ellos. Esto ayuda a evitar que se comparta estado entre distintos grupos de pruebas.

3

Todas las interacciones con la cadena de bloques van a ser asíncronas, así que en lugar de utilizar Promesas y el métodoPromise.prototype.then , aprovecharemos la sintaxisasíncrona/de espera ahora disponible en JavaScript.

4

Si el saludador es veraz (existe), nuestra prueba pasará.

Al ejecutar nuestras pruebas, recibiremos un error parecido a éste:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol

Error: Could not find artifacts for Greeter from any sources
    at Resolver.require (/usr/local/lib/node_modules/truffle/build/webpack:...
    at TestResolver.require (/usr/local/lib/node_modules/truffle/build/...
    at Object.require (/usr/local/lib/node_modules/truffle/build/webpack:...

    ...omitted..

Truffle v5.0.31 (core: 5.0.31)
Node v12.8.0

Esto nos proporciona información procesable. El error nos dice que después de compilar nuestros contratos, Truffle no pudo encontrar uno llamado Greeter. Puesto que aún no hemos creado este contrato, este error es perfectamente razonable. Sin embargo, si ya has creado un contrato y sigues obteniendo este error, es probable que se deba a un error tipográfico en la declaración del contrato que se encuentra en el archivo Solidity o en la declaración artifacts.require.

Vamos a crear el archivo Greeter y a añadir el código del Ejemplo 4-2 para ver qué información nos proporcionará la ejecución de nuestro conjunto de pruebas.

En el terminal, crea el archivo Greeter:

$ touch contracts/Greeter.sol
Ejemplo 4-2. Contrato Greeter vacío
pragma solidity >= 0.4.0 < 0.7.0; 1

contract Greeter { 2

}
1

La línea pragma es una instrucción del compilador. Aquí le decimos al compilador de Solidity que nuestro código es compatible con la versión 0.4.0 de Solidity hasta la versión 0.7.0, pero sin incluirla.

2

Los contratos en Solidity son muy similares a las clases en los lenguajes de programación orientados a objetos. Los datos y funciones o métodos definidos dentro de las llaves de apertura y cierre del contrato quedarán aislados en ese contrato.

Una vez realizados estos cambios, ejecutemos de nuevo nuestras pruebas:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    1) has been deployed successfully
    > No events were emitted


  0 passing (34ms)
  1 failing

  1) Contract: Greeter
       has been deployed successfully:
     Error: Greeter has not been deployed to detected network...
      at Object.checkNetworkArtifactMatch (/usr/local/lib/node_modules/...
      at Function.deployed (/usr/local/lib/node_modules/truffle/build/...
      at processTicksAndRejections (internal/process/task_queues.js:85:5)
      at Context.<anonymous> (test/greeter_test.js:5:21)

El error que aparece aquí indica que nuestro contrato aún no existe en la red; en otras palabras, aún no se ha desplegado. Cada vez que ejecutamos el comando truffle test, Truffle compila primero nuestros contratos y luego los despliega en una red de prueba. Para desplegar nuestro contrato, tenemos que recurrir a otra herramienta del cinturón de herramientas de Truffle: las migraciones.

Las migraciones son scripts escritos en JavaScript que se utilizan para automatizar la implementación de nuestros contratos. El contrato por defecto Migrations que se encuentra en contratos/Migraciones.sol es el contrato que se despliega mediante migraciones/1_inicial_migration.js y es actualmente el único contrato que ha llegado a la red de pruebas. Para añadir nuestro contrato Greeter a la red, tendremos que crear una migración utilizando el código que se encuentra en el Ejemplo 4-3.

En primer lugar, tendremos que crear el archivo que contendrá el código de nuestras migraciones:

$ touch migrations/2_deploy_greeter.js

A continuación, podemos añadir el código del Ejemplo 4-3.

Ejemplo 4-3. Implementación del contrato Greeter
const GreeterContract = artifacts.require("Greeter");

module.exports = function(deployer) {
  deployer.deploy(GreeterContract);
}

Nuestra migración inicial no tiene mucho que hacer. Utilizamos el objeto desplegador proporcionado para desplegar el contrato Greeter. Por ahora, esto es todo lo que tendremos que hacer para que nuestro contrato esté disponible en la red local de pruebas, pero no te preocupes: profundizaremos en las migraciones en el próximo capítulo. Una vez hecho esto, ejecuta las pruebas una vez más:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully


  1 passing (27ms)

¡Éxito! Esta prueba nos permite saber que tenemos todo configurado correctamente y que estamos listos para empezar a implementar funciones.

Decir hola

Este es el punto en el que normalmente utilizaríamos una llamada a alguna variante de printf o println para dar salida a nuestro saludo, pero en Solidity no tenemos acceso a la salida estándar, ni al sistema de archivos, ni a la red, ni a ninguna otra entrada/salida (E/S). Lo que sí tenemos son funciones.

Una vez implementado, nuestro contrato inteligente se almacenará en la red Ethereum en una dirección específica. Estará inactivo hasta que le llegue una solicitud pidiéndole que realice algún trabajo y el trabajo que puede hacer nuestro contrato está definido por nuestras funciones. Lo que queremos es una función que pueda decir "¡Hola, mundo!" y, al igual que antes, empezaremos con una prueba.

En test/greeter_test.js, vamos a añadir la prueba del Ejemplo 4-4.

Ejemplo 4-4. Prueba de ¡Hola, mundo!
describe("greet()", () => {
  it("returns 'Hello, World!'", async () => {
    const greeter = await GreeterContract.deployed();
    const expected = "Hello, World!";
    const actual = await greeter.greet();

    assert.equal(actual, expected, "greeted with 'Hello, World!'");
  });
});

En este caso, establecemos un valor esperado y, a continuación, recuperamos el valor de nuestro contrato y comprobamos si son iguales. Tenemos que marcar la función de prueba como async, ya que vamos a realizar una llamada a nuestra blockchain de prueba local para interactuar con este contrato.

Al ejecutar la prueba se obtiene lo siguiente:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      1) returns 'Hello, World!'
    > No events were emitted


  1 passing (42ms)
  1 failing

  1) Contract: Greeter
       greet()
         returns 'Hello, World!':
     TypeError: greeter.greet is not a function
      at Context.<anonymous> (test/greeter_test.js:13:36)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Cuando nos centramos en el error, vemos que greeter.greet no es una función. Es hora de añadir una función a nuestro contrato. Actualiza el contrato Greeter con la función del Ejemplo 4-5.

Ejemplo 4-5. Añadir la función saludar a Greeter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

}

Aquí creamos una función con el identificador o nombre greet, que no toma ningún parámetro. Tras el identificador, indicamos que nuestra función es una función external. Esto significa que forma parte de la interfaz de nuestro contrato y que se puede llamar desde otros contratos, o desde transacciones, pero no se puede llamar desde dentro del contrato o, al menos, no sin una referencia explícita al objeto sobre el que se llama. Nuestras otras opciones aquí son public, internal, y private.

public Las funciones también forman parte de la interfaz, lo que significa que pueden invocarse desde otros contratos o transacciones, pero además pueden invocarse internamente. Esto significa que puedes utilizar un receptor implícito del mensaje al invocar el método dentro de un método.

internal y las funciones private deben utilizar el receptor implícito o, en otras palabras, no pueden llamarse a un objeto ni a this. La principal diferencia entre estos dos modificadores es que las funciones private sólo son visibles dentro del contrato en el que están definidas, y no en los contratos derivados.

Las funciones que no alterarán el estado de las variables del contrato pueden marcarse como pure o view. Las funciones pure no leen de la cadena de bloques. En su lugar, operan con los datos que se les han pasado o, como en nuestro caso, con datos que no necesitaban ninguna entrada en absoluto. Las funciones view pueden leer datos de la cadena de bloques, pero de nuevo están restringidas en el sentido de que no pueden escribir en la cadena de bloques.

Tras nuestra declaración de que esta función es pure, identificamos lo que esperamos que devuelva nuestra función. Solidity permite múltiples valores de retorno, pero en nuestro caso sólo devolveremos un valor: el tipo string. También indicamos que se trata de un valor que no hace referencia a nada situado en el almacenamiento persistente de nuestro contrato utilizando la palabra clave memory.

El cuerpo de nuestra función devuelve la cadena que buscamos: "¡Hola, mundo!". Esto debería satisfacer los requisitos de nuestra prueba. Pero no te fíes de nuestra palabra: vuelve a ejecutar las pruebas para comprobarlo:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (51ms)


  2 passing (82ms)

Una vez superada esta prueba, pasemos a hacer nuestro contrato un poco más flexible dando a nuestros usuarios la posibilidad de cambiar el saludo.

Dinamizar nuestro contrato

Ahora que nuestro contrato ha devuelto un valor codificado, continuemos y hagamos que el saludo sea dinámico. Para ello, tenemos que añadir otra función que nos permita establecer el mensaje que devolverá nuestra función greet().

Antes hemos mencionado la función "sala limpia" al utilizar la función contract en nuestras pruebas. Esta función desplegará nuevas instancias de nuestros contratos para su uso dentro de la función de devolución de llamada de ese bloque de código. En la prueba que estamos a punto de escribir, queremos asegurarnos de que nuestros cambios de estado permanecen aislados del resto de las pruebas, para que no nos encontremos en una situación en la que el orden de nuestras pruebas afecte al éxito o al fracaso de nuestro conjunto de pruebas. Para ello, crearemos otro bloque contract en nuestro archivo test/greeter-test.js, como se ilustra en el Ejemplo 4-6.

Ejemplo 4-6. Probar que el saludo puede hacerse dinámico
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    });
  });
});

contract("Greeter: update greeting", () => {
  describe("setGreeting(string)", () => {
    it("sets greeting to passed in string", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = "Hi there!";

      await greeter.setGreeting(expected);
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeting was not updated");
    });
  });
});

Reconocerás que la configuración es muy parecida a la de nuestra prueba anterior. Establecemos una variable para que contenga nuestro valor de retorno esperado, que es la cadena que también pasaremos a la función setGreeting. A continuación, actualizamos el saludo y pedimos al greet su valor de retorno. Ambas son llamadas asíncronas que nos obligan a utilizar la palabra clave await. Por último, comprobamos el valor de greet con nuestro valor esperado.

Al ejecutar las pruebas, obtenemos la siguiente salida:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (48ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (111ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:
     TypeError: greeter.setGreeting is not a function
      at Context.<anonymous> (test/greeter_test.js:26:21)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Nuestras pruebas indican que la función setGreeting aún no existe; añadamos esta función a nuestro contrato. De vuelta en nuestro archivo contratos/Greeter.sol, después de nuestra función greet, añade la firma de función del Ejemplo 4-7.

Ejemplo 4-7. Añadir setGreeting() a Greeter
function setGreeting(string calldata greeting) external {

}

Nuestra función setGreeting pretende actualizar el estado de nuestro contrato con un nuevo saludo, lo que significa que necesitamos aceptar un parámetro para este nuevo valor. Se espera que este nuevo valor sea una cadena, y será referido por el identificador greeting. Al igual que nuestra función greet, esta función está pensada para ser llamada desde scripts externos u otros contratos y no se hará referencia a ella internamente.

Dado que esta función se llama desde el mundo exterior, los datos que se pasan como parámetro no forman parte del almacenamiento persistente del contrato, sino que se incluyen como parte de los datos de llamada y deben etiquetarse con la ubicación de datos calldata. La ubicación calldata sólo es necesaria cuando la función se declara como externa y cuando el tipo de datos del parámetro es un tipo de referencia, como un mapping, struct, string o array. Los tipos de valor como int o address no requieren esta etiqueta.

Con la función declarada, si ahora ejecutamos nuestras pruebas, obtendremos la siguiente salida:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (45ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (215ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:

      greeting was not updated
      + expected - actual

      -Hello, World!
      +Hi there!

      at Context.<anonymous> (test/greeter_test.js:29:14)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Revisando el fallo de la prueba, nos enteramos de que el valor devuelto por la función greet no era el esperado. Nuestra función aún no hace nada y, por tanto, el saludo nunca cambió.

Para actualizar una variable en una función y que esa variable esté disponible en otra función, tendremos que almacenar los datos en el almacenamiento persistente del contrato utilizando una variable de estado, como se demuestra en el Ejemplo 4-8.

En nuestro archivo contracts/Greeter.sol, añade el código del Ejemplo 4-8 al contrato Greeter.

Ejemplo 4-8. Añadir variable de estado al contrato Greeter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting;

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

    function setGreeting(string calldata greeting) external {

    }

}

Las variables de estado estarán disponibles para todas las funciones definidas dentro de un contrato, de forma similar a las variables de instancia o variables miembro de otros lenguajes orientados a objetos. También son donde almacenaremos los datos que existirán durante toda la vida de nuestro contrato. Al igual que las funciones, las variables de estado pueden declararse con distintos niveles de modificadores de visibilidad, como public, internal y private. En nuestro ejemplo anterior, utilizamos el modificador private lo que significa que esta variable es accesible en nuestro contrato Greeter.

Nota

Todos los datos de la cadena de bloques son visibles públicamente desde el mundo exterior. Los modificadores de variables de estado sólo restringen cómo se puede interactuar con los datos desde dentro del contrato o desde otros contratos.

Cuando escribimos una función que actualiza una variable de estado, como va a hacer nuestro setGreeting, no podemos nombrar el parámetro igual que nuestra variable de estado. Para evitar este tipo de conflictos, es práctica común anteponer un guión bajo (_) a los nombres de las variables de estado o de los parámetros.

Actualicemos la función setGreeting para establecer la variable de estado, como se muestra en el Ejemplo 4-9.

Ejemplo 4-9. Actualizar variable de estado
function setGreeting(string calldata greeting) external {
    _greeting = greeting;
}

Incluso con este cambio, nuestra prueba seguiría fallando. Lo que queremos hacer ahora es actualizar la función greet para que lea de esta variable de estado, pero hay algunas cosas que debemos tener en cuenta antes de hacer este cambio. La primera es que nuestra función está marcada actualmente como pure. Tendremos que actualizar la función para que sea una función view ya que ahora vamos a acceder a datos almacenados en la cadena de bloques. Una vez que pasemos a leer de la variable de estado en nuestra función greet, ya no tendremos nuestro saludo por defecto y la prueba inicial fallará. Para solucionar estos problemas, daremos a greeting un saludo por defecto de "¡Hola, mundo!". También cambiaremos la función de pure a view y actualizaremos el valor de retorno para que utilice el valor almacenado en greeting. El Ejemplo 4-10 muestra nuestro contrato con todos estos cambios realizados.

Ejemplo 4-10. Lectura de nuestra variable de estado
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting = "Hello, World!";

    function greet() external view returns(string memory) {
        return _greeting;
    }

    function setGreeting(string calldata greeting) external {
        _greeting = greeting;
    }

}

Después de ejecutar nuestras pruebas, ¡veremos que las tres pasan!

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (116ms)


  3 passing (224ms)

Cómo hacer que el Greeter sea propio

Tal y como está ahora, cualquiera puede cambiar el mensaje de nuestro contrato de Bienvenida. Esto puede estar bien en algunos casos, pero también podría llevar a que alguien cambiara el mensaje por algo menos acogedor. Para evitarlo, ahora añadiremos la idea de propiedad al contrato, y luego restringiremos la capacidad de cambiar el saludo al propietario.

Para ello, queremos establecer el propietario del contrato Greeter en la dirección que desplegó el contrato. Esto significa que tendremos que almacenar la dirección durante la inicialización, y para ello, tendremos que escribir una función constructora. También necesitaremos acceder a cierta información del objeto msg. El objeto msg está disponible globalmente e incluye los datos de la llamada, el remitente del mensaje, la firma de la función a la que se llama y el valor (cuánto wei se ha enviado).

Nuestra primera prueba va a afirmar que existe un propietario invocando una función getter de owner. Como esto no depende del cambio de estado de nada, pondremos esta prueba en el bloque inicial de pruebas Greeter, como se muestra en el Ejemplo 4-11.

Ejemplo 4-11. Comprobar que existe un propietario
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });
  });
})

La ejecución de esta prueba generará el siguiente fallo:

$ truffle test

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (54ms)
    owner()
      1) returns the address of the owner
    > No events were emitted

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (274ms)


  3 passing (409ms)
  1 failing

  1) Contract: Greeter
       owner()
         returns the address of the owner:
     TypeError: greeter.owner is not a function
      at Context.<anonymous> (test/greeter_test.js:22:35)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Este fallo me resulta familiar. No tenemos una función definida en nuestro contrato Greeter llamada owner. Como se trata de una función getter, tendremos que añadir una variable de estado que contenga la dirección del propietario, y luego nuestra función deberá devolver esa dirección. El lenguaje Solidity proporciona dos tipos address: uno es address y el otro es address payable. La diferencia entre ellos es que address payable da acceso a los métodos transfer y send, y las variables de este tipo también pueden recibir éter. No vamos a enviar éter a esta dirección y podemos utilizar el tipo address para nuestros fines.

Sigamos adelante y actualicemos el contrato Greeter con estos cambios, como se ilustra en el Ejemplo 4-12.

Ejemplo 4-12. Añadir la variable de estado de propiedad y la función getter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

La ejecución de nuestras pruebas muestra que volvemos a aprobar:

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)
    owner()
      ✓ returns the address of the owner (50ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (125ms)


  4 passing (292ms)

Lo que realmente queremos comprobar aquí es que la dirección del propietario es la misma que la dirección de implementación. Para ello, ahora añadiremos una prueba para comprobar que la dirección del propietario es igual a la cuenta predeterminada (la que se utiliza para desplegar el contrato). Para ello, necesitaremos acceder a las cuentas de nuestro entorno de pruebas, y por suerte Truffle ha hecho que esto sea accesible para su uso a través de la variable accounts. Tendremos que pasarla al bloque de código de prueba de nivel contract, como se ilustra en el Ejemplo 4-13.

Ejemplo 4-13. El propietario de la prueba es el mismo que el de la implementación
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", (accounts) => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });

    it("matches the address that originally deployed the contract", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();
      const expected = accounts[0];

      assert.equal(owner, expected, "matches address used to deploy contract");
    });
  });
})

Observa que nuestro bloque contract pasa ahora un parámetro accounts a la función que contiene nuestros casos de prueba. Nuestra nueva prueba afirma que la primera cuenta es la que ha implementado el contrato Greeter. Cuando ejecutemos las pruebas, obtendremos un fallo que nos informará de que owner y expected no coinciden. Ahora es el momento de escribir esa función constructora.

Hasta ahora hemos estado utilizando el constructor por defecto, constructor() public {}. Ahora tenemos que registrar el msg.sender como propietario cuando se inicialice el contrato. Nuestro contrato Greeter debería parecerse ahora al Ejemplo 4-14.

Ejemplo 4-14. Añadir un constructor al contrato Greeter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

  constructor() public {
    _owner = msg.sender;
  }

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

Con este cambio, nuestras pruebas vuelven a pasar.

Ahora que sabemos quién creó el contrato, podemos crear una restricción para que sólo el propietario pueda actualizar el saludo. Este tipo de control de acceso se hace normalmente con un modificador de función.

Los modificadores de función nos permiten ampliar una función con código que puede ejecutarse antes y/o después de la función. Suelen adoptar la forma de una cláusula de guarda e impedirán que se invoque la función si no se cumple la cláusula, que es exactamente lo que queremos para nuestro contrato Greeter.

Enel Ejemplo 4-15 se han actualizado las pruebas de setGreeting, estableciendo la expectativa de que sólo el propietario puede cambiar el saludo.

Ejemplo 4-15. Prueba de restricción de setGreeting al propietario
contract("Greeter: update greeting", (accounts) => {
  describe("setGreeting(string)", () => {
    describe("when message is sent by the owner", () => {
      it("sets greeting to passed in string", async () => {
        const greeter = await GreeterContract.deployed()
        const expected = "The owner changed the message";

        await greeter.setGreeting(expected);
        const actual = await greeter.greet();

        assert.equal(actual, expected, "greeting updated");
      });
    });

  describe("when message is sent by another account", () => {
    it("does not set the greeting", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = await greeter.greet();

      try {
        await greeter.setGreeting("Not the owner", { from: accounts[1] });
      } catch(err) {
        const errorMessage = "Ownable: caller is not the owner"
        assert.equal(err.reason, errorMessage, "greeting should not update");
        return;
      }
      assert(false, "greeting should not update");
    });
  });
})

En nuestras pruebas actualizadas, volvemos a pasar el parámetro accounts a la función de devolución de llamada contract para que estén disponibles para nuestros casos de prueba. También hemos añadido una segunda sección para el caso en que una cuenta no propietaria envíe el mensaje. Ahora esperamos que se produzca un error cuando una dirección no propietaria intente realizar este cambio. Si te fijas en la llamada a setGreeting, ahora se pasa un segundo parámetro; es un objeto con una propiedad from, y estamos enviando explícitamente este mensaje desde una cuenta diferente. Así es como también podríamos establecer un value (en unidades de wei) para enviarlo al contrato.

Al ejecutar nuestras pruebas debería producirse un fallo como éste:

1 failing

1) Contract: Greeter: update greeting
     setGreeting(string)
       when message is sent by another account
         does not set the greeting:
   AssertionError: greeting should not update

Vamos a crear y aplicar nuestro modificador para arreglar esto. De vuelta en nuestro contrato Greeter, después de el constructor, añade el siguiente código:

modifier onlyOwner() {
  require(
    msg.sender == _owner,
    "Ownable: caller is not the owner"
  );
  _;
}

La sintaxis de los modificadores se parece mucho a la sintaxis de las funciones, pero sin la declaración de visibilidad. Aquí, nuestro modificador utiliza la función require, donde el primer argumento es una expresión que se evaluará como un booleano. Cuando esta expresión da como resultado un falso, la transacción se revierte por completo, es decir, se anulan todos los cambios de estado y el programa detiene la ejecución. La función revert también toma un parámetro de cadena opcional que puede utilizarse para dar más información a quien llama sobre por qué falló la operación.

La última parte de nuestra función modificadora es la línea _;. Esta línea es donde se llamará a la función que se está modificando. Si pones algo después de esta línea, se ejecutará después de que finalice el cuerpo de la función.

Ahora vamos a actualizar nuestra función setGreeter para que utilice el modificador añadiendo la declaración onlyOwner después de external en la definición de la función:

function setGreeting(string calldata greeting) external onlyOwner {
  _greeting = greeting;
}

Ejecutando nuestras pruebas, ¡vemos que todas pasan!

Esto está muy bien, pero vamos a hacer un último cambio antes de terminar este capítulo. En el Capítulo 2, hablamos de OpenZeppelin y de cómo han creado contratos que pueden utilizarse como base para crear fichas. Pues bien, también tienen contratos que implementan la idea de propiedad, y nosotros estamos duplicando parte de ese comportamiento. En lugar de duplicarlo, actualizaremos nuestro contrato Greeter para aprovechar su implementación.

De vuelta a nuestro terminal, en el directorio raíz de nuestra aplicación, introduce el siguiente comando:

$ npm install openzeppelin-solidity

Una vez completado esto, actualiza la parte superior del contrato Greeter para que tenga el aspecto del Ejemplo 4-16.

Ejemplo 4-16. Heredar de Ownable
pragma solidity >= 0.4.0 < 0.7.0;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract Greeter is Ownable {
  ...rest of file...

Aquí hemos añadido una sentencia import que extraerá todos los símbolos globales del archivo importado, como Ownable, y los pondrá a disposición del ámbito actual. Lo siguiente que hay que observar es que nuestro contrato Greeter hereda ahora de Ownable mediante la sintaxis is. Solidity admite la herencia múltiple al igual que Python o incluso C++. Las clases que heredan se enumeran separadas por una coma.

Con esto en su sitio, podemos eliminar nuestras implementaciones del modificador onlyOwner, la función getter owner y la función constructora, ya que Ownable proporciona estas definiciones. Esto puede parecer exagerado, y normalmente estaría de acuerdo con ese pensamiento. Sin embargo, utilizar código que haya sido sometido a una auditoría exhaustiva y bien probado es prudente cuando se trabaja con contratos inteligentes, ya que la seguridad de los contratos inteligentes es de vital importancia.

Con esto terminamos nuestro contrato Greeter. Pero antes de seguir adelante, reflexionemos sobre lo que hemos aprendido en este capítulo.

Resumen

Hasta ahora en nuestro viaje, hemos aprendido a crear un nuevo proyecto de contrato inteligente utilizando truffle init, que proporcionó la estructura de directorios para nuestra aplicación. Esta estructura incluye directorios para alojar nuestros contratos, pruebas y migraciones.

Escribimos una prueba de implementación que nos ayudó a guiarnos a través de la migración inicial necesaria para introducir nuestro contrato en la red de pruebas. Esto permitió que nuestras pruebas posteriores interactuaran con el contrato desplegado.

Por último, empezamos a explorar el lenguaje Solidity y conocimos los distintos modificadores de visibilidad de las funciones (external, public, internal, private). Esos mismos modificadores también están disponibles para las variables de estado (con la excepción de external) utilizadas en la persistencia de datos en la blockchain. También implementamos el concepto de Ownable y lo refactorizamos para utilizar el contrato OpenZeppelin mediante herencia.

En el próximo capítulo, vamos a ver cómo implementar nuestro contrato localmente y también en una de las redes de prueba disponibles públicamente.

Get Desarrollo práctico de contratos inteligentes con Solidity y Ethereum now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.