WEB3DEV

Cover image for NFTs em Tempo Real: Oficina de Construção de Smart Contracts Dinâmicos
Dimitris Carvalho Calixto
Dimitris Carvalho Calixto

Posted on

NFTs em Tempo Real: Oficina de Construção de Smart Contracts Dinâmicos

Esse artigo foi escrito por: Muriel Pinho e traduzido por Dimitris Calixto, artigo original disponível aqui

Neste tutorial, vamos construir um NFT dinâmico. Esperamos que esteja familiarizado com os NFTs e talvez até tenha olhado a base de alguns projetos populares como BAYC, Cool Cats, e outros. Caso contrário, talvez você queira verificar este artigo primeiro sobre lançar um NFT a partir do zero.

Contexto

Tradicionalmente, existem dois tipos de contratos inteligentes NFT como as variantes ERC721 e ERC1155 que podem armazenar informação de imagem:

  1. O contrato armazena um URI acessível através da função tokenURI(). Esta URI aponta para um local de armazenamento descentralizado como IPFS ou Arvweave e aplicações como Opensea podem consultar estes dados diretamente a partir da fonte.
  2. O contrato gera a imagem diretamente através de camadas SVG e constrói os metadados on-chain. Isto é mais complexo do que simplesmente apontar para alguns metadados já gerados off-chain. O Protocolo Noun da NounsDAO é um excelente recurso para este tipo de abordagem.

Image

Para este tutorial, vamos utilizar uma abordagem híbrida para gerar metadados. A imagem que os metadados contêm será armazenada no IPFS, no entanto, a geração do JSON será feita on-chain. Isto facilita a criação dinâmica dos metadados em tempo real, nos dando ao mesmo tempo a flexibilidade de alterar a imagem envolvida pelos metadados com pouco esforço.

Antes de continuarmos, é importante notar o efeito que isto pode ter num projeto NFT. Imagine alterar os metadados de uma coleção de uma blue-chip como a BAYC, manipulando as imagens ou mesmo alterando os atributos para tornar alguns itens mais raros. Se tudo o que é preciso é que um programador atualize o URI de um NFT para mudar completamente o visual, então os colecionadores estão realmente depositando toda a sua confiança nas equipas que entregam.

É aqui que entra o conceito de proveniência e é algo que não é suficientemente mencionado com os NFTs. Se os NFTs utilizarem apenas IPFS, os criadores devem publicar um_ checksum_ (hash) imutável da pasta de metadados depois de revelarem. Dessa forma, os usuários podem sempre verificar off-chain que os metadados não foram violados. Se os metadados forem apenas gerados on-chain, então há menos riscos.

No caso dos NFTs dinâmicos onde os metadados e as imagens podem mudar, os usuários finais devem ser sensibilizados para como e quando as coisas mudarão. No caso de metadados off-chain (IPFS), um novo hash de proveniência pode ser atualizado para o contrato cada vez que os metadados mudam, de modo a haver uma pista de auditoria completa e comprovável.

NFTs em Tempo Real ⏳

Agora que compreendemos como os NFT podem ser atualizados, e se pudéssemos usar dados do mundo real, eventos, ou mesmo a autêntica aleatoriedade para atualizar dinamicamente os metadados nos nossos contratos inteligentes? Bem-vindo ao mundo do Chainlink.

Chainlink _é uma rede oráculo descentralizada que pode alimentar dados _off-chain em contratos inteligentes, dando a eles essencialmente a capacidade de solicitar e responder a qualquer API off-chain.

Image

Este tutorial assume que você já tem a MetaMask e que está familiarizado com a capacidade de mudar redes. Iremos utilizar o Rinkeby Testnet para tudo adiante.

Nota: Se nunca utilizou redes de teste antes, você precisará ativar esse recurso. Minha conta > Definições > Avançadas > Mostrar redes de teste.

A seguir, precisamos de financiar a nossa carteira com alguns Rinkeby ETH e LINK. Cada chamada para um Oráculo custará algum LINK que vai para os operadores do nó como uma taxa de serviço. Dirija-se a https://faucets.chain.link/rinkeby e preencha a sua conta com algum ETH e LINK de teste.

Image

Se nunca importou tokens antes, você também vai querer importar o endereço do token LINK para aparecer na MetaMask. Procure o "Não está vendo seu token?'' Importar tokens na parte de baixo da tela "Ativos".

Para Rinkeby, o endereço é: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709

Image

Hora de construir 👷

