Em fevereiro de 2021, Figma CEO Dylan Fields vendeu um pedaço de Arte NFT por $ 7,5 milhões . Da mesma forma, o cofundador do Twitter Jack Dorsey vendeu seu primeiro tweet no Twitter como um NFT por $ 2.915.835,47 .

Jack Dorsey First Tweet NFT

Um NFT (token não fungível) é uma nova tecnologia fascinante que representa a propriedade de um ativo digitalmente. Neste tutorial, vamos cobrir algumas informações importantes, configurar serviços de terceiros e, finalmente, codificar e implantar nosso próprio NFT no Ropsten Testnet.

Vamos começar!

Informações básicas

Antes de criar nosso próprio NFT, vamos dar uma olhada nas tecnologias e recursos que fazem os NFTs funcionarem.

Fungível vs. não fungível

Fungibilidade é essencialmente a capacidade de trocar um item por um item semelhante de mesmo valor. Considere uma nota de cinco dólares. Sempre é igual ao mesmo valor em qualquer lugar do mundo. Você pode trocar cinco notas de um dólar por uma única nota de cinco dólares, e elas valem o mesmo valor o tempo todo.

Por outro lado, os itens não fungíveis não têm o mesmo valor em comparação uns com os outros. Por exemplo, uma réplica exata da Mona Lisa não é igual em valor à pintura original, apesar de ser a mesma em todos os aspectos. Os itens não fungíveis são inerentemente únicos e não podem ser considerados equivalentes a nenhum outro item.

Um item pode ser fungível e não fungível. Por exemplo, embora dois assentos na classe econômica de um avião tenham o mesmo preço, uma pessoa pode atribuir valor sentimental a um assento na janela, diminuindo o valor de todos os outros assentos para essa pessoa.

Blockchain

Um blockchain é um banco de dados público ou digital livro-razão que mantém o controle das transações. Ele é replicado em vários sistemas de computador que fazem parte da cadeia. Vamos construir nosso NFT no blockchain Ethereum.

Minting ERC721 tokens

Minting é o processo de criação de algo pela primeira vez ou, no nosso caso, de publicação de uma instância exclusiva de nosso token ERC721 no blockchain. ERC-721 é o padrão para criar um NFT, e um token ERC721 é um representação única de conteúdo digital publicado no blockchain Ethereum. Dois tokens nunca são iguais, então cada vez que você cunhar um novo token com o mesmo bloco de código, um novo endereço será gerado.

Contratos inteligentes e NFTs

Contratos inteligentes são programas simples implantados no blockchain e executados no estado em que se encontram, o que significa eles não são controlados por um usuário. Podemos usar um contrato inteligente para criar e rastrear nossos tokens.

Um NFT é um armazenamento digital de dados em conformidade com o padrão ERC-721 e reside em um blockchain público. Os NFTs contêm informações ou dados sobre o ativo que representam, que pode ser um item digital como um Tweet ou um item físico como um moletom .

Um contrato inteligente pode ser considerado um NFT se implementar o padrão ERC-721 e um NFT for uma instância de um contrato inteligente. Cada vez que criamos um novo NFT, usamos o código de contrato inteligente que foi implantado no blockchain.

Redes públicas: Mainnet vs. Testnet

Ethereum usa várias redes. A rede usada na produção é geralmente chamada de Mainnet e as outras, que são usadas para teste, são chamadas de Testnet. Vamos implantar o NFT que criamos no Ropsten Testnet , um Testnet de prova de trabalho para Ethereum.

Observe que, quando eventualmente implementarmos nosso NFT, seja para produção ou para Mainnet, o histórico de transações e saldos que temos no Ropsten Testnet não serão transferidos. Pense na Testnet como um ambiente de teste/desenvolvimento público e na Mainnet como um ambiente de produção.

Redes privadas

Uma rede é considerada privada se seus nós não estiverem conectados ao blockchain público. Você pode executar o blockchain Ethereum em uma rede privada, como sua máquina local, ou em um grupo de máquinas, como redes de consórcio, que não são acessíveis na Mainnet ou Testnet.

Executar o blockchain Ethereum em um grupo de máquinas como uma intranet exigiria a validação de transações com um , um software Ethereum em execução em um cliente que verifica blocos e dados de transação.

