Recentemente comecei um tutorial de programação de smart contract NFT em Solidity, usando o padrão ERC-721 aqui no blog e esta é a segunda parte deste tutorial. Caso não tenha implementado a primeira parte ainda, ela é obrigatória e pode ser encontrada neste link.
Na primeira parte implementamos um contrato MyNFT e todas as 9 funções e 3 eventos obrigatórios do padrão. No entanto o padrão é beeem minimalista, o que quer dizer que ele apenas determina como se dará a interação para transferência, delegação e verificação de propriedade dos NFTs registrados no contrato. Ele não determina por exemplo como você pode mintar/cunhar NFTs, destrui-los, impedir o minting de novos e por aí vai, tarefas bem comuns nesse meio.
Além disso, você deve ter sentido falta do conteúdo das NFTs em si. Afinal, tudo o que registramos até o momento foram tokens representados por ids. Mas se eu sou o dono do NFT de id 57, o que afinal isso representa? Uma imagem? Um MP3? Um imóvel físico? O padrão define como os metadados podem ser armazenados e falaremos disso também nesta parte 2.
Vamos lá!
Minting
Duas tarefas muito comuns em contratos de NFT são o Minting e o Burning. Mas o que são eles? Vamos começar explicando minting e futuramente volto no Burning.
Minting é o ato de cunhar tokens e usamos esta palavra ao invés de criar ou gerar em alusão ao processo físico de cunhagem de moedas, muito usado na antiguidade. Mintar um NFT nada mais é do que criá-lo de fato, geralmente já transferindo a sua propriedade para alguém. E embora uma função de mint não esteja presente no padrão ERC-721, ela é quase onipresente nos contratos deste tipo, tendo abaixo uma sugestão de como implementá-la.
1
2
3
4
5
6
7
8
|
uint internal _lastId;
function mint() public {
_lastId += 1;
_balanceOf[msg.sender]++;
_ownerOf[_lastId] = msg.sender;
emit Transfer(address(0), msg.sender, _lastId);
}
|
Na função acima nós usamos uma variável de estado para criar um id auto-incremental, ou seja, a cada token mintado, o id nunca vai se repetir e sempre será 1 a mais que o token anterior. Além da geração do id, a função de mint acima assume que o msg.sender será o dono do token gerado, por isso adicionamos o token no mapping de ownerOf e emitimos o evento de transferência, para sinalizar (aprenda a escutar eventos aqui).
A função acima te dá uma ideia de como mintar, mas não ajuda muito quando o assunto é o quê mintar. Se eu quiser registrar uma foto? Um MP3? Como faço? Nesses casos, embora tecnicamente seja possível salvar os bytes do arquivo digital na blockchain, o mais comum é salvarmos apenas a URL do arquivo em questão, que estará armazenado em outro local, para economizar nas transações, principalmente de grandes coleções. Essa abordagem é tão comum que é prevista na especificação 721 como um opcional, definido pela interface abaixo (copie e cole no seu arquivo MyNFT.sol, junto das demais interfaces).
1
2
3
4
5
6
7
|
interface ERC721Metadata {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 _tokenId) external view returns (string memory);
}
|
Essa interface define que as coleções de NFTs devem possuir um nome, uma abreviação (symbol) e que devem registrar uma URL para cada token cunhado e que esta URL deve levar para um JSON com os meta-dados do token, cujo formato também é incluído na especificação como sendo abaixo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
“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.”
}
}
}
|
Resumindo: o JSON do NFT deve ter as informações pertinentes ao mesmo, o que for necessário, e deve ser hospedado publicamente na Internet (sendo que o mais comum é utilizar a IPFS para isso). Abaixo um exemplo real de JSON de NFT de uma coleção chamada Meta Lion Kingdom. Apenas omiti algumas informações para facilitar, mas você verá que a ideia é a mesma da especificação, embora tenha incluído coisas adicionais.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
{
“dna”: “hash com o DNA dele”,
“name”: “Meta Lion #456”,
“description”: “Meta Lion Kingdom is a NFT collection that is connecting people with the African wildlife and culture…”,
“image”: “url da imagem”,
“edition”: 456,
“date”: 123456,
“attributes”: [{
“trait_type”: “Clothes”,
“value”: “POLO-BLACK”
},{
“trait_type”: “Necklace”,
“value”: “NECKLACE-PAW-POLO”
},
...
]
}
|
Implementando Metadados
Para servir de exemplo de implementação de metadados (o JSON que citei acima), vamos ajustar a nossa função de mint para que ela inclua a URL do JSON do seu NFT. Eu não vou mostrar aqui como criar o JSON ou como hospedá-lo, pois foge do escopo do tutorial. Vou partir do pressuposto que você já possui esses JSON em algum lugar ou que vai criá-los posteriormente.
1
2
3
4
5
6
7
8
9
10
|
contract MyNFT is ERC721, ERC165, ERC721Metadata {
function supportsInterface(
bytes4 interfaceId
) external pure returns (bool) {
return
interfaceId == 0x80ac58cd || //721
interfaceId == 0x01ffc9a7 || //165
interfaceId == 0x5b5e139f; //721Metadata
}
|
Depois, o próximo passo é implementarmos as funções obrigatórias da nova interface: name, symbol e tokenURI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function name() external view returns (string memory) {
return “MyNFT Collection”;
}
function symbol() external view returns (string memory) {
return “MFC”;
}
mapping(uint => string) internal _uris;
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(_ownerOf[tokenId] != address(0), “Not minted”);
return _uris[tokenId];
}
|
A função name deve retornar o nome da coleção, a função symbol deve retornar a abreviatura e a função tokenUri deve, com base no id do token passado por parâmetro, retornar a URI do JSON com os metadados dele. Ou erro em caso dele não ter sido mintado ainda.
Repare que criei um mapping para registrar a uri de cada um dos tokens (tokenId => uri). Essa URL vai ser gerada automaticamente na função de minting, que temos de ajustar. Antes de fazer isso, adicione uma importação no topo do seu arquivo para incluirmos a biblioteca Strings.sol da OpenZeppelin (se estiver usando HardHat você deve instalar a lib de contratos da OpenZeppelin e referenciar de lá).
1
|
import “https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol”;
|
Essa biblioteca tem várias funções utilitárias muito úteis para manipulação de strings, incluindo uma que converte número para string, que vamos precisar abaixo já que nosso id é numérico.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function mint() public {
_lastId += 1;
_balanceOf[msg.sender]++;
_ownerOf[_lastId] = msg.sender;
_uris[_lastId] = string.concat(
“https://www.luiztools.com.br/nfts/”,
Strings.toString(_lastId),
“.json”
);
emit Transfer(address(0), msg.sender, _lastId);
}
|
Aqui está uma sugestão didática onde eu salvo no mapping de uris a URL do novo token cunhado. Usei aqui a função string.concat (disponível desde Solidity 0.8.12) para juntar uma URL base qualquer com o id do token (usei Strings.toString para converter o número para texto) e a extensão .json. Atenção que esta URL é fictícia viu, ela não vai funcionar se você tentar acessar.
Entenda esta função de mint com URI apenas como uma sugestão pois ela pode variar enormemente. Uma outra opção muito comum é não armazenarmos as URLs, mas sim apenas a base URL e gerar sempre a URL dinamicamente quando a função tokenURI for chamada.
Sugestões de Minting
Aqui vão algumas ideias de variação que podem inclusive ser combinadas:
- Self-Mint: no constructor, apenas para você registrar sua propriedade na blockchain (mostrei na parte 1);
- Mint-URI: onde o requisitante passa a URI que quer registrar na blockchain;
- Mint-Payable: onde o requisitante tem de pagar pelo NFT que vai mintar;
- Mint-Hash: onde o id do token é gerado a partir de alguma função de hashing, como keccak256 para não ser adivinhável;
- Mint-Random: onde o requisitante ganha uma NFT aleatória da coleção;
- Mint-Bytes: onde os bytes da propriedade digital são registrados na própria blockchain (alto custo, baixa performance, alta descentralização e alta segurança);
- Mint-IPFS: onde o arquivo do NFT é salvo na IPFS e apenas o hash dele é salvo na blockchain;
- Mint-Backend: onde só quem faz o mint é o seu backend, exigindo a passagem por parâmetro de quem vai receber o token (menos taxas pro usuário);
- Mint-AccessControl: onde só pode mintar quem tem alguma permissão específica no contrato (explico aqui);
Obviamente o céu é o limite no tocante a variações da função mint e espero ter trazido um exemplo relevante.
*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.