dApp React + web3 para enviar tokens a un contrato

En este tutorial aprenderemos a enviar tokens a un contrato desde Metamask dApp React + web3 y a recibir tokens a nuestra dirección desde un contrato.
Como siempre usaremos la blockchain de Meter para desplegar el contrato. En la parte de frontend usaremos React y la librería Web3.

Interactuaremos con los tokens MTR, MTRG y VOLT que ya existen en la blockchain de Meter.

MTR es el token nativo de Meter, el cual es usado para el pago del gas de las transacciones. Al igual que ocurre con ETH en la blockchain de Ethereum, no es necesario importar ningún contrato.

Para los tokens MTRG y VOLT usaremos las siguientes direcciones de sus respectivos contratos.

MTRG:
Testnet: 0x8A419Ef4941355476cf04933E90Bf3bbF2F73814
Mainnet: 0x228ebBeE999c6a7ad74A6130E81b12f9Fe237Ba3

VOLT:
Testnet: De momento no hay un contrato para VOLT en la testnet
Mainnet: 0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0

Este es el código del contrato que usaremos para recibir y enviar tokens.

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Payable {
  uint public mtrBalance;
  uint public mtrgBalance;
  uint public voltBalance;

  function receiveMTR() public payable {
    mtrBalance += msg.value;
  }

  function receiveMTRG() external payable {
    address tokenMTRG = 0x8A419Ef4941355476cf04933E90Bf3bbF2F73814; // Testnet
    IERC20 paymentToken = IERC20(tokenMTRG);

    uint amount = paymentToken.allowance(msg.sender,address(this));
    paymentToken.transferFrom(msg.sender,address(this),amount);

    mtrgBalance += amount;
  }

  function receiveVOLT() external payable {
    address voltMTRG = 0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0; // Mainnet
    IERC20 paymentToken = IERC20(voltMTRG);

    uint amount = paymentToken.allowance(msg.sender,address(this));
    paymentToken.transferFrom(msg.sender,address(this),amount);

    voltBalance += amount;
  }

  function transferMTR(address toAddress, uint transferAmount) public{
    mtrBalance -= transferAmount;
    payable(toAddress).transfer(transferAmount);
  }

  function transferMTRG(address toAddress, uint transferAmount) public{
    mtrgBalance -= transferAmount;
    address tokenMTRG = 0x8A419Ef4941355476cf04933E90Bf3bbF2F73814; // Testnet
    IERC20 paymentToken = IERC20(tokenMTRG);
    paymentToken.transfer(toAddress, transferAmount);
  }

  function transferVOLT(address toAddress, uint transferAmount) public{
    voltBalance -= transferAmount;
    address voltMTRG = 0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0; // Mainnet
    IERC20 paymentToken = IERC20(voltMTRG);
    paymentToken.transfer(toAddress, transferAmount);
  }
}

Para poder recibir MTR en el contrato necesitamos crear una función que sea payable. La variable global msg.value contiene la cantidad de MTR que se ha enviado en la transacción.

function receiveMTR() public payable {
  mtrBalance += msg.value;
}

Para poder recibir MTRG en el contrato necesitamos crear una función que sea external payable. Los tokens ERC20 funcionan de una manera distinta al token MTR nativo. Todo token ERC20 primero tiene que ser aprobado por el usuario (en la aplicación, usando Metamask) en un proceso de dos pasos. Primero el dApp preguntará al usuario que firme una transacción con la cantidad de tokens que desea permitir gastar. Se trata de una cantidad de tokens pre validados el cual el dApp podrá gastar, ya sea en su totalidad con una sola transacción o repartida en varias transacciones.

En el caso de nuestra dApp, la segunda petición de Metamask le pedirá permiso para enviar todos los tokens a la dirección del contrato. La primera transacción se encarga de cargar fondos “gastables” para el contrato. Sería como recargar una billetera virtual con cierta cantidad de dinero (en nuestro caso tokens) pero que siguen en manos del usuario.

La segunda transacción se encarga de enviar esos fondos al contrato. Esta segunda transacción equivaldría a enviar cierta cantidad de fondos que tiene nuestra billetera virtual a la dirección del contrato. El código Solidity encargado de realizar este segundo paso es el siguiente.

El código de la línea uint amount = paymentToken.allowance(msg.sender,address(this)); se encarga de extraer la cantidad de tokens indicada de la billetera virtual.