HardHat e Ganache são dois exemplos de ambientes de desenvolvimento de blockchain Ethereum que você pode executar em sua máquina local para compilar, testar, implantar e depurar seu aplicativo de contrato inteligente.

Executaremos nosso aplicativo em uma rede pública para que ele possa ser acessado por qualquer pessoa conectada à rede.

Torneiras

Para testar nosso aplicativo, precisamos obter Ether (ETH), a criptomoeda Ethereum, de uma torneira. Torneiras, como o Ropsten Faucet , são aplicativos da web que permitem especificar e enviar teste ETH para um endereço, que você pode usar para completar transações em um Testnet.

O preço da ETH nas bolsas é determinado pelas transações que ocorrem na Mainnet a qualquer momento. Se você optar por executar seu aplicativo Ethereum em uma rede privada, não precisa testar o ETH.

Nós e clientes

Como mencionado anteriormente, os nós verificam os blocos e os dados da transação. Você pode criar seu próprio nó usando clientes como Geth e OpenEthereum e contribua com o blockchain Ethereum validando transações e blocos no blockchain.

Você pode pular o processo de criação de seu próprio nó e, em vez disso, usar um hospedado na nuvem com um plataforma node-as-a-service como Alchemy . Podemos passar rapidamente do desenvolvimento para a produção e garantir que obtenhamos métricas importantes para nosso aplicativo.

Usaremos a API Alchemy para implantar nosso aplicativo no blockchain Ropsten. A alquimia foi descrita como AWS para blockchains e fornece ferramentas de desenvolvedor que nos permitem ver insights sobre o desempenho de nosso aplicativo.

Construindo o NFT

Pré-requisitos

  • Node.js e npm
  • conhecimento básico de JavaScript

Gosto de fotografia e tiro muitas fotos no meu dispositivo móvel. Que melhor maneira de proteger meu trabalho do que cunhar um NFT que eu possa transferir para qualquer pessoa que adore minhas fotos? Eles podem então usar os dados NFT no Ropsten Testnet ou no Mainnet para provar que possuem os direitos da imagem original.

Criaremos um NFT que identifica a foto que tirei do Rio Osun abaixo, que se acredita ter poderes de cura únicos.

Osun River NFT Art

Crie uma conta Alchemy

Usaremos Alchemy para codificar nosso NFT, permitindo-nos pular o processo de execução de um nó Ethereum em nossa máquina local.

Navegue até o painel do Alchemy, onde você verá uma tela intitulada “Crie seu primeiro aplicativo”. Usei meu nome como o nome da equipe e chamei o aplicativo de”The Osun River NFT”.

Selecione Ropsten como a rede de teste para o aplicativo.

Criar App Alchemy

Clique no botão Criar aplicativo para continuar.

Na próxima tela, selecione o plano gratuito. Na tela a seguir, você pode evitar inserir informações de pagamento clicando no botão Ignorar por enquanto , mas pode optar por fazer isso mais tarde. Na última tela, selecione a opção Capacidade limitada .

Agora, você verá nosso aplicativo listado em seu painel.

Alchemy App Listed Dashboard

Crie uma conta Ethereum

Precisamos criar uma carteira para manter uma conta Ethereum. Para implantar nosso aplicativo em uma rede, precisaremos pagar uma taxa denominada em ETH, conhecida como taxas de gás. Ao testar nosso aplicativo, podemos usar ETH fictício para concluir o processo, que recuperaremos de uma torneira mais tarde.

Criaremos uma conta Ethereum usando MetaMask , uma carteira virtual que está disponível como extensão do Chrome .

Depois de instalar o MetaMask e criar uma conta, abra a extensão MetaMask no Chrome e selecione Ropsten Test Network na lista de redes.

Metamask Extension Chrome

MetaMask irá gerar automaticamente um endereço de carteira denominado em ETH. Basta clicar em Conta 1 para copiar o endereço de sua carteira.

Obtendo ETH de uma torneira

Vamos enviar Ether para nossa nova carteira usando o Ropsten Faucet. Primeiro, insira o endereço de sua carteira, que você pode copiar acima, e o site enviará 1ETH para sua carteira.

Ether Ropsten Faucet Site

Você pode confirmar verificando sua carteira MetaMask.

Metamask Wallet 1eth

Configurando nosso token

