Cómo crear una Wallet Multifirma con Solidity y React

ReactSolidity

En este tutorial aprenderás a crear una wallet multifirma, también conocida como wallet multisig, en la red the Ethereum con Solidity. Una wallet multifirma es una wallet que solamente permite gastar Ether si dos o más direcciones aprueban la transacción.

Crearemos el Smart Contract usando Remix y en esta ocasión pondremos el código en producción. Primero tendremos que aprobar una serie de direcciones determinadas. Cualquiera de estas direcciones podrá solicitar la realización de una transferencia. Tras ello, las otras direcciones podrán aprobar o no la transferencia. Cuando una determinada cantidad de direcciones haya aprobado la transferencia, entonces se enviarán los Ethers al destinatario de la misma.

Tienes disponible el repositorio con el código del proyecto en el siguiente enlace:

Requisitos

Antes de comenzar este tutorial, es recomendable que tengas ciertos fundamentos básicos de Solidity. Si no los tienes, echa primero un ojo al siguiente tutorial:

También necesitarás tener Node.js y el gestor de paquetes npm instalados en tu sistema. Si no tienes estas herramientas instaladas, puedes consultar cómo instalar Node.js. También deberías saber moverte por la línea de comandos. Aunque no es imprescindible, deberías saber de qué va React. Para aprender lo básico, puedes consultar los siguientes tutoriales:

Para crear el Smart Contract usaremos Remix, el IDE online de Solidity más utilizado, aunque luego importarás el contrato a sistema.

Creación del Smart Contract

Vamos a comenzar creando el Smart Contract de la wallet. Se trata de un contrato sencillo en donde únicamente nos tendremos que preocupar de las direcciones que pueden aprobar transferencias realizadas por la Wallet. Para crearlo, crea un archivo llamado Wallet.sol con la declaración del contrato en su interior:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Wallet
{
    // Código
}

Comenzaremos declarando la propiedad que contendrá las direcciones validadores del contrato, que será un array al que llamaremos direccionesValidadoras. También almacenaremos el número mínimo de validaciones en la variable minValidaciones, de tipo uint. A este último número también se le conoce como quorum:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Wallet
{
    uint public minValidaciones;
    address[] public direccionesValidadoras;

    // Código
}

Además, también tendremos que declarar la piedra angular del contrato, que es la estructura de las transferencias que los dueños de las wallet deseen enviar para su aprobación. Esta estructura recibirá el nombre de Transferencia y la declararemos usando la sentencia struct. Esta estructura contendrá los siguientes valores:

  • ID: La propiedad id será de tipo uint contendrá el identificador de la transferencia.
  • Cantidad: La propiedad cantidad será de tipo uint y contendrá la cantidad de Ethers a enviar.
  • Destinatario: La propiedad destinatario será de tipo address y contendrá la dirección  a la que se envían los Ethers. Debe declararse usando el modificador payable, ya que nuestro contrato podrá enviar Ethers a esta dirección.
  • Número de aprobaciones: La propiedad numAprobaciones, de tipo uint, contendrá el número de aprobaciones que las direcciones del contrato han realizado.
  • Enviada: La propiedad enviada es un flag de tipo bool que nos dirá si la transferencia ha sido enviada o no.

Hemos definido la estructura de las transferencias, pero necesitamos almacenarlas en algún lado. Para ello usaremos un array al que llamaremos transferencias, que contendrá valores de tipo Transferencia:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Wallet
{
    uint public minValidaciones;
    address[] public direccionesValidadoras;
    struct Transferencia
    {
        uint id;
        uint cantidad;
        address payable destinatario;
        uint numAprobaciones;
        bool enviada;
    }
    Transferencia[] public transferencias;
    mapping(address => mapping(uint => bool)) public aprobaciones;

    // Código
}

Tal y como ves, también hemos agregado un mapping al que hemos dado el nombre de aprobaciones. Usaremos este mapping para almacenar  la aprobaciones realizadas por cada dirección. Por ello, la clave del mapping será de tipo address, mientras que su valor será de tipo mapping. Es decir; usaremos otro mapping para así enlazar cada dirección con múltiples transferencias. La clave de este segundo mapping anidado será el identificador de la transferencia, de tipo uint, mientras que el valor será de tipo bool, indicando así si una dirección ha aprobado una transferencia o no.

Creación del constructor

En este apartado crearemos el constructor para el contrato, en donde inicializaremos las variables minValidaciones y direccionesValidadoras:

constructor(address[] memory _direccionesValidadoras, uint _minValidaciones)
{
    minValidaciones = _minValidaciones;
    direccionesValidadoras = _direccionesValidadoras;
}

La declaración de este constructor implica que cuando pongamos en marcha el contrato, tendremos que proporcionar tanto la lista de direcciones como el número mínimo de direcciones que necesitan validar las transferencias para que sean aprobadas.

Obtención de las direcciones validadoras

A continuación vamos a crear una función que nos permita obtener las direcciones validadoras, contenidas en un array. Dado que hemos declarado la propiedad direccionesValidadoras como pública, Solidity creará automáticamente una función get para obtener su valor. Sin embargo, esta función solamente nos permitirá obtener direcciones específicas del array, cuando lo que queremos es obtenerlas todas.

Para obtener todas las direcciones validadoras crearemos la función getDireccionesValidadoras, que definimos con el modificador external, ya que se podrá acceder a ella desde fuera del contrato. También la declaramos como view, indicando que la función podrá leer la blockchain pero no modificar su estado, siendo una función de solo lectura. Mediante la sentencia returns indicamos que la función debe devolver un array que contiene elementos de tipo address:

function getDireccionesValidadoras() external view returns(address[] memory)  
{
    return direccionesValidadoras;
}

Creación de una transferencia

Ahora vamos a crear otra función que nos permita crear transferencias desde la wallet. Solamente una dirección de las contenidas en la propiedad direccionesValidadoras llamará a esta función. En concreto, la llamará cuando su dueño quiera sugerir una nueva transferencia que podrá o no ser aprobada.

Llamaremos a esta función crearTransferencia, aceptando como argumentos cantidad y destinatario, conteniendo la cantidad a enviar y la dirección del destinatario respectivamente:

function crearTransferencia(uint cantidad, address payable destinatario) external
{
    transferencias.push(Transferencia(
        transferencias.length,
        cantidad,
        destinatario,
        0,
        false
    ));
}

Lo que hace esta función es instanciar una nueva transferencia e insertarla en el array transferencias. Como identificador de las transferencias hemos usado la longitud del array de transferencias. Este identificador lo obtenemos usando la propiedad length del array, que inicialmente contiene el valor 0. Cuando insertemos la primera transferencia este valor pasará a ser 1 y así sucesivamente, incrementándose en una unidad.

Obtención de las transferencias

Vamos a crear una función que nos permita obtener la lista de transferencias que hemos almacenado. A esta función le daremos el nombre de getTransferencias, y la definiremos con los modificadores external y view, al igual que la función getDireccionesValidadoras. Mediante la sentencia returns indicaremos que la función devuelva un array de elementos de tipo Transferencia:

function getTransferencias() external view returns(Transferencia[] memory)  
{
    return transferencias;
}

Validando una transferencia

Vamos a crear una función que nos permita aprobar una transferencia. Para ello vamos a definir una nueva función llamada aprobarTransferencia, que recibirá el identificador de la transferencia como argumento. En esta función, almacenaremos que la dirección que ejecuta la función ha aprobado la transferencia. También incrementaremos el número de probaciones. Si el número de aprobaciones es el requerido para enviar la transferencia, entonces la enviaremos a la dirección de destino.

Lo primero que haremos en esta función es usar la sentencia require para validar que la transferencia no se haya enviado ya. Del mismo modo, también la usaremos para comprobar que la transferencia no haya sido aprobada.

Luego estableceremos que la dirección que ha ejecutado la función ha aprobado la transferencia, estableciendo el elemento correspondiente del mapping de aprobaciones como true. También incrementaremos el número de aprobaciones para la transferencia referenciada, contenida en el array transferencias. Si el número de aprobaciones contenido en la propiedad numAprobaciones de la transferencia es todavía menor que el número de validaciones necesarias para aprobar la transferencia, entonces usamos la sentencia return para salir de al función.

function aprobarTransferencia(uint id) external
{
    require(transferencias[id].enviada == false, 'La transferencia ya se ha enviado');
    require(aprobaciones[msg.sender][id] == false, 'Ya has aprobado la transferencia');
    aprobaciones[msg.sender][id] = true;
    transferencias[id].numAprobaciones++;

    if (transferencias[id].numAprobaciones < minValidaciones) return;
        
    address payable destinatario = transferencias[id].destinatario;
    uint cantidad = transferencias[id].cantidad;
    destinatario.transfer(cantidad);
    transferencias[id].enviada = true;
}

Si ya se han alcanzado las aprobaciones requeridas, entonces obtenemos la dirección del destinatario de la transferencia así como la cantidad a enviar, usado la sentencia destinatario.transfer para enviar la cantidad especificada en la propiedad cantidad.

Finalmente, establecemos que la transferencia ha sido enviada estableciendo a true el valor de la propiedad enviada de la misma.

A veces creo que sobreexplico y que sueno demasiado pedante a quien me lee. Por otro lado veo en los comentarios que son precisamente estas explicaciones las que se agradecen, así que prefiero pecar de explicar en exceso. Recuerda siempre que el objetivo de estos tutoriales no es el de completarlos, sino el del valor agregado de hacer tus propias modificaciones cuando lo termines.

Recepción de Ethers

Vamos a crear una función que nos permita agregar ETH al contrato, ya que después de todo estamos creando una wallet y de algún lugar han de venir los fondos.

