O padrão ERC-721 foi proposto para NFTs (Non-Fungible Tokens). NFT refere-se aos tokens únicos, o que significa que cada token dentro do contrato inteligente será diferente dos outros. Pode ser usado para identificar coisas únicas, por exemplo, algum tipo de imagem, bilhetes de loteria, colecionáveis, pinturas, ou mesmo música, etc. Lembrando que o ERC-20 foi para os tokens fungíveis, caso você não tenha lido o artigo sobre ERC-20, considere lê-lo primeiro. O melhor projeto do mundo real para o exemplo ERC-721 é . É um jogo em cima do blockchain, confira este jogo incrível para conhecimento com diversão. o Cryptokitties Mas como isso funciona? Como essas imagens, músicas e pinturas são armazenadas em contratos inteligentes? Bem, essas coisas não são armazenadas dentro do contrato, em vez disso, o contrato apenas aponta para um ativo externo. Há um número, mostrando a propriedade. O recurso principal é a imagem ou algum outro dado anexado ao tokenId por meio de um URI. Vamos mergulhar um pouco mais. tokenId Atribuímos um URI, que é um texto JSON, ao ID do token. E esse texto JSON contém os detalhes e o caminho para a imagem ou qualquer outro ativo. E o usuário possui apenas o . Por exemplo, meu endereço tem o saldo do 4, agora a imagem ou qualquer tipo de dado anexado a esse será meu. Eu possuo esses dados porque possuo o . tokenid tokenid tokenid tokenid Agora temos que armazenar essas duas coisas, ou seja, a imagem e o texto JSON (URI) em algum lugar para que qualquer mercado NFT possa buscar a imagem e exibi-la em seu site. Você pode estar pensando por que precisamos do URI de metadados, por que não atribuir diretamente o URI da imagem ao ID do token? Bem, você deve se lembrar por que precisamos dos padrões ERC em primeiro lugar, sim, para que seja fácil para os aplicativos do lado do cliente se conectarem aos contratos. Da mesma forma, os metadados que escrevemos para NFTs devem estar em um formato adequado recomendado pelo mercado NFT para que eles possam buscar facilmente os dados desse arquivo JSON e exibir os dados em seu site. Vamos ver a coisa de armazenamento agora. Armazenando os dados Existem duas maneiras de armazenar a imagem e o URI. Podemos hospedá-lo on-chain (dentro do blockchain) ou off-chain (fora do blockchain). Os dados on-chain são armazenados no blockchain, mas ocupam muito espaço que não é acessível. Pense em pagar milhares de reais na hora de implantar seu contrato. Não soa bem. Além disso, o blockchain também oferece armazenamento limitado, você não pode armazenar arquivos grandes nele. Portanto, temos a opção de hospedar os dados fora do blockchain. Através do nosso site usando AWS ou qualquer outro serviço de nuvem. Mas isso mata a ideia principal de uma blockchain, ou seja, descentralização. E se o servidor travar ou alguém o hackear? Como haveria um único servidor hospedando todos os dados. Portanto, precisamos de algo mais robusto a ataques e também descentralizado, e esse algo é o IPFS (Inter Planetary File System), que é um sistema de armazenamento distribuído. O IPFS possui vários nós semelhantes à ideia de blockchain que armazena os dados. Mas você não precisa pagar nenhuma taxa. Podemos hospedar nossas imagens e também o arquivo JSON (URI) no IPFS e, ao fazer isso, forneceremos um CID exclusivo (sem sentido aleatório) que aponta diretamente para os dados e atribuímos um CID específico ao ID do token específico. Falaremos mais sobre a parte técnica depois primeiro vamos entender a interface para o padrão ERC-721. INTERFACE ERC-721 ver como é a interface ERC-721. Vamos Este padrão tem as seguintes funcionalidades: Transferência de tokens de uma conta para outra. Obtendo o saldo atual do token de uma conta. Obtendo o proprietário de um token específico. O fornecimento total do token disponível na rede. Além dessas, também possui algumas outras funcionalidades, como aprovar que uma quantidade de token de uma conta possa ser movida por uma conta de terceiros. Agora vamos dar uma olhada na interface do ERC-721. pragma solidity ^0.4.20; interface ERC721 { event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool); } Break Discriminação Retorna o número de tokens na conta do proprietário. Como acompanhamos os saldos? Bem, é sempre a mesma coisa, definimos um mapeamento que rastreia o número total de tokens em uma carteira. mapping(endereço => uint) saldos públicos; Lembre-se, ele apenas soma o número total de tokens e retorna o mesmo, esse mapeamento não conhece os proprietários do token, pois apenas informa o número de IDs de token que um endereço possui. balanceOf function balanceOf(address _owner) external view returns (uint256); Localiza o proprietário de um NFT. Agora, esta função nos informa sobre o proprietário de um determinado NFT. Mantemos esses dados semelhantes aos anteriores. mapeamento(uint => endereço) saldos públicos; NFTs atribuídos a endereços zero são considerados inválidos e as consultas sobre eles devem gerar um erro. 2. ownerOf function ownerOf(uint256 _tokenId) external view returns (address); Transfere a propriedade de um NFT de um endereço para outro. Ele deve apenas definir o proprietário de um tokenId para o novo endereço e também atualizar o mapeamento . Deve lançar um erro nos seguintes casos: 3. safeTransferFrom balances é o proprietário atual, um operador autorizado ou o endereço aprovado para este NFT msg.sender não é o proprietário atual _from é o endereço zero. __to_ não é um NFT válido __tokenId_ Quando a transferência é concluída, esta função verifica se é um contrato inteligente (tamanho do código > 0). Nesse caso, ele chama a função em e lança se o valor de retorno não for igual a este: __to_ onERC721Received __to_ bytes4(keccak256("onERC721Received(address, address,uint256, bytes)")). O que foi isso? É por isso que a função é chamada de transferência segura, falaremos mais sobre isso mais tarde. function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; : Isso funciona de forma idêntica à função acima com um parâmetro de dados extra, exceto que esta função apenas define os dados como “ ”. 4. safeTransferFrom function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; Como essas duas funções têm o mesmo nome? Ao contrário do Javascript, o Solidity oferece suporte à sobrecarga de função. significa que podemos definir duas funções com os mesmos nomes, a única condição é que os parâmetros sejam diferentes, fazendo diferença na assinatura da função. Não sabe o que são assinaturas de função? Aqui você vai. Curiosidade: Link para responder transfere a propriedade de um NFT. Funciona da mesma forma que a função , a única diferença é que não chama a chamada de segurança ( ) no destinatário. Assim, criando um risco de perder o NFT. 5. transferFrom safeTransferFrom onERC721Received function transferFrom(address _from, address _to, uint256 _tokenId) external payable; o mesmo conceito da função de aprovação ERC20, mas com mais funcionalidade e lógica. Para que esta função funcione, definimos um mapeamento aninhado: Nesse mapeamento, o uint refere-se ao ID do token e ao endereço que é aprovado para executar operações nesse tokenId específico. Essa função de aprovação deve verificar se é o proprietário atual ou um operador do proprietário do token. Se essa verificação for aprovada, somente o mapeamento deverá ser atualizado. Qual é a operadora aqui? Veja a próxima função. 6. aprovar mapping(uint =>address) internal allowance; msg.sender function approve(address _approved, uint256 _tokenId) external payable; funciona como a função de aprovação anterior, mas em vez de aprovar um endereço para um único ID de token, essa função deve aprovar e endereçar para lidar com todos os tokens pertencentes a um determinado endereço. O que isso significa? Isso significa que você pode criar um operador de endereço para seus NFTs. Esse endereço será o proprietário de seus NFTs até que você revogue a propriedade. Desta vez, usamos novamente o mapeamento: O primeiro endereço define o booleano true ou false para o segundo endereço aprovar ou revogar, respectivamente. 7. setApprovalForAll mapping(address => mapping(address=>bool))internal operator; function setApprovalForAll(address _operator, bool _approved) external; é semelhante à função de permissão na interface ERC-20. Retorna o aprovado para um único NFT. Ele verifica o mapeamento que atualizamos na função de aprovação acima. 8. getApproved address function getApproved(uint256 _tokenId) external view returns (address); semelhante à função acima. Ele consulta se um é um autorizado para outro . Em vez de retornar o aprovado de um determinado , esta função deve retornar o do para o fornecido que atualizamos na função 9. isApprovedForAll address operator address address tokenId address operator address setApprovalForAll function isApprovedForAll(address _owner, address _operator) external view returns (bool); Eventos para ERC-721 emite quando a propriedade de qualquer NFT é alterada por qualquer mecanismo. Este evento é emitido quando os NFTs são criados e destruídos : Durante a criação do contrato, qualquer número de NFTs pode ser criado e atribuído sem emitir Transferência. No momento de qualquer transferência, o endereço aprovado para essa NFT (se houver) é redefinido como nenhum. Transferência: (from == 0) (to == 0). Exceção event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); é emitida quando o endereço aprovado para uma NFT é alterado ou reafirmado. O endereço zero indica que não há endereço aprovado. Quando um evento de transferência é emitido, isso também indica que o endereço aprovado para essa NFT (se houver) foi redefinido para nenhum. A aprovação event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); é emitido quando um operador é habilitado ou desabilitado para um proprietário. A operadora pode gerenciar todas as NFTs do proprietário. ApproveForAll event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); Esta foi a interface para os contratos ERC 721. Agora vamos aprender mais sobre suas implementações e regras. ERC721TokenReceiver Antes da implementação, há mais uma coisa. Lembra que falamos sobre uma função chamada quando falamos sobre a transferência de tokens? Vamos falar sobre isso agora. Se o destinatário de um token ERC-721 for um contrato, ele precisará ser funcional com os tokens ERC-721. Porque e se não houver funcionalidade para interagir com os tokens nesse contrato do receptor? Os tokens ficarão bloqueados nesse contrato para sempre. Ninguém pode levar esses tokens para qualquer endereço, pois não há funcionalidade. Para superar essa vulnerabilidade, o receptor do token ERC-721 deve implementar a interface do receptor, caso contrário, nenhum dos contratos pode enviar qualquer token ERC-721 para esse contrato. Vejamos a interface do receptor. onERC721Received interface contém apenas uma função para implementar e que é esta função deve tratar o recebimento de uma NFT. O contrato inteligente ERC-721 chama essa função no destinatário após uma . Esta função pode lançar para reverter e rejeitar a transferência. Devoluções diferentes do valor mágico DEVEM resultar na reversão da transação. Esta onERC721Received, transfer interface ERC721TokenReceiver { function onERC721Received(address _operator,address _from,uint256 _tokenId,bytes _data) external returns(bytes4); } Aqui você pode estar pensando como o contrato de token saberia se o receptor implementou a interface ou não? Portanto, antes de enviar os tokens, você deve primeiro verificar esta parte. Para isso, vamos ver a implementação do ERC-721. Aqui está a função privada que você deve chamar dentro do antes de enviar os tokens. Se retornar true, somente a transação deverá ocorrer. ERC721TokenReceiver openzeppelin safeTransfer() Bem, isso não garante a funcionalidade de retirar as NFTs do contrato do receptor, pois está apenas verificando a implementação da função do receptor, mas não a funcionalidade de transferir as NFTs do contrato. Então, qual é o significado disso? Esse método pode nos dizer que o autor do contrato receptor estava pelo menos ciente desse método, o que significa que ele deve ter implementado a funcionalidade para transferir os NFTs para fora do contrato. E, claro, ninguém gostaria que seus tokens ficassem presos dentro de um contrato. e ERC721Metadados ERC721Enumerable As interfaces acima eram obrigatórias para um contrato de token ERC-721. Agora, algumas interfaces são teoricamente opcionais, mas tornam o contrato mais claro e mais utilizável. Estes são e Vamos pegá-los um por um. ERC721Metadata ERC721Enumerable. a extensão de metadados é usada principalmente para interrogar o contrato para o nome do token. Agora vamos ver a interface de metadados. Isso é bastante simples de entender. Metadados ERC721: interface ERC721Metadata/* is ERC721 */ { /// @notice A descriptive name for a collection of NFTs in this /// contract function name()external view returns (string _name); /// @notice An abbreviated name for NFTs in this contract function symbol()external view returns (string _symbol); /// @notice A distinct Uniform Resource Identifier (URI) for a given /// asset. /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined /// in RFC3986. The URI may point to a JSON file that conforms to /// the "ERC721Metadata JSON Schema". function tokenURI(uint256 _tokenId)external view returns (string); Isso apenas fornece os dados sobre os NFTs. ERC721Enumerable- interface ERC721Enumerable/* is ERC721 */ { /// @notice Count NFTs tracked by this contract /// @return A count of valid NFTs tracked by this contract, where /// each one of them has an assigned and queryable owner not equal /// to the zero address function totalSupply()external view returns (uint256); /// @notice Enumerate valid NFTs /// @dev Throws if `_index` >= `totalSupply()`. /// @param _index A counter less than `totalSupply()` /// @return The token identifier for the `_index`th NFT, /// (sort order not specified) function tokenByIndex(uint256 _index)external view returns (uint256); /// @notice Enumerate NFTs assigned to an owner /// @dev Throws if `_index` >= `balanceOf(_owner)` or if /// `_owner` is the zero address, representing invalid NFTs. /// @param _owner An address where we are interested in NFTs owned /// by them /// @param _index A counter less than `balanceOf(_owner)` /// @return The token identifier for the `_index`th NFT assigned to /// `_owner`, /// (sort order not specified) function tokenOfOwnerByIndex(address _owner,uint256 _index)external view returns(uint256); ERC165 Agora, como saberíamos se uma interface foi implementada por um contrato ou não? Para isso, precisamos sair do tópico, ou seja, que tem uma função: ERC-165, function supportsInterface(bytes4 interfaceID) external view returns (bool); Essa função usa o interfaceId que você deseja verificar como um argumento e o compara com o interfaceId com o qual deseja verificar. Vou lhe dizer o que é um interfaceId, tenha paciência comigo. Primeiro veja a implementação desta função pelo openzeppelin. Vamos entender mais sobre interfaceId nesta função ; type(IERC721).interfaceId é obtido por duas operações principais. interfaceID 1. keccak 256 haxixe 2. Operação XOR keccak256 é um algoritmo que pega uma entrada e cospe alguma string aleatória em bytes. Agora vamos descobrir o keccak hash para esta função. function supportsInterface(bytes4 interfaceID) external view returns (bool); A sintaxe se parece com ṭthis. ; bytes4(keccak256('supportsInterface(bytes4)') Agora temos o keccak hash para esta função. Lembre-se, você não precisa passar toda a função dentro do parâmetro para keccak256, mas apenas a assinatura. A assinatura significa apenas o nome e o tipo de parâmetro, nem mesmo o nome do parâmetro está incluído. Depois de obter o hash keccak256 de todas as funções, realizamos a operação XOR entre elas e o resultado que obtemos é o interfaceId. Vamos ver como. As operações pegam entradas e fornecem algumas saídas depois de compará-las. A saída se uma, e apenas uma, das entradas for verdadeira. Se ambas as entradas forem falsas ou ambas forem verdadeiras, a saída será . XOR true false Você não precisa entender a matemática por trás do XOR. Apenas lembre-se, você pega o hash de cada função e os passa para o portão XOR e o valor obtido é o interfaceID. Portanto, a ideia principal é pegar o keccak hash das funções e obter sua saída XOR. essa saída é o interfaceId e depois disso, você pode verificar se um contrato tem o mesmo interfaceID. Se sim, isso significa que o contrato está implementando a interface desejada. keccak256 Não se preocupe, a solidity tornou isso fácil para você. Vamos aprender tomando como exemplo a interface para ERC721Metadata. interface ERC721Metadata/* is ERC721 */ { function name()external view returns (string _name); function symbol()external view returns (string _symbol); function tokenURI(uint256 _tokenId)external view returns (string); } Aqui temos três funções. Então eu escrevi este pedaço de código para fazer você entender. Observe aqui o símbolo de acento circunflexo . Este símbolo representa a operação XOR entre dois valores. ^ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16 ; interface metadata { function name()external view returns (string memory _name ); function symbol()external view returns (string memory _symbol); function tokenURI(uint256 _tokenId)external view returns (string memory a); } contract { //old and lengthy way by applying the keccak256 algorithm and performing XOR. function getHashOldWay ()public pure returns(bytes4){ return bytes4(keccak256('name()')) ^ bytes4(keccak256('symbol()'))^ bytes4(keccak256('tokenURI(uint256)')); } //Improved way function getHashNewWay ()public pure returns(bytes4){ metadata a; return a.name.selector ^ a.symbol.selector ^ a.tokenURI.selector; } //this is the latest simplest way to get the interfaceId function getHashLatestWay ()public pure returns(bytes4){ return type(metadata).interfaceId; } } Ohh nós fomos muito para o ERC165 que esquecemos do ERC721, vamos voltar a ele. Como o opensea (ou qualquer outro mercado) busca dados do contrato inteligente? Este é o momento certo para entender os URIs de token que ajudam os vários mercados NFT a identificar os dados que você atribuiu a um determinado tokenId. Como Opensea é o mercado mais famoso, vamos tomá-lo como exemplo. A Opensea espera que seu contrato ERC721 retorne um tokenURI chamando uma função . Vejamos o contrato openzeppelin para isso. tokenURI() TokenURIs podem ser de dois tipos. 1. Mesmo URI base para todos os tokens, variando pelo tokenId. Dessa forma, atribuímos um URI base para todos os tokens e depois concatenamos o tokenId a ele, obtendo o URI de metadados. Isso só é possível quando você atribuiu sua coleção de forma que o link seja o mesmo para todos os arquivos JSON de metadados, a única diferença seria o tokenId. Por exemplo, Este link aponta para a pasta, o que significa que este é o baseURI. https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/ Para obter um único metadado, precisamos acessar https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/1.json E agora você precisa retornar o tokenURI do contrato de forma que ele vá diretamente para os metadados desse token específico e o mercado possa buscá-lo. Fazemos isso concatenando o baseURI com o tokenId e codificando-o com abi. Observe esta função abaixo. function tokenURI(uint tokenId) override public view returns(string memory) { return (string(abi.encodePacked( "https://gateway.pinata.cloud/ipfs/QmYLwrqMmzC3k4eZu7qJ4MZJ4SNYMgqbRJFLkyiPtUBZUP/",Strings.toString(tokenId),".json")) ); } Esta é a função que o marketplace irá chamar para obter a URI e exibir todos os dados que fornecemos no arquivo JSON. 2. URIs diferentes para cada token. Essa é outra maneira de atribuir URIs localizados em locais diferentes. E o link IPFS para um arquivo individual se parece com isso. Desta forma, você não pode simplesmente concatenar o tokenId com baseURI para obter este link, em vez disso, este link deve ser anexado diretamente ao tokenId on-chain. E, para isso, observe o contrato acima, no qual estamos definindo um mapeamento privado para atribuir um URI a um tokenId específico. E posteriormente também definindo uma função que popula o mapeamento com uma determinada URI. E a função substitui a função pai adicionando uma nova funcionalidade de retornar o URI mapeado. https://ipfs.filebase.io/ipfs/Qma65D75em77UTgP5TYXT4ZK5Scwv9ZaNyJPdX9DNCXRWc ERC721URIStorage tokenURI Mais uma coisa a observar é o formato do esquema JSON. O JSON deve conter os dados de forma padronizada para que o marketplace possa buscar os dados desejados e exibir os mesmos. O esquema JSON padrão para ERC721. { "title": "Asset Metadata", "type": "object", "properties": { "name": { "type": "string", "description": "Identifies the asset to which this NFT represents" }, "description": { "type": "string", "description": "Describes the asset to which this NFT represents" }, "image": { "type": "string", "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive." } } } Portanto, essas são as coisas que você deve saber se estiver interessado no desenvolvimento de contratos inteligentes ERC-721 (NFT). : Problemas Existem vários problemas com escalabilidade no padrão ERC721. O problema surge ao transferir os tokens porque o ERC-721 permite que você transfira um token por vez. Isso significa que, se você quiser enviar 5 NFTs para alguém, precisará realizar 5 transações. Isso é bastante claro para você quanto espaço seria necessário no blockchain. É por isso que nas corridas de touro a rede enfrenta problemas, pois há muita pressa na rede e as tarifas de gás aumentam rapidamente. ERC-1155 resolve esses problemas. Como? Vamos falar sobre isso em um post separado. Se você entendeu o padrão ERC721 completamente, o ERC1155 não será muito difícil de entender. Publicado também . aqui