Vamos começar a codificar nosso token NFT! Primeiro, crie um novo diretório para nosso projeto e inicialize o npm:

 mkdir the-osun-river-nft && cd the-osun-river-nft
npm init-y

Precisamos configurar o Hardhat , um ambiente de desenvolvimento para Ethereum que nos permite compilar nosso aplicativo em nosso local máquina e funcionalidade de teste antes de implantar no Ropsten Testnet.

Para instalar o Hardhat como uma dependência de desenvolvimento em nosso projeto, execute:

 npm install-D hardhat

Agora, inicialize o Hardhat usando o comando npx hardhat :

Em itialize Hardhat NPX Hardhat Command

Selecione Criar um hardhat.config.js vazio. Usaremos esse arquivo para definir a configuração do nosso projeto mais tarde.

Agora, configuraremos duas novas pastas em nosso projeto: uma conterá o código do nosso contrato inteligente e a outra conterá os scripts que implantam e interagem com o código do contrato inteligente:

 contratos mkdir e scripts mkdir

Criando um contrato inteligente

Contratos inteligentes são simplesmente aplicativos que podem ser executados no blockchain Ethereum. Eles são escritos em uma linguagem chamada Solidity .

Nosso código de contrato inteligente será baseado na implementação do OpenZeppelin ERC721. ERC721 é o padrão para representar a propriedade de NFTs, e os contratos do OpenZeppelin nos fornecem alguma flexibilidade no uso do ERC721.

Instale a biblioteca de contratos OpenZeppelin:

 npm i @ openzeppelin/controls @ 4.0.0

No seu diretório contratos , crie um arquivo OsunRiverNFT.sol usando o código abaixo. Deixei vários comentários que esclarecem cada linha:

//Contrato baseado em https://docs.openzeppelin.com/contracts/3.x/erc721
//SPDX-License-Identifier: MIT
solidez do pragma ^ 0.7.3; //implementa o padrão ERC721
importar"@ openzeppelin/contratos/token/ERC721/ERC721.sol";
//acompanha o número de tokens emitidos
import"@ openzeppelin/contract/utils/Counters.sol";
import"@ openzeppelin/contract/access/Ownable.sol"; //Acessar o método Ownable garante que apenas o criador do contrato inteligente pode interagir com ele
o contrato TorNFT é ERC721, Ownable { usando contadores para contadores.Contador; Counters.Counter private _tokenIds; //o nome e o símbolo para o NFT construtor () public ERC721 ("TheOsunRiver","TOR") {} //Crie uma função para cunhar/criar o NFT //o receptor recebe um tipo de endereço. Este é o endereço da carteira do usuário que deve receber o NFT cunhado usando o contrato inteligente //tokenURI recebe uma string que contém metadados sobre o NFT função createNFT (receptor de endereço, string de memória tokenURI) public onlyOwner retorna (uint256) { _tokenIds.increment (); uint256 newItemId=_tokenIds.current (); _mint (receptor, newItemId); _setTokenURI (newItemId, tokenURI); //retorna o id para o token recém-criado return newItemId; }
}

Para que nosso NFT seja um token ERC721 válido, ele deve atender a todos os padrões ERC721. import"@ openzeppelin/contratos/token/ERC721/ERC721.sol"; garante isso importando os padrões ERC721 em nosso arquivo.

Conectando a MetaMask ao projeto

Agora, conectaremos nossa carteira MetaMask ao nosso projeto. Cada transação em uma carteira virtual requer uma chave privada para ser concluída, então precisamos pegar nossa chave privada MetaMask.

No navegador Chrome, abra a extensão MetaMask, clique nos três pontos no canto superior direito e selecione a opção detalhes da conta . Em seguida, clique no botão Botão Exportar chave privada . Digite sua senha para ver sua chave privada e copie-a.

Metamask Exportar chave privada do navegador Chrome

É importante manter nossa chave privada protegida dentro da base de código do nosso projeto para evitar que ela seja exposta ao usar plataformas de controle de versão como o GitHub. Para manter nossas chaves seguras, instalaremos o pacote dotenv :

 npm i dotenv

Crie um arquivo .env na raiz do seu projeto e, em seguida, adicione sua chave privada MetaMask anterior a ele. Você também adicionará seu alquimia API_URL , que pode ser encontrado navegando até seu Painel de controle do Alchemy, clicando no menu suspenso Aplicativos , selecionando o aplicativo criado anteriormente e, em seguida, botão Exibir chave :

