WEB3DEV

Cover image for Web 3 Parte I: Como implementar, testar e implantar um Contrato Inteligente de NFT com Truffle
Banana Labs
Banana Labs

Posted on

Web 3 Parte I: Como implementar, testar e implantar um Contrato Inteligente de NFT com Truffle

Com a recente emergência da blockchain, começamos a ouvir mais sobre termos como Contratos Inteligentes, NFTs e assim por diante. No entanto, a verdadeira questão continua sendo o significado dos termos mencionados.

Contratos inteligentes são basicamente programas armazenados em uma blockchain que são executados quando condições predeterminadas são atendidas. Os contratos inteligentes podem armazenar pequenas quantidades de dados em estruturas de dados comuns, que é um componente crítico dos casos de uso de tokenização que mapeiam identificadores de token para identificadores de proprietário para rastrear quem possui qual token.

NFTs (non-fungible tokens) são tokens baseados em blockchain, cada um representando um ativo exclusivo, como uma obra de arte, conteúdo digital ou mídia. Um NFT pode ser pensado como um certificado digital irrevogável de propriedade e autenticidade de um determinado ativo, seja ele digital ou físico.

Neste artigo, vamos implementar, testar e implantar contratos inteligentes de NFT, para isso, usaremos as seguintes ferramentas:

Truffle: uma ferramenta de desenvolvimento que fornece a base para um projeto de contrato inteligente
OpenZeppelin: uma biblioteca de contrato inteligente que fornece uma variedade de funções utilitárias
Polygon: uma blockchain EVM que será usada para a implantação do contrato

Configuração do projeto

Crie um novo diretório para conter seu projeto Truffle.

mkdir NFTContract` e `cd NFTContract
Enter fullscreen mode Exit fullscreen mode

Inicialize o projeto executando este comando:

npm init
Enter fullscreen mode Exit fullscreen mode

Instale todas as dependências necessárias.

npm install truffle chai @openzeppelin/contracts @truffle/hdwallet-provider dotenv bignumber.js
Enter fullscreen mode Exit fullscreen mode

Inicialize a base para o projeto

truffle init ou npx truffle init
Enter fullscreen mode Exit fullscreen mode

Após rodar o comando acima, deverá gerar um projeto com a seguinte estrutura:

estrutura de diretórios

Os nomes dos diretórios são autoexplicativos. Todos os nossos contratos inteligentes, arquivos de script e scripts de teste serão mantidos em seus respectivos diretórios (pastas). Todos os detalhes de configuração estão contidos no arquivo truffle-config.js.

Implementação De Contrato Inteligente

