Este tutorial irá guiá-lo através dos primeiros passos com um fluxo de trabalho de cunhagem de tokens ERC721 compatível com EVM (máquina virtual da Ethereum) na Avalanche usando Node.js REPL.
Aqui está uma visão geral do que vamos aprender:
- Avalanche x Ethereum
- Configuração de nós locais da Avalanche
- Financiamento de uma conta de teste com AVAX
- Cunhagem de tokens ERC-721 da Avalanche
Você achará isso bastante útil se estiver migrando da Ethereum ou outra blockchain compatível com EVM e desejar reutilizar seu contrato inteligente de NFT.
Requisitos
Este tutorial pressupõe que você tenha alguma familiaridade com Javascript, terminal Unix e Solidity.
Avalanche X Ethereum
Ao contrário da Ethereum, a Avalanche é uma rede de multi-blockchains, uma das quais executa uma bifurcação (fork) da máquina virtual da Ethereum (EVM) e é compatível com a Ethereum.
A Avalanche é composta por 3 sub-redes: a cadeia X, a cadeia P e a cadeia C.
-
Cadeia X: lida com trocas de valor e executa a Máquina virtual da Avalanche (com namespaced
avm
). -
Cadeia P: lida com a plataforma/protocolo (núcleo) e é capaz de criar novas blockchains arbitrárias (com namespaced
platform
). - Cadeia C: a cadeia compatível com a EVM capaz de executar contratos inteligentes em Solidity e dApps. Possui endereços compatíveis com a Ethereum (strings hexadecimais prefixadas com "0x" concatenadas com os 20 bytes mais à direita da chave pública ECDSA de hash Keccak-256).
A maioria das confusões acontece para os iniciantes ao tentar distinguir entre essas diferentes sub-redes. É importante observar que apenas a cadeia C tem compatibilidade com a EVM e endereços compatíveis com a Ethereum. A maioria dos dApps estará interagindo com esta cadeia.
Agora que aprendemos sobre a infraestrutura da Avalanche, vamos nos preparar para construir!
Configurando
A maneira mais rápida de começar é executar um grupo de nós do simulador localmente. Para fazer isso, siga estas etapas:
Instalar e baixar
-
Instale o Go. Certifique-se de definir a variável
$GOPATH
para o local onde você mantém o código Go (ou seja,$HOME/go
). -
Clone o Avalanchego (nó da Avalanche) e o simulador local da Avalanche. Certifique-se de que eles estejam em
$GOPATH/src/github.com/ava-labs
para que a próxima etapa funcione. Por exemplo, se o meu$GOPATH
estiver atualmente definido como~/mycode/go
, oavalanchego
e oava-sim
devem estar localizados em~/mycode/go/src/github.com/ava-labs/avalanchego
e~/mycode/go/src/github.com/ava-labs/ava-sim
, respectivamente. - Certifique-se de que você tenha o Node.js em seu sistema, baixando-o da página do Node.js. Precisaremos disso para criar o NFT.
Executar nós do simulador local
Para interagir com a blockchain localmente, teremos que executar alguns nós localmente. A Avalanche fornece uma maneira simples de fazer isso com um script de simulador que executa 5 nós em sua máquina local.
Primeiro, construa os programas avalanchego
e ava-sim
nos repositórios baixados. Eles são projetos Go que precisam ser compilados em programas executáveis. Ambos os projetos incluem um script shell útil que cria automaticamente o projeto localizado em seu diretório raiz em /scripts/build.sh
conforme mostrado abaixo (você pode encontrar o script de construção no mesmo local em ava-sim
):
avalancego/
├── ...
└──scripts
├── ansible
├── aws
├── build.sh
├── ...
└── versions.sh
Crie cada repositório Go executando o script shell incluído em cada repositório digitando ./scripts/build.sh
em seu terminal no nível raiz. O primeiro .
é a forma como o Unix executa um executável. Se você receber um erro de seu shell mencionando permissão negada, digite chmod +x scripts/build.sh
para transformar o script em um executável.
Em seguida, mude para o repositório ava-sim
, execute o simulador com ./scripts/run.sh
. Esse script executa os executáveis em avalanchego
, portanto, certifique-se de que ele esteja no $GOPATH
ao lado do ava-sim
. O simulador executa uma rede local de 5 nós escutando em portas diferentes. Neste tutorial, usaremos o nó que escuta na porta 9650.
Se o simulador for executado com sucesso, você deve vê-lo impresso no stdout (dispositivo de saída padrão) semelhante ao mostrado abaixo:
...
INFO [04-06|13:45:41] node/node.go#1051: node version is: avalanche/1.7.1
INFO [04-06|13:45:41] node/node.go#1052: node ID is: NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5
INFO [04-06|13:45:41] node/node.go#1053: current database version: v1.4.5
INFO [04-06|13:45:41] node/node.go#1051: node version is: avalanche/1.7.1
INFO [04-06|13:45:41] node/node.go#1052: node ID is: NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg
INFO [04-06|13:45:41] node/node.go#1053: current database version: v1.4.5
INFO [04-06|13:45:41] node/node.go#489: initializing API server
INFO [04-06|13:45:41] node/node.go#489: initializing API server
INFO [04-06|13:45:41] api/server/server.go#82: API created with allowed origins: [*]
INFO [04-06|13:45:41] api/server/server.go#82: API created with allowed origins: [*]
...
O simulador é executado como um processo em primeiro plano, portanto, abra um novo terminal para continuar.
Bom trabalho! Você está executando nós da Avalanche que consistem em todas as sub-redes em sua máquina local.
Criar um usuário de armazenamento de chaves e adicionar fundos de teste
Para obter algum fundo de teste na AVAX, temos que criar um usuário de armazenamento de chaves usando um nome de usuário e senha no nó de destino (neste caso, é o nó que está em execução na porta 3650). Com seu próprio nome de usuário e senha, envie uma solicitação para este ponto de extremidade:
curl -X POST --data '{
"jsonrpc":"2.0",
"id" :1,
"method" :"keystore.createUser",
"params" :{
"username":"MYUSERNAME",
"password":"MYPASSWORD"
}
}' -H 'Content-Type: application/json' 127.0.0.1:9650/ext/keystore
Substitua MYUSERNAME
e MYPASSWORD
pelo seu próprio nome de usuário e senha, respectivamente.
Importante: você só deve criar um usuário de armazenamento de chaves em um nó que você opera, pois o operador do nó pode acessar sua senha de texto simples.
Observe a seguinte chave privada pré-financiada (chamada de chave "ewoq" no documento da Avalanche), que é uma chave privada fornecida para obter financiamento para sua conta local convenientemente. Vamos importar essa chave privada para um endereço de cadeia C:
PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN
curl --location --request POST '127.0.0.1:9650/ext/bc/C/avax'
--header 'Content-Type: application/json' \
--data-raw '{
"method": "avax.importKey",
"params": {
"username":"MYUSERNAME",
"password":"MYPASSWORD",
"privateKey":"PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"
},
"jsonrpc": "2.0",
"id": 1
}'
Observe o C
no URI da rota que indica a cadeia com a qual queremos interagir? Nós só nos interessa a cadeia C porque estamos cunhando um NFT ERC721. No entanto, no futuro, você pode e estará trabalhando com outras cadeias usando a API pública.
Leia mais em: Funding a Local Network (Financiando uma rede local).
Integrar com a Metamask
Configure a Metamask para se conectar a um endereço RPC personalizado da rede local:
Configurações de rede de teste local (Simulador local da Avalanche)
Nome da rede: Avalanche Local
Novo URL RPC: http://localhost:9650/ext/bc/C/rpc (para a cadeia C)
ID da cadeia: 43112
Símbolo: AVAX
Explorador: N/A
Você pode verificar as configurações para a rede de teste e a rede principal aqui.
Rede de teste local: a porta de escuta pode não ser a 9650, dependendo se você executa um programa de exemplo no nó
avalanchego
ou no script de execuçãoavalanche-simulator
. Este último é recomendado para início rápido e terá um nó de escuta na porta 9650.
Crie uma nova conta na Metamask importando esta chave privada 0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027
fornecida na Metamask, que é uma conta de teste apenas para testes locais.
Se tudo correr bem, você deve ter uma carteira da Metamask AVAX financiada para construir seu aplicativo localmente:
Nó público da API
A Avalanche mantém um gateway de API público, que você pode usar no desenvolvimento rápido sem precisar executar seu próprio nó.
Criar um projeto de nó
Com o Node.js já instalado, digite o seguinte na linha de comando para criar um diretório de projeto:
mkdir hello-avax && cd hello-avax
npm init --yes
Em seguida, no diretório hello-avax
, instale alguns pacotes com:
npm install --save-dev hardhat
Em seguida, digite npx hardhat
no diretório raiz. O CLI do Hardhat deve imprimir algumas opções para configurar o projeto:
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
>
> 👷 Bem vindos ao Hardhat v2.9.3 👷
>
> ? O que você quer fazer? …
> ❯ Criar um projeto de amostra básico
> Criar um projeto de amostra avançado
> Criar um projeto de amostra avançado que usa TypeScript
> Criar um hardhat.config.js vazio
> Sair
Selecione a primeira opção "Criar um projeto de amostra básico (create a basic sample project)" e escolha "Y" para todas as perguntas. O Hardhat irá instalar todas as dependências necessárias e criar alguns diretórios para você, como /contracts
e /scripts
.
Em seguida, copie esta configuração e cole-a no hardhat.config.js
no nível raiz e salve-a:
// hardhat.config.js
import { task } from "hardhat/config"
import { BigNumber } from "ethers"
import "@nomiclabs/hardhat-waffle"
const FORK_FUJI = false
const FORK_MAINNET = false
const forkingData = FORK_FUJI ? {
url: 'https://api.avax-test.network/ext/bc/C/rpc',
} : FORK_MAINNET ? {
url: 'https://api.avax.network/ext/bc/C/rpc'
} : undefined
task("accounts", "Imprime a lista de contas", async (args, hre) => {
const accounts = await hre.ethers.getSigners()
accounts.forEach((account) => {
console.log(account.address)
})
})
task("balances", "Imprime a lista de saldos das contas AVAX", async (args, hre) => {
const accounts = await hre.ethers.getSigners()
for (const account of accounts){
const balance = await hre.ethers.provider.getBalance(
account.address
);
console.log(`${account.address} has balance ${balance.toString()}`);
}
})
export default {
solidity: {
compilers: [
{
version: "0.5.16"
},
{
version: "0.6.2"
},
{
version: "0.6.4"
},
{
version: "0.7.0"
},
{
version: "0.8.0"
},
{
version: "0.8.1"
}
]
},
networks: {
hardhat: {
gasPrice: 225000000000,
chainId: !forkingData ? 43112 : undefined,
},
local: {
url: 'http://localhost:9650/ext/bc/C/rpc',
gasPrice: 225000000000,
chainId: 43112,
accounts: [
"0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027",
"0x7b4198529994b0dc604278c99d153cfd069d594753d471171a1d102a10438e07",
"0x15614556be13730e9e8d6eacc1603143e7b96987429df8726384c2ec4502ef6e",
"0x31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc",
"0x6934bef917e01692b789da754a0eae31a8536eb465e7bff752ea291dad88c675",
"0xe700bdbdbc279b808b1ec45f8c2370e4616d3a02c336e68d85d4668e08f53cff",
"0xbbc2865b76ba28016bc2255c7504d000e046ae01934b04c694592a6276988630",
"0xcdbfd34f687ced8c6968854f8a99ae47712c4f4183b78dcc4a903d1bfe8cbf60",
"0x86f78c5416151fe3546dece84fda4b4b1e36089f2dbc48496faf3a950f16157c",
"0x750839e9dbbd2a0910efe40f50b2f3b2f2f59f5580bb4b83bd8c1201cf9a010a"
]
}
}
}
Agora, com os nós do simulador local ainda em execução, execute os seguintes comandos:
npx hardhat accounts --network local
> 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
> 0x9632a79656af553F58738B0FB750320158495942
> 0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430
> ...
Espero que você veja alguns endereços impressos. Em seguida, verifique os saldos com:
npx hardhat balances --network local
> 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 50000000000000000000000000
> 0x9632a79656af553F58738B0FB750320158495942 has balance 0
> 0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 0
> ...
Se você tiver criado corretamente um usuário de armazenamento de chaves e adicionado fundos de teste, deverá ver um dos endereços ricos (wealthy addresses) mostrados acima.
Desenvolver um contrato inteligente de NFT
Se você já possui um contrato inteligente compatível com a EVM para cunhagem de NFTs, pode pular esta seção. No entanto, é muito divertido acompanhar!
Vamos criar tokens não fungíveis ERC721 com seus próprios atributos. Para simplificar, qualquer conta poderá chamar um método mintTo
para cunhar itens.
Estaremos usando o contrato inteligente ERC721 padrão do Open Zeppelin. Instale-o em seu projeto com o npm install @openzeppelin/contracts
.
No diretório /contracts
, remova o arquivo Greeter.sol
gerado, crie um novo arquivo chamado Filet.sol
e digite o código a seguir (opcionalmente, você pode copiá-lo e colá-lo, mas estará perdendo a oportunidade de exercitar suas habilidades programação):
// contracts/Filet.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract Filet is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("Filet", "FILET") {}
function mintTo(address player, string memory tokenURI)
public
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
Como estamos implementando o ERC721URIStorage
para o nosso contrato de NFT, obtemos o método especial _setTokenURI(tokenId, tokenURI)
gratuitamente sobre a interface regular do ERC721
. O ERC721URIStorage
é uma extensão da interface IERC721 com a conveniente capacidade de configurar e obter o URI de metadados para cada token. Obteremos esse URI carregando o ativo em nft.storage.
Você pode criar seu próprio nome de contrato, nome de token e símbolo de token em vez de "Filet", mas certifique-se de usar o nome de forma consistente.
Agora compile o contrato Filet.sol
usando este comando do hardhat em seu nível raiz:
npx hardhat compile
> Compiling 1 file with 0.8.0
> Compilation finished successfully
Em seguida, crie um arquivo de script nomeado deploy.js
no diretório scripts
e digite o seguinte código antes de salvá-lo:
import {
Contract,
ContractFactory
} from "ethers"
import { ethers } from "hardhat"
const deploy = async (contractName) => {
const Contract = await ethers.getContractFactory(contractName)
const contract = await Contract.deploy()
await contract.deployed()
console.log(`${contractName} deployed to: ${contract.address}`)
}
const main = async () => {
await deploy("Filet")
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error)
process.exit(1)
})
Este script usa a biblioteca de Ethers para implantar o contrato no(s) nó(s) da Avalanche local(is). Agora, podemos implantar o contrato com este comando do Hardhat:
npx hardhat run scripts/deploy.js --network local
> Filet deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Agora temos um contrato de token implantado em 0x5FbDB2315678afecb367f032d93F642f64180aa3
. Anote esta saída de endereço, pois a sua provavelmente será diferente.
Interagindo com o contrato
Agora, vamos ativar o console do desenvolvedor do Hardhat para começar a brincar com nosso contrato Filet
de forma interativa:
npx hardhat console --network local
> Welcome to Node.js v14.18.1.
> Type ".help" for more information.
> >
Ao interagir com o prompt do console, estamos aprendendo cada etapa de maneira discreta, sem nos distrairmos com a interface do usuário.
Agora, digite o seguinte no prompt para inicializar o objeto do contrato (lembre-se de usar seu endereço de contrato em vez do mostrado abaixo):
>> const Filet = await ethers.getContractFactory("Filet")
>> const filet = await Filet.attach("0x5FbDB2315678afecb367f032d93F642f64180aa3")
Em seguida, examinamos as contas e os saldos:
>> const accounts = await ethers.provider.listAccounts()
>> accounts
> [
> '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
> '0x9632a79656af553F58738B0FB750320158495942',
> '0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430',
> // ...
> ]
O array, sem surpresa, deve conter todos os endereços listados com o comando anterior npx hardhat accounts
. Vamos selecionar um dos endereços para examinar o saldo do token FILET (estamos escolhendo a segunda conta).
>> const balance = (await filet.balanceOf(accounts[1])).toString()
> 0
Obviamente, o endereço 0x9632a79656af553F58738B0FB750320158495942
, pertencente à segunda conta com a qual acessamos com o accounts[1],
não possui nenhum token FILET. Antes de podermos cunhar um token para o endereço, teremos que carregar os metadados, como a imagem, o nome e a descrição que queremos vincular ao token para nft.storage.
Carregando o ativo e cunhando o token
Antes de continuarmos, instale as bibliotecas nft.storage
e mime
no nível raiz do projeto com npm install nft.storage mime --save
.
Agora, faremos o upload dos metadados de um NFT - imagem, nome e descrição - para nft.storage e usaremos o URI IPFS resultante no mintTo
. Para escrever um script que possa ser importado e executado no nó REPL do Hardhat, crie um arquivo chamado upload.mjs
dentro do diretório /scripts
com o seguinte código (substituindo a variável NFT_STORAGE_KEY
pela sua própria chave API).
import { NFTStorage, File } from 'nft.storage'
import mime from 'mime'
import fs from 'fs'
import path from 'path'
const NFT_STORAGE_KEY = 'YOUR_OWN_API_KEY'
async function storeNFT(imagePath, name, description) {
const image = await fileFromPath(imagePath)
const nftstorage = new NFTStorage({ token: NFT_STORAGE_KEY })
return nftstorage.store({
image,
name,
description,
})
}
async function fileFromPath(filePath) {
const content = await fs.promises.readFile(filePath)
const type = mime.getType(filePath)
return new File([content], path.basename(filePath), { type })
}
async function upload(imagePath, name, description) {
const result = await storeNFT(imagePath, name, description)
return result
}
export { upload }
Retorne ao nó REPL e importe a função upload
do script. Copie um arquivo de imagem de sua escolha para o diretório raiz para usar como a imagem do NFT.
>> const { upload } = await import("./scripts/upload.mjs")
>> const result = await upload("./pickleheart.png", "Pickleheart", "Image of Pickleheart Filet")
>> result
> Token {
> ipnft: 'bafyreicb3ewk33keh77mwxhmhdafxsjlkflichr2mjnyim6tbq3qjkwcue',
> url: 'ipfs://bafyreicb3ewk33keh77mwxhmhdafxsjlkflichr2mjnyim6tbq3qjkwcue/metadata.json'
> }
Observe que recebemos a resposta como um objeto Token
com a propriedade url
. Podemos usar este URI fornecido para cunhar nosso primeiro NFT para outra conta:
>> const _tx = await filet.mintTo(accounts[1], result.url)
Em seguida, verifique o saldo da conta receptora:
>> const balance = (await filet.balanceOf(account[1])).toString()
>> balance
> 1
Além disso, podemos verificar o proprietário atual do NFT chamando ownerOf(tokenId)
. Como cunhamos apenas o primeiro token, o tokenId
é 1:
>> const tokenId = 1
>> const ownerAddress = (await filet.ownerOf(tokenId))
>> ownerAddress === account[1].address
> true
O endereço de recebimento agora possui 1 FILET. Também confirmamos que ele é o proprietário do primeiro NFT.
Por padrão, ethers
usa o primeiro endereço como o signatário da transação, portanto, é o account[0]
que assina a cunhagem quando chamamos mintTo(recipientAddress, tokenURI
.
💡 O que foi devolvido por
mintTo(...)
?
Você pode se perguntar por que, quando chamamosmintTo(...)
anteriormente, atribuímos o valor retornado a uma variável não utilizada chamada_tx
, em vez de usar o ID do tokenUInt256
retornado esperado na próxima chamada, para oownerOf(tokenId)
examinar o proprietário do token.
Isso ocorre porque o valor de retorno de uma função Solidity não pura ou sem visualização está disponível apenas quando a função é chamada na cadeia (do mesmo contrato ou de outro contrato). Em nosso caso, nosso pequeno prompt do Hardhat é um cliente fora da cadeia. Por exemplo, o métodobalanceOf(...)
é uma função de visualização porque só lê a partir da cadeia.
Quando uma função não pura ou sem visualização é chamada fora da cadeia, o valor de retorno é sempre o hash da transação, não o valor de retorno pretendido da função. Portanto, quando chamamosmintTo(...)
, recebemos o objeto de transação e não o ID do token que esperávamos. Nós o atribuímos a_tx
para enfatizar que era um objeto de transação (tx) e o precedemos com um sublinhado para enfatizar que não estávamos utilizando-o.
Saiba mais sobre funções puras e de visualização e sobre inscrição em um evento para obter o valor retornado. Além disso, confira o métodocallStatic
, que é útil para testar suas chamadas e valores retornados.
Recuperando os metadados de um token
A etapa final é recuperar os metadados de cada token do IPFS para que você possa exibir a imagem, o nome e a descrição do token na web. Com o ERC721Storage, podemos chamar tokenURI(uint256 tokenId)
para recuperar o tokenURI armazenado na cadeia:
>> let ipfsURI = await filet.tokenURI(tokenId.toNumber())
>> ipfsURI
> 'ipfs://bafyreicb3ewk33keh77mwxhmhdafxsjlkflichr2mjnyim6tbq3qjkwcue/metadata.json'
Como existem navegadores que ainda não suportam URLs IPFS de forma nativa, bem como a API fetch
padrão, o cliente do Javascript nft.storage inclui uma função auxiliar que converte esse URI IPFS em uma versão HTTPS por meio do gateway nftstorage.link. No console, você pode importar esta função toGatewayURL
do nft.storage
:
>> const { toGatewayURL } = await import("nft.storage")
>> const { href } = await toGatewayURL(ipfsURI)
>> href
> 'https://nftstorage.link/ipfs/bafyreicb3ewk33keh77mwxhmhdafxsjlkflichr2mjnyim6tbq3qjkwcue/metadata.json'
🎉 Parabéns! Você aprendeu a construir uma loja NFT na Avalanche. Agora vá em frente e conquiste o mundo!
Esse artigo foi escrito pela NFT School e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Top comments (0)