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
Connpx hardhat
el asistente de hardhat empezará a ayudarte a configurar tu proyecto.
- Elige
Create a basic sample project
- Elige por defecto
Hardhat project root
porque 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 unanetwork
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',
};
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"]
}
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);
});
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"
}
}
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"
}
}
]
}
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!');
});
});
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"
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;
}
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;
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);
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];
}
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;
}
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");
}
Despliégalo localmente solo para probar que todo funcione como debe ser. Deberías ver algo así:
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");
});
});
});
Ejecuta tus pruebas con yarn test
, las cuales verifican si todo sucede como se espera:
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>
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] : [],
},
},
};
¿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:
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:
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:
- Soporte a slint para la biblioteca de prueba typescript
- Soporte para el despliegue hardhat, un Plugin de Hardhat para manejar de mejor modo el despliegue
- Añadir soporte al plugin cobertura solidity
- Añadir soporte al plugin reporte de gas hardhat
- TBD
¿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!
- GitHub: https://github.com/StErMi
- Twitter: https://twitter.com/StErMi
- Medium: https://twitter.com/StErMi
- Dev.to: https://twitter.com/StErMi
Discussion (0)