Ya hemos visto un tutorial acerca de cómo recibir ETH en Solidity. Por ello, podríamos crear un función que reciba los ETH, definida con los modificadores external y payable. Sin embargo, por convención, usaremos la función especial receive, definida sin la sentencia function aunque con los modificadores external y payable:

receive() external payable {}

Esta función se ejecutará automáticamente al intentar enviar ETH al contrato.

Control de acceso

Hemos creado todas las funciones que vamos a necesitar en nuestro Smart Contract para este proyecto. Sin embargo, ahora mismo cualquier poseedor de una dirección en la red podrá ejecutar las funciones externas crearTransferencia y aprobarTransferencia.

En este apartado vamos a centrarnos en el control de acceso a las funciones crearTransferencia y aprobarTransferencia. Para limitar el acceso a dichas funciones a los usuarios que estén en el array direccionesValidadoras, vamos a crear un modificador de función. Luego aplicaremos el modificador a las funciones que hemos mencionado. Bastará con crear un único modificador, ya que la lógica de acceso a ambas funciones es la misma.

Un modificador es una porción de código que puedes agregar a otras funciones, de modo que sea un bloque compartido. Vamos a crear el modificador puedeAprobar.

modifier puedeAprobar()
{
    bool accesoPermitido = false;
    for (uint i = 0; i < direccionesValidadoras.length; i++) {
        if (direccionesValidadoras[i] == msg.sender) {
            accesoPermitido = true;
            break;
        }
    }

    require(accesoPermitido == true, 'Acceso denegado');
    _;
}

Lo que hacemos es comprobar si la dirección que ejecuta la función se encuentra entre las direcciones que pueden aprobar transferencias. Si lo está, damos el valor true a la variable accesoPermitido. Luego, usamos la sentencia require para comprobar su valor, devolviendo un mensaje de error si no lo está. Finalmente incluimos el código de la función usando el marcador _.

Para saber más acerca de los modificadores de funciones en Solidity, consulta el tutorial en donde explico cómo se usan los modificadores de función en Solidity.

Recapitulando

Ya hemos terminado nuestro contrato. A continuación puedes ver el código completo:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Wallet
{
    uint public minValidaciones;
    address[] public direccionesValidadoras;
    struct Transferencia
    {
        uint id;
        uint cantidad;
        address payable destinatario;
        uint numAprobaciones;
        bool enviada;
    }

    Transferencia[] public transferencias;
    mapping(address => mapping(uint => bool)) public aprobaciones;

    constructor(address[] memory _direccionesValidadoras, uint _minValidaciones)
    {
        minValidaciones = _minValidaciones;
        direccionesValidadoras = _direccionesValidadoras;
    }

    function getDireccionesValidadoras() external view returns(address[] memory)  
    {
        return direccionesValidadoras;
    }

    function crearTransferencia(uint cantidad, address payable destinatario) external puedeAprobar()
    {
        transferencias.push(Transferencia(
            transferencias.length,
            cantidad,
            destinatario,
            0,
            false
        ));
    }

    function getTransferencias() external view returns(Transferencia[] memory)  
    {
        return transferencias;
    }

    function aprobarTransferencia(uint id) external puedeAprobar()
    {
        require(transferencias[id].enviada == false, 'La transferencia ya se ha enviado');
        require(aprobaciones[msg.sender][id] == false, 'Ya has aprobado la transferencia');
        aprobaciones[msg.sender][id] = true;
        transferencias[id].numAprobaciones++;

        if (transferencias[id].numAprobaciones < minValidaciones) return;
        
        address payable destinatario = transferencias[id].destinatario;
        uint cantidad = transferencias[id].cantidad;
        destinatario.transfer(cantidad);
        transferencias[id].enviada = true;
    }

    receive() external payable {}

    modifier puedeAprobar()
    {
        bool accesoPermitido = false;
        for (uint i = 0; i < direccionesValidadoras.length; i++) {
            if (direccionesValidadoras[i] == msg.sender) {
                accesoPermitido = true;
                break;
            }
        }

        require(accesoPermitido == true, 'Acceso denegado');
        _;
    }
}

Creación del proyecto con Truffle

Ya hemos creado nuestro contrato, pero todavía tenemos que probarlo. Sí; podrías probarlo con Remix, pero esto no es lo que harás con un contrato real por diversos motivos. Uno de ellos es que los contratos son inmutables, lo que significa que una vez desplegado el contrato no podrás modificarlo, para bien o para mal. Otro motivo es que si introduces un error o expones el contrato a un hackeo, las pérdidas podrían ser enormes.

Los Smart Contracts nos dan ciertas garantías de funcionalidad, pero como programadores nos expone a una serie de riesgos a evitar, ya que no podremos arreglar los errores.

Instalación de Truffle

Truffle es una herramientas que nos permite testear Smart Contracts. Si no tienes instalado Truffle en tu sistema, puedes instalarlo en tu sistema usando el siguiente comando:

npm i -g npm

Para más información acerca de la instalación de Truffle, consulta cómo instalar Truffle.

Inicialización del proyecto

Vamos a crear un directorio para el proyecto al que llamaremos wallet-multifirma, así que créalo usando la interfaz de tu sistema o abre una ventana de línea de comandos y ejecuta el siguiente comando:

mkdir wallet-multifirma

Luego accede al directorio usando el comando cd e inicia un proyecto con Truffle usando el siguiente comando:

truffle init

Truffle creará los directorios del proyecto, que son los siguientes:

  • contracts: Aquí incluirás los Smart Contracts creados con Solidity.
  • migrations: Aquí incluirás el archivo de migración que usará  el frontend de tu aplicación, de modo que pueda conocer la estructura de los diferentes contratos.
  • test: Aquí incluirás los tests del Smart Contract.

Truffle también creará el archivo truffle-config.js, en donde indicarás la versión de Solidity que usarás el proyecto. En este archivo también se incluyen diversos parámetros de configuración de la blockchain de desarrollo local que se incluyen con Truffle.

Configuración del proyecto

Vamos a configurar algunos parámetros del proyecto, como por ejemplo al versión de Solidity que se usa.

Para este proyecto usamos al versión 0.8.17 de Solidity, así que edita el archivo truffle-config.js con tu editor preferido y busca la sección compilers. Debes asegurar de que el valor de la opción version tiene el valor 0.8.17.

Ahora vamos a crear un nuevo contrato en donde incluiremos el código del contrato que hemos creado con Remix. Para ello crea el archivo Wallet.sol en el interior del directorio /contracts. Luego editar el archivo Wallet.sol con tu editor preferido, que en mi caso es VS Code, y pega el código del contrato que has creado con Remix.

Ahora sitúate en el directorio raíz del proyecto e inicializa el proyecto con Node.js usando el comando el siguiente comando:

npm init -y

Ahora vamos a instalar un paquete que nos va a ayudar a la hora de crear los tests. Se trata de los test helpers de OpenZeppelin. Para instalarlos usa el siguiente comando:

npm install @openzeppelin/test-helpers

Con esto ya hemos configurado el proyecto.

Testeo del proyecto con Truffle

Vamos a ver cómo crear tests para nuestro proyecto. Los tests en Truffle se crean usando JavaScript, por lo que los archivos de los tests tendrán la extensión .js. Para empezar, crea el archivo wallet.js en el interior del directorio /test y edítalo.

A pesar de trabajar con JavaScript, tendrás que importar los contratos creados con Solidity. Para ello tendrás que inyectarlos, importándolos en el archivo JavaScript como un artefacto, usando la sentencia artifacts.require.

Para empezar a crear los test, vamos a importar el contrato Wallet.sol en el interior del archivo wallet.js mediante la siguiente sentencia:

const Wallet =  artifacts.require('Wallet');

Luego importa el helper expectRevert de OpenZeppelin:

const { expectRevert } = require('@openzeppelin/test-helpers');

Ahora vamos a definir el código que se ejecutará antes de cada test. Básicamente, tendremos que crear una nueva instancia del contrato. Si consultas el constructor del contrato, tendrás que proporcionar una serie de direcciones validadores mediante le argumento _direccionesValidadoras y también el número mínimo de validaciones mediante el argumento _minValidaciones.

Para referenciar el contrato usamos la sentencia contract, que acepta el contrato como primer argumento y una función callback como segundo argumento:

contract('Wallet', (cuentas) => {
  let wallet;
  beforeEach(async () => {
      // Código
  });
});

En la blockchain que usa Truffle cuando se ejecutan los tests, también se crean 10 direcciones de ejemplo con cierta cantidad de ETH. Podemos obtener estas direcciones mediante el primer parámetro de la función callback, al pasarle la variable cuentas.

En el interior de la función callback hemos usado una sentencia beforeEach, que nos permite especificar la función callback que se ejecutará antes de cada test que vayamos a crear. Esta función tendrá que ser asíncrona, por lo que la hemos definido como async, ya que tal y como veremos a continuación, usaremos la sentencia await en su interior. Si no estás acostumbrado a usar programación asíncrona, te recomiendo que consultes el tutorial de programación asíncrona en JavaScript antes de continuar.

Lo que vamos a hacer en el interior de la función callback es desplegar el contrato a la blockchain de Truffle. Para ello usamos el método new del contrato que hemos importado como un artefacto. Ahora debes echar un ojo al constructor del contrato que hemos creado con Solidity. Tal y como puedes ver, acepta un array de direcciones validadoras como primer argumento y el número mínimo de validaciones como segundo argumento.

Lo que vamos a hacer, sobre el código anterior, es pasar un array como primer parámetro con tres de las direcciones que Truffle nos proporciona. Como segundo argumento proporcionamos un número entero que representará el número de validaciones, que por ejemplo puede ser 2:

contract('Wallet', (cuentas) => {
  let wallet;
  beforeEach(async () => {
    wallet = await Wallet.new([cuentas[0], cuentas[1], cuentas[2]], 2);
    // Código
  });
});

