Les NFT sont devenus un élément central de l'écosystème web3 depuis le boom de 2021, évoluant au-delà des objets de collection vers des cas d'usage concrets tels que l'accès aux adhésions, la tokenisation d'actifs, les objets de jeu et l'identité numérique.
Des plateformes no-code comme Phosphor ont facilité plus que jamais la création, la personnalisation et le déploiement de NFT pour les créateurs et les développeurs, grâce à un Creator Studio intuitif et une suite d'API, supprimant ainsi la nécessité de construire des contrats NFT personnalisés de zéro. Cependant, pour les développeurs souhaitant approfondir leur compréhension de Solidity et des expériences natives web3, apprendre à construire une marketplace NFT reste une compétence fondamentale.
Dans ce guide, nous allons parcourir le processus de développement et de déploiement d'une marketplace NFT basique mais entièrement fonctionnelle sur Linea. À la fin, vous aurez les connaissances nécessaires pour lancer votre propre marketplace et acquérir une meilleure maîtrise du développement de smart contracts, des applications décentralisées et des interactions web3.
Prérequis
Vous devez avoir une compréhension de base de Solidity.
Vous devez avoir Nodejs et Foundry installés sur votre PC.
Présentation du contrat
Cette marketplace NFT prendra en charge les fonctionnalités suivantes :
Mise en vente de NFT : Les utilisateurs peuvent définir un prix et mettre en vente leurs NFT, avec les détails stockés de manière sécurisée dans le smart contract.
Achat de NFT : Les acheteurs peuvent acquérir des NFT mis en vente en payant l'Ether requis, garantissant une transaction fluide.
Achat groupé de NFT : Plusieurs NFT peuvent être achetés en une seule transaction pour réduire les frais de gas.
Retrait de la vente de NFT : Les propriétaires peuvent retirer leurs NFT de la vente, les renvoyant dans leurs portefeuilles.
Configuration du projet
Nous utiliserons le framework Foundry pour ce projet. Une fois Foundry installé, exécutez la commande ci-dessous pour initier un nouveau projet :
forge init marketOuvrez le dossier market sur Vscode ou votre éditeur de code favori, et supprimez les fichiers scripts/counter.s.sol, src/counter.sol et test/counter.t.sol.
Installer toutes les dépendances
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitCode complet du smart contract de la marketplace NFT
Créez un fichier market.sol dans le dossier src et ajoutez le code ci-dessous au fichier.
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);
}
}
Fonctionnalités clés du code
Struct Listing
Cet élément est essentiel pour implémenter des fonctionnalités telles que la mise en vente, l'achat et le retrait de la vente dans une marketplace NFT décentralisée. Il offre un moyen clair et efficace de gérer les annonces tout en garantissant que la marketplace conserve un enregistrement du vendeur et du prix de chaque article — deux éléments cruciaux pour son fonctionnement.
struct Listing {
address seller;
uint256 price;
}
Mapping des annonces
Cette ligne de code définit un mapping imbriqué en Solidity, qui organise et stocke les annonces pour la marketplace NFT. Le mapping externe utilise l'adresse du contrat NFT comme clé, permettant au smart contract de catégoriser les annonces par leurs collections NFT respectives. Cela garantit que la marketplace peut prendre en charge plusieurs collections NFT.
Le mapping interne utilise l'ID du token (un identifiant unique pour chaque NFT) comme clé, permettant une gestion efficace de plusieurs annonces sous un seul contrat NFT.
mapping(address => mapping(uint256 => Listing)) public listings;
Mise en vente de NFT
La fonction listNFT permet aux utilisateurs de mettre en vente leurs NFT sur la marketplace. Elle prend trois paramètres : le contrat NFT, l'ID du token et le prix. Le contrat NFT désigne l'adresse de la collection depuis laquelle l'utilisateur souhaite mettre en vente un NFT, l'ID du token est l'identifiant unique du NFT au sein de cette collection, et le prix est le montant auquel l'utilisateur souhaite vendre le NFT.
La fonction vérifie d'abord que l'utilisateur est bien le propriétaire du NFT et s'assure que le prix est supérieur à zéro. Une fois ces vérifications effectuées, le NFT est transféré du vendeur vers la marketplace, le mapping des annonces est mis à jour, et l'événement Listed est émis avec les paramètres corrects.
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);
}
Achat de NFT
La fonction buyNFT permet aux utilisateurs d'acheter un NFT mis en vente sur la marketplace. Elle prend deux paramètres : nftContract, l'adresse du smart contract du NFT, et tokenId, l'identifiant unique du NFT en cours d'achat.
La fonction vérifie d'abord que l'acheteur a envoyé suffisamment d'Ether pour couvrir le prix du NFT. Une fois validé, l'annonce est supprimée du mapping des annonces, le paiement est transféré de manière sécurisée au vendeur, et le NFT est transféré du contrat de la marketplace vers l'adresse de l'acheteur, finalisant ainsi l'achat.
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);
}
Achat de plusieurs NFT
La fonction buyMultipleNFT permet à un utilisateur d'acheter plusieurs NFT en une seule transaction. Elle calcule d'abord le prix total en additionnant les prix de tous les NFT listés dans le tableau tokenIds. La fonction vérifie ensuite que l'acheteur a envoyé suffisamment de fonds pour couvrir le montant total.
Une fois les fonds validés, la fonction itère à travers le tableau tokenIds, appelant la fonction buyNFT pour chaque NFT. Cela garantit que chaque NFT est acheté individuellement, que le paiement est transféré au vendeur et que les NFT sont envoyés à l'acheteur — en suivant le même processus que la fonction 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]);
}
}
Retrait de la vente de NFT
La fonction delistNFT permet à un vendeur de retirer son NFT de la marketplace. Elle vérifie d'abord que le NFT est bien en vente et que l'appelant en est le propriétaire légitime. Une fois validé, l'annonce est supprimée de la marketplace, retirant effectivement le NFT de la vente.
La fonction transfère ensuite le NFT au vendeur, lui garantissant de récupérer la propriété de son actif. Enfin, l'événement Delisted est émis pour confirmer que le NFT a été retiré avec succès de la 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);
}
Événements clés
Le contrat émet des événements pour les actions essentielles de la marketplace :
Listed : Déclenché lorsqu'un NFT est mis en vente, enregistrant des détails tels que le vendeur, le prix et les informations du NFT.
Purchase : Émis lorsqu'un acheteur acquiert un NFT, garantissant la traçabilité des transactions.
Delisted : Indique qu'un NFT a été retiré avec succès de la marketplace, assurant la transparence.
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
Présentation de la suite de tests
Créez un fichier NFT.sol dans le dossier src et ajoutez le code ci-dessous au fichier.
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;
}
}
Créez un fichier market.t.sol dans le dossier test et ajoutez le code ci-dessous au fichier.
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));
}
}
La suite de tests ci-dessus garantit que le contrat de la marketplace NFT fonctionne comme prévu dans l'ensemble de ses fonctionnalités principales. Voici une présentation détaillée des tests :
Configuration de l'environnement de test
La fonction setUp initialise l'environnement en déployant les contrats NFTMarketplace et NFT. Elle crée trois NFT pour user1 à des fins de test. Un contrat NFT fictif (NFT) est déployé avec une fonction mint pour générer des NFT, tandis que le contrat de la marketplace (NFTMarketplace) est instancié. User1 possède tous les NFT créés, permettant de tester les fonctionnalités de mise en vente et de retrait de la vente.
Test de la mise en vente de NFT
La fonction test_Listing vérifie que user1 peut mettre en vente un NFT sur la marketplace.
User1 autorise la marketplace à transférer le tokenId 0.
User1 met en vente le NFT à un prix de 1 Ether.
Le test confirme que la fonction listNFT fonctionne correctement et que le NFT est transféré vers la marketplace.
Test d'achat d'un seul NFT
La fonction test_BuyingNFT simule l'achat d'un NFT mis en vente par user2.
User1 met en vente le tokenId 0 pour 1 Ether.
User2 reçoit 10 Ether et appelle la fonction buyNFT, en envoyant 1 Ether pour acheter le NFT.
Après l'achat, le test vérifie que user2 est le nouveau propriétaire du tokenId 0.
Test d'achat de plusieurs NFT
La fonction test_BuyingMultipleNFT teste la capacité d'acheter plusieurs NFT en une seule transaction.
User1 met en vente trois NFT (tokenId 0, 1, 2) à 1 Ether chacun.
User2 appelle buyMultipleNFT, en passant les IDs des tokens et en envoyant 3 Ether.
Le test s'assure que user2 devient propriétaire des trois NFT et que la marketplace traite correctement les achats multiples.
Test du retrait de la vente de NFT
La fonction test_DelistingNFT vérifie que user1 peut retirer son NFT de la marketplace.
User1 met en vente le tokenId 0 pour 1 Ether et appelle delistNFT.
Le NFT est renvoyé à user1, et le test confirme que la propriété a été restituée à user1.
Exécuter les tests
forge testDéploiement et vérification sur Linea
Ajoutez les variables requises au fichier .env, en vous assurant qu'il se trouve à la racine du projet :
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Ajoutez les détails ci-dessous à foundry.toml
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Créez un Makefile et ajoutez les détails ci-dessous :
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
Déploiement
make deployVérification
make verifyConclusion
Construire une marketplace NFT sur Linea offre aux développeurs une expérience pratique en développement de smart contracts, en programmation Solidity et en interactions web3. Dans ce guide, nous avons couvert les fonctionnalités principales d'une marketplace décentralisée, notamment la mise en vente, l'achat, l'achat groupé et le retrait de la vente de NFT, tout en garantissant des transactions sécurisées et efficaces.
Nous avons également parcouru l'implémentation et les tests du smart contract avec Foundry, ainsi que le processus de déploiement et de vérification sur Linea. En suivant ce guide, vous disposez désormais des connaissances fondamentales pour construire, tester et déployer votre propre marketplace NFT.
Pour d'autres tutoriels pratiques et des analyses approfondies du développement blockchain, consultez la chaîne YouTube MetaMask Developer, et restez informé de nos dernières actualités en nous suivant sur Twitter.