コンテンツへスキップ

LineaでNFTマーケットプレイスを構築する

Lineaで分散型NFTマーケットプレイスを構築・デプロイする方法を学びましょう。

10分で読める
LineaでNFTマーケットプレイスを構築する

NFTは2021年のブームを経てweb3エコシステムの中核を担う存在となり、コレクタブルの枠を超えて、メンバーシップアクセス、資産のトークン化、ゲームアイテム、デジタルアイデンティティといった現実世界のユースケースへと進化しています。

Phosphorのようなノーコードプラットフォームは、直感的なCreator StudioとAPIスイートを通じて、クリエイターや開発者がNFTをミント・カスタマイズ・デプロイしやすい環境を実現し、カスタムNFTコントラクトをゼロから構築する必要をなくしました。しかし、SolidityやWeb3ネイティブな体験への理解を深めたい開発者にとって、NFTマーケットプレイスの構築方法を学ぶことは、依然として基礎的なスキルです。

このガイドでは、Linea上で基本的かつ完全に機能するNFTマーケットプレイスを開発・デプロイするプロセスを順を追って解説します。最終的には、独自のマーケットプレイスを立ち上げるための知識を習得し、スマートコントラクト開発、分散型アプリケーション、web3インタラクションへの理解をより深めることができます。

前提条件

  • Solidityの基本的な理解が必要です。

  • PCにNodejsとFoundryがインストールされている必要があります。

コントラクトの概要

このNFTマーケットプレイスは、以下の機能をサポートします:

  1. NFTの出品:ユーザーは価格を設定してNFTを出品でき、詳細はスマートコントラクトに安全に保存されます。

  2. NFTの購入:購入者は必要なEtherを支払うことで出品中のNFTを購入でき、スムーズな取引が保証されます。

  3. NFTの一括購入:ガスコストを削減するため、1回のトランザクションで複数のNFTを購入できます。

  4. NFTの出品取消:オーナーはNFTを販売から取り下げ、ウォレットに返却できます。

プロジェクトのセットアップ

このプロジェクトにはFoundryフレームワークを使用します。Foundryをインストールした状態で、以下のコマンドを実行して新しいプロジェクトを開始します:

forge init market

Vscodeまたはお好みのコードエディタで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-commit

NFTマーケットプレイス スマートコントラクトの完全なコード

srcフォルダにmarket.solファイルを作成し、以下のコードをファイルに追加します。

// SPDX-License-Identifier: MIT
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;
    }
    // NFT Contract => Token ID => Listing
    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");
        _;
    }
    /**
     * @dev List an NFT for sale.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to list.
     * @param price The price of the NFT in wei.
     */
    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);
    }
    /**
     * @dev Buy a listed NFT.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to buy.
     */
    function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        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]);   
        }
    }
    /**
     * @dev Delist an NFT from the marketplace.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to delist.
     */
    function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
}
コピー

コードの主な機能

Listing構造体 分散型NFTマーケットプレイスにおける出品、購入、出品取消などの機能を実装するうえで不可欠です。各アイテムの出品者と価格(どちらもマーケットプレイスの運営に欠かせない情報)を記録しながら、出品を管理するための明確かつ効率的な手段を提供します。

struct Listing {
        address seller; // NFT seller
        uint256 price;  // NFT price
    }
コピー

Listingマッピング このコード行はSolidityのネストされたマッピングを定義し、NFTマーケットプレイスの出品を整理・保存します。外側のマッピングはNFTコントラクトアドレスをキーとして使用し、スマートコントラクトが各NFTコレクションごとに出品を分類できるようにします。これにより、マーケットプレイスが複数のNFTコレクションをサポートできます。

内側のマッピングはトークンID(各NFTの一意の識別子)をキーとして使用し、単一のNFTコントラクト配下の複数の出品を効率的に管理できます。

 // NFT Contract => Token ID => Listing
    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");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        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];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        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ファイルを作成し、以下のコードをファイルに追加します。

// SPDX-License-Identifier: MIT
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;
    }
    /// @dev Function to mint a new NFT
    function mint(address to) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId);
    }
    /// @dev Override base token URI for metadata
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    /// @dev Function to update the base token URI
    function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
        _baseTokenURI = newBaseTokenURI;
    }
    /// @dev Function to retrieve the current token ID count
    function getTokenCount() external view returns (uint256) {
        return _tokenIdCounter;
    }
}
コピー

testフォルダにmarket.t.solファイルを作成し、以下のコードをファイルに追加します。

// SPDX-License-Identifier: UNLICENSED
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 test

Lineaへのデプロイと検証

必要な変数を**.env**ファイルに追加します。このファイルがプロジェクトのルートレベルに配置されていることを確認してください:

LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
コピー

以下の詳細を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でフォローしてお確かめください。

AIによる翻訳です。エラーが含まれている可能性があります。常に情報を確認してください。

翻訳を評価する

この記事の執筆者:

  • Kingsley Okonkwo

    Kingsleyは、認定Ethereumブロックチェーン開発者であり、Consensysでテクニカルライターとして勤務し、開発者ツールを専門としています。Consensys入社以前は、GigsterやBraintrustなど複数のプラットフォームでフリーランスのバックエンド開発者として活躍していました。5年にわたるソフトウェア開発経験を持ち、JavaScript(Node.js & React)、GoLang、SQL、Dockerなどのツールに精通しています。Kingsleyは、最新のweb3技術を活用したステップバイステップのチュートリアルやハウツーガイドの作成を得意とし、他の開発者がより優れたdappを構築できるよう支援しています。現在はドバイを拠点とし、余暇にはバスケットボールやサッカーを楽しんでいます。

    すべての記事を読む