Ahora vamos a crear una transacción en el contrato para agregar cierta cantidad de ETH al mismo. Para ello vamos a usar web3, que es una librería que usaremos en el frontend de esta aplicación, de la que quizás ya hayas escuchado hablar:

contract('Wallet', (cuentas) => {
  let wallet;
  beforeEach(async () => {
      wallet = await Wallet.new([cuentas[0], cuentas[1], cuentas[2]], 2);
      await web3.eth.sendTransaction({ from: cuentas[0], to: wallet.address, value: 10000});
  });
  // Tests
});

Lo que hemos hecho ha sido usar el método sendTransaction, que acepta un objeto como argumento:

  • Mediante el campo from especificamos la dirección que realiza la transferencia, que en este caso será cuentas[0], que es la primera dirección del array cuentas generado por Truffle.
  • Mediante el campo to especificamos el destinatario, que en este caso será wallet.address, que es la propia dirección de nuestra wallet.
  • Mediante el campo value especificamos la cantidad a enviar en GWEI, introduciendo 10000 a modo de ejemplo, que es una pequeña cantidad. Para más información acerca de las diferentes unidades de ETH, consulta el tutorial en donde explico cómo enviar ETH en la red Ethereum. Y no se por qué, pero acabo de recordar esta canción.

Una vez hemos configurado el código que se ejecutará antes de cada test, es hora de comenzar a crearlos.

Testeando el constructor

Si echas un ojo al constructor del contrato que hemos creado en el archivo Wallet.sol, verás que acepta como parámetros el array _direccionesValidadoras y el número _minValidaciones.  Lo que vamos a hacer es testear que estos valores sean los correctos una vez desplegado el contrato en la blockchain.

Edita el archivo wallet.js y continúa en donde lo hemos dejado en el apartado anterior, justo después de bloque beforeEach, que es en donde creamos cada test. Para cada test vamos a usar la función it, que acepta como argumentos una descripción del test y una función asíncrona con el código del test.

En el primer test vamos a usar la función getDireccionesValidadoras del contrato para obtener las direcciones del array direccionesValidadoras del contrato.

También vamos a usar la función minValidaciones para obtener el contenido de la propiedad minValidaciones del contrato. Dado que esta propiedad es pública, Solidity creará automáticamente por nosotros la función minValidaciones, que nos permitirá obtener su valor. Luego usaremos la sentencia assert para comprobar que el número de direcciones validadoras sea igual a 3, que es el número que hemos definido al desplegar el contrato. También comprobaremos que cada una de las direcciones del array direccionesValidadoras del contrato se corresponda con cada una de las direcciones del array cuentas de Truffle. Finalmente, comprobaremos que el en valor de la propiedad minValidaciones se corresponda con el valor que hemos pasado al constructor al desplegar el contrato:

it ('Debe tener valores correctos para las direcciones validadores y el número mínimo de validaciones', async () => {
  const direccionesValidadoras = await wallet.getDireccionesValidadoras();
  const minValidaciones = await wallet.minValidaciones();

  assert(direccionesValidadoras.length === 3);
  assert(direccionesValidadoras[0] === cuentas[0]);
  assert(direccionesValidadoras[1] === cuentas[1]);
  assert(direccionesValidadoras[2] === cuentas[2]);
  assert(minValidaciones.toNumber() === 2);
});

Tal y como ves, hemos usado el método toNumber con el  número minValidaciones. El motivo es que los números de Solidity no siempre se corresponden con los de JavaScript, pudiendo existir errores de desbordamiento. Por ello, Truffle convierte los valores numéricos de Solidity a instancias de una clase proporcionada por esta librería. Para obtener su valor usamos el método toNumber.

Para testear el contrato y que se ejecute el test que hemos creado, accede de nuevo a la línea de comandos y usa el comando truffle test:

Este es el código del archivo wallet.js con lo que hemos creado hasta ahora:

const { assert } = require("console");

const Wallet =  artifacts.require('Wallet');

contract('Wallet', (cuentas) => {
  let wallet;
  beforeEach(async () => {
      wallet = await Wallet.new([cuentas[0], cuentas[1], cuentas[2]], 2);
      await web3.eth.sendTransaction({ from: cuentas[0], to: wallet.address, value: 10000});
  });

  it ('Debe tener valores correctos para las direcciones validadores y el número mínimo de validaciones', async () => {
    const direccionesValidadoras = await wallet.getDireccionesValidadoras();
    const minValidaciones = await wallet.minValidaciones();

    assert(direccionesValidadoras.length === 3);
    assert(direccionesValidadoras[0] === cuentas[0]);
    assert(direccionesValidadoras[1] === cuentas[1]);
    assert(direccionesValidadoras[2] === cuentas[2]);
    assert(minValidaciones.toNumber() === 2);
  });
});

A continuación vamos a crear tests para cada una de las funciones de nuestro contrato.

Testeando la función crearTransferencia

Vamos a tester la función crearTransferencia, que permite que las direcciones del array direccionesValidadoras pueden crear transferencias, aceptando como argumentos la cantidad a enviar y el destinatario.

En esta función hemos usado el modificador puedeAprobar, así que debemos comprobar que el modificador funciona correctamente. Del mismo modo, también debemos comprobar que se devuelva un mensaje de error cuando una dirección que no está en el array de direcciones validadoras llame a esta función.

Vamos a crear el siguiente test y luego lo explicaremos:

it ('Debe crear una transferencia', async () => {
  await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
  const transferencias = await wallet.getTransferencias();
  assert(transferencias.length === 1);
  assert(transferencias[0].id === '0');
  assert(transferencias[0].cantidad === '1000');
  assert(transferencias[0].destinatario === cuentas[3]);
  assert(transferencias[0].numAprobaciones === '0');
  assert(transferencias[0].enviada === false);
});

Lo primero que hemos hecho ha sido crear una transferencia de prueba usando la función crearTransferencia, a la que le pasamos 1000 como cantidad, que especificamos en su primero parámetro. Recuerda que al desplegar el contrato hemos agregado 10000, así que no podemos superar ese valor. Como segundo parámetro pasamos la dirección del destinatario, que será cuentas[3], por escoger alguna del array de Truffle que no esté entre las direcciones validadoras del contrato. Como último parámetro, Truffle nos permite personalizar ciertas opciones, como quién ejecuta la función. Para ello usamos un objeto, estableciendo una de las direcciones validadoras como valor from, que en nuestro caso es cuentas[0], aunque podríamos seleccionar cualquier da las otras dos:

await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});

Una vez agregada la transferencia, la hemos obtenido del array de transferencias del contrato usando el método getTransferencias:

const transferencias = await wallet.getTransferencias();

Luego hemos comprobado que la longitud del array de transferencias obtenido sea de 1, ya que hemos agregado una sola transferencia:

assert(transferencias.length === 1);

Luego comprobamos los valores de la transferencia. Primero comprobamos que el identificador o id de la transferencia debe ser 0 luego que el valor de la propiedad cantidad sea 1000, que el destinatario sea cuentas[3], que el número de aprobaciones contenido en la propiedad numAprobaciones sea 0 y que el valor de la propiedad enviada sea false:

assert(transferencias[0].id === '0');
assert(transferencias[0].cantidad === '1000');
assert(transferencias[0].destinatario === cuentas[3]);
assert(transferencias[0].numAprobaciones === '0');
assert(transferencias[0].enviada === false);

Hemos comparado los calores numéricos de la transacción con una cadena que incluye los dígitos entre comillas. Esto ocurre porque los números de Solidity no son compatibles con los de JavaScript. En el caso de las estructuras, Truffle no nos devuelve instancias creadas por la librería antes mencionada, sino cadenas.

Si ahora regresas a la línea de comandos y ejecutas el comando truffle test, deberías ver cómo este test también se cumple:

Por ahora hemos creado un test en el que la dirección desde la que llamamos a la función crearTransferencia se encuentra en el array direccionesValidadoras. Sin embargo todavía tenemos que testear el resultado de la función cuando la llamamos desde una dirección que no se encuentra en el array.

Dado que hemos usado el modificador puedeAprobar, debemos comprobar que obtenemos el mensaje Acceso denegado como respuesta. Vamos a agregar un nuevo test en el que vamos a crear la transferencia desde la dirección cuentas[4]. Gracias al helper expectRevert comprobaremos que se devuelva el mensaje Acceso denegado como resultado:

it ('No se debe crear una transferencia si el sender no es una dirección validadora', async () => {
  await expectRevert(
    wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[4]}),
    'Acceso denegado'
  );
});

Si ahora usas el comando truffle test en la consola, deberías ver que los tres tests se completan con éxito:

Y con esto ya hemos terminado de testear esta función.

Testeando la función aprobarTransferencia

La función aprobarTransferencia permite que las direcciones validadoras puedan aprobar las transferencias. Una vez se alcance el número de validaciones mínimo establecida en la propiedad minValidaciones del contrato, se enviará la transferencia.

Vamos a testear primero el caso en el que enviamos una transferencia cuando aún no se ha alcanzado el número mínimo de validaciones necesarias para enviarla:

it ('Se debe incrementar el número de aprobaciones', async () => {
  await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
  await wallet.aprobarTransferencia(0, {from: cuentas[0]});
  const transferencias = await wallet.getTransferencias();
  const balance = await web3.eth.getBalance(wallet.address);

  assert(transferencias[0].numAprobaciones === '1');
  assert(transferencias[0].enviada === false);
  assert(balance === '10000');
});

Lo primero que hemos hecho ha sido crear una transferencia de 1000 GWEI. Luego hemos aprobado la transferencia desde la dirección cuantas[0], que está entre la direcciones validadoras. Luego hemos obtenido las transferencias del contrato en la constante transferencias y también el balance del contrato en la constante balance, usando en este último caso la función getBalance disponible en la librería web3.

