π¨ NFT Integration into MyZubster Marketplace
Extending MyZubster to support NonβFungible Tokens (NFTs) β unique, indivisible digital assets like art, collectibles, and real estate.
π Introduction
MyZubster already supports fungible tokens (ERCβ20 style) with a full order book. Now it's time to add NFTs β assets that are unique and cannot be divided.
NFTs are perfect for:
πΌοΈ Digital art and collectibles
π Unique properties (each house is different)
ποΈ Tickets and memberships
π Inβgame items
π Certificates and proof of authenticity
We'll extend the marketplace to handle NFT listings, purchases, and management, using Tari as the underlying blockchain for NFT minting and transfer.
π§± Architecture
text
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MyZubster NFT Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. π¨ Mint NFT (via Tari) β β
β β β User uploads metadata (name, description, image) β β
β β β Tari wallet mints a unique NFT with a token ID β β
β β β NFT is assigned to the user's Tari address β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 2. π List NFT for Sale β β
β β β User selects an NFT from their holdings β β
β β β Sets a price (in XMR or stable token) β β
β β β NFT is locked (no doubleβspending) β β
β β β Order appears in the marketplace β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 3. π Purchase NFT β β
β β β Buyer selects an NFT order β β
β β β Pays the price (via Monero or token) β β
β β β On payment confirmation: β β
β β β NFT is transferred to buyer's Tari address β β
β β β Funds are released to seller β β
β β β Order status becomes "filled" β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 4. π Cancel NFT Order β β
β β β Seller cancels an open order β β
β β β NFT is unlocked and returned to seller's holdings β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 5. π View NFT Holdings β β
β β β User can see all owned NFTs β β
β β β Includes metadata, token ID, and transfer history β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ποΈ Database Models
Extend OrderBook Model to Support NFTs
We need to add a field to distinguish NFT orders from fungible token orders.
javascript
// models/OrderBook.js (add these fields)
const OrderBookSchema = new mongoose.Schema({
// ... existing fields ...
// Add for NFT support
assetType: {
type: String,
enum: ['fungible', 'nft'],
default: 'fungible'
},
nftTokenId: {
type: String,
default: null,
// The token ID of the NFT on Tari blockchain
},
nftMetadata: {
type: Object,
default: null,
// Stores name, description, image, etc.
}
});
Create NFT Model (optional, but useful for tracking)
We can also create a separate NFT model to track all NFTs in the system, even those not listed.
javascript
// models/NFT.js
const mongoose = require('mongoose');
const NFTSchema = new mongoose.Schema({
tokenId: {
type: String,
required: true,
unique: true,
// The unique identifier on Tari blockchain
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
name: { type: String, required: true },
description: { type: String, default: '' },
imageUrl: { type: String, default: '' },
metadata: { type: Object, default: {} },
// Additional fields like royalty percentage, etc.
royalty: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
module.exports = mongoose.model('NFT', NFTSchema);
Update TokenHolding Model (optional)
We can use the existing TokenHolding model for NFTs as well, but since NFTs are indivisible, we might want a separate collection. However, to keep it simple, we can use TokenHolding with amount: 1 for NFTs. We'll add a tokenType field to distinguish.
javascript
// models/TokenHolding.js (add this field)
tokenType: {
type: String,
enum: ['fungible', 'nft'],
default: 'fungible'
}
π Tari Integration for NFTs
We already have tariService.js with basic functions. We need to add NFTβspecific functions.
Update tariService.js
javascript
// services/tariService.js (add these functions)
/**
- Mint a new NFT on Tari */ async function mintNFT(name, description, ownerAddress, metadata = {}) { try { const response = await tariWalletRequest('mint_nft', { name, description, owner: ownerAddress, metadata: JSON.stringify(metadata), }); return { tokenId: response.token_id, address: response.address, ...response }; } catch (error) { console.error('β Error minting NFT:', error); throw error; } }
/**
- Transfer NFT to a new owner */ async function transferNFT(tokenId, fromAddress, toAddress) { try { const response = await tariWalletRequest('transfer_nft', { token_id: tokenId, from: fromAddress, to: toAddress, }); return response; } catch (error) { console.error('β Error transferring NFT:', error); throw error; } }
/**
- Get NFT details by token ID */ async function getNFTDetails(tokenId) { try { const response = await tariWalletRequest('get_nft', { token_id: tokenId, }); return response; } catch (error) { console.error('β Error getting NFT details:', error); return null; } }
π API Endpoints for NFT Marketplace
We'll extend the existing marketplace routes to handle NFTs.
- List NFT for Sale (POST /api/marketplace/sell/nft) javascript
// routes/marketplace.js
router.post('/sell/nft', auth, async (req, res) => {
try {
const { nftTokenId, price } = req.body;
// Verify the NFT exists and belongs to the user
const nft = await NFT.findOne({ tokenId: nftTokenId, owner: req.user._id });
if (!nft) {
return res.status(404).json({ error: 'NFT not found or not owned' });
}
// Check if NFT is already listed
const existingOrder = await OrderBook.findOne({
nftTokenId: nftTokenId,
status: 'open'
});
if (existingOrder) {
return res.status(400).json({ error: 'NFT already listed for sale' });
}
// Create an order (amount is always 1 for NFTs)
const order = new OrderBook({
token: null, // no fungible token
seller: req.user._id,
amount: 1,
price: price,
totalPrice: price,
status: 'open',
assetType: 'nft',
nftTokenId: nftTokenId,
nftMetadata: {
name: nft.name,
description: nft.description,
imageUrl: nft.imageUrl,
}
});
await order.save();
// Lock the NFT (prevent double listing)
// We could mark the NFT as locked in the NFT model
// Or use TokenHolding with lockedAmount
res.status(201).json({ success: true, order });
} catch (error) {
console.error('Error listing NFT:', error);
res.status(500).json({ error: error.message });
}
});
- Buy NFT (POST /api/marketplace/buy/nft/:orderId) javascript
router.post('/buy/nft/:orderId', auth, async (req, res) => {
try {
const order = await OrderBook.findById(req.params.orderId);
if (!order) return res.status(404).json({ error: 'Order not found' });
if (order.status !== 'open') return res.status(400).json({ error: 'Order not open' });
if (order.assetType !== 'nft') return res.status(400).json({ error: 'Not an NFT order' });
// Create a payment (Monero or token) β reuse existing payment flow
const payment = await moneroService.createPayment(
order._id,
req.user._id,
order.totalPrice
);
// The payment will be monitored by PaymentMonitor
// When confirmed, the NFT will be transferred automatically
res.json({ success: true, payment });
} catch (error) {
console.error('Error buying NFT:', error);
res.status(500).json({ error: error.message });
}
});
- Cancel NFT Order (DELETE /api/marketplace/order/:orderId)
We already have this endpoint. Just ensure it also unlocks the NFT.
javascript
// In the existing cancelSellOrder function, add:
const order = await OrderBook.findById(orderId);
if (order.assetType === 'nft') {
// Unlock the NFT (remove locked status)
// Update NFT model or TokenHolding
}
- Get NFT Holdings (GET /api/nft/holdings) javascript
router.get('/holdings', auth, async (req, res) => {
try {
const nfts = await NFT.find({ owner: req.user._id });
res.json({ success: true, nfts });
} catch (error) {
console.error('Error fetching NFT holdings:', error);
res.status(500).json({ error: error.message });
}
});
- Mint NFT (POST /api/nft/mint) javascript
router.post('/mint', auth, async (req, res) => {
try {
const { name, description, imageUrl, metadata } = req.body;
// Get user's Tari address (from User model or .env)
// For simplicity, we'll use a fixed wallet address for now
const ownerAddress = process.env.TARI_WALLET_ADDRESS;
const nftData = await tariService.mintNFT(
name,
description,
ownerAddress,
metadata || {}
);
// Save NFT in our database
const nft = new NFT({
tokenId: nftData.tokenId,
owner: req.user._id,
name,
description,
imageUrl,
metadata: metadata || {},
});
await nft.save();
// Also create a TokenHolding entry for the NFT (with amount: 1)
const holding = new TokenHolding({
user: req.user._id,
token: null, // no fungible token
amount: 1,
lockedAmount: 0,
tokenType: 'nft',
nftTokenId: nftData.tokenId,
});
await holding.save();
res.status(201).json({ success: true, nft });
} catch (error) {
console.error('Error minting NFT:', error);
res.status(500).json({ error: error.message });
}
});
- Get All NFT Orders (GET /api/marketplace/orders/nft) javascript
router.get('/orders/nft', async (req, res) => {
try {
const orders = await OrderBook.find({
assetType: 'nft',
status: 'open'
}).populate('seller', 'username');
res.json(orders);
} catch (error) {
console.error('Error fetching NFT orders:', error);
res.status(500).json({ error: error.message });
}
});
π» Frontend Updates
Add a new page /nft-marketplace that displays NFT orders with thumbnails, names, and prices. Also add a /my-nfts page to view and list owned NFTs.
Example React Component (NFT Marketplace)
jsx
import React, { useState, useEffect } from 'react';
import api from '../utils/axiosConfig';
const NFTMarketplace = () => {
const [nfts, setNfts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchNFTOrders = async () => {
try {
const res = await api.get('/marketplace/orders/nft');
setNfts(res.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchNFTOrders();
}, []);
const handleBuy = async (orderId) => {
try {
await api.post(/marketplace/buy/nft/${orderId});
alert('Purchase initiated. Please complete the Monero payment.');
} catch (err) {
alert('Error: ' + err.response?.data?.error);
}
};
if (loading) return
Loading...;return (
{nfts.map(order => (
alt={order.nftMetadata?.name || 'NFT'}
className="w-full h-48 object-cover rounded"
/>
{order.nftMetadata?.name}
{order.nftMetadata?.description}
{order.price} XMR
onClick={() => handleBuy(order._id)}
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Buy
))}
);
};
export default NFTMarketplace;
π§ͺ Testing the NFT Flow
- Mint an NFT bash
TOKEN=$(curl -s -X POST http://localhost:3002/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Test123!"}' | jq -r '.token')
curl -X POST http://localhost:3002/api/nft/mint \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My First NFT",
"description": "A unique digital asset",
"imageUrl": "https://example.com/nft.png",
"metadata": { "assetType": "art", "value": 100 }
}' | jq '.'
- List NFT for Sale bash
Assume NFT token ID from previous step
NFT_TOKEN_ID="tari_nft_123456"
curl -X POST http://localhost:3002/api/marketplace/sell/nft \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nftTokenId": "'$NFT_TOKEN_ID'",
"price": 10
}' | jq '.'
- View NFT Orders bash
curl http://localhost:3002/api/marketplace/orders/nft | jq '.'
- Buy NFT (simulate payment)
Use the same payment flow as before.
π Current Status
Feature Status
NFT Model β
Ready
Tari NFT Minting β
Ready
NFT Listing π Implemented in this guide
NFT Buying π Implemented in this guide
NFT
Top comments (0)