WEB3DEV Español

Cover image for Cómo desplegar tu primer contrato inteligente en Ethereum con Solidity y Hardhat
Hector
Hector

Posted on

Cómo desplegar tu primer contrato inteligente en Ethereum con Solidity y Hardhat

Este artículo es una traducción de StErMi, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.

Estaba planeando hacer un tutorial sobre cómo desplegar tu primer contrato inteligente de un NFTs pero, mientras lo escribía, entendí que más de la mitad del contenido era sobre cómo configurar Hardhat para tener el entorno de desarrollo perfecto, listo para que puedas hacerlo en tu siguiente proyecto.

Así que empecemos desde ahí y, cuando haga el tutorial sobre los NFTs, puedes aprovechar el trabajo que haremos hoy, para que desarrolles tu próximo contrato inteligente. ¿Continuamos?

¿Qué vas a aprender hoy?

  • Configurar Hardhat para compilar, desplegar, probar y depurar tu código de Solidity
  • Escribir un contrato inteligente
  • Probar tu contrato inteligente
  • Desplegar tu contrato inteligente en Rinkeby
  • Verificar tu contrato inteligente en Etherscan

Hardhat

Hardhat es la herramienta principal que estaremos usando hoy y es imprescindible si necesitas desarrollar y depurar tu contrato inteligente de forma local, antes de desplegarla en la red de prueba y eventualmente, en la red principal.

¿Qué es Hardhat?

Hardhat es un entorno de desarrollo para compilar, desplegar, probar y depurar tu software de Ethereum. Ayuda a los desarrolladores a gestionar y automatizar las tareas recurrentes que son inherentes al proceso de construir contratos inteligentes y dApps, así cómo introduce, fácilmente, más funcionalidades alrededor del flujo de trabajo. Esto quiere decir que, esencialmente: compila, ejecuta y prueba contratos inteligentes.

¿Por qué usar Hardhat?

  • Puedes ejecutar Solidity localmente para desplegar, ejecutar, probar y depurar tus contratos, sin tener que lidiar con el entorno en vivo.
  • Obtienes rastreamientos del stack de Solidity, console.log, y mensajes con errores explícitos cuando las transacciones fallan. Si, lo leíste bien, ¡console.log en tu contrato de Solidity!
  • Muchísimos *plugins para añadir funcionalidades geniales que te permiten integrarlas a tus herramientas existentes. ¡Usaremos un par de ellas para sentir la magia!
  • Apoyo total nativo para TypeScript.

Enlaces útiles para aprender más:

Crea tu primer proyecto Hardhat

¡Es hora de abrir tu Terminal, tu Visual Studio code IDE y empezar a construir!

¿Qué proyecto vamos a construir?

En este tutorial, vamos a construir un contrato inteligente “fácil”. Será muy simple pero nuestro objetivo principal, es configurar Hardhat y desplegar el contrato, no para el propósito del contrato.

¿Qué hará el contrato?

  • Rastrear el “propósito” del contrato en un string que será públicamente visible.
  • Puedes cambiar el propósito sólo si pagas más que el dueño anterior
  • No puedes cambiar el propósito si ya eres el dueño
  • Puedes retirar tus ETH sólo si no eres el dueño del propósito actual.
  • Emitir un evento PurposeChange cuando el propósito ha cambiado.

Crea el proyecto, inícialo y configura Hardhat

Abre tu terminal y vamos. ¡Llamaremos a nuestro proyecto propósito mundial!

mkdir world-purpose
cd world-purpose
npm init -y
yarn add --dev hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Connpx hardhat el asistente de hardhat empezará a ayudarte a configurar tu proyecto.

  • Elige Create a basic sample project
  • Elige por defecto Hardhat project rootporque en este tutorial - sólo vamos a configurar hardhat.
  • Confirma para añadir .gitignore
  • Confirma para instalar sample project's dependecies with yarn esto instalará algunas dependencias requeridas para tu proyecto, que usaremos en el camino.