Seguidamente hemos comprobado que el número de aprobaciones contenido en la propiedad numAprobaciones de la transferencia se ha incrementado. También nos hemos asegurado de que la propiedad enviada sigue teniendo el valor false, ya que solamente se ha realizado una aprobación. Finalmente hemos comprobado que el balance del contrato sigue siendo de 10000 GWEI.

Regresa a la terminal y ejecuta de nuevo el comando truffle test:

A continuación vamos a comprobar qué ocurre cuando se aprueba una transferencia y se alcanza el número mínimo de aprobaciones, momento en el que se debe enviar la transferencia.

En este caso tenemos que comprobar el balance inicial y el final de la dirección a la que enviamos la transferencia, comprobando que se ha incrementado. También tendremos que comprobar el número de aprobaciones de la transferencia y que la propiedad enviada tenga el valor true:

it ('Se debe enviar la transferencia cuando se alcanza el número mínimo de aprobaciones', async () => {
  const balanceInicialDestinatario =  web3.utils.toBN(await web3.eth.getBalance(cuentas[3]));

  await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
  await wallet.aprobarTransferencia(0, {from: cuentas[0]});
  await wallet.aprobarTransferencia(0, {from: cuentas[1]});

  const balanceFinalDestinatario =  web3.utils.toBN(await web3.eth.getBalance(cuentas[3]));
  const transferencias = await wallet.getTransferencias();
  const balance = await web3.eth.getBalance(wallet.address);

  assert(balanceFinalDestinatario.sub(balanceInicialDestinatario).toNumber() === 1000);
  assert(transferencias[0].numAprobaciones === '2');
  assert(transferencias[0].enviada === true);
  assert(balance === '9000');
});

Primero hemos obtenido el balance inicial de la dirección cuentas[3] en la constante balanceInicialDestinatario. Nos interesa obtener un valor numérico, así que hemos usado el helper toBN, de modo que obtengamos un objeto de tipo BN, capaz de soportar los valores numéricos de Solidity. Si has usado el framework Laravel de PHP alguna vez, los objetos BN son algo así como lo equivalente a los objetos de tipo Carbon cuando trabajas con fechas:

const balanceInicialDestinatario = web3.utils.toBN(await web3.eth.getBalance(cuentas[3]));

Luego hemos creado la transferencia usando la función crear crearTransferencia y luego la hemos aprobado dos veces usando la función aprobarTransferencia, primero desde la dirección cuentas[0] y luego desde la dirección cuentas[1]:

await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
await wallet.aprobarTransferencia(0, {from: cuentas[0]});
await wallet.aprobarTransferencia(0, {from: cuentas[1]});

Tras aprobar la transferencia dos veces, se debe enviar, por lo que si ahora obtenemos el balance del destinatario en la constante balanceFinalDestinatario y restamos este valor al balance inicial, la diferencia ha de ser de 1000. Para la resta usamos el método sub de los objetos de tipo BN:

const balanceFinalDestinatario =  web3.utils.toBN(await web3.eth.getBalance(cuentas[3]));
assert(balanceFinalDestinatario.sub(balanceInicialDestinatario).toNumber() === 1000);

Finalmente, hemos obtenido la transferencia y el balance del contrato, comprobando que el número de aprobaciones de la transferencia es de 2, el valor de la propiedad enviada es true y que que en el balance se ha reducido en 1000 GWEI.

De nuevo, vuelve a la terminal y ejecuta de nuevo el comando truffle test:

Ahora vamos a comprobar qué ocurre cuando se aprueba desde una dirección que no está entre las validadoras. En este caso debemos comprobar que el acceso no está permitido usando la función expectRevert:

await expectRevert(
  wallet.aprobarTransferencia(0, {from: cuentas[4]}),
  'Acceso denegado'
);

Vamos a de nuevo a la terminal con el comando truffle test:

Ahora vamos a comprobar qué ocurre cuando la transferencia se aprueba desde una dirección validadora pero dicha dirección ya la ha aprobado previamente. Por ejemplo, vamos a ejecutar la función aprobarTransferencia desde la dirección cuentas[1] dos veces:

it ('No se debe aprobar la transferencia si el sender ya la ha aprobado', async () => {
  await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
  await wallet.aprobarTransferencia(0, {from: cuentas[1]}),

  await expectRevert(
    wallet.aprobarTransferencia(0, {from: cuentas[1]}),
    'Ya has aprobado la transferencia'
  );
});

Tal y como puedes ver, el test también se ejecuta correctamente:

Y ya solamente nos queda un último test, amig@. Vamos a comprobar que obtenemos el mensaje de error que hemos definido en el contrato cuando intentamos aprobar una transferencia que ya ha sido enviada:

it ('No se debe enviar la transferencia si ya ha sido aprobada', async () => {
  await wallet.crearTransferencia(1000, cuentas[3], {from: cuentas[0]});
  await wallet.aprobarTransferencia(0, {from: cuentas[0]}),
  await wallet.aprobarTransferencia(0, {from: cuentas[1]}),

  await expectRevert(
    wallet.aprobarTransferencia(0, {from: cuentas[2]}),
    'La transferencia ya se ha enviado'
  );
});

Ya solamente nos falta ejecutar el comando truffle test una última vez:

Y con esto ya hemos terminado de crear todos los tests necesarios.

Migración y despliegue local del contrato

Nuestra intención es la de crear una aplicación que nos permita interactuar con el contrato. Para desarrollar la aplicación necesitamos desplegar el contrato en una blockchain local usando Truffle. Esta blockchain se ejecuta en la memoria de tu sistema, así que no es algo que vaya a permanecer en tu dispositivo.

Crea el archivo de migración

El archivo de migración te permite especificar cómo quieres desplegar el contrato en la blockchain de Ethereum. Los tests que hemos creado ya usaban una blockchain local, aunque esto no es suficiente cuando queremos ejecutar una aplicación real.

Para crear el archivo de migración, crea el archivo 1_migracion_wallet.js en el directorio /migrations del proyecto. Hemos llamado al archivo de esta forma para que las migraciones se ejecuten en orden en caso de que agreguemos una migración de otro contrato, como podría ser 2_migracion_monedero.js.

Edita el archivo 1_migracion_wallet.js y comienza importando tanto la librería web3 como el contrato de la Wallet:

const { web3 } = require("@openzeppelin/test-helpers/src/setup");
const Wallet =  artifacts.require('Wallet');

Luego exporta la siguiente función asíncrona, que recibe como argumentos el deployer, que te permite desplegar el contrato, la información de la red mediante el argumento _network y las cuentas que crea Truffle en la blockchain local:

module.exports = async (deployer, _network, cuentas) => {
  await deployer.deploy(Wallet, [cuentas[0], cuentas[1], cuentas[2]], 2)
  const wallet = await Wallet.deployed();

  web3.eth.sendTransaction({from: cuentas[0], to: wallet.address, value: 10000});
}

Lo que hemos hecho en esta función es desplegar el contrato usando la función deploy, que recibe como primer argumento el contrato que se va a desplegar, que en nuestro caso es el contrato representado por el artefacto Wallet. Los siguientes argumentos son los que acepta el constructor del contrato, por lo que le pasamos un array con algunas de las cuentas que genera Truffle. También pasaremos el número mínimo de validaciones. Esto se corresponde con los argumentos _direccionesValidadoras y _minValidaciones del constructor.

Hemos usado await con el método deploy, ya que queremos esperar a que el contrato esté desplegado. Luego obtenemos la wallet una vez desplegada en la constante wallet. Finalmente, hemos usado la librería el método sendTransaction de la librería web3 para enviar cierta cantidad de GWEI desde la dirección cuentas[0] a la dirección del contrato.

Este es el contenido completo final del archivo 1_migracion_wallet.js:

const { web3 } = require("@openzeppelin/test-helpers/src/setup");
const Wallet =  artifacts.require('Wallet');

module.exports = async (deployer, _network, cuentas) => {
  await deployer.deploy(Wallet, [cuentas[0], cuentas[1], cuentas[2]], 2)
  const wallet = await Wallet.deployed();

  web3.eth.sendTransaction({from: cuentas[0], to: wallet.address, value: 10000});
}

Despliega el contrato localmente

Ahora vamos a usar la migración, desplegando el contrato en al blockchain local de Truffle. Para empezar, abre tu terminal y sitúate en el directorio raíz del proyecto. Luego usa el comando truffle develop:

Truffle mostrará por pantalla las 10 direcciones de prueba que crea en la blockchain local. También se muestran las claves privadas asociadas, en caso de que te hagan falta. Finalmente también se muestra la frase mnemónica de las direcciones, algo que será de utilidad cuando integremos la aplicación con metamask.

Recuerda que esta blockchain está en la memoria de tu sistema, por lo que se eliminará cuando cierres el proceso correspondiente o la ventana de la terminal de comandos.

Ahora ejecuta las migraciones usando el comando migrate --reset:

Mediante el uso del flag --reset nos aseguramos de que Truffle detecte cambios en los contratos y no despliegue las versiones anteriores que pueda tener en caché. Tal y como ves, se ha hecho el deploy del contrato y luego se han ejecutado las migraciones. Además también se han generado los artefactos del contrato en el directorio /contracts.

Podrás ver la dirección del contrato en la blockchain, que en este caso es la siguiente:

0xccef8d726ce1da33ef48689a7400449692e0c3fc0fd9686f62e9def587001494

También podrás ver el número de bloques y  el bloque actual, así como el balance del contrato una vez se le ha deducido el coste del despliegue, que ha sido de 0.003817290375 ETH, tal y como se especifica en la sección final.

Recuerda que es importante que mantengas esta ventana abierta para que no se elimine la blockchain de la memoria de tu sistema. Si la cierras, ejecuta de nuevo los comandos truffle develop y migrate --reset.

Información acerca del contrato