Alchemy Dashboard View Key

 METAMASK_PRIVATE_KEY="yourMetamaskPrivateKey"
API_URL="https://eth-ropsten.alchemyapi.io/v2/your-api-key"

Configuração de Ether.js

Ether.js é uma biblioteca que simplifica a interação com o blockchain Ethereum. Usaremos o plugin Ether para Hardhat:

 npm i-D @ nomiclabs/hardhat-ethers'ethers@^5.0.0'

Vá para o arquivo hardhat.config.js que criamos anteriormente para adicionar algumas das novas dependências que instalamos:

/**
* @type import ('hardhat/config'). HardhatUserConfig
*/
require ('dotenv'). config ();
require ("@ nomiclabs/hardhat-ethers");
const {API_URL, METAMASK_PRIVATE_KEY}=process.env;
module.exports={ solidez:"0.7.3", defaultNetwork:"ropsten", redes: { capacete de segurança: {}, ropsten: { url: API_URL, contas: [`0x $ {METAMASK_PRIVATE_KEY}`] } },
}

Vamos examinar o que temos em nosso arquivo de configuração:

    Pacote

  • dotenv : permite-nos usar variáveis ​​de ambiente em nosso aplicativo Node.js
  • require ("@ nomiclabs/hardhat-ethers") : executa os métodos amigáveis ​​fornecidos pelo Ether em nossos scripts de implantação
  • defaultNetwork : especifica qual rede Hardhat deve usar ao implantar nosso aplicativo (Ropsten Testnet)
  • contas : uma chave privada gerada por MetaMask que permite que nosso aplicativo se conecte a nossa carteira virtual MetaMask para completar uma transação
  • url : especifica a URL em que nosso aplicativo Node.js está hospedado (servidores Alchemy)

Vamos nos conectar ao Ropsten Testnet por meio dos nós hospedados em nosso URL. Você pode ler mais sobre os arquivos de configuração no Hardhat .

Agora, vamos executar a tarefa compilar que o Hardhat fornece para verificar se tudo funciona corretamente:

 compilação do hardhat npx

Você deve ver uma mensagem de sucesso como esta abaixo. Você pode ignorar os avisos que aparecem no terminal.

Hardhat Compile Mensagem de sucesso da tarefa

Criação de um script de implantação

Agora que concluímos nosso código de contrato inteligente, vamos escrever os scripts necessários para implantar nosso contrato inteligente no blockchain Ethereum.