Ahora la estructura de tu proyecto debería tener estos archivos/carpetas:

  • Contracts donde todos tus contratos estarán almacenados
  • Scripts donde todos tus scripts/tareas estarán almacenadas
  • Test donde tus contratos inteligentes estarán almacenados
  • Hardhat.config.js donde configurarás tu proyecto hardhat

Comandos importantes de Hardhat y conceptos que debes dominar

  • Archivo de configuración de Hardhat
  • Tareas de Hardhat
  • npx hardhat node - Ejecuta el nodo de tareas para iniciar el servidor JSON-RPC sobre la Red Hardhat (tu blockchain local de ethereum)
  • npx hardhat test - Para ejecutar pruebas almacenadas en el archivo de pruebas
  • npx hardhat compile - Para compilar todo el proyecto, construyendo tu contrato inteligente
  • npx hardhat clean - Para limpiar el caché y borrar los contratos inteligentes compilados
  • npx hardhat run —-network <network> script/path - Para ejecutar un script en particular, en una network en particular

Añade el soporte TypeScript

Como hemos dicho, Hardhat soporta typescript de forma nativa y lo estaremos usando. No cambia mucho usar javascript o typescript pero, esta es mi preferencia personal porque pienso que harás menos errores y te ayudará a entender mejor cómo usar las librerías externas.

Vamos a instalar algunas dependencias necesarias:

yarn add --dev ts-node typescript
yarn add --dev chai @types/node @types/mocha @types/chai

Cambia la configuración del archivo externo de hardhat a .ts y actualiza el contenido con esto:

import { task } from 'hardhat/config';
import '@nomiclabs/hardhat-waffle';
// Esta es una muestra de la tarea Hardhat. Para aprender a cómo crear el tuyo, ve a
// https://hardhat.org/guides/create-task.html
task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => {
 const accounts = await hre.ethers.getSigners();
 for (const account of accounts) {
   console.log(account.address);
 }
});
// Necesitas exportar un objeto para establecer tu configuración
// Ve a https://hardhat.org/config/ to learn more
export default {
 solidity: '0.8.4',
};
Enter fullscreen mode Exit fullscreen mode

Ahora crea un tsconfig.json por defecto, en el archivo principal de tu proyecto con este contenido:

{
 "compilerOptions": {
   "target": "es2018",
   "module": "commonjs",
   "strict": true,
   "esModuleInterop": true,
   "outDir": "dist"
 },
 "include": ["./scripts", "./test"],
 "files": ["./hardhat.config.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Ahora actualiza el archivo de prueba y el archivo script al archivo typescript .ts y cambia el código:

import { expect } from 'chai';
import { ethers } from 'hardhat';
describe('Greeter', function () {
 it("Should return the new greeting once it's changed", async function () {
   const Greeter = await ethers.getContractFactory('Greeter');
   const greeter = await Greeter.deploy('Hello, world!');
   await greeter.deployed();
   expect(await greeter.greet()).to.equal('Hello, world!');
   const setGreetingTx = await greeter.setGreeting('Hola, mundo!');
   // espera a que la transacción sea minada
   await setGreetingTx.wait();
   expect(await greeter.greet()).to.equal('Hola, mundo!');
 });
});
// Requerimos que el Entorno de Ejecución de Hardhat (Runtime Environment) sea explícitamente aquí. Esto es opcional
// pero es útil para ejecutar el script de forma independiente a través de `node <script>`.
// Cuando se ejecute el script con `npx hardhat run <script>` encontrarás el Hardhat
// miembros del Entorno de Ejecución  disponible en el alcance global.
import { ethers } from 'hardhat';
async function main() {
 // Hardhat siempre ejecutará la tarea de compilar cuando se ejecute el script con su comando
 // línea de interfaz.
 // Si este script se ejecuta directamente usando `node` es recomendable que invoques la compilación
 // manualmente para asegurarte que todo esté compilado
 // await hre.run('compile');
 // Recibimos el contrato para desplegar
 const Greeter = await ethers.getContractFactory('Greeter');
 const greeter = await Greeter.deploy('Hello, Hardhat!');
 await greeter.deployed();
 console.log('Greeter deployed to:', greeter.address);
}
// Recomendamos este patrón para que puedas usar async/await en todas partes
// y manejar apropiadamente los errores.
main()
 .then(() => process.exit(0))
 .catch((error) => {
   console.error(error);
   process.exit(1);
 });
Enter fullscreen mode Exit fullscreen mode

Vamos a probar que todo esté funcionando correctamente:

npx hardhat test
npx hardhat node
npx hardhat run --network localhost scripts/sample-script.ts

Vamos a añadir algunos plugins de Hardhat y códigos utilitarios

Añade solhint, solhint-plugin-prettier y el plugin hardhat-solhint

Ahora queremos añadir el soporte a solhint , una utilidad linting para el código de Solidity que nos ayudará a seguir reglas estrictas mientras desarrollamos nuestro contrato inteligente. Estas reglas son útiles para seguir tanto el código estándar de las mejores prácticas de estilos como para adherirse a los mejores parámetros de seguridad.

Solhint+solhint prettier

yarn add --dev solhint solhint-plugin-prettier prettier prettier-plugin-solidity

Añade la configuración del archivo .solhint.json de tu archivo principal:

{
 "plugins": ["prettier"],
 "rules": {
   "prettier/prettier": "error"
 }
}
Enter fullscreen mode Exit fullscreen mode

Añade el archivo de configuración .prettierrc y añade estilos como lo prefieras. Esta es mi elección personal:

{
 "arrowParens": "always",
 "singleQuote": true,
 "bracketSpacing": false,
 "trailingComma": "all",
 "printWidth": 120,
 "overrides": [
   {
     "files": "*.sol",
     "options": {
       "printWidth": 120,
       "tabWidth": 4,
       "useTabs": false,
       "singleQuote": false,
       "bracketSpacing": false,
       "explicitTypes": "always"
     }
   }
 ]
}
Enter fullscreen mode Exit fullscreen mode

Si quieres saber más sobre solhint y su plugin prettier, revisa la documentación de su sitio:

Hardhat+solhint

Si quieres saber más, sigue la documentación solhint plugin de Hardhat.

yarn add --dev [@nomiclabs/hardhat-solhint](url)http://twitter.com/nomiclabs/hardhat-solhint

Y actualiza la configuración de hardhat añadiendo esta línea a la importación

import “@nomiclabs/hardhat-solhint”;

Typechain para el soporte de contrato tipeado

Esta parte es totalmente opcional. Como dije, me gusta typescript y me gusta usarlo en todo lo que pueda, cuando sea posible. Añadir soporte para los contratos tipeados me permite saber perfectamente cuales funcionalidades están disponibles, cuáles parámetros de cada tipo son necesarios y que están ejecutando.

Podrías seguir sin problemas, pero te sugiero que sigas este paso.

¿Por qué typechain?

  • Uso de Configuración Cero - Ejecuta la tarea _compilación_como siempre y los artefactos de Typechain automáticamente serán generados en el directorio principal llamado typechain
  • Generación incrementada - Solo los archivos recompilados tendrán sus types regenerados.
  • Sin fricción - El type de retorno de ethers.getContractFactory será tipeado apropiadamente, no hay necesidad de lanzamientos

Si quieres profundizar en este tipo de proyectos y saber todas las opciones de configuración posible, puedes ver los siguientes enlaces:

Hagámoslo:

yarn add --dev typechain @typechain/hardhat @typechain/ethers-v5

Añade estas importaciones al archivo de configuración de tu Hardhat

import '@typechain/hardhat'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'

Añade "resolveJsonModule": true a tu tsconfig.json

Ahora cuando compiles tu contrato typechain, generará los types correspondientes en el archivo typechain y, ¡podrás usarlo en tus pruebas y en la aplicación web!

Actualiza los archivos de prueba para usar los types generados de Typechain

import {ethers, waffle} from 'hardhat';
import chai from 'chai';
import GreeterArtifact from '../artifacts/contracts/Greeter.sol/Greeter.json';
import {Greeter} from '../typechain/Greeter';
const {deployContract} = waffle;
const {expect} = chai;
describe('Greeter', function () {
 let greeter: Greeter;
 it("Should return the new greeting once it's changed", async function () {
   const signers = await ethers.getSigners();
   greeter = (await deployContract(signers[0], GreeterArtifact, ['Hello, world!'])) as Greeter;
   expect(await greeter.greet()).to.equal('Hello, world!');
   const setGreetingTx = await greeter.setGreeting('Hola, mundo!');
   // espera a que la transacción sea minada
   await setGreetingTx.wait();
   expect(await greeter.greet()).to.equal('Hola, mundo!');
 });
});
Enter fullscreen mode Exit fullscreen mode

Actualiza los scripts en package.json

Esta parte es totalmente opcional pero, te permite ejecutar el comando de forma fácil y preconfigurarlo. Abre tu package.json y reemplazalo con la sección de scripts con estos comandos:

"clean": "npx hardhat clean",
"chain": "npx hardhat node",
"deploy": "npx hardhat run --network localhost scripts/deploy.ts",
"deploy:rinkeby": "npx hardhat run --network rinkeby scripts/deploy.ts",
"test": "npx hardhat test"
Enter fullscreen mode Exit fullscreen mode

Ahora, si todo lo que quieres es ejecutar el nodo Hardhat, puedes escribir en tu consola yarn chain y ¡boom! ¡Estamos listos para seguir!

Si te has saltado la parte de TypeScript y sólo quieres usar JavaScript, cambia estos .ts a .js y todo funcionará como debe ser

Desarrollando el contrato inteligente

Bien, ahora estamos listos de verdad. Renombra Greeter.sol en tu archivo contracts a WorldPurpose.sol y empezaremos a construir desde ahí.

Nuestro contrato necesita hacer las siguientes cosas:

  • Rastrear el “propósito” del contrato en una estructura
  • Puedes cambiar el propósito sólo si pagas más que el dueño anterior
  • No puedes cambiar el propósito si ya eres el dueño
  • Puedes retirar tus ETH sólo si no eres el dueño del propósito actual
  • Emite un evento PurposeChange cuando el propósito ha cambiado

Conceptos que debes dominar

Código del Contrato Inteligente

Vamos a empezar creando una Estructura para almacenar la información del Propósito

/// @notice el Propósito de la estructura
struct Purpose {
   address owner;
   string purpose;
   uint256 investment;
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a definir algunas variables de estado para rastrear tanto el Propósito actual como la inversión del usuario

/// @notice Rastrea la inversión del usuario 
mapping(address => uint256) private balances;
/// @notice Rastrea el propósito actual mundial
Purpose private purpose;
Enter fullscreen mode Exit fullscreen mode

Por último, pero no menos importante, define un Evento que será emitido cuando un nuevo Propósito Mundial sea hecho

/// @notice Evento para rastrear el nuevo Propósito
event PurposeChange(address indexed owner, string purpose, uint256 investment);
Enter fullscreen mode Exit fullscreen mode

Vamos a crear algunas funciones utilitarias para obtener el purpose actual y el balance del usuario, que sigue en el contrato. Necesitamos hacer eso porque las variables balances y purpose son private así que no pueden ser accedidas directamente desde un contrato/aplicaciones web3 externas. Necesitamos exponerlas a través de estas funciones:

/**
@notice // Reconocimiento para el propósito actual
@return currentPurpose // El propósito mundial actual activo
*/
function getCurrentPurpose() public view returns (Purpose memory currentPurpose) {
   return purpose;
}
/**
@notice // Obtén la cantidad total de inversión que has hecho. Regresa a ambas inversiones bloqueadas y desbloqueadas.
@return balance // El balance que aún tienes en el contrato
*/
function getBalance() public view returns (uint256 balance) {
   return balances[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Ahora, vamos a crear la función setPurpose. Toma un parámetro como entrada: purpose. La función debe ser payable porque queremos aceptar algún Ether con el fin de establecer el propósito (esos ether podrán ser reembolsables por el dueño que tiene su propósito sustituído por alguien más).

La transacción será revert si alguna de las condiciones no se cumplen:

  • El parámetro de entrada purpose está vacío
  • El msg.value (la cantidad de ether enviada con la transacción) está vacío (0 ethers)
  • El msg.value es menor que el propósito actual
  • El dueño del propósito mundial actual intenta reemplazar su propósito (msg.sender debe ser diferente al del dueño actual)
/**
@notice // Modifier para verificar que el dueño anterior del propósito no es el mismo que el del nuevo dueño
*/
modifier onlyNewUser() {
   // Verifica que el nuevo dueño no sea el actual
   require(purpose.owner != msg.sender, "You cannot override your own purpose");
   _;
}
/**
@notice // Define el nuevo propósito mundial
@param _purpose // El contenido del nuevo propósito
@return newPurpose // El nuevo propósito activo mundial
*/
function setPurpose(string memory _purpose) public payable onlyNewUser returns (Purpose memory newPurpose) {
   // Verifica que el nuevo dueño nos ha enviado suficientes fondos para reemplazar el propósito anterior
   require(msg.value > purpose.investment, "You need to invest more than the previous purpose owner");
   // Verifica si el nuevo propósito está vacío
   bytes memory purposeBytes = bytes(_purpose);
   require(purposeBytes.length > 0, "You need to set a purpose message");
   // Actualiza el propósito con el nuevo
   purpose = Purpose(msg.sender, _purpose, msg.value);
   // Actualiza el valor del remitente
   balances[msg.sender] += msg.value;
   // Emite el evento PurposeChange
   emit PurposeChange(msg.sender, _purpose, msg.value);
   // Retorna el nuevo propósito
   return purpose;
}
Enter fullscreen mode Exit fullscreen mode

Si todo funciona, colocamos el nuevo propósito, actualizamos el balance y emitimos el evento.

Ahora, queremos permitirle a los usuarios retirar su inversión de propósitos anteriores. Toma en cuenta que solo la inversión del propósito actual está “bloqueada”. Solo será desbloqueada cuando una nueva persona coloque el nuevo propósito.

/**
@notice // Retira los fondos del propósito anterior. Si tienes un propósito activo, esos fondos estarán "bloqueados"
*/
function withdraw() public {
   // Obtén el balance del usuario
   uint256 withdrawable = balances[msg.sender];
   // Ahora necesitamos verificar cuánto puede retirar el usuario
   address currentOwner = purpose.owner;
   if (currentOwner == msg.sender) {
       withdrawable -= purpose.investment;
   }
   // Verifica que el usuario tiene suficiente en su balance para retirar
   require(withdrawable > 0, "You don't have enough withdrawable balance");
   // Actualiza el balance
   balances[msg.sender] -= withdrawable;
   // Transfiere el balance
   (bool sent, ) = msg.sender.call{value: withdrawable}("");
   require(sent, "Failed to send user balance back to the user");
}
Enter fullscreen mode Exit fullscreen mode

Despliégalo localmente solo para probar que todo funcione como debe ser. Deberías ver algo así:

Image description

El despliegue exitoso del contrato en un blockchain local

Añade la prueba local

No detallaré sobre el código dentro del archivo de prueba pero te explicaré el concepto detrás de ello. Siempre deberías crear casos de prueba para tu contrato. Es una forma rápida de entenderlo si la lógica del contacto funciona como se espera y te permite prevenir el despliegue de algo que no está funcionando.

Mi flujo de trabajo es así: tomo cada función y pruebo que hagan lo que espero que hagan en ambos casos: exitosos y revertidos. Nada más, nada menos.

Cuando escribes pruebas para contratos de Solidity, vas a tener que usar Waffle. Ve a su página web para que le eches un vistazo a la librería. Está muy bien hecha y ofrece muchas utilidades.

Ahora, elimina el archivo que tienes en tu carpeta test, crea una nueva llamada worldpurpose.ts y reemplaza el contenido con esto:

import {ethers, waffle} from 'hardhat';
import chai from 'chai';
import WorldPurposeArtifact from '../artifacts/contracts/WorldPurpose.sol/WorldPurpose.json';
import {WorldPurpose} from '../typechain/WorldPurpose';
import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers';
const {deployContract} = waffle;
const {expect} = chai;
describe('WorldPurpose Contract', () => {
 let owner: SignerWithAddress;
 let addr1: SignerWithAddress;
 let addr2: SignerWithAddress;
 let addrs: SignerWithAddress[];
 let worldPurpose: WorldPurpose;
 beforeEach(async () => {
   // eslint-disable-next-line no-unused-vars
   [owner, addr1, addr2, ...addrs] = await ethers.getSigners();
   worldPurpose = (await deployContract(owner, WorldPurposeArtifact)) as WorldPurpose;
 });
 describe('Test setPurpose', () => {
   it("set purpose success when there's no purpose", async () => {
     const purposeTitle = 'Reduce the ETH fee cost in the next 3 months';
     const purposeInvestment = ethers.utils.parseEther('0.1');
     await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
       value: purposeInvestment,
     });
     // Verifica que el propósito haya sido definido
     const currentPurpose = await worldPurpose.getCurrentPurpose();
     expect(currentPurpose.purpose).to.equal(purposeTitle);
     expect(currentPurpose.owner).to.equal(addr1.address);
    expect(currentPurpose.investment).to.equal(purposeInvestment);
     // Verifica que el balance haya sido actualizado
     const balance = await worldPurpose.connect(addr1).getBalance();
     expect(balance).to.equal(purposeInvestment);
   });
   it('override the prev purpose', async () => {
     await worldPurpose.connect(addr2).setPurpose("I'm the old world purpose", {
       value: ethers.utils.parseEther('0.1'),
     });
     const purposeTitle = "I'm the new world purpose!";
     const purposeInvestment = ethers.utils.parseEther('0.11');
     await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
       value: purposeInvestment,
     });
     // Verfiica que el propósito haya sido definido
     const currentPurpose = await worldPurpose.getCurrentPurpose();
     expect(currentPurpose.purpose).to.equal(purposeTitle);
     expect(currentPurpose.owner).to.equal(addr1.address);
    expect(currentPurpose.investment).to.equal(purposeInvestment);
     // Verifica que el balance haya sido actualizado
     const balance = await worldPurpose.connect(addr1).getBalance();
     expect(balance).to.equal(purposeInvestment);
   });
   it('Check PurposeChange event is emitted ', async () => {
     const purposeTitle = "I'm the new world purpose!";
     const purposeInvestment = ethers.utils.parseEther('0.11');
     const tx = await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
       value: purposeInvestment,
     });
     await expect(tx).to.emit(worldPurpose, 'PurposeChange').withArgs(addr1.address, purposeTitle, purposeInvestment);
   });
   it("You can't override your own purpose", async () => {
     await worldPurpose.connect(addr1).setPurpose("I'm the new world purpose!", {
       value: ethers.utils.parseEther('0.10'),
     });
     const tx = worldPurpose.connect(addr1).setPurpose('I want to override the my own purpose!', {
       value: ethers.utils.parseEther('0.11'),
     });
     await expect(tx).to.be.revertedWith('You cannot override your own purpose');
   });
   it('Investment needs to be greater than 0', async () => {
     const tx = worldPurpose.connect(addr1).setPurpose('I would like to pay nothing to set a purpose, can I?', {
       value: ethers.utils.parseEther('0'),
     });
     await expect(tx).to.be.revertedWith('You need to invest more than the previous purpose owner');
   });
   it('Purpose message must be not empty', async () => {
     const tx = worldPurpose.connect(addr1).setPurpose('', {
       value: ethers.utils.parseEther('0.1'),
     });
     await expect(tx).to.be.revertedWith('You need to set a purpose message');
   });
   it('New purpose investment needs to be greater than the previous one', async () => {
     await worldPurpose.connect(addr1).setPurpose("I'm the old purpose!", {
       value: ethers.utils.parseEther('0.10'),
     });
     const tx = worldPurpose
       .connect(addr2)
       .setPurpose('I want to pay less than the previous owner of the purpose, can I?', {
         value: ethers.utils.parseEther('0.01'),
       });
     await expect(tx).to.be.revertedWith('You need to invest more than the previous purpose owner');
   });
 });
 describe('Test withdraw', () => {
   it('Withdraw your previous investment', async () => {
     const firstInvestment = ethers.utils.parseEther('0.10');
     await worldPurpose.connect(addr1).setPurpose('First purpose', {
       value: ethers.utils.parseEther('0.10'),
     });
     await worldPurpose.connect(addr2).setPurpose('Second purpose', {
       value: ethers.utils.parseEther('0.11'),
     });
     const tx = await worldPurpose.connect(addr1).withdraw();
     // Revisa que mi balance actual en el contrato sea 0
     const balance = await worldPurpose.connect(addr1).getBalance();
     expect(balance).to.equal(0);
     // Verifica que volví a tener mi cartera en toda la importación
     await expect(tx).to.changeEtherBalance(addr1, firstInvestment);
   });
   it('Withdraw only the unlocked investment', async () => {
     const firstInvestment = ethers.utils.parseEther('0.10');
     await worldPurpose.connect(addr1).setPurpose('First purpose', {
       value: ethers.utils.parseEther('0.10'),
     });
     await worldPurpose.connect(addr2).setPurpose('Second purpose', {
       value: ethers.utils.parseEther('0.11'),
     });
   const secondInvestment = ethers.utils.parseEther('0.2');
     await worldPurpose.connect(addr1).setPurpose('Third purpose from the first addr1', {
       value: secondInvestment,
     });
     const tx = await worldPurpose.connect(addr1).withdraw();
    // En este caso, el usuario solo puede retirar si es su primera inversión
     // La segunda sigue estando "bloqueado" porque él es el dueño del propósito actual
     // Verifica que mi balance actual en el contrato es 0
     const balance = await worldPurpose.connect(addr1).getBalance();
     expect(balance).to.equal(secondInvestment);
     // Verifica que volví a tener mi cartera en toda la importación
     await expect(tx).to.changeEtherBalance(addr1, firstInvestment);
   });
   it('You cant withdraw when your balance is empty', async () => {
     const tx = worldPurpose.connect(addr1).withdraw();
     await expect(tx).to.be.revertedWith("You don't have enough withdrawable balance");
   });
  it('Withdraw only the unlocked investment', async () => {
     await worldPurpose.connect(addr1).setPurpose('First purpose', {
      value: ethers.utils.parseEther('0.10'),
     });
     const tx = worldPurpose.connect(addr1).withdraw();
     // Tus fondos siguen estando "bloqueados" porque tú eres el dueño del propósito actual
     await expect(tx).to.be.revertedWith("You don't have enough withdrawable balance");
   });
 });
});
Enter fullscreen mode Exit fullscreen mode