Neste momento, você deve estar se perguntando o que estamos construindo e como estaremos usando oráculos _Chainlink _para atualizar dinamicamente o nosso NFT. Existem toneladas de Data Providers no mercado de Chainlink, mas para este tutorial, vamos utilizar o Chainlink VRF (função aleatória verificável) Oracle para atribuir NFTs aleatoriamente durante a cunhagem. Há dois componentes-chave que tornam o VRF único e interessante:

  1. A aleatoriedade vem de uma fonte verdadeiramente aleatória. A blockchain Ethereum é completamente determinista*, o que significa que qualquer método de tentar criar aleatoriedade dentro de um contratos inteligentes não é realmente aleatório, mas pseudo-aleatória, e pode ser calculada antes do tempo.
  2. A aleatoriedade é verificável, o que significa que qualquer pessoa pode validar a forma como a aleatoriedade foi gerada através de prova criptográfica. Para mais informações sobre como isto funciona, consulte este post detalhado por Chainlink.
  • A blockchain precisa ser determinista para que todos os nós possam chegar a um consenso sobre cada bloco e chegar ao acordo sobre o próximo estado da cadeia.

Com estas duas características, podemos criar NFTs verdadeiramente aleatórias e verificar a fonte da aleatoriedade. Muito bem!

Image

Metadados 💾

Antes de saltarmos para o código, como em qualquer NFT, precisamos de alguns metadados que descrevem o NFT e contenham o ponteiro para a imagem do NFT no IPFS. Uma vez que os NFT que queremos criar apontam para imagens aleatórias, precisamos de uma forma de preservar os metadados, tais como tokenId enquanto ainda sendo capazes de aleatorizar o ponteiro da imagem.

Para conseguir isso, vamos gerar dinamicamente os metadados on-chain e devolvê-los como uma cadeia codificada de base64. Se não estiver familiarizado com a base64, lembra algo parecido com isto:


data:application/json;base64,eyJuYW1lIjoidG9rZW4gIyAwIiwgImRlc2NyaXB0aW9uIjoiQSBkeW5hbWljIE5GVCIsICJpbWFnZSI6ICJodHRwczovL2dhdGV3YXkucGluYXRhLmNsb3VkL2lwZnMvUW1YSDJlTmtIUzNXOFZveG5TcWhxWDVFSzE4cjkyNjg2a3VtS3VXM0dEeThVdi82LnBuZyJ9

Enter fullscreen mode Exit fullscreen mode

Os navegadores são capazes de analisar isso automaticamente, daí o porquê OpenSea ainda ser capaz de renderizar o NFT. Inclua isso para a sua barra de pesquisa e observe o que acontece.

Deve obter alguns metadados como:


{"name":"token # 0", "description":"A dynamic NFT", "image": "[https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/6.png](https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/6.png)"}

Enter fullscreen mode Exit fullscreen mode

Para este tutorial, retirei 10 imagens de https://unsplash.com/ e as armazenei no IPFS via piñata para que possamos ir diretamente para o código. O _baseTokenURI que vamos utilizar é:

https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/
Enter fullscreen mode Exit fullscreen mode

Note-se que isto apenas aponta para as imagens e os metadados reais serão gerados on-chain.Você pode sempre atualizar isto, trazendo as suas próprias 10 imagens.

Remix 💻

Vamos abrir uma nova instância de Remix no navegador e criar um novo arquivo chamado dynamicNFT.sol. A seguir copie e cole o seguinte código no arquivo:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
import "base64-sol/base64.sol";

