Publicado
- 14 min read
Cómo crear tu propio smart contract con Solidity y Hardhat
Introducción
En esta oportunidad, vamos a explorar el potencial de los smart contracts de Ethereum mediante el uso de Solidity y Hardhat como nuestro entorno de desarrollo. A través de la creación de una pequeña aplicación, desplegaremos en las redes de prueba de Polygon, Avalanche para demostrar las capacidades que nos ofrece el ecosistema de blockchain. Al final del proceso, tendremos una mejor comprensión del alcance y la versatilidad de los smart contracts y su aplicación práctica en el mundo real.
Instalación de dependencias
Para desarrollar nuestro contrato inteligente usaremos Harhat como IDE ya que nos permitirá probarlo dentro de un entorno local antes de llevarlo en una red pruebas o principal. Ademas de que nos proporciona herramientas de automatización de tareas, como pruebas automatizadas, pruebas de cobertura de código y generación de documentación, lo que nos facilita mucho el desarrollo y depuración de contratos inteligentes. Ademas de que se adapta muy fácilmente a proyectos de cualquier tamaño y complejidad.
Para realizar la instalación de Hardhat primero necesitamos Node.js ya que este hace uso de este entorno de ejecución. Si ya cuentas con Node.js omite este paso, de lo contrario es necesario ir al sitio oficial, descargarlo e instalarlo para la plataforma que utilices.
Ahora a instalar Hardhat, para ello debemos seguir los pasos de instalación desde su sitio web.
Una vez instalado deberíamos de ver el siguiente resultado al ejecutar el comando para la creación de un nuevo proyecto:
$ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.9.9 👷
? What do you want to do? …
❯ Create a JavaScript project
Create a TypeScript project
Create an empty hardhat.config.js
Quit
en mi caso creare uno basado en JavaScript, pero puedes crearlo basado en TypeScript o vació si es el caso. El proyecto lo llamare notes_dapp
.
Creación de smart contract
Dentro de la carpeta de contracts definiremos el smart contract correspondiente
// notes_dapp/contracts/TaskList.sol
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
contract TaskList {
struct Task {
bytes32 description;
bool completed;
}
mapping(uint => Task) public tasks;
uint public taskCount;
function addTask(bytes32 _description) public {
taskCount++;
tasks[taskCount] = Task(_description, false);
}
function completeTask(uint _taskId) public {
require(tasks[_taskId].completed == false, "Task is already completed");
tasks[_taskId].completed = true;
}
function deleteTask(uint _taskId) public {
require(tasks[_taskId].completed == true, "Can only delete completed tasks");
delete tasks[_taskId];
}
function getTask(uint _taskId) public view returns (bytes32, bool){
return (tasks[_taskId].description, tasks[_taskId].completed);
}
function getTaskCount() public view returns (uint) {
return taskCount;
}
}
- Task: Estructura que nos permitirá la descripción de la tarea y el status de la misma
- tasks: Map que contendrá todas las tareas que vayamos definiendo a futuro
- completeTask: Función que nos permitirá marcar como completada determinada tarea
- deleteTask: Nos permitirá eliminar una tarea en especifico
- getTask: Obtiene una tarea mediante un id proporcionado, podremos obtener los la descripción y el status de la tarea
- getTaskCount: Función que nos permite obtener el numero de tareas
Podremos compilar el smart contract mediante el siguiente comando:
❯ npx hardhat compile
Compiled 2 Solidity files successfully
Nos dice que se compilaron dos archivos, esto debido a que por default hardhat genera un smart contract de ejemplo llamado Lock.sol
, esto solo para los proyectos que no son generados de forma vacía, el cual fue nuestro caso. Por lo que deberías de ver algo asi en tu carpeta contracts
contracts/
├── Lock.sol
└── TaskList.sol
Tests
Otra parte fundamental para cualquier proyecto de desarrollo de software es el apartado de los tests, estos nos permitirán reducir posibles errores en desarrollos posteriores y como buenos programadores que debemos ser no omitiremos estos de nuestro proyecto.
Por lo que, dentro de la carpeta test
agregare el archivo con los tests correspondientes:
// notes_dapp/test/TaskList.js
const { expect } = require('chai')
const { ethers } = require('hardhat')
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers')
describe('Task list contract contract', function () {
async function deployContract() {
const contract = await ethers.getContractFactory('TaskList')
const deploy = await contract.deploy()
return { deploy }
}
it('Contract Deploy', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const description = ethers.utils.formatBytes32String(_description)
await deploy.addTask(description)
const taskOne = await deploy.getTask(1)
expect(taskOne[0], _description)
expect(taskOne[1], false)
})
it('Test, when add two tasks', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const _description2 = 'test description'
const description = ethers.utils.formatBytes32String(_description)
const description2 = ethers.utils.formatBytes32String(_description2)
await deploy.addTask(description)
await deploy.addTask(description2)
const taskOne = await deploy.getTask(1)
const taskTwo = await deploy.getTask(2)
expect(taskOne[0], _description)
expect(taskOne[1], false)
expect(taskTwo[0], _description2)
expect(taskTwo[1], false)
expect(await deploy.getTaskCount()).to.equals(2)
})
it('Test, set complete task, successfully', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const description = ethers.utils.formatBytes32String(_description)
await deploy.addTask(description)
await deploy.completeTask(1)
const taskOne = await deploy.getTask(1)
expect(taskOne[1], true)
})
it('Test, Error trying to complete a completed task', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const description = ethers.utils.formatBytes32String(_description)
await deploy.addTask(description)
await deploy.completeTask(1)
await expect(deploy.completeTask(1)).to.be.revertedWith('Task is already completed')
})
it('Test, Delete task', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const description = ethers.utils.formatBytes32String(_description)
await deploy.addTask(description)
await deploy.completeTask(1)
const task = await deploy.getTask(1)
expect(task[1]).to.equals(true)
await deploy.deleteTask(1)
})
it('Test, Error deleting task', async function () {
const { deploy } = await loadFixture(deployContract)
const _description = 'test description'
const description = ethers.utils.formatBytes32String(_description)
await deploy.addTask(description)
const task = await deploy.getTask(1)
expect(task[1]).to.equals(false)
await expect(deploy.deleteTask(1)).to.be.revertedWith('Can only delete completed tasks')
})
})
Podemos correr los tests ejecutando:
❯ npx hardhat test
Lock
Deployment
✔ Should set the right unlockTime (2831ms)
✔ Should set the right owner
✔ Should receive and store the funds to lock
✔ Should fail if the unlockTime is not in the future (89ms)
Withdrawals
Validations
✔ Should revert with the right error if called too soon (89ms)
✔ Should revert with the right error if called from another account (45ms)
✔ Shouldn't fail if the unlockTime has arrived and the owner calls it (45ms)
Events
✔ Should emit an event on withdrawals (38ms)
Transfers
✔ Should transfer the funds to the owner (89ms)
Task list contract contract
✔ Contract Deploy (109ms)
✔ Test, when add two tasks (291ms)
✔ Test, set complete task, successfully (44ms)
✔ Test, Error trying to complete a completed task (62ms)
✔ Test, Delete task (60ms)
✔ Test, Error deleting task (70ms)
15 passing (4s)
Puedes eliminar el archivo de test para el smart contract generado por hardhat al iniciar el proyecto, en mi caso no lo hice pero son archivos que no necesitaremos.
Deploy
Una vez generados nuestros tests para los casos de nuestro smart contract crearemos el script de deploy el cual nos servirá para realizar el deploy en las diferentes redes de prueba donde funcionara nuestro contrato.
Dentro de la carpeta scripts/
agregaremos el archivo TaskListDeploy.js
con el siguiente contenido
const hre = require('hardhat')
async function main() {
const Notes = await hre.ethers.getContractFactory('TaskList')
const note = await Notes.deploy()
await note.deployed()
console.log(`Note dApp deployed on ${note.address}`)
}
main().catch((error) => {
console.log(error)
process.exitCode = 1
})
Por el momento nos aseguraremos de que nuestro contrato sea desplegado en nuestra red de pruebas ya proporcionada por hardhat. Para esto iremos al archivo:
hardhat.config.js
y agregaremos la red por default.
Hardhat al ser una herramienta completa para el desarrollo blockchain ya nos proporciona una red de pruebas local sin la necesidad de ejecutar alguna otra aplicación que simule ser un nodo.
También nos da la posibilidad de ejecutar el nodo mediante el comando npx hardhat node
(HardHat node).
En este caso no es necesario ejecutar el nodo.
require("@nomicfoundation/hardhat-toolbox")
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.18",
+ defaultNetwork: "hardhat",
}
Ahora probaremos que nuestro contrato se despliegue correctamente en la red por defecto que hemos seleccionado:
❯ npx hardhat run scripts/TaskListDeploy.js
Note dApp deployed on 0x5FbDB2315678afecb367f032d93F642f64180aa3
si todo salio bien podremos ver la dirección de despliegue del contrato que en este caso pertenece a una dirección de nuestro nodo de pruebas. Ya que verificamos que funciona el despliegue y que el comportamiento es el que esperamos podremos desplegar nuestro contrato den las diferentes redes que se mencionaron en la introducción.
Polygon
Para poder desplegar nuestro contrato en la red de Polygon necesitamos contar con una billetera de criptomonedas, en mi caso hare uso de Metamask, pero serviría cualquiera que nos permita conectarnos las redes principales y de pruebas de las redes que usaremos.(Guía inicial de Metamask)
Posteriormente necesitaremos agregar la red pruebas de Polygon en nuestra billetera, para ello necesitamos lo siguiente:
- Nombre de la red: Mumbai TestNet
- Dirección RPC: https://rpc-mumbai.maticvigil.com
- Identificador de cadena: 80001
- Símbolo de moneda: MATIC
- Dirección de explorador de bloques:https://mumbai.polygonscan.com
Debería de verse algo similar:
Paso siguiente es añadir fondos a nuestra billetera, para ello necesitamos ir al siguiente sitio (https://faucet.polygon.technology/), nos aseguramos de que se encuentre seleccionada la red Mumbai y que el token sea MATIC, colocamos la dirección de nuestra billetera y damos click en submit.
se abrirá una ventana donde confirmaremos la transacción
después de un par de minutos podremos ver 0.2 Matic en nuestra dirección
Ahora agregaremos la red de MATIC a la configuración de hardhat asi como las variables de entorno correspondientes:
require("@nomicfoundation/hardhat-toolbox")
+ require("dotenv").config()
const MUMBAI_URL= process.env.MUMBAI_URL;
const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.18",
defaultNetwork: "hardhat",
+ networks: {
+ mumbai: {
+ url: MUMBAI_URL,
+ accounts: [MUMBAI_PRIVATE_KEY]
+ }
+ }
Instalar el paquete dotenv
mediante el gestor de maquetes de tu preferencia, en mi caso en yarn
$ yarn add dotenv
Se crea el archivo .env
en la raíz del proyecto con el siguiente contenido:
MUMBAI_URL=https://rpc-mumbai.maticvigil.com
MUMBAI_PRIVATE_KEY=TU_CLAVE_PRIVADA
La clave la obtienes de la siguiente forma:
Ya que esta configurada la red estamos listos para desplegar el contrato mediante el comando:
$ npx hardhat run scripts/TaskListDeploy.js --network mumbai
después de ejecutar el comando deberíamos de tener lo siguiente:
$ npx hardhat run scripts/TaskListDeploy.js --network mumbai
Notes dApp deployed on {tu dirección de contrato}
Para visualizarlo vamos al explorador de mumbai y colocamos la dirección del contrato, se debería de poder visualizar la transacción
Continuaremos con la creación de un script en la carpeta de scripts que nos permitirá leer la información del contrato una vez desplegado en la red de pruebas, agregaremos a nuestro archivo de variables .env
la dirección del contrato
MUMBAI_URL=https://rpc-mumbai.maticvigil.com
MUMBAI_PRIVATE_KEY=TU_CLAVE_PRIVADA
+CONTRACT_ADDRESS=TU_DIRECCIÓN_DE_CONTRATO
posteriormente crearemos el siguiente archivo donde agregaremos lo siguiente
// scripts/TaskList.js
const hre = require('hardhat')
const abi = require('../artifacts/contracts/TaskList.sol/TaskList.json')
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS
const MUMBAI_URL = process.env.MUMBAI_URL
const PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY
async function main() {
const contractAddress = CONTRACT_ADDRESS
const contractABI = abi.abi
const provider = new hre.ethers.providers.JsonRpcProvider(MUMBAI_URL)
const signer = new hre.ethers.Wallet(PRIVATE_KEY, provider)
const taskList = new hre.ethers.Contract(contractAddress, contractABI, signer)
// Nos permite agregar nuevas tareas
// const description = hre.ethers.utils.formatBytes32String("Deploy de contrato en tesnet");
// await taskList.addTask(description);
const tasks = await taskList.getTaskCount()
console.log(`Task number: ${tasks}`)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
mediante el script anterior podremos interactuar con nuestro contrato sin necesidad de crear una interfaz web, pero claro el objetivo es que este se comunique con un frontend ;), ahora si ejecutamos el script deberíamos de ver 0 tareas cargadas.
$ npx hardhat run scripts/TaskList.js
Task number: 0
Si se descomenta el código y lo ejecutamos de nuevo estaríamos creando la tarea que especifiquemos, ejecutamos nuevamente el comando anterior
npx hardhat run scripts/TaskList.js
Task number: 0
este nos arrojara que son 0 tareas creadas, pero si ejecutamos nuevamente pero antes comentando el código de creación este nos devolverá ahora el total de una tarea
npx hardhat run scripts/TaskList.js
Task number: 1
y podremos ver la transacción en el explorador de bloques
Podemos agregar la función de marcar como terminada la tarea
// scripts/TaskList.js
const hre = require("hardhat");
const abi = require("../artifacts/contracts/TaskList.sol/TaskList.json");
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
const MUMBAI_URL = process.env.MUMBAI_URL;
const PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
async function main(){
const contractAddress = CONTRACT_ADDRESS;
const contractABI = abi.abi;
const provider = new hre.ethers.providers.JsonRpcProvider(MUMBAI_URL);
const signer = new hre.ethers.Wallet(PRIVATE_KEY, provider);
const taskList = new hre.ethers.Contract(contractAddress, contractABI, signer);
// Nos permite agregar nuevas tareas
// const description = hre.ethers.utils.formatBytes32String("Deploy de contrato en tesnet");
// await taskList.addTask(description);
+ // Permite marcar como terminada una tarea
+ await taskList.completeTask(1);
+ // Elimina una tarea
+ // await taskList.deleteTask(1);
const tasks = await taskList.getTaskCount();
console.log(`Task number: ${tasks}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
si ejecutamos de nuevo tendremos como resultado un numero de tareas de 1 sin embargo podremos ver en el explorador la transacción que marca como completada la tarea
npx hardhat run scripts/TaskList.js
Task number: 1
De la misma forma podemos probar todas las funciones de nuestro contrato, y asi es como finalizamos con el despliegue de contratos inteligentes en Polygon, ahora aprenderás a hacerlo en la red de Avalanche.
Avalanche
Para el despliegue del contrato en la red de Avalanche necesitamos configurar la información de la red en el archivo de configuración de hardhat hardhat.config.js
require("@nomicfoundation/hardhat-toolbox")
require("dotenv").config()
const MUMBAI_URL= process.env.MUMBAI_URL;
const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
+ const FUJI_URL = process.env.FUJI_URL;
+ const FUJI_PRIVATE_KEY = process.env.FUJI_PRIVATE_KEY;
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.18",
defaultNetwork: "hardhat",
networks: {
mumbai: {
url: MUMBAI_URL,
accounts: [MUMBAI_PRIVATE_KEY]
+ },
+ fuji: {
+ url: FUJI_URL,
+ accounts: [FUJI_PRIVATE_KEY],
+ }
}
};
y dentro del archivo .env
agregamos las variables correspondientes
MUMBAI_URL=https://rpc-mumbai.maticvigil.com
MUMBAI_PRIVATE_KEY=TU_CLAVE_PRIVADA
CONTRACT_ADDRESS=TU_DIRECCIÓN_DE_CONTRATO
+FUJI_URL=https://api.avax-test.network/ext/bc/C/rpc
+FUJI_PRIVATE_KEY=TU_CLAVE_PRIVADA
Ahora necesitamos agregar fondos a nuestra billetera para poder pagar las tarifas de gas al momento de desplegar el contrato, para ello vamos al siguiente sitio https://faucet.avax.network/, colocaremos la dirección de billetera y pediremos monedas de la red de pruebas de Avalanche
Una vez que los fondos se encuentren en nuestra billetera podremos desplegar el contrato
$ npx hardhat run scripts/TaskListDeploy.js --network fuji
Notes dApp deployed on {tu dirección de contrato}
para poder visualizar el contrato desplegado vamos a snowtrace colocamos en el buscador la dirección del contrato y podremos visualizarlo ya desplegado en la blockchain de pruebas de Avalanche
Para probar el contrato comentaremos las credenciales que pertenecían a la red de Polygon y colocaremos las correspondientes a la red de Avalanche
const hre = require("hardhat");
const abi = require("../artifacts/contracts/TaskList.sol/TaskList.json");
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
const MUMBAI_URL = process.env.MUMBAI_URL;
-const PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
+const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY;
+const FUJI_URL = process.env.FUJI_URL;
+const FUJI_PRIVATE_KEY = process.env.FUJI_PRIVATE_KEY;
async function main(){
const contractAddress = CONTRACT_ADDRESS;
const contractABI = abi.abi;
// const provider = new hre.ethers.providers.EtherscanProvider("maticmum", process.env.MATIC_PRIVATE_KEY);
- const provider = new hre.ethers.providers.JsonRpcProvider(MUMBAI_URL);
+ const provider = new hre.ethers.providers.JsonRpcProvider(FUJI_URL);
- const signer = new hre.ethers.Wallet(PRIVATE_KEY, provider);
+ const signer = new hre.ethers.Wallet(FUJI_PRIVATE_KEY, provider);
const taskList = new hre.ethers.Contract(contractAddress, contractABI, signer);
// Nos permite agregar nuevas tareas
const description = hre.ethers.utils.formatBytes32String("Deploy de contrato en tesnet");
await taskList.addTask(description);
// Permite marcar como terminada una tarea
// await taskList.completeTask(1);
// Elimina una tarea
//await taskList.deleteTask(1);
const tasks = await taskList.getTaskCount();
console.log(`Task number: ${tasks}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
por ultimo solo probaremos la creación y actualización a completado de nuestra tarea
npx hardhat run scripts/TaskList.js
Task number: 0
nos dirá que nuestro total de tareas son 0, pero si vamos al explorador de bloques veremos la transacción, por ultimo probaremos la actualización de la tarea comentando el código de creación y se descomenta la función completeTask
, ejecutaremos de nuevo
npx hardhat run scripts/TaskList.js
Task number: 1
veremos que el numero de tareas es 1, y en el explorador de bloques podremos visualizar la transacción
de esta forma ya contamos con un smart contract funcional que podríamos llevar a las redes principales de las cadenas de bloques, para ello solo basta con agregar las direcciones correspondientes a su respectiva mainnet y se configuran en el file hardhat.config.js
asi como en el archivo .env
colocar las URL rpc correspondientes.
Conclusiones
Como se podemos observar el uso de hardhat como entorno de desarrollo es sumamente potente, ya que nos permite realizar pruebas y testing de forma local con todas las herramientas necesarias ya integradas, ahorrándonos mucho tiempo en instalar software extra. Por otro lado las redes de Polygon y Avalanche son muy económicas para el despliegue de contratos inteligentes dentro de su mainnet y en cuanto a pruebas estas te proporcionan todo un entorno muy completo para que puedas validar tus desarrollos, en lo personal estas dos redes son las que mas me han agradado hasta el momento.
Si llegaste hasta aquí espero que el tutorial te haya servido tanto si eres nuevo en el mundo de la blockchain o si ya tienes experiencia, y que esta tecnología te parezca tan interesante como a mi, seguiré agregando posts relacionados a blockchain y sobre otro temas relacionados, asi que no te pierdas de novedades.