Ejecuta tus pruebas con yarn test, las cuales verifican si todo sucede como se espera:

Image description

La ejecución de las pruebas

Despliega el contrato en la red de prueba

=====COMIENZO DE LA EXENCIÓN DE RESPONSABILIDAD=====

NO USES TU CARTERA PRIVADA PRINCIPAL PARA EL PROPÓSITO DE HACER PRUEBAS

USA UNA CARTERA ALTERNATIVA O CREA UNA NUEVA CARTERA

=====FIN DE LA EXENCIÓN DE RESPONSABILIDAD=====

Ya hemos visto cómo desplegar el contrato en nuestra cadena Hardhat local pero ahora, queremos hacerlo para la red de prueba de Rinkeby (el procedimiento es el mismo que el de la red principal pero no es el enfoque de este tutorial).

Para desplegar en la cadena local, necesitamos escribir, en la consola, este comando:

npx hardhat run --network localhost scripts/deploy.ts

Para desplegar tus contratos en otra red, solo necesitamos cambiar el valor del parámetro de --network pero, antes de hacer eso, necesitamos hacer unos pasos de preparación.

Obtén algunos ETH del la red de prueba Faucet

Despliega el costo de gas del contacto ya que necesitamos algunos de Faucet. Elige una red de prueba y ve al faucet para tener fondos.