contract DynamicNFT is VRFConsumerBase, ERC721Enumerable, Ownable{
    using SafeMath for uint256;
    using Strings for uint256;
    using Strings for uint8;

    // VRF Variáveis
    bytes32 public keyHash;
    uint256 public  fee;
    uint256 public randomResult;

    // ERC721 Variáveis

    // Dados do token
    uint256 public TOKEN_PRICE;
    uint256 public MAX_TOKENS;
    uint256 public MAX_MINTS;

    // Metadados
    string public _baseTokenURI;

    // Maps
    mapping(uint256 => uint256) public randomMap; // mapeia um tokenID para um número aleatório
    mapping(bytes32 => uint256) public requestMap; // mapeia um requestID para um tokenID

    /**
     * Constructor inherits VRFConsumerBase
     * 
     * Network: Rinkeby
     * Chainlink VRF Coordinator address: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B
     * LINK token address:                0x01BE23585060835E02B77ef475b0Cc51aA1e0709
     * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311
     */
    constructor(
        address _link,
        address _coordinator, 
        bytes32 _keyhash,
        uint256 _fee,
        string memory name, 
        string memory symbol, 
        string memory baseURI,
        uint256 tokenPrice,
        uint256 maxTokens,
        uint256 maxMints
    ) 
    VRFConsumerBase(_coordinator, _link)
    ERC721(name, symbol)
    {
        // Chainlink definidores
        keyHash = _keyhash;
        fee = _fee;

        // ERC721 definidores
        setTokenPrice(tokenPrice);
        setMaxTokens(maxTokens);
        setMaxMints(maxMints);
        setBaseURI(baseURI);
    }

    /* ========== FUNÇÕES ERC721  ========== */
    function setBaseURI(string memory baseURI) public onlyOwner {
        _baseTokenURI = baseURI;
    }

    function setMaxMints(uint256 maxMints_) public onlyOwner {
        MAX_MINTS = maxMints_;
    }

    function setTokenPrice(uint256 tokenPrice_) public onlyOwner {
        TOKEN_PRICE = tokenPrice_;
    }

    function setMaxTokens(uint256 maxTokens_) public onlyOwner {
        MAX_TOKENS = maxTokens_;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function mintTokens(uint256 numberOfTokens) public payable {
        require(numberOfTokens <= MAX_MINTS, "Can only mint max purchase of tokens at a time");
        require(totalSupply().add(numberOfTokens) <= MAX_TOKENS, "Purchase would exceed max supply of Tokens");
        require(TOKEN_PRICE.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");

        for(uint256 i = 0; i < numberOfTokens; i++) {
            uint256 mintIndex = totalSupply();
            if (mintIndex < MAX_TOKENS) {
                _safeMint(msg.sender, mintIndex);

                // request a random number from VRF oracle
                bytes32 requestId = getRandomNumber();
                // map request to tokenId
                requestMap[requestId] = mintIndex;
            }
        }
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        // constrói metdata de tokenId
        return constructTokenURI(tokenId);
    }

    function constructTokenURI(uint256 tokenId)
        public
        view
        returns (string memory)
    {
        // obtem um número aleatório do mapa
        uint256 randomNumber = randomMap[tokenId];
        // constrói tokenURI de randomNumber
        string memory randomTokenURI = string(abi.encodePacked(_baseTokenURI, randomNumber.toString(), ".png"));

        // metadata
        string memory name = string(abi.encodePacked("token #", tokenId.toString()));
        string memory description = "A dynamic NFT";

        // prettier-ignore
        return string(
            abi.encodePacked(
                'data:application/json;base64,',
                Base64.encode(
                    bytes(
                        abi.encodePacked('{"name":"', name, '", "description":"', description, '", "image": "', randomTokenURI, '"}')
                    )
                )
            )
        );
    }

    /** 
     * Requests randomness 
     */
    function getRandomNumber() public returns (bytes32 requestId) {
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");
        return requestRandomness(keyHash, fee);
    }

    /**
     * Callback function used by VRF Coordinator
     */
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        randomResult = randomness;
        // restringir número aleatório entre 1-10
        uint256 modRandom = randomResult % 10 + 1;
        // obtem tokenId que criou a solicitação
        uint256 tokenId = requestMap[requestId];
        // armazenar resultado aleatório em mapa de imagem de token
        randomMap[tokenId] = modRandom;
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma parte deste código deve ser semelhante ao simpleNFT.sol do tutorial, lançando um NFT do começo. Muito deve ser novo e relacionado com a funcionalidade Chainlink VRF. Vamos mergulhar:

Linha 57: Inicializa o VRFConsumerBase com o endereço _coordenador e o endereço _link token. A base de consumidores é o que irá impulsionar os pedidos e chamadas de retorno da chainlink.
Linhas 103-105: Solicita um novo número aleatório do getRandomNumber() e mapeia o requestId para o mintIndex(tokenId). O requestId é utilizado porque as chamadas para o coordenador VRF são assíncronas e podem voltar em qualquer ordem, pelo que precisamos de uma forma de rastrear qual token fez o pedido específico.
Linhas 156-162: fulfillRandomnessis é a função de chamada de retorno que Chainlink chamará assim que tiver um número aleatório para fornecer. As linhas seguintes limitam o número aleatório de 1-10 e armazenam-no em outro mapa randomMap que mapeia o tokenId para o número modRandom.

As próximas funções e linhas de código são responsáveis pela geração on-chain dos metadados.

Linhas 110-114: tokenURIis é o método que os mercados como o OpenSea utilizam para obter informações sobre o NFT. Na linha 114, os metadados são devolvidos do auxiliar do método constructTokenURIwhich, que toma o tokenId como parâmetro.
Linhas 117-142: constructTokenURIis onde a magia acontece. A linha 123 usa o tokenId para pegar o número aleatório armazenado no mapa randomMap. A linha 125 constrói a imagem completa URI ao concatenar _baseTokenURI e o randomNumber pegado logo acima.

Dica pro: Em solidity, você pode efetuar interpolação de strings usando o método muito útil abi.encodePacked().

Linhas 128-129: Estas linhas geram o nome e símbolo que serão utilizados nos metadados. Nota na linha 128, o nome é gerado de forma dinâmica cada vez que o método é chamado.
Linhas 132-142: É aqui que a norma de metadados é construída e convertida em base64. Note que base64 é muito eficiente ao codificar informação SVG, mas no nosso caso temos apenas um URI IPFS, então o ganho de armazenamento não é tão grande.

Deployment

Esperamos que você tenha uma melhor compreensão de como este contrato funciona e que esteja pronto para fazer o deploy.

Para compilar o seu contrato, dirija-se ao segundo guia do lado esquerdo do Remix e certifique-se de que está utilizando a versão do Solidity 0.8.1 . Em Configurações do Compilador, também vai querer **Ativar a otimização **e definir o número de execuções para 200. As otimizações reduzem o bytecode mas têm algumas compensações, consulte este link para mais informações.

Finalmente, certifique-se de que você está compilando o contrato correto: DynamicNFT (dynamicNFT.sol)

Image

Com o contrato compilado e pronto para realizar o deploy, precisamos passar em alguns argumentos construtores antes de efetuarmos as nossas transações.

Image

Certifique-se de que está conectado à rede Rinkeby (4) através do ambiente Injected Web3. Os parâmetros do construtor são os seguintes:

  1. _link: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 - link o endereço do token
  2. _coordinator: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B - VRF endereço do oracle
  3. _keyHash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 - utilizado para gerar aleatoriedade
  4. _fee: 100000000000000000- 0.1 LINK
  5. nome: Minha NFT dinâmica - diverta-se com este
  6. símbolo: DNFT- divirta-se com esse também
  7. baseURI: https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/ - lembre-se que a barra frontal é importante
  8. tokenPrice: 0 - a menos que seja rico em Rinkeby
  9. maxTokens: 10 - isto é baseado no número de imagens armazenadas na pasta baseURI
  10. maxMints: 1 - quantos você pode cunhar de cada vez

Interação 🖱️

Uma vez que você tenha essa longa lista de parâmetros copiada no Remix, está na altura de enviar para o Rinkeby Testnet. Vá em frente e clique em transact e espere até que o contrato seja minerado. Mantenha-se a par do endereço do contrato, você pode encontrá-lo na seção de contratos do remix.

Lembre-se, cada chamada para uma Oracle Chainlink custa LINK e no caso da Oracle VRF, este montante é de 0,1 LINK. Você pode até ver este requisito na linha 148:

require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");

Financiando o Contrato com Link 💸

Podemos enviar LINK diretamente para o contrato utilizando a MetaMask. Abrir a aba de assets, encontrar LINK, e enviar 1 LINK para o endereço do contrato que anotou acima.

Image

Cunhagem 🔮

Neste momento, devemos estar prontos para cunhar alguns NFT dinâmicos. Esta parte é bastante simples. mintTokens, o método está sob o contrato que você implantou, digite em 1, e depois transacione!

Image

Vá em frente e faça isso algumas vezes mais se você quiser, então você conseguirá uma variedade de imagens no OpenSea.

Tenha em mente que a cada cunhagem é enviado um pedido ao _Chainlink _VRF Oracle e a resposta é tratada de forma assíncrona. Isso significa que pode levar algum tempo para que números aleatórios afetem os metadados e, portanto, a imagem NFT pode não aparecer imediatamente.

OpenSea 🖼️

Vamos ver o que aparece no OpenSea. Você vai querer usar https://testnets.opensea.io/ que é o equivalente de Rinkeby do OpenSea e digitar no seu endereço ETH.

Image

Viva🎉

Parabéns por ter chegado até ao fim! Ao longo deste tutorial você aprendeu:

  1. Como usar o _Chainlink _VRF para gerar aleatoriedade provável
  2. Como interpretar a aleatoriedade para cunhar NFTs
  3. Como utilizar os tokens LINK para financiar os Contratos Chainlink
  4. Como funciona a arquitetura de pedido/resposta do Chainlink
  5. Como gerar dinamicamente metadados on-chain

Serviços utilizados 🔨

Alcance 📱

Se você tiver alguma dúvida, sinta-se à vontade para deixar comentários ou seguir no Twitter em https://twitter.com/ultrasoundchad.

Este post também faz parte da reunião _Chainlink _X ETHSD. Nós veja em:

  1. https://www.meetup.com/eth-sd/
  2. https://twitter.com/EthSanDiego

Top comments (0)