El código de la línea paymentToken.transferFrom(msg.sender,address(this),amount); se encarga de enviar esos tokens de la billetera virtual a la dirección del contrato.

function receiveMTRG() external payable {
  address tokenMTRG = 0x8A419Ef4941355476cf04933E90Bf3bbF2F73814; // Testnet
  IERC20 paymentToken = IERC20(tokenMTRG);

  uint amount = paymentToken.allowance(msg.sender,address(this));
  paymentToken.transferFrom(msg.sender,address(this),amount);

  mtrgBalance += amount;
}

Para recibir VOLT en el contrato los pasos son los mismos que con MTRG, excepto que la dirección del contrato del ERC20 token es diferente. Hay que recordar que no existe un contrato para VOLT en la testnet de Meter.
Dependiendo del token que queramos usar introduciremos una dirección u otra como parámetro en la función IERC20().

function receiveVOLT() external payable {
  address voltMTRG = 0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0; // Mainnet
  IERC20 paymentToken = IERC20(voltMTRG);

  uint amount = paymentToken.allowance(msg.sender,address(this));
  paymentToken.transferFrom(msg.sender,address(this),amount);

  voltBalance += amount;
}

Transferir tokens MTR desde un contrato a un dirección es muy sencillo. Tan solo es necesario una línea de código donde le indicamos el destinatario y la cantidad.

function transferMTR(){
  payable(address).transfer(transferAmount);
}

Por último, para enviar tokens ERC20 del contrato a una dirección necesitaremos indicar el token que deseamos enviar y ejecutar la función transfer, pasando como argumentos la dirección de destino y la cantidad.

Para enviar tokens MTRG.

function transferMTRG(){
  address tokenMTRG = 0x8A419Ef4941355476cf04933E90Bf3bbF2F73814; // Testnet
  IERC20 paymentToken = IERC20(tokenMTRG);
  paymentToken.transfer(address, transferAmount);
}

Para enviar tokens VOLT.

function transferVOLT(){
  address voltMTRG = 0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0; // Mainnet
  IERC20 paymentToken = IERC20(voltMTRG);
  paymentToken.transfer(address, transferAmount);
}

En las siguientes funciones averiguaremos cómo enviar MTR, MTRG & VOLT a un contrato usando React y la librería web3.

Para enviar MTR, al ser el token nativo de Meter (el equivalente a ETH en Ethereum), solo necesitaremos firmar una petición con Metamask.

async sendMTR(amount) {
  let accounts = await web3.eth.getAccounts();
    
  try {
    await contract.methods.receiveMTR().send({
      from: accounts[0],
      value: web3.utils.toWei(amount, "ether"),
    });
  } catch(err)
  {
    console.log(err);
    return;
  }
}

Para enviar MTRG al contrato desde nuestra dApp necesitaremos firmar dos transacciones. La primera de ellas la invoca la función approve, la cual carga nuestro billetero virtual. La segunda de ellas es invocada por la función transferMTRG, transfiriendo los fondos de la billetera virtual contrato.

async sendMTRG(amount) {
  
  let accounts = await web3.eth.getAccounts();

  let contractAddress = "YOUR_DEPLOYED_CONTRACT_ADDRESS" //TESTNET
  let mtrgTokenAddress = "0x8A419Ef4941355476cf04933E90Bf3bbF2F73814"; //TESTNET
  //let mtrgTokenAddress = "0x228ebBeE999c6a7ad74A6130E81b12f9Fe237Ba3"; //MAINNET
    
  var abi = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"burn","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_value","type":"uint256"}],"name":"burnFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Burn","type":"event"}];
  var token = new web3.eth.Contract(abi, mtrgTokenAddress);

  try {
    await token.methods.approve(contractAddress, web3.utils.toWei(amount, "ether")).send({
      from: accounts[0]
    });  
  } catch (error) {
    console.log(error);  
    return;
  }

  try {
    await token.methods.transferMTRG().send({
      from: accounts[0]
    });  
  } catch (error) {
    console.log(error);
    return;
  }

}

Para transferir los tokens VOLT los pasos son los mismos que con MTRG, enviando el contrato de VOLT en este caso.