En nuestro caso, decidimos usar Rinkeby así que ve y obtén fondos allí.

Elige un proveedor Ethereum

Para que podamos desplegar nuestro contrato, necesitamos una forma de interactuar directamente con la red de Ethereum. Ahora mismo, tenemos dos opciones posibles:

En nuestro caso, vamos a usar Alchemy. Así que, ve allí, crea una cuenta, crea una nueva aplicación y agarra la Llave API Rinkeby.

Actualiza el archivo de Configuración Hardhat

Instala las dependencias dotenv. Esta librería: nodejs, nos permite crear un archivo de entorno .env para almacenar nuestras variables sin tener que exponerlas al código fuente.

yarn add --dev dotenv

Ahora crea, en el directorio principal de tu proyecto, el archivo .env y copia y pega toda la información que has recopilado:

RENKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR_ALCHEMY_APP_ID>
PRIVATE_KEY=<YOUR_BURNER_WALLET_PRIVATE_KEY>
Enter fullscreen mode Exit fullscreen mode

Voy a volver a repetir. No uses tu cartera principal para el propósito de hacer pruebas. ¡Crea una cartera separada o usa una cartera alternativa para hacer este tipo de pruebas!

Este último paso es para actualizar el archivo hardhat.config.ts para añadir dotenv y actualizar la información rinkeby.

