DEV Community

Daniel Ioni
Daniel Ioni

Posted on

🎨 NFT Integration into MyZubster Marketplace Extending MyZubster to support Non‑Fungible Tokens (NFTs)

🎨 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
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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 });
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Error listing NFT:', error);
res.status(500).json({ error: error.message });
}
});

  1. 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 });
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Error buying NFT:', error);
res.status(500).json({ error: error.message });
}
});

  1. 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
}

  1. 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 });
}
});

  1. 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 });
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Error minting NFT:', error);
res.status(500).json({ error: error.message });
}
});

  1. 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 => (

src={order.nftMetadata?.imageUrl || '/placeholder.png'}
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

  1. 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 '.'

  1. 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 '.'

  1. View NFT Orders bash

curl http://localhost:3002/api/marketplace/orders/nft | jq '.'

  1. 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)