No diretório scripts , crie um novo arquivo chamado deploy.js :

 função assíncrona main () { const [implantador]=espera ethers.getSigners (); console.log ("Implementando contratos com a conta:", deployer.address); console.log ("Saldo da conta:", (await deployer.getBalance ()). toString ()); const TOR=espera ethers.getContractFactory ("TorNFT"); //Start deployment, returning a promise that resolves to a contract object const tor=await TOR.deploy(); console.log("Contract deployed to address:", tor.address); } main() .then(()=> process.exit(0)) .catch(error=> { console.error(error); process.exit(1); });

Now, we can run the Hardhat deploy task:

npx hardhat run scripts/deploy.js--network ropsten

We add the --network ropsten flag to tell Hardhat to connect to a specific network, in our case, Ropsten.

After a few seconds, we’ll see that our smart contract has been successfully deployed to the Ropsten Testnet. From our terminal logs, we can see the newly created address for our smart contract.

Smart Contract Successful Address Display

Now, let’s confirm that our smart contract is deployed to the blockchain. Head to the Ropsten Etherscan and paste your contract address into the search bar. You should see details about your contract within one minute.

Ropsten Etherscan Contract Details Display

If you check your Ethereum wallet on MetaMask, you’ll notice that the amount of ETH you have has been reduced on account of the gas fees required to process transactions. Now, we’ve successfully deployed our smart contract to the Ropsten Testnet!

Minting an NFT

Our smart contract code takes in two arguments: the receiver address and a tokenURI. The tokenURI links to data that we would like to attach our token to. To use the receiver address, simply pass it in the wallet address of the user you’re giving the token to.

Data stored on the blockchain needs to be processed, verified, and replicated across multiple networks, making storing data on the blockchain very expensive. Uploading an entire image to the blockchain is not advisable, and you can store only the metadata for the NFT instead.

Although the URL for an NFT can be stored on the blockchain, the link may go offline at any time. Additionally, anyone who has access to the content at a URL may change it.

An example is when an NFT artist pulled the rug on NFTs he had sold on OpenSea, meaning he changed the original images that he’d sold to buyers. The link to those images was still present on the blockchain, however, the original content had been completely altered.

Therefore, we need a way to store data that’s affordable, persistent, decentralized, and immutable.

Using IPFS

IPFS is a distributed system for storing and accessing files that uses content addressing to tackle the problem above. Any piece of data that is uploaded to IPFS will be issued a unique content identifier (CID). Once a CID is generated for a piece of data, that CID will always represent that data, and the data cannot be changed.

Here’s an example IPFS URI:

ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi

To generate an IPFS URI, you simply need to prefix the CID with ipfs://. In a browser, IPFS URIs follow the format:

https://ipfs.io/ipfs/{CID}

The URI will be resolved by your user agent (browser) to display the content. There are a few browsers able to resolve IPFS URIs, but we’ll use Google Chrome browser version 90.

Setting up web3.js

Let’s continue adding metadata to our NFT. We’ll install the Alchemy Web3 package:

npm install @alch/alchemy-web3

According to its docs, web3.js is a collection of libraries that allows you to interact with a local or remote Ethereum node using HTTP, IPC, or WebSocket.

Alchemy wraps around the Web3.js library, extending its functionality by offering automatic retries and robust WebSocket support.

Setting up scripts to mint NFTs

Now, it’s time to write the scripts to mint our NFT.

In your scripts folder, create a file called mint-nft.js. Then, add the following block of code:

require('dotenv').config();
const API_URL=process.env.API_URL;
const { createAlchemyWeb3 }=require("@alch/alchemy-web3");
const alchemyWeb3=createAlchemyWeb3(API_URL);
const contract=require("../artifacts/contracts/OsunRiverNFT.sol/TorNFT.json"); 

Uploading NFT metadata to Pinata

Pinata is a platform for using IPFS protocol to store our NFT’s metadata. If you haven’t already, create an account.

Once you’re signed in, select the teal upload button, click File, then select your NFT image.

Upload NFT Metadata Pinata

Once the image is successfully uploaded, you’ll see it on your dashboard. Make sure to take note of the alphanumeric characters under the IPFS CID column above. We’ll use them later.

Now that we’ve uploaded our image to Pinata, let’s create a JSON file to hold information about our NFT. We will store the URI on the blockchain when an NFT is minted.

In the root of your application, create a file called nft-metadata.json and add the following information. Remember to change the image value to use the CID that was generated when you uploaded your image to Pinata:

{ "description":"An image of the Osun River captured on the suspended bridge at the Osogbo Sacred Grove.", "image":"https://ipfs.io/ipfs/Qmf1r8igsCAFTFjrQrK7jk6uD6YKVYo5dGu7cvm9TmPhJV", "photographer":"Adebola Adeniran"
}

Notice that for the image attribute, we have added the IPFS CID from our Pinata dashboard with the prefix https://ipfs.io/ipfs/. Save the file, then head back to Pinata to upload the file.

Upload File Pinata

You should see both the image and JSON files on our dashboard.

Creating an instance of our contract

To mint the NFT, grab the contract address that was created when our smart contract was deployed to the Ropsten Testnet. You can see this in our terminal logs. Our contract address is 0x9436f34035a48856.

Create Instance Smart Contract

Head back into the scripts/mint-nft.js file and add the following code:

const contractAddress="0x9436f34035a48856";
const nftContract=new alchemyWeb3.eth.Contract(contract.abi, contractAddress);

Let’s update our .env file with our public Ethereum address, which is the same account address we copied earlier. Add the address to our .env file:

METAMASK_PRIVATE_KEY="Our metamask Private key"
API_URL="Our alchemy API URL"
METAMASK_PUBLIC_KEY="Our metamask public key"

Next, we’ll need to create a transaction.

Add the following code into our mint-nft.js file. I’ve added comments to explain what we’re doing at each step:

const METAMASK_PUBLIC_KEY=process.env.METAMASK_PUBLIC_KEY;
const METAMASK_PRIVATE_KEY=process.env.METAMASK_PRIVATE_KEY; async function mintNFT(tokenURI) { //get the nonce-nonce is needed for security reasons. It keeps track of the number of //transactions sent from our address and prevents replay attacks. const nonce=await alchemyWeb3.eth.getTransactionCount(METAMASK_PUBLIC_KEY,'latest'); const tx={ from: METAMASK_PUBLIC_KEY,//our MetaMask public key to: contractAddress,//the smart contract address we want to interact with nonce: nonce,//nonce with the no of transactions from our account gas: 1000000,//fee estimate to complete the transaction data: nftContract.methods .createNFT("0x9436f34035a48856", tokenURI) .encodeABI(),//call the createNFT function from our OsunRiverNFT.sol file and pass the account that should receive the minted NFT. };
}

I’ve created a new MetaMask wallet and passed the wallet address in tx.data above. You can also pass in our METAMASK_PUBLIC_KEY if you wish. In production, the wallet address passed here should be the wallet address of the NFT’s recipient.

Now that the transaction is created, we’ll need to sign off of the transaction using our METAMASK_PRIVATE_KEY.

Add the following block of code to the mint-nft.js file within the mintNFT function:

 const signPromise=alchemyWeb3.eth.accounts.signTransaction( tx, METAMASK_PRIVATE_KEY ); signPromise .then((signedTx)=> { alchemyWeb3.eth.sendSignedTransaction( signedTx.rawTransaction, function (err, hash) { if (!err) { console.log( "The hash of our transaction is:", hash, "\nCheck Alchemy's Mempool to view the status of our transaction!" ); } senão { console.log( "Something went wrong when submitting our transaction:", errar ); } } ); }) .catch((err)=> { console.log("Promise failed:", err); });

Finally, we need to copy the IPFS CID hash from the nft-metadata.json file we uploaded to Pinata earlier and pass that into our mintNFT function when it’s called:

mintNFT("https://ipfs.io/ipfs/QmdZMtdApdeobM5iCRcWqAMByfG4No8tW4oheb7jQjKgTm")//pass the CID to the JSON file uploaded to Pinata

If you open the link above in our browser, you should see our nft-metadata.json file with the CID above:

NFT Metadata JSON CID

Now, we can run node scripts/mint-nft.js in our terminal to mint our NFT. Wait a few seconds, and you should get a response like the image below in our terminal.

Mint NFT Terminal

Now, we’ll go to the Alchemy Mempool, which tracks the status of all the transactions happening on our account without having to use Etherscan.

Alchemy Mempool NFT Data Dashboard

We can see information about our newly minted NFT on Etherscan, as well as the URL that links to our nft-metadata.json file on the Ethereum blockchain.

Newly Minted NFT Etherscan

Scroll down to input data, then click the decode input data button. You should see the data we passed to our createNFT function in the contracts/OsunRiverNFT.sol file: the receiving wallet address and the URL for the JSON file that holds our NFT’s metadata.

Decode Input Data Etherscan

If you search for the contract address used to mint the NFT on Etherscan, you’ll see a record of all the NFTs that have been minted, or more accurately, all the transactions that have occurred using this smart contract.

Contract Address Etherscan NFT Record

Adding our NFT to our MetaMask wallet

  1. Check connection to the Ropsten Test Network
  2. Open MetaMask wallet extension
  3. Click the add token button
  4. Copy the contract address of your new token from Etherscan and paste it into MetaMask. MetaMask will automatically generate the token’s symbol.
  5. Click next to add the token to your wallet

Add NFT Metamask Wallet

Metamask Wallet Balance

Conclusion

And with that, we’ve learned how to:

  • Create and deploy a smart contract to the Ropsten Testnet
  • Mint NFTs by deploying the smart contract to a blockchain
  • Add metadata to our NFT using content addressing protocol in IPFS via Piñata
  • View our NFT in our MetaMask wallet

Hopefully, you see why NFTs are causing such commotion. This is a fascinating technology that is likely to stick around a while.

In production, the steps are exactly the same as in this tutorial. The only difference is that you’ll now be interacting with the Mainnet rather than a Testnet.

You can see all the code for this project on my GitHub.

The post How to create NFTs with JavaScript appeared first on LogRocket Blog.