En el tope del archivo añade require("dotenv").config(); y ahora actualiza la configuración, como esto:

const config: HardhatUserConfig = {
 solidity: '0.8.4',
 networks: {
   rinkeby: {
     url: process.env.RENKEBY_URL || '',
     accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
   },
 },
};
Enter fullscreen mode Exit fullscreen mode

¿Estamos listos? ¡Vamos a desplegarlo!

npx hardhat run --network rinkeby scripts/deploy.ts

Si todo va bien, deberías ver algo así en tú consola:

Image description

Si copias y pegas la dirección del contrato y la pegas en Etherscan Rinkeby, ¡deberías poder verla en vivo! El mío está localizado aquí:

0xAf688A3405531e0FC274F6453cD8d3f3dB07D706

Obtén tu contrato verificado por Etherscan

La verificación del código fuente provee transparencia para los usuarios que interactúan con los contratos inteligentes. Subiendo el código fuente, Etherscan coincidirá el código compilado con el de la blockchain. Así como los contratos, un “contrato inteligente” debería proveer a los usuarios finales con más información sobre qué están “firmando digitalmente” y les da a los usuarios la oportunidad para auditar el código para verificar, independientemente, que de hecho haga lo que se supone que deba hacer.

Así que hagámoslo. Es realmente un paso muy fácil ya que Hardhat provee un Plugin específico para hacerlo: hardhat-etherscan.

