NFTは2021年のブームを経てweb3エコシステムの中核を担う存在となり、コレクタブルの枠を超えて、メンバーシップアクセス、資産のトークン化、ゲームアイテム、デジタルアイデンティティといった現実世界のユースケースへと進化しています。
Phosphorのようなノーコードプラットフォームは、直感的なCreator StudioとAPIスイートを通じて、クリエイターや開発者がNFTをミント・カスタマイズ・デプロイしやすい環境を実現し、カスタムNFTコントラクトをゼロから構築する必要をなくしました。しかし、SolidityやWeb3ネイティブな体験への理解を深めたい開発者にとって、NFTマーケットプレイスの構築方法を学ぶことは、依然として基礎的なスキルです。
このガイドでは、Linea上で基本的かつ完全に機能するNFTマーケットプレイスを開発・デプロイするプロセスを順を追って解説します。最終的には、独自のマーケットプレイスを立ち上げるための知識を習得し、スマートコントラクト開発、分散型アプリケーション、web3インタラクションへの理解をより深めることができます。
前提条件
Solidityの基本的な理解が必要です。
PCにNodejsとFoundryがインストールされている必要があります。
コントラクトの概要
このNFTマーケットプレイスは、以下の機能をサポートします:
NFTの出品:ユーザーは価格を設定してNFTを出品でき、詳細はスマートコントラクトに安全に保存されます。
NFTの購入:購入者は必要なEtherを支払うことで出品中のNFTを購入でき、スムーズな取引が保証されます。
NFTの一括購入:ガスコストを削減するため、1回のトランザクションで複数のNFTを購入できます。
NFTの出品取消:オーナーはNFTを販売から取り下げ、ウォレットに返却できます。
プロジェクトのセットアップ
このプロジェクトにはFoundryフレームワークを使用します。Foundryをインストールした状態で、以下のコマンドを実行して新しいプロジェクトを開始します:
forge init marketVscodeまたはお好みのコードエディタでmarketフォルダを開き、scripts/counter.s.sol、src/counter.sol、test/counter.t.solを削除してください。
すべての依存関係をインストール
forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commitNFTマーケットプレイス スマートコントラクトの完全なコード
srcフォルダにmarket.solファイルを作成し、以下のコードをファイルに追加します。
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);
}
}
コードの主な機能
Listing構造体
分散型NFTマーケットプレイスにおける出品、購入、出品取消などの機能を実装するうえで不可欠です。各アイテムの出品者と価格(どちらもマーケットプレイスの運営に欠かせない情報)を記録しながら、出品を管理するための明確かつ効率的な手段を提供します。
struct Listing {
address seller;
uint256 price;
}
Listingマッピング
このコード行はSolidityのネストされたマッピングを定義し、NFTマーケットプレイスの出品を整理・保存します。外側のマッピングはNFTコントラクトアドレスをキーとして使用し、スマートコントラクトが各NFTコレクションごとに出品を分類できるようにします。これにより、マーケットプレイスが複数のNFTコレクションをサポートできます。
内側のマッピングはトークンID(各NFTの一意の識別子)をキーとして使用し、単一のNFTコントラクト配下の複数の出品を効率的に管理できます。
mapping(address => mapping(uint256 => Listing)) public listings;
NFTの出品
listNFT関数は、ユーザーがマーケットプレイスにNFTを出品できるようにします。NFTコントラクト、トークンID、価格の3つのパラメータを受け取ります。NFTコントラクトはユーザーが出品したいNFTのコレクションアドレスを指し、トークンIDはそのコレクション内でのNFTの一意の識別子、価格はユーザーがNFTを売却したい金額です。
この関数はまず、ユーザーがNFTのオーナーであることを検証し、価格がゼロより大きいことを確認します。これらのチェックが通過すると、NFTは出品者からマーケットプレイスに転送され、listingsマッピングが更新され、正しいパラメータとともに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);
}
NFTの購入
buyNFT関数は、マーケットプレイスに出品されたNFTをユーザーが購入できるようにします。nftContract(NFTのスマートコントラクトアドレス)とtokenId(購入するNFTの一意の識別子)の2つのパラメータを受け取ります。
この関数はまず、購入者がNFTの価格をカバーするのに十分なEtherを送付したことを確認します。検証後、出品はlistingsマッピングから削除され、支払いは安全に出品者に転送され、NFTはマーケットプレイスコントラクトから購入者のアドレスに転送されて購入が完了します。
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);
}
複数NFTの購入
buyMultipleNFT関数は、ユーザーが1回のトランザクションで複数のNFTを購入できるようにします。まずtokenIds配列に列挙されたすべてのNFTの価格を合計して総額を計算します。次に、購入者が総額をカバーするのに十分な資金を送付したことを確認します。
資金の検証後、関数はtokenIds配列を反復処理し、各NFTに対してbuyNFT関数を呼び出します。これにより、各NFTが個別に購入され、支払いが出品者に転送され、NFTが購入者に送付されます。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]);
}
}
NFTの出品取消
delistNFT関数は、出品者がマーケットプレイスからNFTを取り下げられるようにします。まずNFTが出品中であること、および呼び出し元が正当なオーナーであることを確認します。検証後、出品はマーケットプレイスから削除され、NFTは実質的に販売から取り下げられます。
次に、NFTは出品者に返却され、資産のオーナーシップが回復します。最後に、Delistedイベントが発行され、NFTがマーケットプレイスから正常に削除されたことが確認されます。
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);
}
主なイベント
コントラクトはマーケットプレイスの重要なアクションに対してイベントを発行します:
Listed:NFTが出品されたときにトリガーされ、出品者、価格、NFT情報などの詳細を記録します。
Purchase:購入者がNFTを購入したときに発行され、トランザクションの追跡可能性を確保します。
Delisted:NFTがマーケットプレイスから正常に削除されたことを示し、透明性を維持します。
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
テストスイートの概要
srcフォルダにNFT.solファイルを作成し、以下のコードをファイルに追加します。
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;
}
}
testフォルダにmarket.t.solファイルを作成し、以下のコードをファイルに追加します。
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));
}
}
上記のテストスイートは、NFTマーケットプレイスコントラクトがコア機能全体にわたって意図通りに動作することを確認します。各テストの内容は以下の通りです:
テスト環境のセットアップ
setUp関数は、NFTMarketplaceコントラクトとNFTコントラクトの両方をデプロイすることで環境を初期化します。テスト用にuser1へ3つのNFTをミントします。モックNFTコントラクト(NFT)はNFTを生成するmint関数とともにデプロイされ、マーケットプレイスコントラクト(NFTMarketplace)がインスタンス化されます。User1はミントされたすべてのNFTを所有しており、出品および出品取消機能のテストが可能です。
NFT出品のテスト
test_Listing関数は、user1がマーケットプレイスにNFTを出品できることを確認します。
User1はマーケットプレイスにtokenId 0の転送を承認します。
User1はNFTを1 Etherの価格で出品します。
テストはlistNFT関数が正しく動作し、NFTがマーケットプレイスに転送されることを確認します。
単一NFT購入のテスト
test_BuyingNFT関数は、user2による出品NFTの購入をシミュレートします。
User1はtokenId 0を1 Etherで出品します。
User2は10 Etherを受け取り、buyNFT関数を呼び出して1 EtherでNFTを購入します。
購入後、テストはuser2がtokenId 0の新しいオーナーであることを確認します。
複数NFT購入のテスト
test_BuyingMultipleNFT関数は、1回のトランザクションで複数のNFTを購入する機能をテストします。
User1は3つのNFT(tokenId 0, 1, 2)をそれぞれ1 Etherで出品します。
User2はbuyMultipleNFTを呼び出し、トークンIDを渡して3 Etherを送付します。
テストはuser2が3つすべてのNFTのオーナーになること、およびマーケットプレイスが複数の購入を正しく処理することを確認します。
NFT出品取消のテスト
test_DelistingNFT関数は、user1がマーケットプレイスからNFTを取り下げられることを確認します。
User1はtokenId 0を1 Etherで出品し、delistNFTを呼び出します。
NFTはuser1に返却され、テストはオーナーシップがuser1に戻ったことを確認します。
テストの実行
forge testLineaへのデプロイと検証
必要な変数を**.env**ファイルに追加します。このファイルがプロジェクトのルートレベルに配置されていることを確認してください:
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
以下の詳細をfoundry.tomlに追加します:
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Makefileを作成し、以下の詳細を追加します:
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
デプロイ
make deploy検証
make verifyまとめ
Linea上でNFTマーケットプレイスを構築することで、開発者はスマートコントラクト開発、Solidityプログラミング、web3インタラクションの実践的な経験を積むことができます。このガイドでは、安全かつ効率的なトランザクションを確保しながら、NFTの出品、購入、一括購入、出品取消を含む分散型マーケットプレイスのコア機能を解説しました。
また、Foundryを使用したスマートコントラクトの実装とテスト、およびLinea上でのデプロイと検証プロセスについても説明しました。このガイドに従うことで、独自のNFTマーケットプレイスを構築、テスト、デプロイするための基礎知識を習得できました。
ブロックチェーン開発に関するより実践的なチュートリアルや詳細な解説については、MetaMask Developer YouTubeチャンネルをご覧ください。また、最新の情報はTwitterでフォローしてお確かめください。