NFTs se tornaram uma parte central do ecossistema web3 desde o boom de 2021, evoluindo além dos colecionáveis para casos de uso no mundo real, como acesso a memberships, tokenização de ativos, itens de jogos e identidade digital.
Plataformas no-code como o Phosphor tornaram mais fácil do que nunca para criadores e desenvolvedores mintarem, personalizarem e implantarem NFTs por meio de um intuitivo Creator Studio e conjunto de APIs, eliminando a necessidade de construir contratos NFT personalizados do zero. No entanto, para desenvolvedores que desejam aprofundar seu conhecimento em Solidity e experiências nativas do web3, aprender a construir um marketplace de NFTs continua sendo uma habilidade fundamental.
Neste guia, vamos percorrer o processo de desenvolvimento e implantação de um marketplace de NFTs básico, mas totalmente funcional, na Linea. Ao final, você terá o conhecimento necessário para lançar seu próprio marketplace e obter uma compreensão mais sólida do desenvolvimento de smart contracts, aplicações descentralizadas e interações web3.
Pré-requisitos
Você deve ter um entendimento básico de Solidity.
Você deve ter o Nodejs e o Foundry instalados no seu computador.
Visão geral do contrato
Este marketplace de NFTs suportará as seguintes funcionalidades:
Listagem de NFTs para venda: Os usuários podem definir um preço e listar seus NFTs, com os detalhes armazenados com segurança no smart contract.
Compra de NFTs: Os compradores podem adquirir NFTs listados pagando o valor em Ether necessário, garantindo uma transação sem complicações.
Compra em lote de NFTs: Múltiplos NFTs podem ser comprados em uma única transação para reduzir os custos de gas.
Remoção de listagem de NFTs: Os proprietários podem remover seus NFTs da venda, devolvendo-os às suas carteiras.
Configuração do projeto
Usaremos o framework Foundry para este projeto. Com o Foundry instalado, execute o comando abaixo para iniciar um novo projeto:
forge init marketAbra a pasta market no Vscode ou no seu editor de código favorito e exclua os arquivos scripts/counter.s.sol, src/counter.sol e test/counter.t.sol
Instalar todas as dependências
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitCódigo completo do smart contract do marketplace de NFTs
Crie um arquivo market.sol na pasta src e adicione o código abaixo ao arquivo.
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace {
struct Listing {
address seller;
uint256 price;
}
mapping(address => mapping(uint256 => Listing)) public listings;
event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
modifier isSeller(address nftContract, uint256 tokenId, address spender) {
require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
_;
}
modifier isListed(address nftContract, uint256 tokenId) {
require(listings[nftContract][tokenId].price > 0, "Not listed");
_;
}
modifier notListed(address nftContract, uint256 tokenId) {
require(listings[nftContract][tokenId].price == 0, "Already listed");
_;
}
function listNFT(address nftContract, uint256 tokenId, uint256 price)
external
notListed(nftContract, tokenId)
{
require(price > 0, "Price must be greater than zero");
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
listings[nftContract][tokenId] = Listing(msg.sender, price);
emit Listed(nftContract, tokenId, msg.sender, price);
}
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
emit Purchase(nftContract, tokenId, msg.sender, listing.price);
}
function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
external
payable
{
uint256 totalAmount;
for (uint256 i; i < tokenIds.length; i++){
Listing memory listing = listings[nftContract][tokenIds[i]];
totalAmount += listing.price;
}
require(msg.value >= totalAmount, "Insufficient funds");
for (uint256 i; i < tokenIds.length; i++){
buyNFT(nftContract, tokenIds[i]);
}
}
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
}
Principais funcionalidades do código
Struct de listagem
Isso é essencial para implementar funcionalidades como listagem, compra e remoção de listagem em um marketplace de NFTs descentralizado. Ele fornece uma maneira clara e eficiente de gerenciar listagens, garantindo que o marketplace mantenha um registro do vendedor e do preço de cada item — ambos cruciais para suas operações.
struct Listing {
address seller;
uint256 price;
}
Mapping de listagem
Esta linha de código define um mapping aninhado em Solidity, que organiza e armazena as listagens do marketplace de NFTs. O mapping externo usa o endereço do contrato NFT como chave, permitindo que o smart contract categorize as listagens por suas respectivas coleções de NFTs. Isso garante que o marketplace possa suportar múltiplas coleções de NFTs.
O mapping interno usa o token ID (um identificador único para cada NFT) como chave, permitindo o gerenciamento eficiente de múltiplas listagens em um único contrato NFT.
mapping(address => mapping(uint256 => Listing)) public listings;
Listagem de NFT
A função listNFT permite que os usuários listem seus NFTs para venda no marketplace. Ela recebe três parâmetros: contrato NFT, token ID e preço. O contrato NFT refere-se ao endereço da coleção da qual o usuário deseja listar um NFT, o token ID é o identificador único do NFT dentro dessa coleção, e o preço é o valor pelo qual o usuário deseja vender o NFT.
A função primeiro valida que o usuário é o proprietário do NFT e garante que o preço seja maior que zero. Após essas verificações, o NFT é transferido do vendedor para o marketplace, o mapping de listagens é atualizado e o evento Listed é emitido com os parâmetros corretos.
function listNFT(address nftContract, uint256 tokenId, uint256 price)
external
notListed(nftContract, tokenId)
{
require(price > 0, "Price must be greater than zero");
require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
listings[nftContract][tokenId] = Listing(msg.sender, price);
emit Listed(nftContract, tokenId, msg.sender, price);
}
Compra de NFT
A função buyNFT permite que os usuários comprem um NFT listado no marketplace. Ela recebe dois parâmetros: nftContract, o endereço do smart contract do NFT, e tokenId, o identificador único do NFT sendo comprado.
A função primeiro verifica se o comprador enviou Ether suficiente para cobrir o preço do NFT. Uma vez validado, a listagem é removida do mapping de listagens, o pagamento é transferido com segurança para o vendedor e o NFT é transferido do contrato do marketplace para o endereço do comprador, finalizando a compra.
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
emit Purchase(nftContract, tokenId, msg.sender, listing.price);
}
Compra de múltiplos NFTs
A função buyMultipleNFT permite que um usuário compre múltiplos NFTs em uma única transação. Ela primeiro calcula o preço total somando os preços de todos os NFTs listados no array tokenIds. A função então verifica se o comprador enviou fundos suficientes para cobrir o valor total.
Uma vez validados os fundos, a função itera pelo array tokenIds, chamando a função buyNFT para cada NFT. Isso garante que cada NFT seja comprado individualmente, o pagamento seja transferido para o vendedor e os NFTs sejam enviados ao comprador — seguindo o mesmo processo da função buyNFT.
function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
external
payable
{
uint256 totalAmount;
for (uint256 i; i < tokenIds.length; i++){
Listing memory listing = listings[nftContract][tokenIds[i]];
totalAmount += listing.price;
}
require(msg.value >= totalAmount, "Insufficient funds");
for (uint256 i; i < tokenIds.length; i++){
buyNFT(nftContract, tokenIds[i]);
}
}
Remoção de listagem de NFTs
A função delistNFT permite que um vendedor remova seu NFT do marketplace. Ela primeiro verifica se o NFT está listado e se o chamador é o legítimo proprietário. Uma vez validado, a listagem é excluída do marketplace, removendo efetivamente o NFT da venda.
A função então transfere o NFT de volta para o vendedor, garantindo que ele recupere a propriedade do seu ativo. Por fim, o evento Delisted é emitido para confirmar que o NFT foi removido com sucesso do marketplace.
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
Eventos principais
O contrato emite eventos para ações essenciais do marketplace:
Listed: Acionado quando um NFT é listado, registrando detalhes como o vendedor, preço e informações do NFT.
Purchase: Emitido quando um comprador adquire um NFT, garantindo a rastreabilidade da transação.
Delisted: Indica que um NFT foi removido com sucesso do marketplace, mantendo a transparência.
event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller
Visão geral do conjunto de testes
Crie um arquivo NFT.sol na pasta src e adicione o código abaixo ao arquivo.
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
uint256 private _tokenIdCounter;
string private _baseTokenURI;
event Minted(address indexed to, uint256 tokenId);
constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
_baseTokenURI = baseTokenURI;
}
function mint(address to) external onlyOwner {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter += 1;
_safeMint(to, tokenId);
emit Minted(to, tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
_baseTokenURI = newBaseTokenURI;
}
function getTokenCount() external view returns (uint256) {
return _tokenIdCounter;
}
}
Crie um arquivo market.t.sol na pasta test e adicione o código abaixo ao arquivo.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
NFTMarketplace public market;
NFT public nft;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
nft = new NFT("TestNFT", "tnft", "");
market = new NFTMarketplace();
nft.mint(user1);
nft.mint(user1);
nft.mint(user1);
}
function test_Listing() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
}
function test_BuyingNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
vm.startPrank(user2);
deal(user2, 10 ether);
market.buyNFT{value: 1 ether}(address(nft), 0);
assert(nft.ownerOf(0) == user2);
}
function test_BuyingMultipleNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
nft.approve(address(market), 1);
nft.approve(address(market), 2);
market.listNFT(address(nft), 0, 1 ether);
market.listNFT(address(nft), 1, 1 ether);
market.listNFT(address(nft), 2, 1 ether);
vm.startPrank(user2);
deal(user2, 10 ether);
uint256 [] memory nfts = new uint256[] (3);
nfts[0] = 0;
nfts[1] = 1;
nfts[2] = 2;
market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);
}
function test_DelistingNFT() public {
vm.startPrank(user1);
nft.approve(address(market), 0);
market.listNFT(address(nft), 0, 1 ether);
assert(nft.ownerOf(0) == address(market));
market.delistNFT(address(nft), 0);
assert(nft.ownerOf(0) == address(user1));
}
}
O conjunto de testes acima garante que o contrato do marketplace de NFTs funcione conforme o esperado em suas funcionalidades principais. Veja um resumo dos testes:
Configuração do ambiente de testes
A função setUp inicializa o ambiente implantando os contratos NFTMarketplace e NFT. Ela minta três NFTs para o user1 para fins de teste. Um contrato NFT simulado (NFT) é implantado com uma função mint para gerar NFTs, enquanto o contrato do marketplace (NFTMarketplace) é instanciado. O user1 possui todos os NFTs mintados, permitindo o teste das funcionalidades de listagem e remoção de listagem.
Testando a listagem de NFT
A função test_Listing garante que o user1 possa listar um NFT para venda no marketplace.
O user1 aprova o marketplace para transferir o tokenId 0.
O user1 lista o NFT a um preço de 1 Ether.
O teste confirma que a função listNFT funciona corretamente e que o NFT é transferido para o marketplace.
Testando a compra de um único NFT
A função test_BuyingNFT simula a compra de um NFT listado pelo user2.
O user1 lista o tokenId 0 por 1 Ether.
O user2 recebe 10 Ether e chama a função buyNFT, enviando 1 Ether para comprar o NFT.
Após a compra, o teste verifica que o user2 é o novo proprietário do tokenId 0.
Testando a compra de múltiplos NFTs
A função test_BuyingMultipleNFT testa a capacidade de comprar múltiplos NFTs em uma única transação.
O user1 lista três NFTs (tokenId 0, 1, 2) por 1 Ether cada.
O user2 chama buyMultipleNFT, passando os token IDs e enviando 3 Ether.
O teste garante que o user2 se torne o proprietário dos três NFTs e que o marketplace processe corretamente as múltiplas compras.
Testando a remoção de listagem de NFT
A função test_DelistingNFT verifica que o user1 pode remover seu NFT do marketplace.
O user1 lista o tokenId 0 por 1 Ether e chama delistNFT.
O NFT é devolvido ao user1, e o teste confirma que a propriedade foi revertida para o user1.
Executar testes
forge testImplantação e verificação na Linea
Adicione as variáveis necessárias ao arquivo .env, garantindo que ele esteja localizado na raiz do projeto:
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Adicione os detalhes abaixo ao foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Crie um Makefile e adicione os detalhes abaixo:
deploy:
forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace
Implantação
make deployVerificação
make verifyConclusão
Construir um marketplace de NFTs na Linea proporciona aos desenvolvedores experiência prática em desenvolvimento de smart contracts, programação em Solidity e interações web3. Neste guia, cobrimos as funcionalidades principais de um marketplace descentralizado, incluindo listagem, compra, compra em lote e remoção de listagem de NFTs, garantindo transações seguras e eficientes.
Também percorremos a implementação e os testes do smart contract usando o Foundry, bem como o processo de implantação e verificação na Linea. Seguindo este guia, você agora tem o conhecimento fundamental para construir, testar e implantar seu próprio marketplace de NFTs.
Para mais tutoriais práticos e análises aprofundadas sobre desenvolvimento blockchain, confira o canal do YouTube do MetaMask Developer e fique atualizado com nossos insights mais recentes nos seguindo no Twitter.