Vamos a instalar yarn add --dev @nomiclabs/hardhat-etherscan

E incluye en tu hardhat.config.ts añadiendo import "@nomiclabs/hardhat-etherscan"; al final de tus importaciones.

El siguiente paso es registrar una cuenta en Etherscan y generar una llave API.

Una vez que la tengas, necesitamos actualizar nuestro archivo .env añadiendo esta línea de código:

Y actualizar el hardhat.config.ts añadiendo la configuración necesaria de Etherscan, necesaria para el plugin.

La última cosa para hacer es ejecutar la tarea verify que ha sido añadida por el plugin:

npx hardhat verify --network rinkeby <YOUR_CONTRACT_ADDRESS>

El plugin tiene muchas configuraciones y diferentes opciones, así que revisa la documentación si quieres saber más.

Si todo funciona como debe ser, deberías ver algo así en tú consola:

Image description

Contrato verificado por Etherscan

Si vas de nuevo a la página del contrato de Etherscan, deberías ver una marca de verificación verde sobre la sección del Contrato. ¡Bien hecho!

Conclusión y siguientes pasos

Si quieres usar este solidity-template para iniciar tu siguiente proyecto de contrato inteligente, solo ve al repositorio de GitHub https://github.com/StErMi/solidity-template y usa el botón verde “Use this template” y listo, ¡estás apto para seguir!

Este es solo el comienzo. Planeo añadir más características eventualmente, como por ejemplo:

¿Tienes algún comentario o quieres contribuir al Template de Solidity? ¡Solo crea un Pull Request en la página de GitHub y vamos a discutirlo!

¿Te gustó este contenido? ¡Sígueme para más!

Discussion (0)