async sendVOLT(amount) {
  
  let accounts = await web3.eth.getAccounts();

  let contractAddress = "YOUR_DEPLOYED_CONTRACT_ADDRESS" //TESTNET
  let voltTokenAddress = "0x8Df95e66Cb0eF38F91D2776DA3c921768982fBa0"; //MAINNET
    
  var abi = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"burn","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_value","type":"uint256"}],"name":"burnFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Burn","type":"event"}];
  var token = new web3.eth.Contract(abi, voltTokenAddress);

  try {
    await token.methods.approve(contractAddress, web3.utils.toWei(amount, "ether")).send({
      from: accounts[0]
    });  
  } catch (error) {
    console.log(error);  
    return;
  }

  try {
    await token.methods.transferVOLT().send({
      from: accounts[0]
    });  
  } catch (error) {
    console.log(error);
    return;
  }

}

La variable abi contiene el JSON del contrato ERC20, necesario para poder transferir tokens con web3. El contenido del JSON es idéntico para todos los tokens.

Usaremos Hardhat para desplegar el contrato en la red de testnet de Meter.

npx hardhat init

Creamos el script para desplegar el contrato en la red testnet de Meter (scripts/deploy.js).

// scripts/deploy.js
async function main () {
  const Payable = await ethers.getContractFactory('Payable');
  console.log('Desplegando Payable...');
  const payable = await Payable.deploy();
  await payable.deployed();
  console.log('Payable desplegado en:', payable.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

Finalmente, para poder desplegar el contrato en la mainnet o testnet de Meter necesitamos indicarle a Hardhat una serie de datos. Editamos el fichero hardhat.config y añadimos las siguientes entradas dentro de module.exports.

const METER_TESTNET_PRIVATE_KEY = process.env.METER_TESTNET_PRIVATE_KEY;
const METER_MAINNET_PRIVATE_KEY = process.env.METER_MAINNET_PRIVATE_KEY;

module.exports = {
  solidity: "0.8.4",
  networks: {
    meter_testnet: {
      url: "https://rpctest.meter.io",
      chainId: 83,
      accounts: [`${METER_TESTNET_PRIVATE_KEY}`]
    },
    meter_mainnet: {
      url: "https://rpc.meter.io",
      chainId: 82,
      accounts: [`${METER_MAINNET_PRIVATE_KEY}`]
    }
  } 
};

En el contrato Payable.sol usamos los contratos de OpenZeppelin, por lo que tendremos que instalarlos en nuestro proyecto, ejecutando el siguiente comando.

npm install @openzeppelin/contracts

Ya lo tenemos todo listo para desplegar el contrato en la testnet de Meter. Necesitaremos exportar las variables de entorno METER_TESTNET_PRIVATE_KEY y METER_MAINNET_PRIVATE_KEY. Estas variables contienen la clave privada de la dirección que desplegará el contrato en la blockchain. Recordemos que para poder desplegar un contrato necesitamos MTR. Si nos disponemos de MTR podemos conseguirlos en este enlace https://faucet-warringstakes.meter.io/

Abrimos un terminal y ejecutamos el siguiente comando.

export METER_TESTNET_PRIVATE_KEY=CLAVE_PRIVADA_DIRECCIÓN
export METER_MAINNET_PRIVATE_KEY=CLAVE_PRIVADA_DIRECCIÓN
npx hardhat run --network meter_testnet scripts/deploy.js

Si lo quisiéramos desplegar en la mainnet, ejecutamos el comando de la siguiente manera.

npx hardhat run --network meter_mainnet scripts/deploy.js

Nos deberá devolver la dirección donde se ha desplegado el contrato.

captura1 4

Tendremos que copiar esa dirección y modificar la constante address del fichero payableContract.js

const address = '0x5F0514fa8C662F78800924EdBB074244244C2Fb2';

Por último necesitaremos el ABI en formato JSON que Hardhat ha generado, lo podemos encontrar en la siguiente ruta: artifacts/contracts/Payable.sol/Payable.json
Tan solo tenemos que copiar el JSON de la propiedad abi y pegarlo la constante abi del fichero src/payableContract.js. El JSON solo cambia si realizamos modificaciones en el contrato de Solidity.

Ya estamos en disposición de instalar y ejecutar la dApp.

npm install
npm run start

Una vez ejecutado podremos acceder al navegador e interactuar con el contrato mediante Metamask con toda la funcionalidad descrita en este tutorial.

dApp

Puedes descargarte el proyecto completo aquí:

Si necesitas cualquier tipo de ayuda relacionada con este tutorial puedes unirte al canal: https://t.me/codigoweb3

Deja una respuesta

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