Tal y como hemos visto, cuando ejecutas el comando migrate, también se generan los artefactos de los contratos del proyecto. Se generará uno por contrato y podrás encontrarlos en el directorio /contracts. Si accedes a dicho directorio y abres el archivo Wallet.json, verás un archivo en formato JSON que incluye el nombre del contrato en la propiedad contractName, que en este caso es Wallet.

Seguidamente podrás ver la propiedad abi, que contiene la interfaz del contrato, especificando las diferentes funciones del mismo, sus argumentos y el tipo de dichos argumentos, entre otra cosas. También se incluye la especificación del constructor y de los modificadores que hayas agregado.

Este archivo no se usa directamente por la blockchain. La blockchain utiliza el código hexadecimal compilado de la sección bytecode.

La información del despliegue se encuentra en la propiedad networks, en donde encontrarás la dirección de la blockchain que se ha creado junto con un objeto que contiene la dirección del contrato dentro de la red, contenida en la propiedad address.

Creación el frontend de la aplicación

Ha llegado la hora de crear un frontend para la gestión de la wallet. Vamos a crear una aplicación descentralizada con React y Node.js. No estás obligado a usar React, así que si lo prefieres puedes usar HTML, aunque el uso de alguna librería como React o Vue se ha convertido en casi un estándar en este tipo de aplicaciones.

Para interactuar con el contrato creado con Solidity usaremos la librería Web3.js. Además, también integraremos metamask. No nos complicaremos demasiado con el diseño, así que usaremos el framework CSS Bootstrap. Si nunca has usado Boostrap y tienes tiempo, puedes consultar el tutorial de introducción a Bootstrap, aunque no te hará falta para seguir el tutorial. Basta con que sepas usar CSS.

Inicialización la aplicación

Vamos a usar la herramienta Create React App para inicializar la aplicación. Si no la tienes instalada en tu sistema, ejecuta el siguiente comando para instalarla de forma global:

npm install -g create-react-app

Una vez hayas instalado la herramienta, sitúate en el directorio raíz del proyecto y ejecuta el siguiente comando:

create-react-app frontend

Luego accede al directorio /frontend mediante la terminal y abre el directorio con tu IDE. En el archivo package.json podrás encontrar los paquetes de JavaScript instalados. Es importante que tengas en cuenta que estos paquetes pertenecen únicamente al frontend del proyecto y no al proyecto que has creado con Truffle.

No necesitas configurar Webpack, puesto que se incluye por defecto ya configurado con la herramientas Create React App. Webpack monitorizará los cambios en los archivos del proyecto, compilándolos cada vez que se produzca un cambio en los mismos. Si quieres saber más cosas acerca de Webpack, puedes consultar el tutorial de introducción a Webpack.

Dado que no es el objetivo de este tutorial el de crear algo bonito, vamos a usar el framework CSS Bootstrap. Para instalarlo, accede al directorio /frontend desde la terminal y ejecuta el siguiente comando:

npm install bootstrap

Luego edita el archivo /frontend/src/index.js e importa los siguientes archivos justo antes de la importación del archivo index.css:

import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.bundle.min";

Vamos a instalar también la librería Web3.js, ya que es la que nos permitirá interactuar con nuestro contrato:

npm install web3

También  una librería que nos permitirá detectar la red a usar desde Metamask:

npm install @metamask/detect-provider

Ahora vamos a crear un archivo de configuración que usaremos únicamente durante el desarrollo de la aplicación. Crea el archivo /frontend/.env y agrega la siguiente configuración en su interior:

REACT_APP_WEB3_PROVIDER=http://localhost:9545

Los archivos .env se usan par almacenar diversas opciones de configuración de las aplicaciones. En React, estos valores deben comenzar a la fuerza por el prefijo REACT_APP. Mediante el valor REACT_APP_WEB3_PROVIDER hemos definido la URL de conexión con Ganache. Se trata de la URL http://localhost:9545, que se muestra justo tras ejecutar el comando truffle develop.

Si prefieres configurar otro puerto, también puedes hacerlo siguiendo las instrucciones del archivo truffle-config.js.

Si usas GitHub, no te olvides de agregar el archivo .env al archivo .gitignore, ya que solo usaremos este valor localmente.

Luego edita el archivo /frontend/public/index.html y cambia el título que viene por defeco en la etiqueta title por cualquier otro, como por ejemplo Wallet Multifirma:

<title>Wallet Multifirma</title>

Ahora edita el archivo /frontend/src/App.js y elimina el código que se agrega por defecto, dejándolo así:

function App() {
  return (
    <div className="container">
      <header className="text-center">
        <h1 className="container mt-4">
          Wallet Multifirma
        </h1>
      </header>
    </div>
  );
}

export default App;

Tal y como ves, hemos aplicado algunos estilos básicos. Si quieres, puedes eliminar los archivos /frontend/src/App.css y /frontend/src/logo.svg, ya que no lo vamos a usar.

Ahora vamos a iniciar el servidor que viene con la herramienta Create React App. Para ello, sitúate en el directorio /frontend y ejecuta el siguiente comando:

npm start

Se debería abrir una ventana en tu navegador que apunta al puerto 3000 de tu localhost. Si haces cambios en los archivos del proyecto, el contenido de la aplicación se refrescará automáticamente.

Configuración de Truffle

Existe un directorio de Truffle al que la aplicación creada con React tendrá que acceder. Se trata del directorio que contiene los artefactos generados durante la compilación de Truffle.

Actualmente los artefactos están en directorio al que la app que usamos en el frontend no puede acceder. Para cambiar esto, edita el archivo truffle-config.js y agrega primero la siguiente línea al inicio del archivo:

const path = require('path');

Ahora vamos a agregar la siguiente opción en la sección module.exports para indicar a Truffle que guarde los artefactos en el directorio /frontend/src/contracts:

module.exports = {
  // ...
  contracts_build_directory: path.join(__dirname, "frontend/src/contracts"),
  // ...
}

Por último, ejecuta de nuevo el comando de Truffle migrate --reset para generar los artefactos en el nuevo directorio.

Creación de las interfaces Web3

Vamos a usar la librería Web3 para conectar nuestra aplicación con React con el contrato que hemos creado. Para empezar, crea un nuevo archivo en el directorio /frontend/src al que llamaremos interfaces.js. En este archivo crearemos las funciones que nos permitan interactuar tanto con la blockchain como con nuestro contrato.

En la parte superior del archivo vamos a importar la tanto la librería Web3 como el artefacto con nuestro contrato que hemos generado con Truffle, así como la función detectEthereumProvider:

import Web3 from 'web3';
import Wallet from './contracts/Wallet.json';
import detectEthereumProvider from '@metamask/detect-provider';

Ahora vamos a agregar un método que nos permita obtener una instancia dela librería Web3, conectada con Ganache, que es nuestra blockchain local. Para ello necesitamos saber cuál es la URL de conexión con Ganache. Se trata de la URL http://localhost:9545, que hemos agregado al archivo .env. React nos proporciona acceso a las variables de entorno mediante el objeto process.env, por lo que podremos obtener la URL desde el archivo .env leyendo el valor de la propiedad process.env.REACT_APP_WEB3_PROVIDER.

En principio, esta función podría ser algo tan sencillo como esto:

const getWeb3 = () => {
  return new Web3(process.env.REACT_APP_WEB3_PROVIDER));
}

Si  hacemos esto, podremos leer datos de la blockchain normalmente y enviar transacciones sin firmar, pero esto solo funcionará con Ganache. No funcionará en aplicaciones en producción que funcionen con la testnet o la mainnet the Ethereum. Para ello, tenemos que usar la función detectEthereumProvider que nos proporciona Metamask:

const getWeb3 = () => {
  
  return new Promise( async (resolve, reject) => {

    let provider = await detectEthereumProvider();

    if (provider) {
      await provider.request({ method: 'eth_requestAccounts' });

      try {
        resolve(new Web3(window.ethereum));
      } catch(error) {
        reject(error);
      }
    }

    if (process.env.REACT_APP_WEB3_PROVIDER !== 'undefined') {
      resolve(new Web3(process.env.REACT_APP_WEB3_PROVIDER));
    }
    reject('Debes instalar Metamask');
  });
}

Si no existe un provider, entonces comprobamos si la variable  de entorno process.env.REACT_APP_WEB3_PROVIDER tiene algún valor. Si no lo tiene, entonces usamos reject para devolver un error. De lo contrario devolvemos una nueva instancia de Web3 con dicho valor, que apunta a Ganache, nuestra blockchain local.

Vamos a explicar lo que hemos hecho. Lo primero que hemos hecho en la función getWeb3 ha sido crear una promesa y usar la función detectEthereumProvider. Esta función detectará el provider a usar desde el navegador. El provider no es otra cosa que la URL de la blockchain, que podemos configurar tanto con la Testnet, como con la Mainnet como con Ganache si es que queremos usar nuestra blockchain local con Metamask. Si no sabes  usar promesas, consulta el tutorial en donde explico cómo usar promesas en JavaScript.

Si se encuentra un provider, entonces se ejecuta la siguiente sentencia para pedir permiso al usuario para usar Metamask:

await provider.request({ method: 'eth_requestAccounts' });

Si el usuario tiene instalado Metamask y también ha hecho login en esta extensión, entonces, se inyectará Metamask en el objeto window del navegador, por lo que sencillamente hemos devuelto la instancia Web3 inyectada en el navegador:

resolve(new Web3(window.ethereum));

Ahora vamos a crear una función que nos devuelva una instancia Web3 de nuestro contrato a partir del artefacto JSON que se ha generado en la migración. A la función le daremos el nombre de getWallet y se tratará de una función asíncrona, ya que necesitamos ejecutar la sentencia await en su interior.

En la función, primero obtenemos el identificador de la red. Luego crearemos una nueva instancia de un contrato Web3 usando el método web3.eth.Contract, al que el pasamos el código abi del contrato junto con la dirección del contrato dentro de la red:

