From cdf62713b75795ec011ac3a3ab6a05fccc85f011 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Tue, 9 Jul 2024 11:38:28 +0200 Subject: [PATCH 1/8] add tutorial part 1 Signed-off-by: salaheldinsoliman --- config/tutorials.js | 20 + .../cross-chain-nft-marketplace-part-1.md | 572 ++++++++++++++++++ .../Architecture-V1.png | Bin 0 -> 18291 bytes .../Architecture-V2.png | Bin 0 -> 25446 bytes .../Architecture-V3.png | Bin 0 -> 38365 bytes 5 files changed, 592 insertions(+) create mode 100644 docs/tutorials/cross-chain-nft-marketplace-part-1.md create mode 100644 static/img/tutorials/cross_chain_marketplace/Architecture-V1.png create mode 100644 static/img/tutorials/cross_chain_marketplace/Architecture-V2.png create mode 100644 static/img/tutorials/cross_chain_marketplace/Architecture-V3.png diff --git a/config/tutorials.js b/config/tutorials.js index 8bd51c17773..6804ec63b1b 100644 --- a/config/tutorials.js +++ b/config/tutorials.js @@ -249,4 +249,24 @@ module.exports = [ ], }, ], + [ + pluginTutorial, + { + title: + 'Tutorial - Create a cross-chain NFT market place on ShimmerEVM using Hardhat, Layer0 bridge and IOTA L1', + description: + 'This tutorial is meant to showcase NFT cross-chain functionlity on ISC network', + preview: '/img/tutorials/shimmerevm-hardhat.jpg', + route: 'tutorials/cross-chain-nft-marketplace-part-1', + tags: [ + 'text', + 'shimmer', + 'solidity', + 'shimmerevm', + 'hardhat', + 'iscp', + 'video', + ], + }, + ], ]; diff --git a/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/tutorials/cross-chain-nft-marketplace-part-1.md new file mode 100644 index 00000000000..729df9e18ef --- /dev/null +++ b/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -0,0 +1,572 @@ +# Cross-chain NFT Marketplace: Part I + +In this series of tutorials, we will be building a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on ShimmerEVM Testnet, BNB Testnet, and Shimmer Testnet. + +Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. +In part II, we will bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the marketplace. +Finally, in part III, we will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain by making the contract handle cross-chain transactions. + +The architecture of the marketplace will evolve as we progress through the tutorials. In part I, we will start with this very simple architecture: +![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) + +Then in Part II, we will add the contracts and scripts to manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: +![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) + +Finally, After deploying another instance of the marketplace on the BNB Testnet in Part III, where the contract will handle cross-chain transactions, the architecture will look like this: +![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) + +This enables a user, e.g on BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without needing to switch networks. + + + +## Prerequisites + +- [Node.js v18](https://hardhat.org/tutorial/setting-up-the-environment#installing-node.js) and above supported. + +- [npx](https://www.npmjs.com/package/npx) v7.1.0 and above supported. + +## Set Up + +clone the [tutorial repository](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace) and navigate to the project folder, then run: + +```bash +npm install +``` + + +## Contracts + +For the scope of this part, we wil need two contracts: an NFTMarketplace contract, and an NFT ERC721-compatible contract. +Create a `contracts` folder in the root of the project and add the following files under it: + +### NFTMarketplace.sol + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price); +error ItemNotForSale(address nftAddress, uint256 tokenId); +error NotListed(address nftAddress, uint256 tokenId); +error AlreadyListed(address nftAddress, uint256 tokenId); +error NoProceeds(); +error NotOwner(); +error NotApprovedForMarketplace(); +error PriceMustBeAboveZero(); + +contract NFTMarketPlace is ReentrancyGuard { + struct Listing { + uint256 price; + address seller; + } + + event ItemListed( + address indexed seller, + address indexed nftAddress, + uint256 indexed tokenId, + uint256 price + ); + + event ItemCanceled( + address indexed seller, + address indexed nftAddress, + uint256 indexed tokenId + ); + + event ItemBought( + address indexed buyer, + address indexed nftAddress, + uint256 indexed tokenId, + uint256 price + ); + + mapping(address => mapping(uint256 => Listing)) private s_listings; + mapping(address => uint256) private s_proceeds; + + modifier notListed( + address nftAddress, + uint256 tokenId + ) { + Listing memory listing = s_listings[nftAddress][tokenId]; + if (listing.price > 0) { + revert AlreadyListed(nftAddress, tokenId); + } + _; + } + + modifier isListed(address nftAddress, uint256 tokenId) { + Listing memory listing = s_listings[nftAddress][tokenId]; + if (listing.price <= 0) { + revert NotListed(nftAddress, tokenId); + } + _; + } + + modifier isOwner( + address nftAddress, + uint256 tokenId, + address spender + ) { + IERC721 nft = IERC721(nftAddress); + address owner = nft.ownerOf(tokenId); + if (spender != owner) { + revert NotOwner(); + } + _; + } + + ///////////////////// + // Main Functions // + ///////////////////// + /* + * @notice Method for listing NFT + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + * @param price sale price for each item + */ + function listItem( + address nftAddress, + uint256 tokenId, + uint256 price + ) + external + notListed(nftAddress, tokenId) + isOwner(nftAddress, tokenId, msg.sender) + { + if (price <= 0) { + revert PriceMustBeAboveZero(); + } + IERC721 nft = IERC721(nftAddress); + if (nft.getApproved(tokenId) != address(this)) { + revert NotApprovedForMarketplace(); + } + s_listings[nftAddress][tokenId] = Listing(price, msg.sender); + emit ItemListed(msg.sender, nftAddress, tokenId, price); + } + + /* + * @notice Method for cancelling listing + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + */ + function cancelListing(address nftAddress, uint256 tokenId) + external + isOwner(nftAddress, tokenId, msg.sender) + isListed(nftAddress, tokenId) + { + delete (s_listings[nftAddress][tokenId]); + emit ItemCanceled(msg.sender, nftAddress, tokenId); + } + + /* + * @notice Method for buying listing + * @notice The owner of an NFT could unapprove the marketplace, + * which would cause this function to fail + * Ideally you'd also have a `createOffer` functionality. + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + */ + function buyItem(address nftAddress, uint256 tokenId) + external + payable + isListed(nftAddress, tokenId) + // isNotOwner(nftAddress, tokenId, msg.sender) + nonReentrant + { + // Challenge - How would you refactor this contract to take: + // 1. Abitrary tokens as payment? (HINT - Chainlink Price Feeds!) + // 2. Be able to set prices in other currencies? + // 3. Tweet me @PatrickAlphaC if you come up with a solution! + Listing memory listedItem = s_listings[nftAddress][tokenId]; + if (msg.value < listedItem.price) { + revert PriceNotMet(nftAddress, tokenId, listedItem.price); + } + s_proceeds[listedItem.seller] += msg.value; + // Could just send the money... + // https://fravoll.github.io/solidity-patterns/pull_over_push.html + delete (s_listings[nftAddress][tokenId]); + IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId); + emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); + } + + /* + * @notice Method for updating listing + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + * @param newPrice Price in Wei of the item + */ + function updateListing( + address nftAddress, + uint256 tokenId, + uint256 newPrice + ) + external + isListed(nftAddress, tokenId) + nonReentrant + isOwner(nftAddress, tokenId, msg.sender) + { + //We should check the value of `newPrice` and revert if it's below zero (like we also check in `listItem()`) + if (newPrice <= 0) { + revert PriceMustBeAboveZero(); + } + s_listings[nftAddress][tokenId].price = newPrice; + emit ItemListed(msg.sender, nftAddress, tokenId, newPrice); + } + + /* + * @notice Method for withdrawing proceeds from sales + */ + function withdrawProceeds() external { + uint256 proceeds = s_proceeds[msg.sender]; + if (proceeds <= 0) { + revert NoProceeds(); + } + s_proceeds[msg.sender] = 0; + (bool success, ) = payable(msg.sender).call{value: proceeds}(""); + require(success, "Transfer failed"); + } + + ///////////////////// + // Getter Functions // + ///////////////////// + + function getListing(address nftAddress, uint256 tokenId) + external + view + returns (Listing memory) + { + return s_listings[nftAddress][tokenId]; + } + + function getProceeds(address seller) external view returns (uint256) { + return s_proceeds[seller]; + } +} +``` + +### MyERC721.sol + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyERC721 is ERC721Enumerable, Ownable { + string public _baseTokenURI; + uint32 public _tokenId = 0; + + event EventSetBaseURI(string baseURI); + + constructor( + string memory _name, + string memory _symbol, + string memory _baseTokenLink + ) ERC721(_name, _symbol) { + _baseTokenURI = _baseTokenLink; + } + + function _baseURI() internal view virtual override returns (string memory) { + return _baseTokenURI; + } + + function setBaseURI(string memory baseURI) external onlyOwner { + _baseTokenURI = baseURI; + + emit EventSetBaseURI(baseURI); + } + + function mint() external onlyOwner { + _safeMint(msg.sender, _tokenId, ""); + _tokenId++; + } + + function transfer(address to, uint tokenId) external { + _safeTransfer(msg.sender, to, tokenId, ""); + } + + function isApprovedOrOwner(address spender, uint tokenId) external view virtual returns (bool) { + return _isApprovedOrOwner(spender, tokenId); + } +} +``` + +after adding the contracts, compile them by running: + +```bash +npx hardhat compile +``` + + +## Scripts + +First, create a `scripts` folder in the root of the project and add the following files under it: + +### deploy_marketplace_shimmer.js +First, let's deploy the NFTMarketplace contract to the ShimmerEVM Testnet by running the following script: +```javascript +const fs = require('fs'); +const path = require('path'); + +async function main() { + const NFTMarketplace = await ethers.getContractFactory("NFTMarketPlace"); + const marketplace = await NFTMarketplace.deploy(); + + const marketplaceAddress = await marketplace.getAddress(); + + console.log("NFTMarketPlace deployed to:", marketplaceAddress); + + const addressDirectory = path.join(__dirname, 'addresses'); + fs.mkdirSync(addressDirectory, { recursive: true }); // Ensure the directory exists, create it if it doesn't + + const filePath = path.join(addressDirectory, 'NFTMarketplace.txt'); + fs.writeFileSync(filePath, marketplaceAddress); + + console.log(`Contract address written to ${filePath}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. +run it by executing: + +```bash +npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet +``` + +### deploy_er721_shimmer.js + +```javascript +const fs = require('fs'); +const path = require('path'); + +async function main() { + const MyERC721 = await ethers.getContractFactory("MyERC721"); + const myERC721 = await MyERC721.deploy("SampleToken", "SESA", "SampleTokenURI"); + + const myERC721Address = await myERC721.getAddress(); + + console.log("MyERC721 deployed to:", myERC721Address); + + const addressDirectory = path.join(__dirname, 'addresses'); + fs.mkdirSync(addressDirectory, { recursive: true }); // Ensure the directory exists, create it if it doesn't + + const filePath = path.join(addressDirectory, 'MyERC721.txt'); + fs.writeFileSync(filePath, myERC721Address); + + console.log(`Contract address written to ${filePath}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +This will deploy the MyERC721 contract to the ShimmerEVM Testnet and save the contract address to a file. +run it by executing: + +```bash +npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet +``` + +### mint_nft.js + +After deploying the MyERC721 contract, let's mint an NFT by running the following script: + +```javascript +const fs = require('fs'); +const path = require('path'); + +async function createNFT(myERC721Address) { + + const MyERC721 = await ethers.getContractFactory("MyERC721"); + const myERC721 = MyERC721.attach(myERC721Address); + + const tx = await myERC721.mint(); + await tx.wait(); // Wait for the transaction to be mined +} + +async function main() { + // Read the contract address from the file + const addressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); + const myERC721Address = fs.readFileSync(addressPath, 'utf8').trim(); + + await createNFT(myERC721Address); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +execute the script by running: + +```bash +npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet +``` + + +### approve_myERC721_for_marketplace.js + +To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. +```javascript +const fs = require('fs'); +const path = require('path'); +const { ethers } = require('hardhat'); + +async function approveNFTTransfer(marketplaceAddress, myERC721Address, tokenId) { + // Attach to the deployed MyERC721 contract + const MyERC721 = await ethers.getContractFactory("MyERC721"); + const myERC721 = await MyERC721.attach(myERC721Address); + + // Approve the marketplace to transfer the NFT + const tx = await myERC721.approve(marketplaceAddress, tokenId); + await tx.wait(); // Wait for the transaction to be mined + + console.log(`Approved marketplace at address ${marketplaceAddress} to transfer tokenId ${tokenId}`); +} + +async function main() { + // Load the marketplace address + const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); + const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); + + // Load the MyERC721 contract address + const myERC721AddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); + const myERC721Address = fs.readFileSync(myERC721AddressPath, 'utf8').trim(); + + // Specify the tokenId you want to approve for transfer + const tokenId = 0; // Example token ID, change this to the actual token ID you want to approve + + await approveNFTTransfer(marketplaceAddress, myERC721Address, tokenId); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +execute the script by running: + +```bash +npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet +``` + +### create_listing.js + +After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: + +```javascript +const fs = require('fs'); +const path = require('path'); +const { ethers } = require('hardhat'); + +async function createListing(marketplaceAddress, myERC721Address, tokenId, price) { + // Attach to the deployed MyERC721 contract + const NFTMarketPlace = await ethers.getContractFactory("NFTMarketPlace"); + const marketplace = await NFTMarketPlace.attach(marketplaceAddress); + + // Approve the marketplace to transfer the NFT + const tx = await marketplace.listItem(myERC721Address, tokenId, price); + await tx.wait(); // Wait for the transaction to be mined + + console.log(`Created listing for tokenId ${tokenId} with price ${price}`); +} + +async function main() { + // Load the marketplace address + const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); + const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); + + // Load the MyERC721 contract address + const myERC721AddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); + const myERC721Address = fs.readFileSync(myERC721AddressPath, 'utf8').trim(); + + // Specify the tokenId you want to approve for transfer + const tokenId = 0; // Example token ID, change this to the actual token ID you want to approve + const price = 1; + + await createListing(marketplaceAddress, myERC721Address, tokenId, price); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +execute the script by running: + +```bash +npx hardhat run scripts/create_listing.js --network shimmerevm-testnet +``` + + +### buy_item.js + +Finally, let's buy the NFT by running the following script: + +```javascript +const fs = require('fs'); +const path = require('path'); +const { ethers } = require('hardhat'); + +async function buyItem(marketplaceAddress, nftAddress, tokenId) { + // Attach to the deployed NFTMarketPlace contract + const NFTMarketPlace = await ethers.getContractFactory("NFTMarketPlace"); + const marketplace = await NFTMarketPlace.attach(marketplaceAddress); + + // Call the buyItem function + const tx = await marketplace.buyItem(nftAddress, tokenId, { value: 1 }); // Assuming 1 ETH as payment, adjust accordingly + await tx.wait(); // Wait for the transaction to be mined + + console.log(`Bought item with tokenId ${tokenId} from ${nftAddress}`); +} + +async function main() { + // Load the marketplace address + const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); + const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); + + // Load the NFT contract address (assuming you're buying an NFT from MyERC721 contract) + const nftAddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); + const nftAddress = fs.readFileSync(nftAddressPath, 'utf8').trim(); + + // Specify the tokenId of the NFT you want to buy + const tokenId = 0; // Example token ID, change this to the actual token ID you want to buy + + await buyItem(marketplaceAddress, nftAddress, tokenId); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +``` +execute the script by running: + +```bash +npx hardhat run scripts/buy_item.js --network shimmerevm-testnet +``` + + +## Conclusion +In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file diff --git a/static/img/tutorials/cross_chain_marketplace/Architecture-V1.png b/static/img/tutorials/cross_chain_marketplace/Architecture-V1.png new file mode 100644 index 0000000000000000000000000000000000000000..89b5bc3f01fa83d97843467bf6af911b0cbb5098 GIT binary patch literal 18291 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 V*w_PwY#s#Y-7&gdar40+{}0sdHIM)R literal 0 HcmV?d00001 diff --git a/static/img/tutorials/cross_chain_marketplace/Architecture-V2.png b/static/img/tutorials/cross_chain_marketplace/Architecture-V2.png new file mode 100644 index 0000000000000000000000000000000000000000..7aacf680191b73edfd9c1fcdcfd22d634507e32b GIT binary patch literal 25446 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 B1=j!o literal 0 HcmV?d00001 diff --git a/static/img/tutorials/cross_chain_marketplace/Architecture-V3.png b/static/img/tutorials/cross_chain_marketplace/Architecture-V3.png new file mode 100644 index 0000000000000000000000000000000000000000..13e4b420b2379d7da2d662e632e0ac49455e5d93 GIT binary patch literal 38365 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 Rgt)jAAE@0gxM%+I{{ReU0CxZY literal 0 HcmV?d00001 From 4ea839fd756e6c0674265d0238ebd6ff213afeba Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 13:13:31 +0200 Subject: [PATCH 2/8] explain contracts and use code refrences Signed-off-by: salaheldinsoliman --- .../cross-chain-nft-marketplace-part-1.md | 328 +++++------------- 1 file changed, 94 insertions(+), 234 deletions(-) diff --git a/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/tutorials/cross-chain-nft-marketplace-part-1.md index 729df9e18ef..c672b529717 100644 --- a/docs/tutorials/cross-chain-nft-marketplace-part-1.md +++ b/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -1,126 +1,96 @@ # Cross-chain NFT Marketplace: Part I -In this series of tutorials, we will be building a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on ShimmerEVM Testnet, BNB Testnet, and Shimmer Testnet. +This is the first part of a three-part series that will guide you as you build a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on the ShimmerEVM Testnet and BNB Testnet. Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. -In part II, we will bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the marketplace. -Finally, in part III, we will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain by making the contract handle cross-chain transactions. +The second part of the series will focus on bridging NFTs from another EVM network, BNB Testnet, to the ShimmerEVM Testnet and listing them on the marketplace you created in part I. -The architecture of the marketplace will evolve as we progress through the tutorials. In part I, we will start with this very simple architecture: -![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) +Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. -Then in Part II, we will add the contracts and scripts to manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: -![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) +## Marketplace Architecture Overview +The architecture of the marketplace will evolve as we progress through the tutorials. +### Part I +In part I, we will start with this very simple architecture: +![Cross Chain MarketPlace V1](../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) -Finally, After deploying another instance of the marketplace on the BNB Testnet in Part III, where the contract will handle cross-chain transactions, the architecture will look like this: -![alt text](../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) +### Part II +In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: +![Cross Chain MarketPlace V2](../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) -This enables a user, e.g on BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without needing to switch networks. +### Part III +Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. +This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. +The architecture will look like this: +![Cross Chain MarketPlace V3](../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) ## Prerequisites -- [Node.js v18](https://hardhat.org/tutorial/setting-up-the-environment#installing-node.js) and above supported. - -- [npx](https://www.npmjs.com/package/npx) v7.1.0 and above supported. +- [Node.js](https://nodejs.org) >= v18.0 +- [Hardhat](https://hardhat.org) >= v2.0.0 +- [npx](https://www.npmjs.com/package/npx) >= v7.1.0. ## Set Up -clone the [tutorial repository](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace) and navigate to the project folder, then run: +First, create a new directory for the project and navigate into it: + +```bash +mkdir cross-chain-nft-marketplace +cd cross-chain-nft-marketplace +``` + +Then [bootsrap a new Hardhat project](https://hardhat.org/tutorial/creating-a-new-hardhat-project), by running: ```bash -npm install +npx hardhat init +``` + +## Configuration + +In the `hardhat.config.js` file, update the `networks` object to include the ShimmerEVM Testnet network configuration, as well as the BNB Testnet network configuration. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js ``` + + + ## Contracts -For the scope of this part, we wil need two contracts: an NFTMarketplace contract, and an NFT ERC721-compatible contract. +In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. + Create a `contracts` folder in the root of the project and add the following files under it: ### NFTMarketplace.sol -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.7; - -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; - -error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price); -error ItemNotForSale(address nftAddress, uint256 tokenId); -error NotListed(address nftAddress, uint256 tokenId); -error AlreadyListed(address nftAddress, uint256 tokenId); -error NoProceeds(); -error NotOwner(); -error NotApprovedForMarketplace(); -error PriceMustBeAboveZero(); - -contract NFTMarketPlace is ReentrancyGuard { - struct Listing { - uint256 price; - address seller; - } +The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. - event ItemListed( - address indexed seller, - address indexed nftAddress, - uint256 indexed tokenId, - uint256 price - ); - - event ItemCanceled( - address indexed seller, - address indexed nftAddress, - uint256 indexed tokenId - ); - - event ItemBought( - address indexed buyer, - address indexed nftAddress, - uint256 indexed tokenId, - uint256 price - ); +The main data structures and functions in the contract are: - mapping(address => mapping(uint256 => Listing)) private s_listings; - mapping(address => uint256) private s_proceeds; +- `struct Listing` +Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. +```solidity +struct Listing { + address seller; + uint256 price; +} +``` - modifier notListed( - address nftAddress, - uint256 tokenId - ) { - Listing memory listing = s_listings[nftAddress][tokenId]; - if (listing.price > 0) { - revert AlreadyListed(nftAddress, tokenId); - } - _; - } - modifier isListed(address nftAddress, uint256 tokenId) { - Listing memory listing = s_listings[nftAddress][tokenId]; - if (listing.price <= 0) { - revert NotListed(nftAddress, tokenId); - } - _; - } +- `mapping s_listings` +Maps the token contract address to a mappring of token ID to `Listing`. +```solidity +mapping(address => mapping(uint256 => Listing)) private s_listings; +``` - modifier isOwner( - address nftAddress, - uint256 tokenId, - address spender - ) { - IERC721 nft = IERC721(nftAddress); - address owner = nft.ownerOf(tokenId); - if (spender != owner) { - revert NotOwner(); - } - _; - } - ///////////////////// - // Main Functions // - ///////////////////// +- `function listItem` +Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. + +```solidity /* * @notice Method for listing NFT * @param nftAddress Address of NFT contract @@ -146,21 +116,15 @@ contract NFTMarketPlace is ReentrancyGuard { s_listings[nftAddress][tokenId] = Listing(price, msg.sender); emit ItemListed(msg.sender, nftAddress, tokenId, price); } +``` - /* - * @notice Method for cancelling listing - * @param nftAddress Address of NFT contract - * @param tokenId Token ID of NFT - */ - function cancelListing(address nftAddress, uint256 tokenId) - external - isOwner(nftAddress, tokenId, msg.sender) - isListed(nftAddress, tokenId) - { - delete (s_listings[nftAddress][tokenId]); - emit ItemCanceled(msg.sender, nftAddress, tokenId); - } + + +- `function buyItem` +This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. + +```solidity /* * @notice Method for buying listing * @notice The owner of an NFT could unapprove the marketplace, @@ -192,63 +156,35 @@ contract NFTMarketPlace is ReentrancyGuard { emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); } - /* - * @notice Method for updating listing - * @param nftAddress Address of NFT contract - * @param tokenId Token ID of NFT - * @param newPrice Price in Wei of the item - */ - function updateListing( - address nftAddress, - uint256 tokenId, - uint256 newPrice - ) - external - isListed(nftAddress, tokenId) - nonReentrant - isOwner(nftAddress, tokenId, msg.sender) - { - //We should check the value of `newPrice` and revert if it's below zero (like we also check in `listItem()`) - if (newPrice <= 0) { - revert PriceMustBeAboveZero(); - } - s_listings[nftAddress][tokenId].price = newPrice; - emit ItemListed(msg.sender, nftAddress, tokenId, newPrice); - } +``` - /* - * @notice Method for withdrawing proceeds from sales - */ - function withdrawProceeds() external { - uint256 proceeds = s_proceeds[msg.sender]; - if (proceeds <= 0) { - revert NoProceeds(); - } - s_proceeds[msg.sender] = 0; - (bool success, ) = payable(msg.sender).call{value: proceeds}(""); - require(success, "Transfer failed"); - } - ///////////////////// - // Getter Functions // - ///////////////////// - function getListing(address nftAddress, uint256 tokenId) + +- `function getListing` +gets an NFT listing by its address and `tokenId`. +```solidity +function getListing(address nftAddress, uint256 tokenId) external view returns (Listing memory) { return s_listings[nftAddress][tokenId]; } +``` - function getProceeds(address seller) external view returns (uint256) { - return s_proceeds[seller]; - } -} + +We have now covered all relevant parts of the contract for Part I, and how they would evolve in later steps. This is the full contract code for Part I: + + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol ``` ### MyERC721.sol +A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: + ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.22; @@ -295,7 +231,7 @@ contract MyERC721 is ERC721Enumerable, Ownable { } ``` -after adding the contracts, compile them by running: +After adding the contracts, compile them by running: ```bash npx hardhat compile @@ -307,34 +243,10 @@ npx hardhat compile First, create a `scripts` folder in the root of the project and add the following files under it: ### deploy_marketplace_shimmer.js -First, let's deploy the NFTMarketplace contract to the ShimmerEVM Testnet by running the following script: -```javascript -const fs = require('fs'); -const path = require('path'); - -async function main() { - const NFTMarketplace = await ethers.getContractFactory("NFTMarketPlace"); - const marketplace = await NFTMarketplace.deploy(); - - const marketplaceAddress = await marketplace.getAddress(); - - console.log("NFTMarketPlace deployed to:", marketplaceAddress); - - const addressDirectory = path.join(__dirname, 'addresses'); - fs.mkdirSync(addressDirectory, { recursive: true }); // Ensure the directory exists, create it if it doesn't - - const filePath = path.join(addressDirectory, 'NFTMarketplace.txt'); - fs.writeFileSync(filePath, marketplaceAddress); +The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. - console.log(`Contract address written to ${filePath}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js ``` This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. run it by executing: @@ -344,37 +256,12 @@ npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testn ``` ### deploy_er721_shimmer.js +This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. -```javascript -const fs = require('fs'); -const path = require('path'); - -async function main() { - const MyERC721 = await ethers.getContractFactory("MyERC721"); - const myERC721 = await MyERC721.deploy("SampleToken", "SESA", "SampleTokenURI"); - - const myERC721Address = await myERC721.getAddress(); - - console.log("MyERC721 deployed to:", myERC721Address); - - const addressDirectory = path.join(__dirname, 'addresses'); - fs.mkdirSync(addressDirectory, { recursive: true }); // Ensure the directory exists, create it if it doesn't - - const filePath = path.join(addressDirectory, 'MyERC721.txt'); - fs.writeFileSync(filePath, myERC721Address); - - console.log(`Contract address written to ${filePath}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js ``` -This will deploy the MyERC721 contract to the ShimmerEVM Testnet and save the contract address to a file. -run it by executing: +You can run this script with the following command: ```bash npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet @@ -382,43 +269,16 @@ npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet ### mint_nft.js -After deploying the MyERC721 contract, let's mint an NFT by running the following script: - -```javascript -const fs = require('fs'); -const path = require('path'); - -async function createNFT(myERC721Address) { - - const MyERC721 = await ethers.getContractFactory("MyERC721"); - const myERC721 = MyERC721.attach(myERC721Address); +After you have deployed the `MyERC721` contract, you are ready to mint an NFT using the following script: - const tx = await myERC721.mint(); - await tx.wait(); // Wait for the transaction to be mined -} - -async function main() { - // Read the contract address from the file - const addressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); - const myERC721Address = fs.readFileSync(addressPath, 'utf8').trim(); - - await createNFT(myERC721Address); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js ``` -execute the script by running: +You can run the script by executing the following command: ```bash npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet ``` - - ### approve_myERC721_for_marketplace.js To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. From 08949a8afde40d7a5aed8061e0d4efaae82d0645 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 13:24:13 +0200 Subject: [PATCH 3/8] explain contracts and use code refrences Signed-off-by: salaheldinsoliman --- .../cross-chain-nft-marketplace-part-1.md | 174 ++---------------- 1 file changed, 11 insertions(+), 163 deletions(-) diff --git a/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/tutorials/cross-chain-nft-marketplace-part-1.md index c672b529717..bdc5a299336 100644 --- a/docs/tutorials/cross-chain-nft-marketplace-part-1.md +++ b/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -185,50 +185,8 @@ https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab1186650 A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: -```solidity -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.22; - -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; - -contract MyERC721 is ERC721Enumerable, Ownable { - string public _baseTokenURI; - uint32 public _tokenId = 0; - - event EventSetBaseURI(string baseURI); - - constructor( - string memory _name, - string memory _symbol, - string memory _baseTokenLink - ) ERC721(_name, _symbol) { - _baseTokenURI = _baseTokenLink; - } - - function _baseURI() internal view virtual override returns (string memory) { - return _baseTokenURI; - } - - function setBaseURI(string memory baseURI) external onlyOwner { - _baseTokenURI = baseURI; - - emit EventSetBaseURI(baseURI); - } - - function mint() external onlyOwner { - _safeMint(msg.sender, _tokenId, ""); - _tokenId++; - } - - function transfer(address to, uint tokenId) external { - _safeTransfer(msg.sender, to, tokenId, ""); - } - - function isApprovedOrOwner(address spender, uint tokenId) external view virtual returns (bool) { - return _isApprovedOrOwner(spender, tokenId); - } -} +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol ``` After adding the contracts, compile them by running: @@ -282,46 +240,10 @@ npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet ### approve_myERC721_for_marketplace.js To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. -```javascript -const fs = require('fs'); -const path = require('path'); -const { ethers } = require('hardhat'); - -async function approveNFTTransfer(marketplaceAddress, myERC721Address, tokenId) { - // Attach to the deployed MyERC721 contract - const MyERC721 = await ethers.getContractFactory("MyERC721"); - const myERC721 = await MyERC721.attach(myERC721Address); - - // Approve the marketplace to transfer the NFT - const tx = await myERC721.approve(marketplaceAddress, tokenId); - await tx.wait(); // Wait for the transaction to be mined - - console.log(`Approved marketplace at address ${marketplaceAddress} to transfer tokenId ${tokenId}`); -} - -async function main() { - // Load the marketplace address - const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); - const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); - - // Load the MyERC721 contract address - const myERC721AddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); - const myERC721Address = fs.readFileSync(myERC721AddressPath, 'utf8').trim(); - - // Specify the tokenId you want to approve for transfer - const tokenId = 0; // Example token ID, change this to the actual token ID you want to approve - - await approveNFTTransfer(marketplaceAddress, myERC721Address, tokenId); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/approve_myERC721_for_marketplace.js ``` -execute the script by running: +You can run the script by executing the following command: ```bash npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet @@ -331,97 +253,23 @@ npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: -```javascript -const fs = require('fs'); -const path = require('path'); -const { ethers } = require('hardhat'); - -async function createListing(marketplaceAddress, myERC721Address, tokenId, price) { - // Attach to the deployed MyERC721 contract - const NFTMarketPlace = await ethers.getContractFactory("NFTMarketPlace"); - const marketplace = await NFTMarketPlace.attach(marketplaceAddress); - - // Approve the marketplace to transfer the NFT - const tx = await marketplace.listItem(myERC721Address, tokenId, price); - await tx.wait(); // Wait for the transaction to be mined - - console.log(`Created listing for tokenId ${tokenId} with price ${price}`); -} - -async function main() { - // Load the marketplace address - const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); - const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); - - // Load the MyERC721 contract address - const myERC721AddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); - const myERC721Address = fs.readFileSync(myERC721AddressPath, 'utf8').trim(); - - // Specify the tokenId you want to approve for transfer - const tokenId = 0; // Example token ID, change this to the actual token ID you want to approve - const price = 1; - - await createListing(marketplaceAddress, myERC721Address, tokenId, price); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/create_listing.js ``` -execute the script by running: +You can run the script by executing the following command: ```bash npx hardhat run scripts/create_listing.js --network shimmerevm-testnet ``` - ### buy_item.js Finally, let's buy the NFT by running the following script: -```javascript -const fs = require('fs'); -const path = require('path'); -const { ethers } = require('hardhat'); - -async function buyItem(marketplaceAddress, nftAddress, tokenId) { - // Attach to the deployed NFTMarketPlace contract - const NFTMarketPlace = await ethers.getContractFactory("NFTMarketPlace"); - const marketplace = await NFTMarketPlace.attach(marketplaceAddress); - - // Call the buyItem function - const tx = await marketplace.buyItem(nftAddress, tokenId, { value: 1 }); // Assuming 1 ETH as payment, adjust accordingly - await tx.wait(); // Wait for the transaction to be mined - - console.log(`Bought item with tokenId ${tokenId} from ${nftAddress}`); -} - -async function main() { - // Load the marketplace address - const marketplaceAddressPath = path.join(__dirname, 'addresses', 'NFTMarketplace.txt'); - const marketplaceAddress = fs.readFileSync(marketplaceAddressPath, 'utf8').trim(); - - // Load the NFT contract address (assuming you're buying an NFT from MyERC721 contract) - const nftAddressPath = path.join(__dirname, 'addresses', 'MyERC721.txt'); - const nftAddress = fs.readFileSync(nftAddressPath, 'utf8').trim(); - - // Specify the tokenId of the NFT you want to buy - const tokenId = 0; // Example token ID, change this to the actual token ID you want to buy - - await buyItem(marketplaceAddress, nftAddress, tokenId); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/buy_item.js ``` -execute the script by running: +You can run the script by executing the following command: ```bash npx hardhat run scripts/buy_item.js --network shimmerevm-testnet From 27c67927334cd3524666521f1881a4920d8589e0 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 14:13:32 +0200 Subject: [PATCH 4/8] move tutorial to ISC docs Signed-off-by: salaheldinsoliman --- .../cross-chain-nft-marketplace-part-1.md | 280 ++++++++++++++++++ docs/build/isc/v1.1/sidebars.js | 4 + .../cross-chain-nft-marketplace-part-1.md | 280 ++++++++++++++++++ docs/build/isc/v1.3/sidebars.js | 4 + 4 files changed, 568 insertions(+) create mode 100644 docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md create mode 100644 docs/build/isc/v1.3/docs/tutorials/cross-chain-nft-marketplace-part-1.md diff --git a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md new file mode 100644 index 00000000000..0f460e90060 --- /dev/null +++ b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -0,0 +1,280 @@ +# Cross-chain NFT Marketplace: Part I + +This is the first part of a three-part series that will guide you as you build a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on the ShimmerEVM Testnet and BNB Testnet. + +Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. +The second part of the series will focus on bridging NFTs from another EVM network, BNB Testnet, to the ShimmerEVM Testnet and listing them on the marketplace you created in part I. + +Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. + +## Marketplace Architecture Overview +The architecture of the marketplace will evolve as we progress through the tutorials. +### Part I +In part I, we will start with this very simple architecture: +![Cross Chain MarketPlace V1](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) + +### Part II +In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: +![Cross Chain MarketPlace V2](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) + +### Part III +Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. +This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. +The architecture will look like this: +![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) + + + +## Prerequisites + +- [Node.js](https://nodejs.org) >= v18.0 +- [Hardhat](https://hardhat.org) >= v2.0.0 +- [npx](https://www.npmjs.com/package/npx) >= v7.1.0. + +## Set Up + +First, create a new directory for the project and navigate into it: + +```bash +mkdir cross-chain-nft-marketplace +cd cross-chain-nft-marketplace +``` + +Then [bootsrap a new Hardhat project](https://hardhat.org/tutorial/creating-a-new-hardhat-project), by running: + +```bash +npx hardhat init +``` + +## Configuration + +In the `hardhat.config.js` file, update the `networks` object to include the ShimmerEVM Testnet network configuration, as well as the BNB Testnet network configuration. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js +``` + + + + + +## Contracts + +In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. + +Create a `contracts` folder in the root of the project and add the following files under it: + +### NFTMarketplace.sol + +The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. + +The main data structures and functions in the contract are: + +- `struct Listing` +Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. +```solidity +struct Listing { + address seller; + uint256 price; +} +``` + + +- `mapping s_listings` +Maps the token contract address to a mappring of token ID to `Listing`. +```solidity +mapping(address => mapping(uint256 => Listing)) private s_listings; +``` + + +- `function listItem` +Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. + +```solidity + /* + * @notice Method for listing NFT + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + * @param price sale price for each item + */ + function listItem( + address nftAddress, + uint256 tokenId, + uint256 price + ) + external + notListed(nftAddress, tokenId) + isOwner(nftAddress, tokenId, msg.sender) + { + if (price <= 0) { + revert PriceMustBeAboveZero(); + } + IERC721 nft = IERC721(nftAddress); + if (nft.getApproved(tokenId) != address(this)) { + revert NotApprovedForMarketplace(); + } + s_listings[nftAddress][tokenId] = Listing(price, msg.sender); + emit ItemListed(msg.sender, nftAddress, tokenId, price); + } +``` + + + + +- `function buyItem` +This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. + +```solidity + /* + * @notice Method for buying listing + * @notice The owner of an NFT could unapprove the marketplace, + * which would cause this function to fail + * Ideally you'd also have a `createOffer` functionality. + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + */ + function buyItem(address nftAddress, uint256 tokenId) + external + payable + isListed(nftAddress, tokenId) + // isNotOwner(nftAddress, tokenId, msg.sender) + nonReentrant + { + // Challenge - How would you refactor this contract to take: + // 1. Abitrary tokens as payment? (HINT - Chainlink Price Feeds!) + // 2. Be able to set prices in other currencies? + // 3. Tweet me @PatrickAlphaC if you come up with a solution! + Listing memory listedItem = s_listings[nftAddress][tokenId]; + if (msg.value < listedItem.price) { + revert PriceNotMet(nftAddress, tokenId, listedItem.price); + } + s_proceeds[listedItem.seller] += msg.value; + // Could just send the money... + // https://fravoll.github.io/solidity-patterns/pull_over_push.html + delete (s_listings[nftAddress][tokenId]); + IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId); + emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); + } + +``` + + + + +- `function getListing` +gets an NFT listing by its address and `tokenId`. +```solidity +function getListing(address nftAddress, uint256 tokenId) + external + view + returns (Listing memory) + { + return s_listings[nftAddress][tokenId]; + } +``` + + +We have now covered all relevant parts of the contract for Part I, and how they would evolve in later steps. This is the full contract code for Part I: + + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol +``` + +### MyERC721.sol + +A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol +``` + +After adding the contracts, compile them by running: + +```bash +npx hardhat compile +``` + + +## Scripts + +First, create a `scripts` folder in the root of the project and add the following files under it: + +### deploy_marketplace_shimmer.js +The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js +``` +This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. +run it by executing: + +```bash +npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet +``` + +### deploy_er721_shimmer.js +This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js +``` +You can run this script with the following command: + +```bash +npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet +``` + +### mint_nft.js + +After you have deployed the `MyERC721` contract, you are ready to mint an NFT using the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet +``` +### approve_myERC721_for_marketplace.js + +To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/approve_myERC721_for_marketplace.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet +``` + +### create_listing.js + +After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/create_listing.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/create_listing.js --network shimmerevm-testnet +``` + +### buy_item.js + +Finally, let's buy the NFT by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/buy_item.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/buy_item.js --network shimmerevm-testnet +``` + + +## Conclusion +In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file diff --git a/docs/build/isc/v1.1/sidebars.js b/docs/build/isc/v1.1/sidebars.js index 6e93e225937..ef309f6c2b7 100644 --- a/docs/build/isc/v1.1/sidebars.js +++ b/docs/build/isc/v1.1/sidebars.js @@ -241,6 +241,10 @@ module.exports = { }, ], }, + { type: 'category', + label: 'Tutorials', + items: ['tutorials/cross-chain-nft-marketplace-part-1'] + }, { type: 'category', label: 'Explanations', diff --git a/docs/build/isc/v1.3/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/build/isc/v1.3/docs/tutorials/cross-chain-nft-marketplace-part-1.md new file mode 100644 index 00000000000..0f460e90060 --- /dev/null +++ b/docs/build/isc/v1.3/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -0,0 +1,280 @@ +# Cross-chain NFT Marketplace: Part I + +This is the first part of a three-part series that will guide you as you build a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on the ShimmerEVM Testnet and BNB Testnet. + +Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. +The second part of the series will focus on bridging NFTs from another EVM network, BNB Testnet, to the ShimmerEVM Testnet and listing them on the marketplace you created in part I. + +Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. + +## Marketplace Architecture Overview +The architecture of the marketplace will evolve as we progress through the tutorials. +### Part I +In part I, we will start with this very simple architecture: +![Cross Chain MarketPlace V1](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) + +### Part II +In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: +![Cross Chain MarketPlace V2](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) + +### Part III +Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. +This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. +The architecture will look like this: +![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) + + + +## Prerequisites + +- [Node.js](https://nodejs.org) >= v18.0 +- [Hardhat](https://hardhat.org) >= v2.0.0 +- [npx](https://www.npmjs.com/package/npx) >= v7.1.0. + +## Set Up + +First, create a new directory for the project and navigate into it: + +```bash +mkdir cross-chain-nft-marketplace +cd cross-chain-nft-marketplace +``` + +Then [bootsrap a new Hardhat project](https://hardhat.org/tutorial/creating-a-new-hardhat-project), by running: + +```bash +npx hardhat init +``` + +## Configuration + +In the `hardhat.config.js` file, update the `networks` object to include the ShimmerEVM Testnet network configuration, as well as the BNB Testnet network configuration. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js +``` + + + + + +## Contracts + +In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. + +Create a `contracts` folder in the root of the project and add the following files under it: + +### NFTMarketplace.sol + +The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. + +The main data structures and functions in the contract are: + +- `struct Listing` +Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. +```solidity +struct Listing { + address seller; + uint256 price; +} +``` + + +- `mapping s_listings` +Maps the token contract address to a mappring of token ID to `Listing`. +```solidity +mapping(address => mapping(uint256 => Listing)) private s_listings; +``` + + +- `function listItem` +Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. + +```solidity + /* + * @notice Method for listing NFT + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + * @param price sale price for each item + */ + function listItem( + address nftAddress, + uint256 tokenId, + uint256 price + ) + external + notListed(nftAddress, tokenId) + isOwner(nftAddress, tokenId, msg.sender) + { + if (price <= 0) { + revert PriceMustBeAboveZero(); + } + IERC721 nft = IERC721(nftAddress); + if (nft.getApproved(tokenId) != address(this)) { + revert NotApprovedForMarketplace(); + } + s_listings[nftAddress][tokenId] = Listing(price, msg.sender); + emit ItemListed(msg.sender, nftAddress, tokenId, price); + } +``` + + + + +- `function buyItem` +This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. + +```solidity + /* + * @notice Method for buying listing + * @notice The owner of an NFT could unapprove the marketplace, + * which would cause this function to fail + * Ideally you'd also have a `createOffer` functionality. + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + */ + function buyItem(address nftAddress, uint256 tokenId) + external + payable + isListed(nftAddress, tokenId) + // isNotOwner(nftAddress, tokenId, msg.sender) + nonReentrant + { + // Challenge - How would you refactor this contract to take: + // 1. Abitrary tokens as payment? (HINT - Chainlink Price Feeds!) + // 2. Be able to set prices in other currencies? + // 3. Tweet me @PatrickAlphaC if you come up with a solution! + Listing memory listedItem = s_listings[nftAddress][tokenId]; + if (msg.value < listedItem.price) { + revert PriceNotMet(nftAddress, tokenId, listedItem.price); + } + s_proceeds[listedItem.seller] += msg.value; + // Could just send the money... + // https://fravoll.github.io/solidity-patterns/pull_over_push.html + delete (s_listings[nftAddress][tokenId]); + IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId); + emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); + } + +``` + + + + +- `function getListing` +gets an NFT listing by its address and `tokenId`. +```solidity +function getListing(address nftAddress, uint256 tokenId) + external + view + returns (Listing memory) + { + return s_listings[nftAddress][tokenId]; + } +``` + + +We have now covered all relevant parts of the contract for Part I, and how they would evolve in later steps. This is the full contract code for Part I: + + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol +``` + +### MyERC721.sol + +A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol +``` + +After adding the contracts, compile them by running: + +```bash +npx hardhat compile +``` + + +## Scripts + +First, create a `scripts` folder in the root of the project and add the following files under it: + +### deploy_marketplace_shimmer.js +The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js +``` +This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. +run it by executing: + +```bash +npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet +``` + +### deploy_er721_shimmer.js +This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js +``` +You can run this script with the following command: + +```bash +npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet +``` + +### mint_nft.js + +After you have deployed the `MyERC721` contract, you are ready to mint an NFT using the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet +``` +### approve_myERC721_for_marketplace.js + +To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/approve_myERC721_for_marketplace.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet +``` + +### create_listing.js + +After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/create_listing.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/create_listing.js --network shimmerevm-testnet +``` + +### buy_item.js + +Finally, let's buy the NFT by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/buy_item.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/buy_item.js --network shimmerevm-testnet +``` + + +## Conclusion +In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file diff --git a/docs/build/isc/v1.3/sidebars.js b/docs/build/isc/v1.3/sidebars.js index 6e93e225937..ef309f6c2b7 100644 --- a/docs/build/isc/v1.3/sidebars.js +++ b/docs/build/isc/v1.3/sidebars.js @@ -241,6 +241,10 @@ module.exports = { }, ], }, + { type: 'category', + label: 'Tutorials', + items: ['tutorials/cross-chain-nft-marketplace-part-1'] + }, { type: 'category', label: 'Explanations', From f6697a330636d51e69b08a87910bf6b82f3ea048 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 14:15:55 +0200 Subject: [PATCH 5/8] remove tutorial from docs/tutorial Signed-off-by: salaheldinsoliman --- config/tutorials.js | 20 -- .../cross-chain-nft-marketplace-part-1.md | 280 ------------------ 2 files changed, 300 deletions(-) delete mode 100644 docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md diff --git a/config/tutorials.js b/config/tutorials.js index 6804ec63b1b..8bd51c17773 100644 --- a/config/tutorials.js +++ b/config/tutorials.js @@ -249,24 +249,4 @@ module.exports = [ ], }, ], - [ - pluginTutorial, - { - title: - 'Tutorial - Create a cross-chain NFT market place on ShimmerEVM using Hardhat, Layer0 bridge and IOTA L1', - description: - 'This tutorial is meant to showcase NFT cross-chain functionlity on ISC network', - preview: '/img/tutorials/shimmerevm-hardhat.jpg', - route: 'tutorials/cross-chain-nft-marketplace-part-1', - tags: [ - 'text', - 'shimmer', - 'solidity', - 'shimmerevm', - 'hardhat', - 'iscp', - 'video', - ], - }, - ], ]; diff --git a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md deleted file mode 100644 index 0f460e90060..00000000000 --- a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md +++ /dev/null @@ -1,280 +0,0 @@ -# Cross-chain NFT Marketplace: Part I - -This is the first part of a three-part series that will guide you as you build a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on the ShimmerEVM Testnet and BNB Testnet. - -Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. -The second part of the series will focus on bridging NFTs from another EVM network, BNB Testnet, to the ShimmerEVM Testnet and listing them on the marketplace you created in part I. - -Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. - -## Marketplace Architecture Overview -The architecture of the marketplace will evolve as we progress through the tutorials. -### Part I -In part I, we will start with this very simple architecture: -![Cross Chain MarketPlace V1](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) - -### Part II -In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: -![Cross Chain MarketPlace V2](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) - -### Part III -Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. -This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. -The architecture will look like this: -![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) - - - -## Prerequisites - -- [Node.js](https://nodejs.org) >= v18.0 -- [Hardhat](https://hardhat.org) >= v2.0.0 -- [npx](https://www.npmjs.com/package/npx) >= v7.1.0. - -## Set Up - -First, create a new directory for the project and navigate into it: - -```bash -mkdir cross-chain-nft-marketplace -cd cross-chain-nft-marketplace -``` - -Then [bootsrap a new Hardhat project](https://hardhat.org/tutorial/creating-a-new-hardhat-project), by running: - -```bash -npx hardhat init -``` - -## Configuration - -In the `hardhat.config.js` file, update the `networks` object to include the ShimmerEVM Testnet network configuration, as well as the BNB Testnet network configuration. - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js -``` - - - - - -## Contracts - -In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. - -Create a `contracts` folder in the root of the project and add the following files under it: - -### NFTMarketplace.sol - -The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. - -The main data structures and functions in the contract are: - -- `struct Listing` -Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. -```solidity -struct Listing { - address seller; - uint256 price; -} -``` - - -- `mapping s_listings` -Maps the token contract address to a mappring of token ID to `Listing`. -```solidity -mapping(address => mapping(uint256 => Listing)) private s_listings; -``` - - -- `function listItem` -Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. - -```solidity - /* - * @notice Method for listing NFT - * @param nftAddress Address of NFT contract - * @param tokenId Token ID of NFT - * @param price sale price for each item - */ - function listItem( - address nftAddress, - uint256 tokenId, - uint256 price - ) - external - notListed(nftAddress, tokenId) - isOwner(nftAddress, tokenId, msg.sender) - { - if (price <= 0) { - revert PriceMustBeAboveZero(); - } - IERC721 nft = IERC721(nftAddress); - if (nft.getApproved(tokenId) != address(this)) { - revert NotApprovedForMarketplace(); - } - s_listings[nftAddress][tokenId] = Listing(price, msg.sender); - emit ItemListed(msg.sender, nftAddress, tokenId, price); - } -``` - - - - -- `function buyItem` -This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. - -```solidity - /* - * @notice Method for buying listing - * @notice The owner of an NFT could unapprove the marketplace, - * which would cause this function to fail - * Ideally you'd also have a `createOffer` functionality. - * @param nftAddress Address of NFT contract - * @param tokenId Token ID of NFT - */ - function buyItem(address nftAddress, uint256 tokenId) - external - payable - isListed(nftAddress, tokenId) - // isNotOwner(nftAddress, tokenId, msg.sender) - nonReentrant - { - // Challenge - How would you refactor this contract to take: - // 1. Abitrary tokens as payment? (HINT - Chainlink Price Feeds!) - // 2. Be able to set prices in other currencies? - // 3. Tweet me @PatrickAlphaC if you come up with a solution! - Listing memory listedItem = s_listings[nftAddress][tokenId]; - if (msg.value < listedItem.price) { - revert PriceNotMet(nftAddress, tokenId, listedItem.price); - } - s_proceeds[listedItem.seller] += msg.value; - // Could just send the money... - // https://fravoll.github.io/solidity-patterns/pull_over_push.html - delete (s_listings[nftAddress][tokenId]); - IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId); - emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); - } - -``` - - - - -- `function getListing` -gets an NFT listing by its address and `tokenId`. -```solidity -function getListing(address nftAddress, uint256 tokenId) - external - view - returns (Listing memory) - { - return s_listings[nftAddress][tokenId]; - } -``` - - -We have now covered all relevant parts of the contract for Part I, and how they would evolve in later steps. This is the full contract code for Part I: - - -```solidity reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol -``` - -### MyERC721.sol - -A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: - -```solidity reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol -``` - -After adding the contracts, compile them by running: - -```bash -npx hardhat compile -``` - - -## Scripts - -First, create a `scripts` folder in the root of the project and add the following files under it: - -### deploy_marketplace_shimmer.js -The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js -``` -This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. -run it by executing: - -```bash -npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet -``` - -### deploy_er721_shimmer.js -This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js -``` -You can run this script with the following command: - -```bash -npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet -``` - -### mint_nft.js - -After you have deployed the `MyERC721` contract, you are ready to mint an NFT using the following script: - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js -``` -You can run the script by executing the following command: - -```bash -npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet -``` -### approve_myERC721_for_marketplace.js - -To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/approve_myERC721_for_marketplace.js -``` -You can run the script by executing the following command: - -```bash -npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet -``` - -### create_listing.js - -After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/create_listing.js -``` -You can run the script by executing the following command: - -```bash -npx hardhat run scripts/create_listing.js --network shimmerevm-testnet -``` - -### buy_item.js - -Finally, let's buy the NFT by running the following script: - -```javascript reference -https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/buy_item.js -``` -You can run the script by executing the following command: - -```bash -npx hardhat run scripts/buy_item.js --network shimmerevm-testnet -``` - - -## Conclusion -In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file From ebe338ca17b35fb21a936ce45c2ade22687d1197 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 14:52:25 +0200 Subject: [PATCH 6/8] yarn format Signed-off-by: salaheldinsoliman --- docs/build/isc/v1.1/sidebars.js | 5 +++-- docs/build/isc/v1.3/sidebars.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/build/isc/v1.1/sidebars.js b/docs/build/isc/v1.1/sidebars.js index ef309f6c2b7..8a451319158 100644 --- a/docs/build/isc/v1.1/sidebars.js +++ b/docs/build/isc/v1.1/sidebars.js @@ -241,9 +241,10 @@ module.exports = { }, ], }, - { type: 'category', + { + type: 'category', label: 'Tutorials', - items: ['tutorials/cross-chain-nft-marketplace-part-1'] + items: ['tutorials/cross-chain-nft-marketplace-part-1'], }, { type: 'category', diff --git a/docs/build/isc/v1.3/sidebars.js b/docs/build/isc/v1.3/sidebars.js index ef309f6c2b7..8a451319158 100644 --- a/docs/build/isc/v1.3/sidebars.js +++ b/docs/build/isc/v1.3/sidebars.js @@ -241,9 +241,10 @@ module.exports = { }, ], }, - { type: 'category', + { + type: 'category', label: 'Tutorials', - items: ['tutorials/cross-chain-nft-marketplace-part-1'] + items: ['tutorials/cross-chain-nft-marketplace-part-1'], }, { type: 'category', From 9cfff8113b74304163b4a4775ac89e17c1736e3d Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 10 Jul 2024 15:01:25 +0200 Subject: [PATCH 7/8] fix build error Signed-off-by: salaheldinsoliman --- .../cross-chain-nft-marketplace-part-1.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md diff --git a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md new file mode 100644 index 00000000000..0f460e90060 --- /dev/null +++ b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -0,0 +1,280 @@ +# Cross-chain NFT Marketplace: Part I + +This is the first part of a three-part series that will guide you as you build a cross-chain NFT marketplace using IOTA Smart Contracts (ISC). The marketplace will allow users to trade NFTs on the ShimmerEVM Testnet and BNB Testnet. + +Part I will cover the setup of the project and the deployment of the NFT marketplace contract on the ShimmerEVM Testnet. +The second part of the series will focus on bridging NFTs from another EVM network, BNB Testnet, to the ShimmerEVM Testnet and listing them on the marketplace you created in part I. + +Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. + +## Marketplace Architecture Overview +The architecture of the marketplace will evolve as we progress through the tutorials. +### Part I +In part I, we will start with this very simple architecture: +![Cross Chain MarketPlace V1](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) + +### Part II +In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: +![Cross Chain MarketPlace V2](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) + +### Part III +Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. +This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. +The architecture will look like this: +![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) + + + +## Prerequisites + +- [Node.js](https://nodejs.org) >= v18.0 +- [Hardhat](https://hardhat.org) >= v2.0.0 +- [npx](https://www.npmjs.com/package/npx) >= v7.1.0. + +## Set Up + +First, create a new directory for the project and navigate into it: + +```bash +mkdir cross-chain-nft-marketplace +cd cross-chain-nft-marketplace +``` + +Then [bootsrap a new Hardhat project](https://hardhat.org/tutorial/creating-a-new-hardhat-project), by running: + +```bash +npx hardhat init +``` + +## Configuration + +In the `hardhat.config.js` file, update the `networks` object to include the ShimmerEVM Testnet network configuration, as well as the BNB Testnet network configuration. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js +``` + + + + + +## Contracts + +In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. + +Create a `contracts` folder in the root of the project and add the following files under it: + +### NFTMarketplace.sol + +The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. + +The main data structures and functions in the contract are: + +- `struct Listing` +Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. +```solidity +struct Listing { + address seller; + uint256 price; +} +``` + + +- `mapping s_listings` +Maps the token contract address to a mappring of token ID to `Listing`. +```solidity +mapping(address => mapping(uint256 => Listing)) private s_listings; +``` + + +- `function listItem` +Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. + +```solidity + /* + * @notice Method for listing NFT + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + * @param price sale price for each item + */ + function listItem( + address nftAddress, + uint256 tokenId, + uint256 price + ) + external + notListed(nftAddress, tokenId) + isOwner(nftAddress, tokenId, msg.sender) + { + if (price <= 0) { + revert PriceMustBeAboveZero(); + } + IERC721 nft = IERC721(nftAddress); + if (nft.getApproved(tokenId) != address(this)) { + revert NotApprovedForMarketplace(); + } + s_listings[nftAddress][tokenId] = Listing(price, msg.sender); + emit ItemListed(msg.sender, nftAddress, tokenId, price); + } +``` + + + + +- `function buyItem` +This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. + +```solidity + /* + * @notice Method for buying listing + * @notice The owner of an NFT could unapprove the marketplace, + * which would cause this function to fail + * Ideally you'd also have a `createOffer` functionality. + * @param nftAddress Address of NFT contract + * @param tokenId Token ID of NFT + */ + function buyItem(address nftAddress, uint256 tokenId) + external + payable + isListed(nftAddress, tokenId) + // isNotOwner(nftAddress, tokenId, msg.sender) + nonReentrant + { + // Challenge - How would you refactor this contract to take: + // 1. Abitrary tokens as payment? (HINT - Chainlink Price Feeds!) + // 2. Be able to set prices in other currencies? + // 3. Tweet me @PatrickAlphaC if you come up with a solution! + Listing memory listedItem = s_listings[nftAddress][tokenId]; + if (msg.value < listedItem.price) { + revert PriceNotMet(nftAddress, tokenId, listedItem.price); + } + s_proceeds[listedItem.seller] += msg.value; + // Could just send the money... + // https://fravoll.github.io/solidity-patterns/pull_over_push.html + delete (s_listings[nftAddress][tokenId]); + IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId); + emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price); + } + +``` + + + + +- `function getListing` +gets an NFT listing by its address and `tokenId`. +```solidity +function getListing(address nftAddress, uint256 tokenId) + external + view + returns (Listing memory) + { + return s_listings[nftAddress][tokenId]; + } +``` + + +We have now covered all relevant parts of the contract for Part I, and how they would evolve in later steps. This is the full contract code for Part I: + + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol +``` + +### MyERC721.sol + +A standard ERC721-compatible contract that allows minting and transferring of NFTs, used as an example for the tutorial. The full contract code is as follows: + +```solidity reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol +``` + +After adding the contracts, compile them by running: + +```bash +npx hardhat compile +``` + + +## Scripts + +First, create a `scripts` folder in the root of the project and add the following files under it: + +### deploy_marketplace_shimmer.js +The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js +``` +This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. +run it by executing: + +```bash +npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet +``` + +### deploy_er721_shimmer.js +This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js +``` +You can run this script with the following command: + +```bash +npx hardhat run scripts/deploy_er721_shimmer.js --network shimmerevm-testnet +``` + +### mint_nft.js + +After you have deployed the `MyERC721` contract, you are ready to mint an NFT using the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/mint_nft.js --network shimmerevm-testnet +``` +### approve_myERC721_for_marketplace.js + +To allow the NFTMarketplace contract to transfer the NFT from the seller to the buyer, the seller must approve the marketplace contract to transfer the NFT on their behalf. +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/approve_myERC721_for_marketplace.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/approve_myERC721_for_marketplace.js --network shimmerevm-testnet +``` + +### create_listing.js + +After approving the NFT transfer, let's list the NFT for sale on the marketplace by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/create_listing.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/create_listing.js --network shimmerevm-testnet +``` + +### buy_item.js + +Finally, let's buy the NFT by running the following script: + +```javascript reference +https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/buy_item.js +``` +You can run the script by executing the following command: + +```bash +npx hardhat run scripts/buy_item.js --network shimmerevm-testnet +``` + + +## Conclusion +In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file From 5a40c4601d3fc9ab575ca15578cb09da5e548b87 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman <49910731+salaheldinsoliman@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:09:11 +0200 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Lucas Tortora <85233773+lucas-tortora@users.noreply.github.com> --- .../cross-chain-nft-marketplace-part-1.md | 52 ++++++++++--------- .../cross-chain-nft-marketplace-part-1.md | 6 ++- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md index 0f460e90060..50abd2a9f68 100644 --- a/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md +++ b/docs/build/isc/v1.1/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -8,22 +8,31 @@ The second part of the series will focus on bridging NFTs from another EVM netwo Finally, in part III, you will deploy another instance of the marketplace on the BNB Testnet, making the marketplace truly cross-chain. ## Marketplace Architecture Overview + The architecture of the marketplace will evolve as we progress through the tutorials. + ### Part I + In part I, we will start with this very simple architecture: + ![Cross Chain MarketPlace V1](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V1.png) ### Part II -In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. The architecture will evolve to look like this: + +In Part II, you will add the contracts and scripts to manually bridge NFTs from the BNB Testnet to the ShimmerEVM Testnet and list them on the marketplace. + +The architecture will evolve to look like this: + ![Cross Chain MarketPlace V2](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V2.png) ### Part III + Finally, in part III, you will deploy another marketplace instance on the BNB Testnet, where the contract will handle cross-chain transactions. This enables a user on the BNB Testnet, to view and buy an NFT listed on the ShimmerEVM Testnet and vice versa without switching networks. -The architecture will look like this: -![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) +The architecture will look like this: +![Cross Chain MarketPlace V3](../../../../../../static/img/tutorials/cross_chain_marketplace/Architecture-V3.png) ## Prerequisites @@ -54,10 +63,6 @@ In the `hardhat.config.js` file, update the `networks` object to include the Shi https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/hardhat.config.js ``` - - - - ## Contracts In the first part of the tutorial, you will only need two contracts: the [NFT Marketplace contract](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/ab11866504fe8f72fc54d719a316ec9291839ced/contracts/NFTMarketPlace.sol) and an [NFT ERC721-compatible](https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol) contract. @@ -68,10 +73,12 @@ Create a `contracts` folder in the root of the project and add the following fil The idea behind a marketplace contract is to allow users to list their NFTs for sale and other users to buy them. The contract will handle the transfer of the NFT from the seller to the buyer and the payment from the buyer to the seller. A seller must first allow the marketplace contract to transfer the NFT on their behalf before listing it for sale. +#### Data Structures + The main data structures and functions in the contract are: -- `struct Listing` -Represents an NFT listing with the price and the address of the seller. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. +- `struct Listing`: Represents an NFT listing with the seller's price and address. In further parts of the tutorial, `struct Listing` will be expanded to include more details about the NFT being listed, like the chain it resides on. + ```solidity struct Listing { address seller; @@ -80,15 +87,13 @@ struct Listing { ``` -- `mapping s_listings` -Maps the token contract address to a mappring of token ID to `Listing`. +- `mapping s_listings` : Maps the token contract address to a mapping of token ID to `Listing.` ```solidity mapping(address => mapping(uint256 => Listing)) private s_listings; ``` +#### Functions - -- `function listItem` -Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. In Part II, this function will stay the same, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. +- `function listItem`: Allows a seller to list an NFT for sale by specifying the token contract address, the token ID, and the price. This function will stay the same in Part II, but the `Listing` struct will be expanded to include more details about the NFT being listed, like the chain it resides on. ```solidity /* @@ -118,11 +123,7 @@ Allows a seller to list an NFT for sale by specifying the token contract address } ``` - - - -- `function buyItem` -This handles the transfer of an NFT from a seller to buyer. Same as the `listItem` function, this function will stay the same in Part II, because the NFTs will be bridged manually. +- `function buyItem`: This handles the transfer of an NFT from a seller to the buyer. Similar to the `listItem` function, this function will stay the same in Part II because the NFTs will be bridged manually. ```solidity /* @@ -158,11 +159,7 @@ This handles the transfer of an NFT from a seller to buyer. Same as the `listIte ``` - - - -- `function getListing` -gets an NFT listing by its address and `tokenId`. +- `function getListing`: Gets an NFT listing by its address and `tokenId`. ```solidity function getListing(address nftAddress, uint256 tokenId) external @@ -188,6 +185,7 @@ A standard ERC721-compatible contract that allows minting and transferring of NF ```solidity reference https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/contracts/MyERC721.sol ``` +### Compile the Contracts After adding the contracts, compile them by running: @@ -201,6 +199,7 @@ npx hardhat compile First, create a `scripts` folder in the root of the project and add the following files under it: ### deploy_marketplace_shimmer.js + The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file called `NFTMarketplace.txt`. ```javascript reference @@ -214,11 +213,13 @@ npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testn ``` ### deploy_er721_shimmer.js + This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. ```javascript reference https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js ``` + You can run this script with the following command: ```bash @@ -232,6 +233,7 @@ After you have deployed the `MyERC721` contract, you are ready to mint an NFT us ```javascript reference https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/mint_nft.js ``` + You can run the script by executing the following command: ```bash @@ -277,4 +279,4 @@ npx hardhat run scripts/buy_item.js --network shimmerevm-testnet ## Conclusion -In this first part of the cross-chain NFT marketplace tutorial, we have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. We have also deployed the MyERC721 contract, minted an NFT and then listed it on the marketplace. In the next part, we will manually bridge NFTs from BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file +In this first part of the cross-chain NFT marketplace tutorial, you have set up the project and deployed the NFTMarketplace contract to the ShimmerEVM Testnet. You have also deployed the MyERC721 contract, minted an NFT, and listed it on the marketplace. In the next part, we will manually bridge NFTs from the BNB Testnet and Shimmer Testnet to the ShimmerEVM Testnet and list them on the same marketplace. \ No newline at end of file diff --git a/docs/tutorials/cross-chain-nft-marketplace-part-1.md b/docs/tutorials/cross-chain-nft-marketplace-part-1.md index bdc5a299336..723776ccfbb 100644 --- a/docs/tutorials/cross-chain-nft-marketplace-part-1.md +++ b/docs/tutorials/cross-chain-nft-marketplace-part-1.md @@ -206,8 +206,8 @@ The `deploy_marketplace_shimmer.js` script will deploy the NFTMarketplace contra ```javascript reference https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_marketplace_shimmer.js ``` -This will deploy the NFTMarketplace contract to the ShimmerEVM Testnet and save the contract address to a file. -run it by executing: + +You can run this script with the following command: ```bash npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testnet @@ -216,6 +216,8 @@ npx hardhat run scripts/deploy_marketplace_shimmer.js --network shimmerevm-testn ### deploy_er721_shimmer.js This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. +This script will deploy the `MyERC721` contract to the ShimmerEVM Testnet and save the contract's address to a file called `MyERC721.txt`. + ```javascript reference https://github.com/iota-community/ISC-Cross-Chain-NFT-Marketplace/blob/main/scripts/deploy_erc721_shimmer.js ```