Struct y Mapping en Solidity

por | Abr 3, 2023 | Blockchain, Hardhat, Solidity | 0 Comentarios

Struct And Mapping In Solidity

Struct y Mapping

Continuando el anterior tutorial, en el que modificamos un contrato inteligente que desarrollamos en este post, para poder almacenar nuestro número favorito, en esta ocasión vamos a ver un par de tipos de datos nuevos: Struct y Mapping.

Struct

Se utiliza principalmente para definir un tipo de datos personalizado. Similar a las Clases y/o Interfaces que podemos encontrar en el resto de lenguajes de programación. Para su uso utilizaremos la palabra reservada struct, seguido del nombre que le queramos dar.

struct ToDo {
  string text;
  bool completed;
}

Mapping

Mediante los mappings tenemos una forma de guardar datos en forma de clave:valor, similar al formato JSON. Se emplea la palabra reservada mapping para hacer uso de este tipo de dato.

// Clave: address - Valor: números
mapping(address => uint) public numbers;
 
// Para la dirección 0x000... se guarda el número 100
address addr = 0x000...;
numbers[addr] = 100;
 
// Se retorna el valor guardado de la dirección 0x000...
return numbers[addr];

Tecnologías

El stack de versiones que se usarán para el desarrollo del proyecto es el siguiente.

  • Node (16.18.1)
  • Npm (9.2.0)
  • Typescript (4.8.4)

Objetivo