const getWallet = async web3 => {
  const networkId = await web3.eth.net.getId();

  return new web3.eth.Contract(
    Wallet.abi,
    Wallet.networks[networkId].address
  );
}

Mediante el código abi, le decimos a la librería Web3 cuál es la estructura del contrato y qué funciones están disponibles.

Finalmente, exporta las funciones que hemos creado usando la sentencia export:

export {getWeb3, getWallet};

Este es el código completo del archivo interfaces.js:

import Web3 from 'web3';
import Wallet from './contracts/Wallet.json';
import detectEthereumProvider from '@metamask/detect-provider';

const getWeb3 = () => {

  if (process.env.REACT_APP_WEB3_PROVIDER !== 'undefined' && process.env.REACT_APP_WEB3_PROVIDER !== 'metamask') {
    return new Web3(process.env.REACT_APP_WEB3_PROVIDER);
  }
          
  return new Promise( async (resolve, reject) => {

    let provider = await detectEthereumProvider();

    if (provider) {
      await provider.request({ method: 'eth_requestAccounts' });

      try {
        resolve(new Web3(window.ethereum));
      } catch(error) {
        reject(error);
      }
    }
    reject('Debes instalar Metamask');
  });
}

const getWallet = async web3 => {
  const networkId = await web3.eth.net.getId();

  return new web3.eth.Contract(
    Wallet.abi,
    Wallet.networks[networkId].address
  );
}

export {getWeb3, getWallet};

Integración de React con Web3

Vamos a comenzar a crear la aplicación editando el archivo /frontend/src/App.js, en donde realizaremos la integración de React con Web3.

Edita el archivo /frontend/src/App.js y en la parte superior importa tanto React como los componentes useEffect y useState. Estos dos últimos componentes se usan extensivamente en las últimas versiones de React para crear componentes funcionales usando hooks. Si nunca has usado React de esta forma, te recomiendo que consultes primero el tutorial en donde explico cómo crear una aplicación con React usando Hooks antes de continuar.

Como iba diciendo, importa los componentes mencionados en la primera línea del archivo:

import React, { useEffect, useState } from 'react';

Luego importa las funciones getWeb3 y getWallet del archivo /frontend/src/interfaces.js que hemos creado como interfaces:

import { getWeb3, getWallet } from './interfaces.js';

Luego vamos a editar la función App, definiendo primero los estados del componente y estableciendo su estado como undefined, tras lo cual usaremos la función useEffect para inicializar dichos estados con el resultado que obtengamos de la conexión Web3:

function App() {
  const [web3, setWeb3] = useState(undefined);
  const [cuentas, setCuentas] = useState(undefined);
  const [wallet, setWallet] = useState(undefined);

  useEffect(() => {
    const init = async () => {
      const web3 = await getWeb3();
      setWeb3(web3);

      const cuentas = await web3.eth.getAccounts();
      setCuentas(cuentas);

      const wallet = await getWallet(web3);
      setWallet(wallet); 
    };

    init();
  }, []);

  if (typeof web3 == 'undefined'
    || typeof cuentas == 'undefined'
    || typeof wallet == 'undefined'
  ) {
    return (
      <div className="container">
        <header className="text-center">
          <h1>
              Cargando...
          </h1>
        </header>
      </div>
    );
  }

  return (
    <div className="container">
      <header className="text-center">
        <h1 className="container mt-4">
          Wallet Multifirma
        </h1>
      </header>
    </div>
  );
}

Vamos a explicar lo que hemos hecho. Hemos llamado a al función useEffect, que es equivalente al método componentDidMount. En su interior hemos declarado la función init, en donde hemos obtenido una instancia de Web3, en conexión con la blockchain:

const web3 = getWeb3();
setWeb3(web3);

Luego hemos obtenido las cuentas generadas por Ganache:

const cuentas = await web3.eth.getAccounts();
setCuentas(cuentas);

Finalmente hemos obtenido una instancia del contrato de la Wallet:

const wallet = await getWallet(web3);
setWallet(wallet);

Por defecto hemos mostrado un mensaje de carga como título si alguna de las variables web3, cuentas y wallet no están definidas todavía. Si lo están, entonces mostramos el mensaje Wallet Multifirma.

Obteniendo datos del contrato

Vamos a obtener datos desde nuestro contrato, que está desplegado en Ganache. Necesitamos leer las direcciones validadoras, contenidas en la propiedad direccionesValidadoras. También necesitamos leer el número mínimo de validaciones desde la propiedad minValidaciones del contrato.

Para leer las direcciones validadoras vamos a usar la función getDireccionesValidadoras, mientras que para leer la propiedad minValidaciones vamos a usar el método homónimo minValidaciones, creado por defecto por Solidity, ya que se trata de una propiedad pública.

Edita el archivo /frontend/src/App.js e inicializa también el estado de las variables direccionesValidadoras y minValidaciones:

const [direccionesValidadoras, setDireccionesValidadoras] = useState(undefined);
const [minValidaciones, setMinValidaciones] = useState(undefined);

Luego, tras obtener la instancia de la Wallet en la constante wallet de la función init, obtén tanto las direcciones validadoras como el número mínimo de validaciones del contrato usando el método methods de la instancia del contrato:

const direccionesValidadoras = await wallet.methods.getDireccionesValidadoras().call();
setDireccionesValidadoras(direccionesValidadoras);

const minValidaciones = await wallet.methods.minValidaciones().call();
setMinValidaciones(minValidaciones);

Deberás seguir mostrando el mensaje de carga mientras no se hayan obtenido los datos de estas dos propiedades del contrato, así que también deberás comprobar que su valor no sea undefined:

if (typeof web3 == 'undefined'
  || typeof cuentas == 'undefined'
  || typeof wallet == 'undefined'
  || typeof direccionesValidadoras == 'undefined'
  || typeof minValidaciones == 'undefined'
) {
  return (
    <div className="container">
      <header className="text-center">
        <h1>
          Cargando...
        </h1>
      </header>
    </div>
  );
}

Este es el código completo de archivo /frontend/src/App.js con lo que hemos hecho hasta ahora:

import React, { useEffect, useState } from 'react';
import { getWeb3, getWallet } from './interfaces.js';

function App() {
  const [web3, setWeb3] = useState(undefined);
  const [cuentas, setCuentas] = useState(undefined);
  const [wallet, setWallet] = useState(undefined);
  const [direccionesValidadoras, setDireccionesValidadoras] = useState(undefined);
  const [minValidaciones, setMinValidaciones] = useState(undefined);

  useEffect(() => {
    const init = async () => {
      const web3 = await getWeb3();
      setWeb3(web3);
        
      const cuentas = await web3.eth.getAccounts();
      setCuentas(cuentas);
        
      const wallet = await getWallet(web3);
      setWallet(wallet); 

      const direccionesValidadoras = await wallet.methods.getDireccionesValidadoras().call();
      setDireccionesValidadoras(direccionesValidadoras);
    
      const minValidaciones = await wallet.methods.minValidaciones().call();
      setMinValidaciones(minValidaciones);
    };

    init();
  }, []);

  if (typeof web3 == 'undefined'
    || typeof cuentas == 'undefined'
    || typeof wallet == 'undefined'
    || typeof direccionesValidadoras == 'undefined'
    || typeof minValidaciones == 'undefined'
  ) {
    return (
      <div className="container">
        <header className="text-center">
          <h1>
              Cargando...
          </h1>
        </header>
      </div>
    );
  }

  return (
    <div className="container">
      <header className="text-center">
        <h1 className="container mt-4">
          Wallet Multifirma
        </h1>
      </header>
    </div>
  );
}

export default App;

A continuación vamos a mostrar en nuestra interfaz los datos que hemos obtenido.

Mostrando datos del contrato

Vamos a crear un nuevo componente al que llamaremos Informacion. Para ello crea el archivo /frontend/src/Informacion.js. Recibirá los argumentos direccionesValidadoras y minValidaciones. Este es el código del componente, en donde usamos el método map para recorrer y mostrar las direcciones:

import React from "react";

