Struct y Mapping en 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í.
Analyst Frontend Developer en Comunytek (BBVA CIB – NOVA).
Autodidacta, apasionado de las nuevas tecnologías y de los proyectos DIY.
Trackbacks/Pingbacks