Vamos a modificar nuestro Smart Contract para poder almacenar el número favorito de la dirección que interactúa con el contrato, y la última vez que dicha dirección actualizó su número favorito.

    Modificación FavoriteNumber

    Al principio de nuestro contrato declararemos el struct que define cómo serán los objetos que almacenaremos. Y seguido de éste, el mapping, donde almacenaremos nuestros objetos.

      // SPDX-License-Identifier: MIT
      pragma solidity 0.8.18;
       
      contract FavoriteNumber {
          struct FavoriteNumberData {
              uint favoriteNumber;
              uint lastUpdate;
          }
       
          mapping(address => FavoriteNumberData) private s_addressToFavoriteNumberData;
       
          ...
      }

      Debido a los cambios que hemos hecho en la forma de guardar la información, ahora tendremos que modificar el setter, y el getter, que ahora pasará a ser 2 getters.

        ...
         
        contract FavoriteNumber {
            ...
         
            function setFavoriteNumber(uint favoriteNumber) external {
                if (favoriteNumber > 100) {
                    revert FavoriteNumber__NumberMustBeLower(favoriteNumber);
                }
         
                s_addressToFavoriteNumberData[msg.sender] = FavoriteNumberData({
                    favoriteNumber: favoriteNumber,
                    lastUpdate: block.timestamp
                });
            }
         
            function getFavoriteNumber(address favoriteNumberAddress) public view returns (uint) {
                return s_addressToFavoriteNumberData[favoriteNumberAddress].favoriteNumber;
            }
         
            function getFavoriteNumberLastUpdate(address favoriteNumberAddress) public view returns (uint) {
                return s_addressToFavoriteNumberData[favoriteNumberAddress].lastUpdate;
            }
        }

        Quedando el contrato de la siguiente manera.

          FavoriteNumber.sol

          // SPDX-License-Identifier: MIT
          pragma solidity 0.8.18;
           
          error FavoriteNumber__NumberMustBeLower(uint favoriteNumber);
           
          contract FavoriteNumber {
              struct FavoriteNumberData {
                  uint favoriteNumber;
                  uint lastUpdate;
              }
           
              mapping(address => FavoriteNumberData) private s_addressToFavoriteNumberData;
           
              constructor() { }
           
              function setFavoriteNumber(uint favoriteNumber) external {
                  if (favoriteNumber > 100) {
                      revert FavoriteNumber__NumberMustBeLower(favoriteNumber);
                  }
           
                  s_addressToFavoriteNumberData[msg.sender] = FavoriteNumberData({
                      favoriteNumber: favoriteNumber,
                      lastUpdate: block.timestamp
                  });
              }
           
              function getFavoriteNumber(address favoriteNumberAddress) public view returns (uint) {
                  return s_addressToFavoriteNumberData[favoriteNumberAddress].favoriteNumber;
              }
           
              function getFavoriteNumberLastUpdate(address favoriteNumberAddress) public view returns (uint) {
                  return s_addressToFavoriteNumberData[favoriteNumberAddress].lastUpdate;
              }
          }

          Test

          Ahora solo nos queda actualizar los tests para validar que la información se está guardando y actualizando correctamente. Para ello, vamos a necesitar instalar la librería @nomicfoundation/hardhat-network-helpers, que nos permitirá modificar el estado de la blockchain de prueba de Hardhat. En nuestro caso para simular el minado de bloques.

          npm install --save-dev @nomicfoundation/hardhat-network-helpers

          A continuación los tests actualizados.

          FavoriteNumber.test.ts

          import { assert, expect } from 'chai';
          import { deployments, ethers, getNamedAccounts, network } from 'hardhat';
          
          import { FavoriteNumber } from '../typechain-types';
          import { mine } from '@nomicfoundation/hardhat-network-helpers';
           
          // Only execute tests in local environment
          if (!['hardhat', 'localhost'].includes(network.name)) {
            describe.skip;
          } else {
            describe('FavoriteNumber Unit Tests', () => {
              let favoriteNumberContract: FavoriteNumber;  // FavoriteNumber contract
              let deployer: string;  // Account from alias 'deployer'
              const deployerFavoriteNumber = '10';  // Deployer favorite number
           
              beforeEach(async () => {
                // Gets deployer account
                deployer = (await getNamedAccounts()).deployer;
           
                // Executes deploy scripts with given tags to prepare test environment
                await deployments.fixture(['favorite-number']);
           
                // Gets FavoriteNumber contract
                favoriteNumberContract = await ethers.getContract('FavoriteNumber');
              });
           
              it('Initial favorite number by default', async () => {
                // Gets default favorite number
                const favoriteNumber = (await favoriteNumberContract.getFavoriteNumber(deployer)).toString();
           
                assert.equal(favoriteNumber, '0');
              });
           
              it('Initial favorite number last update by default', async () => {
                // Gets default favorite number last update
                const favoriteNumberLastUpdate = (await favoriteNumberContract.getFavoriteNumberLastUpdate(deployer)).toString();
           
                assert.equal(favoriteNumberLastUpdate, '0');
              });
           
              it('Set and get favorite number', async () => {
                // Updates favorite number
                await favoriteNumberContract.setFavoriteNumber(deployerFavoriteNumber);
           
                // Gets updated favorite number
                const updatedFavoriteNumber = (await favoriteNumberContract.getFavoriteNumber(deployer)).toString();
           
                assert.equal(deployerFavoriteNumber, updatedFavoriteNumber);
              });
           
              it('Set favorite number and get its last update', async () => {
                // Updates favorite number
                const txReceipt = await favoriteNumberContract.setFavoriteNumber(deployerFavoriteNumber);
           
                // Gets transaction block timestamp
                const txBlockTimestamp = (await ethers.provider.getBlock(txReceipt.blockNumber!)).timestamp;
           
                // Mines blocks
                const blocksToMine = 5;
                await mine(blocksToMine);
           
                // Gets favorite number last update
                const favoriteNumberLastUpdate = (await favoriteNumberContract.getFavoriteNumberLastUpdate(deployer)).toString();
                assert.equal(`${txBlockTimestamp}`, favoriteNumberLastUpdate);
           
                // Gets timestamp from new last block
                const latestBlockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
                assert.equal(`${txBlockTimestamp + blocksToMine}`, `${latestBlockTimestamp}`);
              });
           
              it('Set and get favorite number for multiple users', async () => {
                // Deployer updates favorite number
                await favoriteNumberContract.setFavoriteNumber(deployerFavoriteNumber);
           
                // User updates favorite number
                const user = (await ethers.getSigners())[1];
                const userFavoriteNumber = '50';
                favoriteNumberContract.connect(user).setFavoriteNumber(userFavoriteNumber);
           
                // Tester updates favorite number
                const tester = (await ethers.getSigners())[2];
                const testerFavoriteNumber = '98';
                favoriteNumberContract.connect(tester).setFavoriteNumber(testerFavoriteNumber);
           
                // Gets deployer updated favorite number
                const deployerUpdatedFavoriteNumber = (await favoriteNumberContract.getFavoriteNumber(deployer)).toString();
           
                // Gets user updated favorite number
                const userUpdatedFavoriteNumber = (await favoriteNumberContract.getFavoriteNumber(user.address)).toString();
           
                // Gets tester updated favorite number
                const testerUpdatedFavoriteNumber = (await favoriteNumberContract.getFavoriteNumber(tester.address)).toString();
           
                assert.equal(deployerFavoriteNumber, deployerUpdatedFavoriteNumber);
                assert.equal(userFavoriteNumber, userUpdatedFavoriteNumber);
                assert.equal(testerFavoriteNumber, testerUpdatedFavoriteNumber);
              });
           
              it('Cannot set invalid favorite number', async () => {
                // Invalid favorite number
                const invalidFavoriteNumber = '101';
           
                // Tries to update favorite number
                const txReceipt = favoriteNumberContract.setFavoriteNumber(invalidFavoriteNumber);
           
                await expect(txReceipt).to.be.revertedWithCustomError(favoriteNumberContract, 'FavoriteNumber__NumberMustBeLower');
              });
            });
          }
          

          Perfecto. Ahora ejecutamos los tests.

          npx hardhat test
           
          >  FavoriteNumber Unit Tests
          >    √ Initial favorite number by default
          >    √ Initial favorite number last update by default
          >    √ Set and get favorite number
          >    √ Set favorite number and get its last update
          >    √ Set and get favorite number for multiple users
          >    √ Cannot set invalid favorite number
           
          >  6 passing (1s)

          ¡Ya lo tenemos! Hemos modificado el contrato para que ahora almacene las direcciones que interactúan con él, y el momento en el que lo hacen 🥳

          Github

          Todo el código fuente lo puedes encontrar aquí.