export default function Informacion({ direccionesValidadoras, minValidaciones })
{
  return (
    <div className="table-responsive mt-4">
      Min. Validaciones: {minValidaciones}
      <table className="table mt-4">
        <thead>
          <tr>
            <th scope="col">#</th>
            <th scope="col">Dirección</th>
          </tr>
        </thead>
        <tbody>
          {direccionesValidadoras.map((direccion, index) => {
            return (
              <tr key={index}>
                <th scope="row">{index}</th>
                <td>{direccion}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

No hay demasiado que explicar, ya que sencillamente hemos mostrado una tabla con la lista de direcciones validadoras del contrato. Sin embargo, todavía tenemos que incluir este componente en el archivo App.js. Para ello, primero importa el componente Informacion en la parte superior del archivo App.js:

import Informacion from './Informacion.js';

Luego, edita la función return del componente incluyendo el componente Informacion, pasándole como parámetros las direcciones validadoras, contenidas en propiedad direccionesValidadoras y el mínimo número de validaciones, contenidas en la propiedad minValidaciones:

<ListaTransferencias
  transferencias={transferencias}
  aprobarTransferencia={aprobarTransferencia}
/>

Este es el archivo App.js con lo que hemos hecho hasta ahora:

import React, { useEffect, useState } from 'react';
import { getWeb3, getWallet } from './interfaces.js';
import Informacion from './Informacion.js';

function App() {
  const [web3, setWeb3] = useState(undefined);
  const [cuentas, setCuentas] = useState(undefined);
  const [wallet, setWallet] = useState(undefined);
  const [direccionesValidadoras, setDireccionesValidadoras] = useState(undefined);
  const [minValidaciones, setMinValidaciones] = useState(undefined);

  useEffect(() => {
    const init = async () => {
      const web3 = await getWeb3();
      setWeb3(web3);
        
      const cuentas = await web3.eth.getAccounts();
      setCuentas(cuentas);
        
      const wallet = await getWallet(web3);
      setWallet(wallet); 

      const direccionesValidadoras = await wallet.methods.getDireccionesValidadoras().call();
      setDireccionesValidadoras(direccionesValidadoras);
    
      const minValidaciones = await wallet.methods.minValidaciones().call();
      setMinValidaciones(minValidaciones);
    };

    init();
  }, []);

  if (typeof web3 == 'undefined'
    || typeof cuentas == 'undefined'
    || typeof wallet == 'undefined'
    || typeof direccionesValidadoras == 'undefined'
    || typeof minValidaciones == 'undefined'
  ) {
    return (
      <div className="container">
        <header className="text-center">
          <h1>
              Cargando...
          </h1>
        </header>
      </div>
    );
  }

  return (
    <div className="container">
      <header className="text-center">
        <h1 className="container mt-4">
          Wallet Multifirma
        </h1>
      </header>
      <div className="row">
        <div className="col-md-12">
          <Informacion
            direccionesValidadoras={direccionesValidadoras}
            minValidaciones={minValidaciones}
          />
        </div>
      </div>
    </div>
  );
}

export default App;

Enviando una transferencia

Vamos a crear la funcionalidad que nos permita enviar transferencias desde el frontend de la aplicación. Para ello vamos a usar la función crearTransferencia del contrato. Para ello necesitamos un nuevo componente, en donde agregaremos un formulario que permita a los usuarios enviar transferencias.

Para empezar, crear el archivo /frontend/src/FormularioTransferencia. Necesitamos crear el campo cantidad, con la cantidad a enviar, y el campo destinatario, que contendrá la dirección de la cuenta a la que se enviará la transferencia, así como un botón de submit.

Al componente le pasaremos como parámetro la función crearTransferencia de nuestro contrato.

Inicialmente definimos las propiedades  cantidad y destinatario, que configuramos como undefined. También definimos la función submit que se ejecuta cuando se envía el formulario, llamando a la función crearTransferencia con los datos de la transferencia como argumentos:

import React, { useState } from 'react';

export default function FormularioTransferencia({crearTransferencia})
{
  const [cantidad, setCantidad] = useState(undefined);
  const [destinatario, setDestinatario] = useState(undefined);

  const submit = event => {
    event.preventDefault();
    crearTransferencia(cantidad, destinatario);
  }

  return (
    <div className="mt-4">
      <h2>Crear transferencia</h2>
      <form className="mt-4" onSubmit={(event) => submit(event)}>    
        <div className="form-group mt-2">
          <label htmlFor="cantidad">Cantidad</label>
          <input
            id="cantidad"
            min="0"
            className="form-control"
            type="number"
            onChange={event => setCantidad(event.target.value.trim())}
          />
        </div>
        <div className="form-group mt-2">
          <label htmlFor="destinatario">Destinatario</label>
          <input
            id="destinatario"
            className="form-control"
            type="text"
            onChange={event => setDestinatario(event.target.value.trim())}
          />
        </div>
        <button className="btn btn-primary mt-2">Enviar</button>
      </form>
    </div>
  );
}

Lo que hemos hecho en el formulario ha sido ejecutar las funciones setCantidad y setDestinatario cada vez que se produzca un cambio en los campos correspondientes. Para ello hemos usado el evento onChange de JavaScript. Del mismo modo, cuando enviamos el formulario se ejecuta la función submit, que simplemente llama a la función crearTransferencia que hemos pasado como argumento al componente.

Ahora regresa al archivo App.js e importa el componente FormularioTransferencia:

import FormularioTransferencia from './FormularioTransferencia.js';

Luego agrega la función crearTransferencia, que recibirá los argumentos cantidad y destinatario:

const crearTransferencia = (cantidad, destinatario) => {
  wallet.methods
    .crearTransferencia(cantidad, destinatario)
    .send({from: cuentas[0]});
}

Lo que hemos hecho ha sido llamar a la función crearTransferencia del contrato, a la que podemos acceder a través del objeto methods de la instancia contrato. En este caso, dado que modificamos el contrato, hemos usado el método send en lugar del método call. Junto con el método send hemos especificado una de las cuentas desde la que se enviará la instrucción para crear la transferencia.

Ahora ya solo nos falta mostrar el componente FormularioTransferencia en el interior de la sentencia return, pasándole como parámetro la función crearTransferencia:

<div className="container">
  <header className="text-center">
    <h1 className="container mt-4">
      Wallet Multifirma
    </h1>
  </header>
  <div className="row">
    <div className="col-md-6">
      <Informacion direccionesValidadoras={direccionesValidadoras} minValidaciones={minValidaciones}/>
    </div>
    <div className="col-md-6">
      <FormularioTransferencia crearTransferencia={crearTransferencia}/>
    </div>
  </div>
</div>

Este es el archivo App.js con lo que hemos hecho hasta ahora:

import React, { useEffect, useState } from 'react';
import { getWeb3, getWallet } from './interfaces.js';
import Informacion from './Informacion.js';
import FormularioTransferencia from './FormularioTransferencia.js';

function App() {
  const [web3, setWeb3] = useState(undefined);
  const [cuentas, setCuentas] = useState(undefined);
  const [wallet, setWallet] = useState(undefined);
  const [direccionesValidadoras, setDireccionesValidadoras] = useState(undefined);
  const [minValidaciones, setMinValidaciones] = useState(undefined);

  useEffect(() => {
    const init = async () => {
      const web3 = await getWeb3();
      setWeb3(web3);
        
      const cuentas = await web3.eth.getAccounts();
      setCuentas(cuentas);
        
      const wallet = await getWallet(web3);
      setWallet(wallet); 

      const direccionesValidadoras = await wallet.methods.getDireccionesValidadoras().call();
      setDireccionesValidadoras(direccionesValidadoras);
    
      const minValidaciones = await wallet.methods.minValidaciones().call();
      setMinValidaciones(minValidaciones);
    };

    init();
  }, []);

  const crearTransferencia = (cantidad, destinatario) => {
    wallet.methods
      .crearTransferencia(cantidad, destinatario)
      .send({from: cuentas[0]});
  }

  if (typeof web3 == 'undefined'
    || typeof cuentas == 'undefined'
    || typeof wallet == 'undefined'
    || typeof direccionesValidadoras == 'undefined'
    || typeof minValidaciones == 'undefined'
  ) {
    return (
      <div className="container">
        <header className="text-center">
          <h1>
              Cargando...
          </h1>
        </header>
      </div>
    );
  }

  return (
    <div className="container">
      <header className="text-center">
        <h1 className="container mt-4">
          Wallet Multifirma
        </h1>
      </header>
      <div className="row">
        <div className="col-md-6">
          <Informacion direccionesValidadoras={direccionesValidadoras} minValidaciones={minValidaciones}/>
        </div>
        <div className="col-md-6">
          <FormularioTransferencia crearTransferencia={crearTransferencia}/>
        </div>
      </div>
    </div>
  );
}

export default App;

Mostrando la lista de transferencias

A continuación vamos a ver cómo mostrar una lista con las transferencias creadas. Para ello vamos a crear un nuevo componente en el archivo /frontend/src/ListaTransferencias. El componente recibirá como argumentos un array con las transferencias creadas. Lo que haremos será mostrar las transferencias en una tabla. Mostraremos su identificador, la cantidad de las mismas, el destinatario, el número de aprobaciones y si ha sido enviada o no:

import React from "react";

export default function ListaTransferencias({ transferencias})
{
  return (
    <div className="table-responsive mt-4">
    <h2 className="mt-4">Transferencias</h2>
      <table className="table mt-4">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">Cantidad</th>
            <th scope="col">Destinatario</th>
            <th scope="col">Aprobaciones</th>
            <th scope="col">Enviada</th>
          </tr>
        </thead>
        <tbody>
          {transferencias.map((transferencia) => {
            return (
              <tr key={transferencia.id}>
                <th scope="row">{transferencia.id}</th>
                <td>{transferencia.cantidad}</td>
                <td>{transferencia.destinatario}</td>
                <td>{transferencia.numAprobaciones}</td>
                <td>{transferencia.cantidad}</td>
                <td>{transferencia.enviada ? 'Si' : 'No'}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

Ahora vamos a edita el archivo App.js, en donde demos importar el componente creado:

import ListaTransferencias from './ListaTransferencias.js';

Vamos a definir también la propiedad transferencias, que inicializamos como undefined:

const [transferencias, setTransferencias] = useState(undefined);

Vamos obtener las transferencias del contrato usando el método getTransferencias del mismo en el interior de la función useEffect:

const transferencias = await wallet.methods.getTransferencias().call();
setTransferencias(transferencias);

Antes de mostrar la lista de transferencias, debemos esperar a que las hayamos obtenido desde el contrato, mostrando el mensaje de carga mientras tanto:

if (typeof web3 == 'undefined'
    || typeof cuentas == 'undefined'
    || typeof wallet == 'undefined'
    || typeof direccionesValidadoras == 'undefined'
    || typeof minValidaciones == 'undefined'
    || typeof transferencias == 'undefined'
  ) {
    return (
      <div className="container">
        <header className="text-center">
          <h1>
              Cargando...
          </h1>
        </header>
      </div>
    );
  }

Luego agregamos el componente ListaTransferencias a la sentencia return:

<div className="container">
  <header className="text-center">
    <h1 className="container mt-4">
      Wallet Multifirma
    </h1>
  </header>
  <div className="row">
    <div className="col-md-6">
      <Informacion direccionesValidadoras={direccionesValidadoras} minValidaciones={minValidaciones}/>
    </div>
    <div className="col-md-6">
      <FormularioTransferencia crearTransferencia={crearTransferencia}/>
    </div>
  </div>
  <div className="row">
    <div className="col-md-12">
      <ListaTransferencias transferencias={transferencias}/>
    </div>
  </div>
</div>

Tal y como ves, hemos pasado la lista de transferencias al componente. Ahora ya solo nos falta la funcionalidad que nos permita aprobar una transferencia.

Aprobando una transferencia

Ahora vamos a ver cómo aprobar una transferencia. Para ello vamos vamos a editar el archivo App.js. En este archivo crearemos una nueva función llamada aprobarTransferencia. Esta función recibirá como argumento el identificador de la transferencia que se quiere aprobar. Tras aprobar una transferencia, también debemos refrescar la lista de transferencia mediante el método getTransferencias:

const aprobarTransferencia = transferenciaId => {
  wallet.methods
    .aprobarTransferencia(transferenciaId)
    .send({from: cuentas[0]});

  const transferencias = await wallet.methods.getTransferencias().call();
  setTransferencias(transferencias);
}

Vamos a mostrar un botón de aprobación en el componente ListaTransferencias. Para ello, necesitamos pasarle también la función aprobarTransferencia:

<ListaTransferencias
  transferencias={transferencias}
  aprobarTransferencia={aprobarTransferencia}
/>

Ahora edita el componente ListaTransferencias y agrégale el argumento aprobarTransferencia. Luego, muestra el botón de aprobación en el código JSX del mismo junto con un evento onClick. Este evento ejecutará la función aprobarTransferencia y aceptará como argumento el identificador de la transferencia.

Este sería el código final del componente ListaTransferencias:

import React from "react";

export default function ListaTransferencias({ transferencias, aprobarTransferencia })
{
  return (
    <div className="table-responsive mt-4">
    <h2 className="mt-4">Transferencias</h2>
      <table className="table mt-4">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">Cantidad</th>
            <th scope="col">Destinatario</th>
            <th scope="col">Aprobaciones</th>
            <th scope="col">Enviada</th>
            <th scope="col">Acciones</th>
          </tr>
        </thead>
        <tbody>
          {transferencias.map((transferencia) => {
            return (
              <tr key={transferencia.id}>
                <th scope="row">{transferencia.id}</th>
                <td>{transferencia.cantidad}</td>
                <td>{transferencia.destinatario}</td>
                <td>{transferencia.numAprobaciones}</td>
                <td>{transferencia.enviada ? 'Si' : 'No'}</td>
                <td>
                    <button
                        onClick={() => aprobarTransferencia(transferencia.id)}
                    >
                        Aprobar
                    </button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

Integración con Metamask

Ahora ya puedes probar este proyecto en tu navegador y usar Metamask. Para ello tendrás que agregar Ganache a Metamask.

Cuando inicias Truffle con el comando truffle dev, se muestra una frase mnemónica con varias palabras. Podrás agregar estas palabras a Metamask para importar las cuentas generadas por Truffle.

Luego bastará con que agregues una nueva red manualmente a Metamask con los siguientes valores:

  • Nombre de la red: Ganache
  • Nueva dirección URL de RPC: http://localhost:9545
  • Identificador de cadena: 1337
  • Símbolo de moneda: ETH

Ya puedes abrir de nuevo la aplicación en tu navegador, en la URL http://localhost:3000/:

Puedes probar a enviar una transferencia de 10 a alguna de las direcciones de Ganache. Una vez enviada prueba a aprobarla desde varias direcciones.

Existen bastantes mejoras posibles, aunque creo que este tutorial ha sido ya tremendamente extenso. Enhorabuena si has llegado hasta aquí.

Deploy de la aplicación en la testnet

Hasta ahora hemos usado Ganache, que es una blockchain local que se ejecuta en tu ordenador. Ahora vamos a ver cómo puedes desplegar el contrato en la testnet pública de Ethereum. Existen varias testnets te Ethereum, así que la que usaremos será Goerli.

Generando direcciones te Ethereum

Para empezar, vas a necesitar crear varias direcciones en la testnet, ya que las direcciones que usas localmente no serán útiles. Para ello, accede a este servicio y haz scroll hasta la parte inferior de la página. Desde aquí podrás crear direcciones de Ethereum haciendo clic en Generate. Debes dejar las opción de configuración que se seleccionan por defecto. Tras generar una dirección verás en la parte inferior la dirección generada en el campo Address. Luego haz clic en click to reveal para que se muestre la calve privada, en el campo private key.

Debes generar tres direcciones y copiar y pegar tanto la dirección como la clave privada de cada una en algún lugar.

Obtén ETH en tu cuenta de Ethereum

Para poder desplegar la aplicación, necesitas obtener ETH, que en el caso de una testnet será falso ETH que no te costará nada. Para ello puedes usar servicios como este faucet the Goerli. Para usarlo, tendrás que registrarte primero en Alchemy si es que no lo estás ya. Puedes registrarte desde el menú de la aplicación, seleccionando el tipo de cuenta gratuita.

Una vez hayas hecho login con Alchemy, introduce la primera dirección que has generado en el campo en el que se te pide la dirección de la Wallet. Luego haz clic en send me ETH. En la parte inferior podrás ver el ID de la transacción. Si obtienes un error 503, refresca la página y vuelve a intentarlo. El balance de la cuenta debería ser ahora de 0.2 ETH.

Configura el proyecto en Infura

Infura es un servicio que te permite desplegar tus proyectos tanto en las diferentes testnets Ethereum como en la mainnet. Para crear un proyecto sigue estos pasos:

  1. Accede a la web de Infura desde aquí.
  2. Haz clic en login y si no estás registrado, registra una nueva cuenta y confirma tu email.
  3. Al crear un proyecto, introduce Web3 API en el campo network y dale un nombre, que puede ser Wallet Multifirma.
  4. Una vez creado verás que hay una sección llamada Network Endpoints. En la sección de Ethereum selecciona la red Goerli y copia en endpoint que se muestre.

Ahora vamos a configurar el proyecto para que use Infura. Para ello tendrás que instalar un nuevo paquete en el directorio del proyecto:

npm install @truffle/hdwallet-provider

Ahora edita de nuevo el archivo truffle-config.js e importa la librería que hemos instalado en la parte superior del archivo:

const HDWalletProvider = require('@truffle/hdwallet-provider');

Luego haz scroll hasta las sección networks y agrega el provider goerli, que puede que esté comentado, reemplazando MNEMONIC por una frase mnemónica o por un array con las claves privadas que has generado en Vanity ETH. El network_id es 5 para Goerli. También debes reemplazar PROJECT_ID por el ID  de tu proyecto en Infura:

goerli: {
  provider: () => new HDWalletProvider(MNEMONIC, `https://goerli.infura.io/v3/${PROJECT_ID}`, 0, 3),
  network_id: 5
},

También hemos especificado el número de direcciones que queremos generar. Esta es la configuración en mi caso:

goerli: {
  provider: () => new HDWalletProvider(
    [
      c8a8df432d01a2873a2e181b492f2cfb98139f0c197a76519d2c1a04f1f7a366,
      74875092460d00fd0e8c3daf02de1e35a4378ab1ba2e1eae0640469c3b589ca0,
      7f2c89e59a0410912c39cfa51ab442c1dea7c47978ccb7ba091e72989dbc3f00
    ],
    `https://goerli.infura.io/v3/30082fb6c0df48819a9e862f237c8e97`,
    0,
    3
  ),
  network_id: 5
},

Y con esto ya habríamos terminado, aunque es recomendable que guardes las claves privadas de las direcciones en otro archivo. De este modo, no las almacenarás en tu repositorio. Por convención se almacenan en el archivo .secrets.json. Crea este archivo y agrega las claves en su interior:

{
    "private_keys": [
        "c8a8df432d01a2873a2e181b492f2cfb98139f0c197a76519d2c1a04f1f7a366",
        "74875092460d00fd0e8c3daf02de1e35a4378ab1ba2e1eae0640469c3b589ca0",
        "7f2c89e59a0410912c39cfa51ab442c1dea7c47978ccb7ba091e72989dbc3f00"
    ]
}

Ahora edita de nuevo el archivo truffle-config.js e importa la librería fs:

const fs = require('fs');

Luego lee el archivo JSON para obtener las claves:

const secrets = JSON.parse(fs.readFileSync('.secrets.json').toString().trim());

Por último, reemplaza el array con las claves de la configuración de Goerli por la constante secrets:

networks: {
    goerli: {
      provider: () => new HDWalletProvider(
        secrets.privateKeys,
        `https://goerli.infura.io/v3/30082fb6c0df48819a9e862f237c8e97`,
        0,
        3
      ),
      network_id: 5,
      skipDryRun: true
    },
  },

Despliega el proyecto en la red Goerli

Para desplegar el contrato con la Wallet en la red de Goerli basta con que ejecutes el siguiente comando:

truffle migrate --reset --network goerli

Deberías ver un resultado similar a este:

Si ves un error en el puerto 9545, asegúrate de que el valor de la opción skipDryRun es true en al configuración de Truffle.

Es importante que te fijes en la dirección del contrato y que la guardes. En este caso, la dirección es la siguiente:

0xF1F9Ae10a56bB5D3fB9982667B0A291e0F59362D

Con esto, ya habrás desplegado el contrato en la testnet.

Sube el proyecto a Vercel

Primero debes compilar el frontend de la aplicación accediendo al directorio /frontend y ejecutando el siguiente comando:

npm run build

Ahora debes acceder a Netlify o crear una cuenta en este servicio si no la tienes aún. Luego escoge la opción de agregar un proyecto manualmente o Deploy Manually y arrastra el directorio /frontend/build del proyecto a donde se te indique:

En breves instantes tendrás la aplicación disponible en la URL que se muestre por pantalla, que en este ejemplo es esta URL.

Esto ha sido todo.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”