Cómo crear tu propio smart contract con Solidity y Hardhat 🔥🚀
Tags: solidity, blockchain, smartcontract
Published: 2023-05-12 00:26:40

# 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. Además 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.
Además 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](https://nodejs.org), 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](https://hardhat.org/hardhat-runner/docs/getting-started).

Una vez instalado deberíamos de ver el siguiente resultado al ejecutar el comando para la creación de un nuevo proyecto:

```sh
$ 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 crearé uno basado en JavaScript, pero puedes crearlo basado en TypeScript o vació si es el caso. El proyecto lo llamaré `notes_dapp`.

# Creación de smart contract
Dentro de la carpeta de contracts definiremos el smart contract correspondiente

```js
// 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:

```sh
❯ 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`

```sh
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:
```js
// 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:
```sh
❯ 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

```js
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)](https://hardhat.org/hardhat-runner/docs/getting-started#connecting-a-wallet-or-dapp-to-hardhat-network).

En este caso no es necesario ejecutar el nodo.
```js
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:

```sh
❯ 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](https://support.metamask.io/hc/en-us/articles/360015489531-Getting-started-with-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:

![tesnet-connection](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/01_tesnet_connection.png)

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.

![polygon_faucet](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/02_polygon_faucet_site.png)

se abrirá una ventana donde confirmaremos la transacción

![funding_confirmation](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/03_funding_confirmation.png)

después de un par de minutos podremos ver 0.2 Matic en nuestra dirección

![funded_wallet](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/04_funding_wallet.png)

Ahora agregaremos la red de MATIC a la configuración de hardhat así como las variables de entorno correspondientes:

```js
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`
```sh
$ yarn add dotenv
```
Se crea el archivo `.env` en la raíz del proyecto con el siguiente contenido:

```js
MUMBAI_URL=https://rpc-mumbai.maticvigil.com
MUMBAI_PRIVATE_KEY=TU_CLAVE_PRIVADA
```
La clave la obtienes de la siguiente forma:

![private_key](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/05_private_key.png)
![private_key_2](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/06_private_key.png)

Ya que esta configurada la red estamos listos para desplegar el contrato mediante el comando:

```sh
$ npx hardhat run scripts/TaskListDeploy.js --network mumbai
```
después de ejecutar el comando deberíamos de tener lo siguiente:

```sh
$ npx hardhat run scripts/TaskListDeploy.js --network mumbai
Notes dApp deployed on {tu dirección de contrato}
```
Para visualizarlo vamos al [explorador de mumbai](https://mumbai.polygonscan.com/) y colocamos la dirección del contrato, se debería de poder visualizar la transacción

![deployed_contract](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/07_deployed_contract.png)

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

```js
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
```js
// 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.

```sh
$ 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
```sh
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

```sh
npx hardhat run scripts/TaskList.js
Task number: 1
```
y podremos ver la transacción en el explorador de bloques

![first_transaction](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/08_firts_transaction.png)

Podemos agregar la función de marcar como terminada la tarea
```js
// 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

```sh
npx hardhat run scripts/TaskList.js
Task number: 1
```

![mark_complete](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/09_mark_complete.png)

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`
```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

![request_avax](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/10_request_avax.png)

Una vez que los fondos se encuentren en nuestra billetera podremos desplegar el contrato
```sh
$ 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](https://testnet.snowtrace.io/) colocamos en el buscador la dirección del contrato y podremos visualizarlo ya desplegado en la blockchain de pruebas de Avalanche

![snowtrace_contract](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/11_snowtrace_contract.png)

Para probar el contrato comentaremos las credenciales que pertenecían a la red de Polygon y colocaremos las correspondientes a la red de Avalanche

```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;
+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 último solo probaremos la creación y actualización a completado de nuestra tarea
```sh
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 último probaremos la actualización de la tarea comentando el código de creación y se descomenta la función `completeTask`, ejecutaremos de nuevo
```sh
npx hardhat run scripts/TaskList.js
Task number: 1
```
veremos que el número de tareas es 1, y en el explorador de bloques podremos visualizar la transacción

![complete_task](https://literalcoder.s3.amazonaws.com/posts-images/eth-smart-contract/12_complete_task.png)

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` así 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, ahorrandonos 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 más 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, así que no te pierdas de novedades.

Puedes encontrar más posts como este en mi sitio web: [LiteralCoder](https://literalcoder.com/)
# Repositorio
{% github Josh2604/notes-dapp-sample %}