NFTs sind seit dem Boom im Jahr 2021 zu einem zentralen Bestandteil des Web3-Ökosystems geworden und haben sich über Sammlerstücke hinaus zu realen Anwendungsfällen wie Mitgliederzugang, Asset-Tokenisierung, Gaming-Items und digitaler Identität weiterentwickelt.
No-Code-Plattformen wie Phosphor haben es für Creator und Entwickler einfacher denn je gemacht, NFTs über ein intuitives Creator Studio und eine API-Suite zu minten, anzupassen und zu deployen – ohne eigene NFT-Contracts von Grund auf entwickeln zu müssen. Für Entwickler, die ihr Verständnis von Solidity und web3-nativen Erfahrungen vertiefen möchten, bleibt das Erstellen eines NFT-Marktplatzes jedoch eine grundlegende Fähigkeit.
In diesem Leitfaden führen wir Sie durch den Prozess der Entwicklung und des Deployments eines einfachen, aber voll funktionsfähigen NFT-Marktplatzes auf Linea. Am Ende werden Sie das Wissen haben, um Ihren eigenen Marktplatz zu starten, und ein tieferes Verständnis von Smart-Contract-Entwicklung, dezentralen Anwendungen und Web3-Interaktionen gewonnen haben.
Voraussetzungen
Sie sollten über grundlegende Kenntnisse in Solidity verfügen.
Sie sollten Nodejs und Foundry auf Ihrem PC installiert haben.
Überblick über den Contract
Dieser NFT-Marktplatz unterstützt die folgenden Funktionen:
NFTs zum Verkauf anbieten: Nutzer können einen Preis festlegen und ihre NFTs listen, wobei die Details sicher im Smart Contract gespeichert werden.
NFTs kaufen: Käufer können gelistete NFTs durch Zahlung des erforderlichen Ethers erwerben und so eine reibungslose Transaktion gewährleisten.
NFTs im Batch kaufen: Mehrere NFTs können in einer einzigen Transaktion gekauft werden, um Gaskosten zu reduzieren.
NFTs delisten: Eigentümer können ihre NFTs aus dem Verkauf nehmen und sie in ihre Wallets zurückgeben.
Projekt-Setup
Für dieses Projekt verwenden wir das Foundry-Framework. Führen Sie nach der Installation von Foundry den folgenden Befehl aus, um ein neues Projekt zu initiieren:
forge init marketÖffnen Sie den Market-Ordner in VSCode oder Ihrem bevorzugten Code-Editor und löschen Sie scripts/counter.s.sol, src/counter.sol und test/counter.t.sol.
Alle Abhängigkeiten installieren
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitVollständiger Code des NFT-Marktplatz-Smart-Contracts
Erstellen Sie eine market.sol-Datei im src-Ordner und fügen Sie den folgenden Code in die Datei ein.
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);
}
}
Wichtige Funktionen des Codes
Listing-Struct
Dies ist essenziell für die Implementierung von Funktionen wie Listing, Kaufen und Delisting in einem dezentralen NFT-Marktplatz. Es bietet eine klare und effiziente Möglichkeit, Listings zu verwalten und stellt sicher, dass der Marktplatz für jedes Item den Verkäufer und den Preis erfasst – beides entscheidend für den Betrieb.
struct Listing {
address seller;
uint256 price;
}
Listing-Mapping
Diese Codezeile definiert ein verschachteltes Mapping in Solidity, das Listings für den NFT-Marktplatz organisiert und speichert. Das äußere Mapping verwendet die NFT-Contract-Adresse als Schlüssel, sodass der Smart Contract Listings nach ihren jeweiligen NFT-Collections kategorisieren kann. Dies stellt sicher, dass der Marktplatz mehrere NFT-Collections unterstützen kann.
Das innere Mapping verwendet die Token-ID (ein eindeutiger Bezeichner für jeden NFT) als Schlüssel und ermöglicht so eine effiziente Verwaltung mehrerer Listings unter einem einzigen NFT-Contract.
mapping(address => mapping(uint256 => Listing)) public listings;
NFT-Listing
Die listNFT-Funktion ermöglicht es Nutzern, ihre NFTs auf dem Marktplatz zum Verkauf anzubieten. Sie nimmt drei Parameter entgegen: NFT-Contract, Token-ID und Preis. Der NFT-Contract bezieht sich auf die Adresse der Collection, aus der der Nutzer einen NFT listen möchte, die Token-ID ist der eindeutige Bezeichner des NFTs innerhalb dieser Collection, und der Preis ist der Betrag, für den der Nutzer den NFT verkaufen möchte.
Die Funktion überprüft zunächst, ob der Nutzer der Eigentümer des NFTs ist, und stellt sicher, dass der Preis größer als null ist. Sobald diese Prüfungen bestanden sind, wird der NFT vom Verkäufer an den Marktplatz übertragen, das Listings-Mapping wird aktualisiert und das Listed-Event wird mit den korrekten Parametern emittiert.
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);
}
NFT kaufen
Die buyNFT-Funktion ermöglicht es Nutzern, einen auf dem Marktplatz gelisteten NFT zu kaufen. Sie nimmt zwei Parameter entgegen: nftContract, die Adresse des Smart Contracts des NFTs, und tokenId, den eindeutigen Bezeichner des zu kaufenden NFTs.
Die Funktion überprüft zunächst, ob der Käufer genug Ether gesendet hat, um den Preis des NFTs zu decken. Nach der Validierung wird das Listing aus dem Listings-Mapping entfernt, die Zahlung wird sicher an den Verkäufer übertragen und der NFT wird vom Marktplatz-Contract an die Adresse des Käufers übertragen, wodurch der Kauf abgeschlossen wird.
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);
}
Mehrere NFTs kaufen
Die buyMultipleNFT-Funktion ermöglicht es einem Nutzer, mehrere NFTs in einer einzigen Transaktion zu kaufen. Sie berechnet zunächst den Gesamtpreis, indem sie die Preise aller im tokenIds-Array gelisteten NFTs summiert. Die Funktion überprüft dann, ob der Käufer genug Mittel gesendet hat, um den Gesamtbetrag zu decken.
Sobald die Mittel validiert sind, iteriert die Funktion durch das tokenIds-Array und ruft für jeden NFT die buyNFT-Funktion auf. Dies stellt sicher, dass jeder NFT einzeln gekauft wird, die Zahlung an den Verkäufer übertragen wird und die NFTs an den Käufer gesendet werden – gemäß demselben Prozess wie bei der buyNFT-Funktion.
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]);
}
}
NFTs delisten
Die delistNFT-Funktion ermöglicht es einem Verkäufer, seinen NFT vom Marktplatz zu entfernen. Sie überprüft zunächst, ob der NFT gelistet ist und ob der Aufrufer der rechtmäßige Eigentümer ist. Nach der Validierung wird das Listing vom Marktplatz gelöscht, wodurch der NFT effektiv aus dem Verkauf entfernt wird.
Die Funktion überträgt den NFT dann zurück an den Verkäufer und stellt sicher, dass dieser die Eigentümerschaft seines Assets zurückerhält. Abschließend wird das Delisted-Event emittiert, um zu bestätigen, dass der NFT erfolgreich vom Marktplatz entfernt wurde.
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);
}
Wichtige Events
Der Contract emittiert Events für wesentliche Marktplatz-Aktionen:
Listed: Wird ausgelöst, wenn ein NFT gelistet wird, und zeichnet Details wie Verkäufer, Preis und NFT-Informationen auf.
Purchase: Wird emittiert, wenn ein Käufer einen NFT erwirbt, und gewährleistet die Nachverfolgbarkeit von Transaktionen.
Delisted: Zeigt an, dass ein NFT erfolgreich vom Marktplatz entfernt wurde, und wahrt die Transparenz.
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
Überblick über die Test-Suite
Erstellen Sie eine NFT.sol-Datei im src-Ordner und fügen Sie den folgenden Code in die Datei ein.
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;
}
}
Erstellen Sie eine market.t.sol-Datei im test-Ordner und fügen Sie den folgenden Code in die Datei ein.
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));
}
}
Die obige Test-Suite stellt sicher, dass der NFT-Marktplatz-Contract über seine Kernfunktionen hinweg wie vorgesehen funktioniert. Hier ist eine Übersicht der Tests:
Einrichten der Testumgebung
Die setUp-Funktion initialisiert die Umgebung, indem sie sowohl den NFTMarketplace- als auch den NFT-Contract deployt. Sie mintet drei NFTs an user1 für Testzwecke. Ein Mock-NFT-Contract (NFT) wird mit einer mint-Funktion zum Generieren von NFTs deployt, während der Marktplatz-Contract (NFTMarketplace) instanziiert wird. User1 besitzt alle geminteten NFTs, was das Testen von Listing- und Delisting-Funktionen ermöglicht.
NFT-Listing testen
Die test_Listing-Funktion stellt sicher, dass user1 einen NFT auf dem Marktplatz zum Verkauf listen kann.
User1 genehmigt dem Marktplatz die Übertragung von tokenId 0.
User1 listet den NFT zu einem Preis von 1 Ether.
Der Test bestätigt, dass die listNFT-Funktion korrekt funktioniert und dass der NFT an den Marktplatz übertragen wird.
Einzelnen NFT-Kauf testen
Die test_BuyingNFT-Funktion simuliert den Kauf eines gelisteten NFTs durch user2.
User1 listet tokenId 0 für 1 Ether.
User2 erhält 10 Ether und ruft die buyNFT-Funktion auf, wobei 1 Ether zum Kauf des NFTs gesendet wird.
Nach dem Kauf überprüft der Test, ob user2 der neue Eigentümer von tokenId 0 ist.
Mehrfachen NFT-Kauf testen
Die test_BuyingMultipleNFT-Funktion testet die Möglichkeit, mehrere NFTs in einer einzigen Transaktion zu kaufen.
User1 listet drei NFTs (tokenId 0, 1, 2) für je 1 Ether.
User2 ruft buyMultipleNFT auf, übergibt die Token-IDs und sendet 3 Ether.
Der Test stellt sicher, dass user2 Eigentümer aller drei NFTs wird und dass der Marktplatz mehrere Käufe korrekt verarbeitet.
NFT-Delisting testen
Die test_DelistingNFT-Funktion überprüft, ob user1 seinen NFT vom Marktplatz delisten kann.
User1 listet tokenId 0 für 1 Ether und ruft delistNFT auf.
Der NFT wird an user1 zurückgegeben, und der Test bestätigt, dass die Eigentümerschaft auf user1 zurückübertragen wurde.
Test ausführen
forge testDeployment und Verifizierung auf Linea
Fügen Sie die erforderlichen Variablen zur .env-Datei hinzu und stellen Sie sicher, dass sie sich auf der Root-Ebene des Projekts befindet:
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Fügen Sie die folgenden Details zu foundry.toml hinzu:
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Erstellen Sie ein Makefile und fügen Sie die folgenden Details hinzu:
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
Deployment
make deployVerifizierung
make verifyFazit
Der Aufbau eines NFT-Marktplatzes auf Linea bietet Entwicklern praktische Erfahrung in der Smart-Contract-Entwicklung, Solidity-Programmierung und Web3-Interaktionen. In diesem Leitfaden haben wir die Kernfunktionen eines dezentralen Marktplatzes behandelt, darunter Listing, Kaufen, Batch-Kauf und Delisting von NFTs, und dabei sichere und effiziente Transaktionen gewährleistet.
Wir haben auch die Implementierung und das Testen des Smart Contracts mit Foundry sowie den Deployment- und Verifizierungsprozess auf Linea durchgeführt. Durch das Befolgen dieses Leitfadens verfügen Sie nun über das grundlegende Wissen, um Ihren eigenen NFT-Marktplatz zu entwickeln, zu testen und zu deployen.
Für weitere praxisnahe Tutorials und tiefe Einblicke in die Blockchain-Entwicklung besuchen Sie den MetaMask Developer YouTube-Kanal und bleiben Sie mit unseren neuesten Erkenntnissen auf dem Laufenden, indem Sie uns auf Twitter folgen.