Vamos criar um novo arquivo chamado NFT.sol dentro do diretório contracts. Este será nosso primeiro contrato inteligente que nos ajudará na criação e atualização de NFTs.

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFT is ERC721, Ownable {
    address public _contractOwner;
    uint256 public tokenCounter;

    mapping(uint => string) public tokenURIMap;
    mapping(uint => uint) public priceMap;

    event Minted(address indexed minter, uint price, uint nftID, string tokenURI);

    event PriceUpdate(address indexed owner, uint oldPrice, uint newPrice, uint nftID);

    constructor() ERC721("OngamaNFTs", "ONGA") {
        _contractOwner = msg.sender;
        tokenCounter = 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, aqui estamos fazendo o seguinte:

  • Importando o padrão OpenZeppelin ERC721 e todas as funções de utilidade do contrato ownable

  • Herdando o contrato inteligente OpenZeppelin ERC721 em nosso contrato inteligente NFT.sol usando a palavra-chave is.

  • Definindo variáveis para manter o endereço do proprietário do contrato e para acompanhar a contagem de NFT.

  • Definindo mapping para preço NFT e URI de token, uma vez que são informações que precisam ser mantidas na blockchain.

  • Definindo os eventos Minted & PriceUpdate que serão emitidos quando os eventos minted & priceUpdate forem acionados

  • Dado que constructor é sempre a primeira função chamada durante a implantação de um contrato inteligente, Estamos fornecendo diretamente o nome NFT como OngamaNFTs e Onga como o símbolo. O msg.sender é uma palavra-chave que retorna o endereço da conta que invoca o contrato inteligente e estamos definindo a contagem inicial de tokens como um.

Implementando a função mint

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

function mint(string memory _uri, address _toAddress, uint _price) public returns (uint){
  uint _tokenId = tokenCounter;
  priceMap[_tokenId] = _price;

  _safeMint(_toAddress, _tokenId);
  _setTokenURI(_tokenId, _uri);

  tokenCounter++;

  emit Minted(_toAddress, _price, _tokenId, _uri);

  return _tokenId;
}

function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual {
   require(_exists(_tokenId),"ERC721Metadata: URI set of nonexistent token"); 
   tokenURIMap[_tokenId] = _uri;
}
Enter fullscreen mode Exit fullscreen mode

Na função mint acima, estamos persistindo apenas atributos NFT cruciais na blockchain para minimizar o custo, uma vez que armazenar dados diretamente em uma blockchain tem um custo associado, não será financeiramente viável se todos os metadados NFT forem armazenados na blockchain, o restante dos metadados NFT pode ser armazenado em um banco de dados centralizado apontando para os dados NFT armazenados no blockchain usando o atributo token URI.

Depois de definir o ID e o preço do token, estamos chamando o método _safeMint do OpenZeppelin ERC721 que literalmente cria o novo NFT. O método usa dois parâmetros, o endereço do cunhador e o ID de token do NFT recém-cunhado.

O _setTokenURI está atuando como um método auxiliar para definir o URI do token, adicionando a ele o modificador de visibilidade interna, o tornará disponível apenas dentro deste contrato e seus contratos derivados. Para registro, esse método costumava fazer parte dos métodos utilitários ERC721 do OpenZeppelin. No final, estamos incrementando o id do token, emitindo o evento e retornando o id do token.

Implementando a função de atualização de preços

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

function updatePrice(uint _tokenId, uint _price) public returns (bool) {
  uint oldPrice = priceMap[_tokenId];
  require(msg.sender == ownerOf(_tokenId), "Erro, você não é o proprietário");
  priceMap[_tokenId] = _price;

  emit PriceUpdate(msg.sender, oldPrice, _price, _tokenId);

  return true;
}
Enter fullscreen mode Exit fullscreen mode

O método updatePrice leva dois parâmetros que são token id e price, estamos carregando o antigo preço do NFT usando o token id. A palavra-chave require nos ajuda a avaliar as condições que devem ser satisfeitas, caso contrário, um erro é retornado. Basicamente, estamos verificando se o usuário que invoca a função é o proprietário do NFT usando o método ownerOf do OpenZeppelin. Caso a condição seja satisfeita, atualizamos o preço, emitimos o PriceUpdate e retornamos true.

Implementação completa do contrato inteligente

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFT is ERC721, Ownable {
    address public _contractOwner;
    uint256 public tokenCounter;

    mapping(uint => string) public tokenURIMap;
    mapping(uint => uint) public priceMap;

    event Minted(address indexed minter, uint price, uint nftID, string uri);

    event PriceUpdate(address indexed owner, uint oldPrice, uint newPrice, uint nftID);

    constructor() ERC721("OngamaNFTs", "ONGA") {
        _contractOwner = msg.sender;
        tokenCounter = 1;
    }

    function mint(string memory _uri, address _toAddress, uint _price) public returns (uint){
        uint _tokenId = tokenCounter;
        priceMap[_tokenId] = _price;

        _safeMint(_toAddress, _tokenId);
        _setTokenURI(_tokenId, _uri);

        tokenCounter++;

        emit Minted(_toAddress, _price, _tokenId, _uri);

        return _tokenId;
    }

    function _setTokenURI(uint256 _tokenId, string memory _tokenURI) internal virtual {
        require(_exists(_tokenId),"ERC721Metadata: Conjunto de URI de token inexistente"); 
        tokenURIMap[_tokenId] = _tokenURI;
    }


    function updatePrice(uint _tokenId, uint _price) public returns (bool) {
        uint oldPrice = priceMap[_tokenId];
        require(msg.sender == ownerOf(_tokenId), "Erro, você não é o proprietário");
        priceMap[_tokenId] = _price;

        emit PriceUpdate(msg.sender, oldPrice, _price, _tokenId);

        return true;
    }

}
Enter fullscreen mode Exit fullscreen mode

Compile o Contrato Inteligente

Agora que nosso contrato inteligente está pronto, podemos compilá-lo executando o comando:

truffle compile ou npx truffle compile
Enter fullscreen mode Exit fullscreen mode

Se tudo ocorreu conforme o esperado, você receberá a mensagem “Compiled successfully” e a pasta build será criada contendo a ABI do contrato.

Teste Do Contrato Inteligente

Nosso contrato inteligente foi compilado com sucesso. No entanto, um contrato inteligente compilado com sucesso não significa necessariamente que esteja correto! É essencial escrever casos de teste para garantir que ele passe em todos os casos de uso pretendidos e em alguns casos extremos. O teste de contratos inteligentes torna-se ainda mais importante, pois uma vez que um contrato inteligente é implantado na blockchain, ele não pode ser alterado.

Vamos testar os seguintes métodos:

  • Mint NFT
  • Update NFT price

Vamos criar o arquivo NFT.test.js dentro do diretório test onde vamos escrever diferentes casos de teste.

const { accounts, contract, web3 } = require("@openzeppelin/test-environment");
const { expect } = require("chai");
const { BigNumber } = require("bignumber.js");

const [deployer, userMinter] = accounts;

const MyNFTContract = contract.fromArtifact("NFT");

describe("NFT", function () {
  beforeEach(async function () {
    this.contract = await MyNFTContract.new({ from: deployer });
  });

  it("Deve cunhar o NFT com sucesso", async function () {
    const tokenURI = "ape_nft_one";

    const mintResult = await this.contract.mint(
      tokenURI,
      userMinter,
      web3.utils.toWei("12", "ether"),
      { from: userMinter }
    );

    const price = web3.utils.fromWei(
      new BigNumber(mintResult.logs[1].args.price).toString(),
      "ether"
    );
    expect(mintResult.logs[1].args.nftID.toNumber()).to.eq(1);
    expect(mintResult.logs[1].args.uri).to.eq(tokenURI);
    expect(mintResult.logs[1].args.minter).to.eq(userMinter);
    expect(price).to.eq("12");
  });

  it("Deve cunhar o NFT com sucesso ", async function () {
    const tokenURI = "ape_nft_two";
    const mintResult = await this.contract.mint(
      tokenURI,
      userMinter,
      web3.utils.toWei("10", "ether"),
      { from: userMinter }
    );

    const updateResult = await this.contract.updatePrice(
      mintResult.logs[0].args.tokenId.toNumber(),
      web3.utils.toWei("20", "ether"),
      { from: userMinter }
    );

    const oldPrice = web3.utils.fromWei(
      new BigNumber(updateResult.logs[0].args.oldPrice).toString(),
      "ether"
    );

    const newPrice = web3.utils.fromWei(
      new BigNumber(updateResult.logs[0].args.newPrice).toString(),
      "ether"
    );

    expect(updateResult.logs[0].args.owner).to.eq(userMinter);
    expect(oldPrice).to.eq("10");
    expect(newPrice).to.eq("20");
  });
});
Enter fullscreen mode Exit fullscreen mode

Antes de executar o teste, vamos adicionar o comando test dentro de package.json na seção de script.

"test": "truffle compile && mocha --exit --recursive --timeout 10000"
Enter fullscreen mode Exit fullscreen mode

Agora executamos o teste: npm run test e você deve obter o seguinte resultado:

Resultado do Teste

Implante O Contrato Inteligente

Agora é hora de finalmente avançarmos para a implantação de nosso contrato inteligente na rede de teste Polygon (Mumbai), para que possamos interagir com ele usando um aplicativo front-end.

Configure a rede Polygon para sua carteira Metamask usando este artigo e copie a frase de recuperação secreta de 12 palavras. Crie um arquivo .env no diretório do projeto e adicione o seguinte conteúdo:

MNEMONIC = SUA FRASE DE RECUPERAÇÃO SECRETA DE 12 PALAVRAS
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar a rede polygon_testnet no arquivo trufle-config.js que conterá nossa variável de ambiente MNEMONIC e o URL do ponto de extremidade RPC que queremos usar. O ID de rede para a polygon testnet é 80001. Aqui você também pode definir o limite de gás e o preço do gás para transações mais rápidas.

require("dotenv").config();
const HDWalletProvider = require("@truffle/hdwallet-provider");

const mnemonic = process.env.MNEMONIC;

module.exports = {
  networks: {
    polygon_testnet: {
      provider: () =>
        new HDWalletProvider(mnemonic, `https://rpc-mumbai.maticvigil.com`),
      network_id: 80001,
      networkCheckTimeout: 9999,
      confirmations: 4,
      timeoutBlocks: 200,
      skipDryRun: true,
    },
  },

  mocha: {
    // timeout: 100000
  },
  compilers: {
    solc: {
      version: ">=0.4.22 <0.9.0",
      settings: {
        optimizer: {
          enabled: false,
          runs: 200,
        },
        evmVersion: "byzantium",
      },
    },
  },

  db: {
    enabled: false,
  },
};
Enter fullscreen mode Exit fullscreen mode

Nossa configuração de implantação está pronta, só precisamos adicionar o arquivo de migração para nosso contrato NFT. Crie um arquivo chamado 1_initial_deploy.js e adicione o seguinte dentro dele:

const NFT = artifacts.require("NFT");

module.exports = function (deployer) {
  deployer.deploy(NFT);
};
Enter fullscreen mode Exit fullscreen mode

Tudo está definido, vamos executar o comando de implantação:

truffle migrate --network polygon_testnet
Enter fullscreen mode Exit fullscreen mode

Se tudo funcionou bem, você verá algo assim:

1_initial_deploy.js
=====================

   Replacing 'NFT'
   ------------------
   > transaction hash:    0xb693699458cb798aa7bf56140bd0eb9b209f55104b17e674f2f9397c59678867
   > Blocks: 1            Seconds: 12
   > contract address:    0x0b689AC2f5e1D925A0441cE4B3E949f3A15bD0f4
   > block number:        26308301
   > block timestamp:     1652463221
   > account:             0x8182677790c76d5a3CC8298d9643Cf8D4566C564
   > balance:             0.465465328999687315
   > gas used:            3498258 (0x356112)
   > gas price:           3.000000008 gwei
   > value sent:          0 ETH
   > total cost:          0.010494774027986064 ETH

   Pausing for 4 confirmations...
   ------------------------------
   > confirmation number: 5 (block: 2371262) initialized!

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:                   0 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0 ETH
Enter fullscreen mode Exit fullscreen mode

Conclusão

Neste tutorial, aprendemos como escrever um contrato inteligente que pode ser usado para criar e atualizar NFTs, também escrevemos testes para nosso contrato inteligente e finalmente o implantamos na rede de testes Mumbai. Dado que a Polygon é uma cadeia EVM, os mesmos procedimentos podem ser usados para construir e implantar um DAPP em qualquer rede compatível com EVM (Ethereum, Fantom, Binance Smart Chain, Avalanche, Cronos, etc.).

Na segunda parte deste artigo, aprenderemos como indexar eventos de blockchain no protocolo Subgraph.


Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui

Latest comments (0)