diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..97aca2e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.env
+node_modules
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f75b3ab
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+socialfi-music-dapp/
+├── contracts/ # Aptos Move Smart Contracts
+│ ├── music_nft.move # Handles NFT creation, ownership, metadata
+│ ├── royalty_distribution.move # Manages splitting of royalties to NFT owners, artists, platform
+│ ├── fractional_ownership.move # Allows for partial ownership of NFTs
+│ ├── tip_jar.move # Securely handles direct artist tipping
+│ ├── governance.move # (Optional) Community voting on platform decisions
+│ ├── token.move # (Optional) If using social tokens for community membership
+│ └── ... # Other contracts as needed (e.g., for subscriptions)
+├── client/ # Frontend (Web App)
+│ ├── public/ # Static assets
+│ │ ├── index.html
+│ │ ├── favicon.ico
+│ │ ├── logo.png
+│ │ └── ...
+│ ├── src/ # Source code
+│ │ ├── components/ # Reusable UI elements
+│ │ │ ├── MusicNFTCard.jsx
+│ │ │ ├── ArtistProfile.jsx
+│ │ │ ├── LiveStreamPlayer.jsx
+│ │ │ ├── FanClubChat.jsx
+│ │ │ ├── TipJar.jsx
+│ │ │ └── ...
+│ │ ├── pages/ # Main views
+│ │ │ ├── Home.jsx # Music feed
+│ │ │ ├── Explore.jsx # Discover new artists
+│ │ │ ├── ArtistPage.jsx
+│ │ │ ├── FanClubPage.jsx
+│ │ │ └── ...
+│ │ ├── utils/ # Helper functions
+│ │ │ ├── aptos.js # Aptos SDK interaction (transactions, etc.)
+│ │ │ ├── nft.js # NFT handling (metadata, image display, etc.) --- API: 66c8607b.997e5e6cd4be4dd8aa3560fed7d31fa5
+│ │ │ ├── web3Storage.js # (Optional) For decentralized storage
+│ │ │ └── ...
+│ │ ├── context/ # (Optional) Context API for global state
+│ │ ├── hooks/ # Custom React hooks
+│ │ ├── store/ # (Optional) If using Redux/Zustand
+│ │ ├── App.jsx
+│ │ ├── index.jsx
+│ │ └── ...
+│ ├── package.json
+│ ├── .env # Environment variables
+│ └── ...
+server/
+├── src/ # Source code
+│ ├── app.js # Main application entry point (or index.js)
+│ ├── routes/ # API route definitions
+│ │ ├── artists.js
+│ │ ├── nfts.js
+│ │ ├── fanclubs.js
+│ │ ├── live-streams.js
+│ │ └── auth.js # (For authentication/authorization)
+│ ├── controllers/ # Logic for handling API requests
+│ │ ├── artists.js
+│ │ ├── nfts.js
+│ │ ├── fanclubs.js
+│ │ ├── live-streams.js
+│ │ └── auth.js
+│ ├── middleware/
+│ │ ├── auth.js
+│ │ └── errorHandler.js
+│ ├── utils/ # Helper functions (e.g., Aptos interaction)
+│ │ ├── aptos.js # Interaction with Aptos blockchain
+│ │ ├── caching.js # (Optional) Caching to reduce blockchain reads
+│ │ └── ...
+│ └── index.js # (If using a different entry point than app.js)
+├── public/ # Static files (if needed)
+│ ├── images/
+│ └── ...
+├── tests/ # Unit and integration tests
+│ ├── routes/
+│ ├── controllers/
+│ └── ...
+│ ├── package.json
+│ └── ...
+├── scripts/ # Deployment scripts
+│ ├── deploy_contracts.sh
+│ ├── setup_testnet.sh
+│ └── ...
+├── tests/ # Unit, integration tests
+│ ├── contracts/
+│ ├── client/
+│ ├── server/
+│ └── ...
+├── .gitignore
+├── README.md # Project documentation
\ No newline at end of file
diff --git a/client/public/favicon.ico b/client/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/client/public/index.html b/client/public/index.html
new file mode 100644
index 0000000..e5a7f77
--- /dev/null
+++ b/client/public/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ SocialFi Music dApp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/public/logo.png b/client/public/logo.png
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/App.jsx b/client/src/App.jsx
new file mode 100644
index 0000000..4e5f68b
--- /dev/null
+++ b/client/src/App.jsx
@@ -0,0 +1,81 @@
+import React, { useEffect } from 'react';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { WalletProvider, useWallet } from '@aptos-labs/wallet-adapter-react';
+import { PetraWallet, MartianWallet } from 'petra-plugin-wallet-adapter';
+import { createTheme, ThemeProvider, CssBaseline, Box, CircularProgress } from '@mui/material';
+import { ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import Home from './pages/Home';
+import Explore from './pages/Explore';
+import ArtistPage from './pages/ArtistPage';
+import FanClubPage from './pages/FanClubPage';
+import Navbar from './components/Navbar';
+
+// Custom theme for Material UI
+const theme = createTheme({
+ // ... your custom theme settings
+});
+
+function AppContent() {
+ const { connected, connect } = useWallet();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const connectWallet = async () => {
+ try {
+ await connect();
+ setIsLoading(false);
+ } catch (error) {
+ console.error('Error connecting wallet:', error);
+ toast.error('Could not connect wallet'); // Display error message to user
+ }
+ };
+
+ if (!connected) {
+ connectWallet();
+ } else {
+ setIsLoading(false); // If already connected, remove loading state
+ }
+ }, [connected]);
+
+ return (
+
+
+
+
+
+ {isLoading ? ( // Show loading indicator until wallet is connected
+
+
+
+ ) : (
+
+ } />
+ } />
+ } />
+ } />
+ {/* ... other routes as needed */}
+
+ )}
+
+
+ );
+}
+
+function App() {
+ const wallets = [
+ new PetraWallet(),
+ new MartianWallet()
+ ];
+
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/ArtistProfile.jsx b/client/src/components/ArtistProfile.jsx
new file mode 100644
index 0000000..1b4b46d
--- /dev/null
+++ b/client/src/components/ArtistProfile.jsx
@@ -0,0 +1,73 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+import { AptosClient, Types } from 'aptos';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import MusicNFTCard from './MusicNFTCard';
+import FanClubChat from './FanClubChat';
+import { getArtistProfile, getNFTsByArtist } from '../utils/aptos'; // Assuming you have these utility functions
+
+const nodeUrl = 'https://fullnode.devnet.aptoslabs.com/v1';
+const client = new AptosClient(nodeUrl);
+
+const ArtistProfile = () => {
+ const { artistAddress } = useParams();
+ const { account } = useWallet();
+ const navigate = useNavigate();
+ const [artistNFTs, setArtistNFTs] = useState([]);
+ const [artistProfile, setArtistProfile] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const profile = await getArtistProfile(client, artistAddress);
+ if (profile) {
+ setArtistProfile(profile);
+ } else {
+ toast.error('Artist profile not found.');
+ navigate('/'); // Redirect to home if profile doesn't exist
+ }
+
+ const nfts = await getNFTsByArtist(client, artistAddress);
+ setArtistNFTs(nfts);
+ } catch (error) {
+ console.error('Error fetching artist data:', error);
+ toast.error('Error fetching artist data.');
+ }
+ };
+ fetchData();
+ }, [artistAddress, navigate]);
+
+ // ... (handleNFTPurchase and generateTransactionPayload functions remain the same)
+
+ return (
+
+ {artistProfile ? (
+ <>
+ {/* ... (artist profile display) */}
+ >
+ ) : (
+
Loading artist profile...
// Display loading message
+ )}
+
+
NFTs
+ {!artistNFTs.length ? (
+
No NFTs found for this artist.
+ ) : (
+
+ {artistNFTs.map((nft) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default ArtistProfile;
diff --git a/client/src/components/FanClubChat.jsx b/client/src/components/FanClubChat.jsx
new file mode 100644
index 0000000..9f6fdd9
--- /dev/null
+++ b/client/src/components/FanClubChat.jsx
@@ -0,0 +1,83 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useParams } from 'react-router-dom';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+
+const FanClubChat = ({ artistAddress }) => {
+ const { connected, account } = useWallet();
+ const [messages, setMessages] = useState([]);
+ const [newMessage, setNewMessage] = useState('');
+ const chatContainerRef = useRef(null);
+
+ useEffect(() => {
+ // Fetch initial messages from the backend
+ const fetchMessages = async () => {
+ try {
+ const response = await fetch(`/api/fanclubs/${artistAddress}/messages`);
+ const data = await response.json();
+ setMessages(data);
+ } catch (error) {
+ console.error('Error fetching messages:', error);
+ }
+ };
+ fetchMessages();
+
+ // Set up real-time updates (e.g., WebSockets, polling)
+ // ... (Implementation depends on your backend/chat service)
+ }, [artistAddress]);
+
+ useEffect(() => {
+ // Auto-scroll to the bottom when new messages arrive
+ if (chatContainerRef.current) {
+ chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ const handleSendMessage = async () => {
+ if (!connected || !account) {
+ alert('Please connect your wallet to join the chat.');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/fanclubs/${artistAddress}/messages`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sender: account.address,
+ content: newMessage
+ })
+ });
+
+ if (response.ok) {
+ setNewMessage('');
+ // Fetch updated messages (or update via real-time updates)
+ } else {
+ console.error('Error sending message:', response.statusText);
+ }
+ } catch (error) {
+ console.error('Error sending message:', error);
+ }
+ };
+
+ return (
+
+
+ {messages.map((message, index) => (
+
+ ))}
+
+
+ setNewMessage(e.target.value)}
+ />
+
+
+
+ );
+};
+
+export default FanClubChat;
diff --git a/client/src/components/LiveStreamPlayer.jsx b/client/src/components/LiveStreamPlayer.jsx
new file mode 100644
index 0000000..c4c8375
--- /dev/null
+++ b/client/src/components/LiveStreamPlayer.jsx
@@ -0,0 +1,64 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useParams } from 'react-router-dom';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+import Hls from 'hls.js'; // HLS (HTTP Live Streaming) library
+
+const LiveStreamPlayer = () => {
+ const { artistAddress } = useParams(); // Get artist address from route
+ const { connected } = useWallet();
+ const videoRef = useRef(null);
+ const [isLive, setIsLive] = useState(false);
+ const [hls, setHls] = useState(null);
+
+ useEffect(() => {
+ // Check if the artist is live (e.g., fetch from backend)
+ const checkLiveStatus = async () => {
+ try {
+ const response = await fetch(`/api/artists/${artistAddress}/live`); // Example API call
+ const data = await response.json();
+ setIsLive(data.isLive);
+ } catch (error) {
+ console.error('Error checking live status:', error);
+ }
+ };
+
+ checkLiveStatus();
+
+ // Clean up HLS instance on unmount
+ return () => {
+ if (hls) {
+ hls.destroy();
+ }
+ };
+ }, [artistAddress]);
+
+ useEffect(() => {
+ if (isLive && videoRef.current && Hls.isSupported()) {
+ const newHls = new Hls();
+ newHls.loadSource(`/api/artists/${artistAddress}/stream`); // Example stream URL
+ newHls.attachMedia(videoRef.current);
+ newHls.on(Hls.Events.MANIFEST_PARSED, () => {
+ videoRef.current.play();
+ });
+ setHls(newHls);
+ }
+ }, [isLive, artistAddress]);
+
+ return (
+
+ {isLive ? (
+ <>
+ {connected ? ( // Only show video if connected
+
+ ) : (
+
Connect your wallet to watch the live stream.
+ )}
+ >
+ ) : (
+
This artist is not currently live.
+ )}
+
+ );
+};
+
+export default LiveStreamPlayer;
diff --git a/client/src/components/MusicNFTCard.jsx b/client/src/components/MusicNFTCard.jsx
new file mode 100644
index 0000000..7654288
--- /dev/null
+++ b/client/src/components/MusicNFTCard.jsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { AptosClient, Types } from 'aptos'; // Make sure to install the Aptos SDK
+
+const nodeUrl = "https://fullnode.devnet.aptoslabs.com/v1"; // Use the correct node URL
+const client = new AptosClient(nodeUrl);
+
+const MusicNFTCard = ({ nft, onBuy, onTip }) => {
+ const { account, signAndSubmitTransaction } = useWallet();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleBuy = async () => {
+ try {
+ setIsLoading(true);
+
+ // Assuming you have a function to generate the transaction payload
+ const payload = await generateBuyTransactionPayload(account?.address, nft);
+
+ const response = await signAndSubmitTransaction(payload);
+ await client.waitForTransaction(response.hash);
+
+ onBuy(nft);
+ toast.success('NFT purchased successfully!');
+ } catch (error) {
+ console.error("Error buying NFT:", error);
+ toast.error('Error buying NFT');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleTip = async (amount) => {
+ try {
+ setIsLoading(true);
+ // Assuming you have a function to generate the tip transaction payload
+ const payload = await generateTipTransactionPayload(account?.address, nft.artist, amount);
+
+ const response = await signAndSubmitTransaction(payload);
+ await client.waitForTransaction(response.hash);
+
+ onTip(nft, amount);
+ toast.success(`Tipped ${amount} APT!`);
+ } catch (error) {
+ console.error("Error tipping artist:", error);
+ toast.error('Error tipping artist');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {/* Display NFT image, title, artist, price, etc. */}
+
+
{nft.title}
+
By: {nft.artist}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MusicNFTCard;
diff --git a/client/src/components/TipJar.jsx b/client/src/components/TipJar.jsx
new file mode 100644
index 0000000..9b2ad1b
--- /dev/null
+++ b/client/src/components/TipJar.jsx
@@ -0,0 +1,59 @@
+import React, { useState } from 'react';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { AptosClient, Types } from 'aptos'; // Make sure to install the Aptos SDK
+
+const nodeUrl = 'https://fullnode.devnet.aptoslabs.com/v1';
+const client = new AptosClient(nodeUrl);
+
+const TipJar = ({ artistAddress }) => {
+ const { connected, signAndSubmitTransaction, account } = useWallet();
+ const [tipAmount, setTipAmount] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleTip = async () => {
+ if (!connected) {
+ toast.error('Please connect your wallet to tip.');
+ return;
+ }
+
+ const amountInWei = parseInt(tipAmount, 10) * 10 ** 8; // Convert to Aptos wei (1 APT = 10^8 Octas)
+ setIsLoading(true);
+
+ try {
+ const payload: Types.TransactionPayload = {
+ type: "entry_function_payload",
+ function: "0x1::coin::transfer",
+ type_arguments: ["0x1::aptos_coin::AptosCoin"],
+ arguments: [artistAddress, amountInWei],
+ };
+ const response = await signAndSubmitTransaction(payload);
+ await client.waitForTransaction(response.hash);
+ toast.success(`Tipped ${tipAmount} APT!`);
+ } catch (error) {
+ console.error("Error tipping artist:", error);
+ toast.error('Error tipping artist');
+ } finally {
+ setTipAmount('');
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Tip Jar
+ setTipAmount(e.target.value)}
+ placeholder="Enter tip amount in APT"
+ />
+
+
+ );
+};
+
+export default TipJar;
diff --git a/client/src/index.jsx b/client/src/index.jsx
new file mode 100644
index 0000000..85e7832
--- /dev/null
+++ b/client/src/index.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App';
+import './index.css';
+import { SnackbarProvider } from 'notistack'; // For error and notification handling
+
+const rootElement = document.getElementById('root');
+const root = createRoot(rootElement);
+
+root.render(
+
+ {/* Display up to 3 snackbars at a time */}
+
+
+
+);
diff --git a/client/src/pages/ArtistPage.jsx b/client/src/pages/ArtistPage.jsx
new file mode 100644
index 0000000..271e127
--- /dev/null
+++ b/client/src/pages/ArtistPage.jsx
@@ -0,0 +1,87 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, Link } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { getArtistProfile, getNFTsByArtist, isArtistLive } from '../utils/aptos';
+import { AptosClient, Types } from 'aptos';
+import MusicNFTCard from '../components/MusicNFTCard';
+import TipJar from '../components/TipJar';
+import LiveStreamPlayer from '../components/LiveStreamPlayer';
+import FanClubChat from '../components/FanClubChat';
+
+const nodeUrl = "https://fullnode.devnet.aptoslabs.com/v1"; // Use the correct node URL
+const client = new AptosClient(nodeUrl);
+
+const ArtistPage = () => {
+ const { artistAddress } = useParams();
+ const [artistProfile, setArtistProfile] = useState(null);
+ const [nfts, setNFTs] = useState([]);
+ const [isLive, setIsLive] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const profile = await getArtistProfile(client, artistAddress);
+ if (profile) {
+ setArtistProfile(profile);
+ } else {
+ toast.error('Artist profile not found.');
+ // Handle artist not found (e.g., redirect)
+ }
+
+ const nftData = await getNFTsByArtist(client, artistAddress);
+ setNFTs(nftData);
+
+ const liveStatus = await isArtistLive(artistAddress);
+ setIsLive(liveStatus);
+ } catch (error) {
+ console.error('Error fetching artist data:', error);
+ toast.error('Error fetching data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [artistAddress]);
+
+ const handleNFTPurchase = (nft) => {
+ // Handle NFT purchase logic
+ console.log('NFT purchased:', nft);
+ };
+
+ return (
+
+ {isLoading ? (
+
Loading...
+ ) : (
+ <>
+ {artistProfile && (
+
+
{artistProfile.name}
+
{artistProfile.bio}
+
+
+ )}
+
+ {isLive &&
}
+
+
+
Music NFTs
+
+ {nfts.map((nft) => (
+
+ ))}
+
+
+ {/* Optionally, display other content like:
+
+ */}
+ >
+ )}
+
+ );
+};
+
+export default ArtistPage;
diff --git a/client/src/pages/Explore.jsx b/client/src/pages/Explore.jsx
new file mode 100644
index 0000000..f51e2ad
--- /dev/null
+++ b/client/src/pages/Explore.jsx
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { getFeaturedGenres, getFeaturedArtists } from '../utils/aptos';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css'; // Import toast styles
+
+const Explore = () => {
+ const [featuredGenres, setFeaturedGenres] = useState([]);
+ const [featuredArtists, setFeaturedArtists] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const fetchedGenres = await getFeaturedGenres();
+ setFeaturedGenres(fetchedGenres);
+
+ const fetchedArtists = await getFeaturedArtists();
+ setFeaturedArtists(fetchedArtists);
+ } catch (error) {
+ console.error('Error fetching explore data:', error);
+ toast.error('Error fetching data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ return (
+
+ {isLoading ? (
+
Loading...
+ ) : (
+ <>
+
+
Featured Genres
+
+ {featuredGenres.map((genre) => (
+ -
+ {genre.name}
+
+ ))}
+
+
+
+
+
Featured Artists
+
+ {featuredArtists.map((artist) => (
+
+ {/* Display artist card/thumbnail here */}
+
+
{artist.name}
+
+ ))}
+
+
+ >
+ )}
+
+ );
+};
+
+export default Explore;
diff --git a/client/src/pages/FanClubPage.jsx b/client/src/pages/FanClubPage.jsx
new file mode 100644
index 0000000..78a3baa
--- /dev/null
+++ b/client/src/pages/FanClubPage.jsx
@@ -0,0 +1,82 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useWallet } from '@aptos-labs/wallet-adapter-react';
+import { AptosClient, Types } from 'aptos';
+import { getFanClubInfo, getExclusiveContent, checkFanClubMembership } from '../utils/aptos';
+import FanClubChat from '../components/FanClubChat';
+
+const nodeUrl = 'https://fullnode.devnet.aptoslabs.com/v1'; // or your custom node URL
+const client = new AptosClient(nodeUrl);
+
+const FanClubPage = () => {
+ const { artistAddress } = useParams();
+ const { account } = useWallet();
+ const navigate = useNavigate();
+ const [fanClubInfo, setFanClubInfo] = useState(null);
+ const [exclusiveContent, setExclusiveContent] = useState([]);
+ const [isMember, setIsMember] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const info = await getFanClubInfo(client, artistAddress);
+ if (info) {
+ setFanClubInfo(info);
+ } else {
+ toast.error('Fan club not found.');
+ navigate('/'); // Redirect if fan club doesn't exist
+ }
+
+ // Fetch exclusive content regardless of membership status
+ const content = await getExclusiveContent(artistAddress);
+ setExclusiveContent(content);
+
+ // Check if the user is a member (only if the account is connected)
+ if (account) {
+ const membershipStatus = await checkFanClubMembership(client, account.address, artistAddress);
+ setIsMember(membershipStatus);
+ }
+ } catch (error) {
+ console.error('Error fetching fan club data:', error);
+ toast.error('Error fetching data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchData();
+ }, [artistAddress, account]); // Re-fetch data when artistAddress or account changes
+
+ return (
+
+ {isLoading ? (
+
Loading...
+ ) : (
+ <>
+ {fanClubInfo && (
+ <>
+
{fanClubInfo.name}
+
{fanClubInfo.description}
+
+ {/* Display exclusive content if the user is a member */}
+ {isMember ? (
+
+
Exclusive Content
+ {/* ... map over exclusiveContent and display each item ... */}
+
+ ) : (
+
Join the fan club to access exclusive content!
+ )}
+
+
+ >
+ )}
+ >
+ )}
+
+ );
+};
+
+export default FanClubPage;
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx
new file mode 100644
index 0000000..c073fa5
--- /dev/null
+++ b/client/src/pages/Home.jsx
@@ -0,0 +1,80 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import MusicNFTCard from '../components/MusicNFTCard';
+import LiveStreamPlayer from '../components/LiveStreamPlayer';
+import { getAllNFTs, getTrendingArtists, getFeaturedLiveStream } from '../utils/aptos';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css'; // Import toast styles
+
+const Home = () => {
+ const [nfts, setNFTs] = useState([]);
+ const [trendingArtists, setTrendingArtists] = useState([]);
+ const [featuredLiveStream, setFeaturedLiveStream] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const fetchedNFTs = await getAllNFTs();
+ setNFTs(fetchedNFTs);
+
+ const fetchedTrendingArtists = await getTrendingArtists();
+ setTrendingArtists(fetchedTrendingArtists);
+
+ const fetchedFeaturedLiveStream = await getFeaturedLiveStream();
+ setFeaturedLiveStream(fetchedFeaturedLiveStream);
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ toast.error('Error fetching data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ const handleNFTPurchase = (nft) => {
+ // Handle NFT purchase logic here
+ console.log('NFT purchased:', nft);
+ };
+
+ return (
+
+ {isLoading ? (
+
Loading...
+ ) : (
+ <>
+ {featuredLiveStream && (
+
+
Featured Live: {featuredLiveStream.artistName}
+
+
+ )}
+
+
+
Trending Artists
+
+ {trendingArtists.map((artist) => (
+ -
+ {artist.name}
+
+ ))}
+
+
+
+
+
Explore Music NFTs
+
+ {nfts.map((nft) => (
+
+ ))}
+
+
+ >
+ )}
+
+ );
+};
+
+export default Home;
diff --git a/client/src/utils/aptos.js b/client/src/utils/aptos.js
new file mode 100644
index 0000000..cbaaad3
--- /dev/null
+++ b/client/src/utils/aptos.js
@@ -0,0 +1,160 @@
+import { AptosClient, Types } from 'aptos';
+import { NODE_URL } from '../config';
+
+const client = new AptosClient(NODE_URL);
+
+// --- Artist Profile ---
+
+export const getArtistProfile = async (client, artistAddress) => {
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ const profileResource = resources.find(
+ (r) => r.type === '0x1::Coin::CoinStore<0x1::aptos_coin::AptosCoin>'
+ );
+ if (!profileResource) {
+ console.warn("Artist profile resource not found.");
+ return null;
+ }
+
+ const profileData = profileResource.data;
+ if (!profileData.name || !profileData.bio || !profileData.profile_image_url) {
+ console.error("Incomplete artist profile data.");
+ return null;
+ }
+
+ return {
+ name: profileData.name,
+ bio: profileData.bio,
+ profileImage: profileData.profile_image_url,
+ // ... (Other fields from your Artist profile resource)
+ };
+ } catch (error) {
+ console.error("Error fetching artist profile:", error);
+ throw error; // Re-throw the error for handling in the component
+ }
+};
+
+// --- NFTs ---
+
+export const getAllNFTs = async () => {
+ try {
+ const response = await fetch(`/api/nfts`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ } catch (error) {
+ console.error('Error fetching all NFTs:', error);
+ throw error;
+ }
+};
+
+export const getNFTsByArtist = async (client, artistAddress) => {
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ // Ensure the resources are actually an array
+ if (!Array.isArray(resources)) {
+ console.error("Unexpected response format: resources is not an array");
+ return [];
+ }
+
+ const nftResources = resources.filter(r =>
+ r.type.startsWith("0x3::token::TokenStore") // adjust if using a different token standard
+ );
+
+ const nfts = await Promise.all(
+ nftResources.map(async (resource) => {
+ const tokenDataId = resource.data.token_data_id;
+ const tokenData = await client.getTokenData(
+ tokenDataId.creator,
+ tokenDataId.collection,
+ tokenDataId.name
+ );
+
+ return {
+ tokenId: tokenDataId.name, // Assuming this is how you identify NFTs
+ title: tokenData.name,
+ artist: tokenDataId.creator,
+ metadata_uri: tokenData.uri,
+ // ... other fields from your NFT resource
+ };
+ })
+ );
+
+ return nfts;
+ } catch (error) {
+ console.error("Error fetching artist's NFTs:", error);
+ throw error;
+ }
+};
+
+// --- Live Stream ---
+
+export const isArtistLive = async (artistAddress) => {
+ try {
+ const response = await fetch(`/api/artists/${artistAddress}/live`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ return data.isLive || false; // Default to false if the field is missing
+ } catch (error) {
+ console.error("Error checking live status:", error);
+ return false;
+ }
+};
+
+// --- Fan Club ---
+
+export const getFanClubInfo = async (client, artistAddress) => {
+ // ... Fetch fan club info from the blockchain or your backend API
+ // Example using blockchain (assuming you have a fan club resource in your Move module):
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ const fanClubResource = resources.find(r => r.type === "0x42::fan_club::FanClub");
+ if (fanClubResource) {
+ return fanClubResource.data;
+ } else {
+ return null; // Fan club not found
+ }
+ } catch (error) {
+ console.error("Error fetching fan club info:", error);
+ return null;
+ }
+};
+
+export const getExclusiveContent = async (artistAddress) => {
+ // ... Fetch exclusive content from your backend API
+ const response = await fetch(`/api/fanclubs/${artistAddress}/exclusive-content`);
+ return response.json();
+};
+
+export const checkFanClubMembership = async (client, userAddress, artistAddress) => {
+ // ... Check if the user owns the required NFT or token
+ const nftResources = await client.getAccountResources(userAddress);
+ // check for a specific NFT
+ return nftResources.some(resource =>
+ resource.type === "0x1::token::TokenStore" &&
+ resource.data.token_data_id.creator === artistAddress
+ );
+};
+
+
+// --- Trending Artists and Genres ---
+export const getTrendingArtists = async () => {
+ // ... Fetch trending artists from your backend API
+ const response = await fetch(`/api/trending-artists`);
+ return response.json();
+};
+
+export const getFeaturedGenres = async () => {
+ // ... Fetch featured genres from your backend API
+ const response = await fetch(`/api/featured-genres`);
+ return response.json();
+};
+
+export const getFeaturedLiveStream = async () => {
+ // ... Fetch the currently featured live stream from your backend API
+ const response = await fetch('/api/live-streams/featured');
+ return response.json();
+};
\ No newline at end of file
diff --git a/client/src/utils/nft.js b/client/src/utils/nft.js
new file mode 100644
index 0000000..e17e86f
--- /dev/null
+++ b/client/src/utils/nft.js
@@ -0,0 +1,116 @@
+import { NFTStorage, File } from 'nft.storage';
+import { toast } from 'react-toastify';
+import { NODE_URL } from '../config';
+import { AptosClient, Types } from 'aptos';
+
+const client = new AptosClient(NODE_URL);
+
+const NFT_STORAGE_API_KEY = process.env.REACT_APP_NFT_STORAGE_API_KEY;
+
+// Function to store NFT data and media on IPFS
+export async function storeNFT(nft) {
+ const client = new NFTStorage({ token: NFT_STORAGE_API_KEY });
+
+ try {
+ // File Preparation (assuming Base64 encoded audio and image)
+ const musicFile = new File([convertBase64ToUint8Array(nft.audio)], 'music.mp3', {
+ type: 'audio/mpeg', // Adjust if using a different audio format
+ });
+ const imageFile = new File([convertBase64ToUint8Array(nft.image)], 'cover.jpg', {
+ type: 'image/jpeg',
+ });
+
+ // Storing on IPFS with Metadata
+ const metadata = await client.store({
+ name: nft.title,
+ description: nft.description,
+ image: imageFile,
+ properties: {
+ artist: nft.artist,
+ genre: nft.genre, // Add more properties as needed
+ },
+ animation_url: musicFile, // Optional: include the music file itself
+ });
+
+ toast.success('NFT metadata stored successfully on IPFS!');
+ return metadata.url;
+ } catch (error) {
+ console.error('Error storing NFT on IPFS:', error);
+ toast.error('Error storing NFT. Please try again later.');
+ throw error; // Re-throw for handling in the component
+ }
+}
+
+// Helper Functions
+
+function convertBase64ToUint8Array(base64String) {
+ const base64Data = base64String.replace(/^data:.*,/, ''); // Remove data URI prefix
+ const binaryString = atob(base64Data);
+ const array = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ array[i] = binaryString.charCodeAt(i);
+ }
+ return array;
+}
+
+// Store NFT data and media on IPFS
+export async function storeNFT(nft) {
+ const client = new NFTStorage({ token: NFT_STORAGE_API_KEY });
+
+ try {
+ // File Preparation
+ const musicFile = new File([convertBase64ToUint8Array(nft.audio)], 'music.mp3', { type: 'audio/mpeg' });
+ const imageFile = new File([convertBase64ToUint8Array(nft.image)], 'cover.jpg', { type: 'image/jpeg' });
+
+ // Storing on IPFS with Metadata
+ const metadata = await client.store({
+ name: nft.title,
+ description: nft.description,
+ image: imageFile,
+ properties: {
+ artist: nft.artist,
+ genre: nft.genre,
+ royaltyPercentage: nft.royaltyPercentage, // Example: '10' for 10%
+ // Add more NFT-specific properties as needed
+ },
+ animation_url: musicFile,
+ });
+ toast.success('NFT metadata stored successfully on IPFS!');
+ return metadata.url;
+ } catch (error) {
+ console.error('Error storing NFT on IPFS:', error);
+ toast.error('Error storing NFT. Please try again later.');
+ throw error;
+ }
+ }
+
+ // Check if a user owns a specific NFT
+ export async function checkNFTOwnership(userAddress, nftTokenId, artistAddress) {
+ const resourceType = `${artistAddress}::music_nft::MusicNFT`;
+ try {
+ const resources = await client.getAccountResources(userAddress);
+ return resources.some(resource => resource.type === resourceType &&
+ resource.data.token_id.token_data_id.name === nftTokenId
+ );
+ } catch (error) {
+ console.error('Error checking NFT ownership:', error);
+ return false;
+ }
+ }
+
+
+ // Fetch the royalty percentage for a specific NFT
+ export async function getNFTRoyaltyPercentage(nftTokenId, artistAddress) {
+ const resourceType = `${artistAddress}::music_nft::MusicNFT`;
+ try {
+ const nftResource = await client.getAccountResource(artistAddress, resourceType);
+ if(nftResource) {
+ return nftResource.data.royalty;
+ } else {
+ throw new Error(`Could not find NFT resource with type ${resourceType}`);
+ }
+ } catch (error) {
+ console.error('Error getting NFT royalty percentage:', error);
+ throw error;
+ }
+ }
\ No newline at end of file
diff --git a/client/src/utils/web3Storage.js b/client/src/utils/web3Storage.js
new file mode 100644
index 0000000..a9c8fcc
--- /dev/null
+++ b/client/src/utils/web3Storage.js
@@ -0,0 +1,98 @@
+import { Web3Storage, getFilesFromPath } from 'web3.storage';
+import { toast } from 'react-toastify';
+import mime from 'mime-types'; // for detecting file type
+
+// Replace with your actual Web3.Storage API token
+const WEB3_STORAGE_API_TOKEN = process.env.REACT_APP_WEB3_STORAGE_API_KEY;
+
+const client = new Web3Storage({ token: WEB3_STORAGE_API_KEY });
+
+// Store NFT data and files on Web3.Storage
+export async function storeNFT(nft) {
+ try {
+ // File Preparation
+ const files = [];
+ // Process the image file
+ if(nft.image) {
+ const imgType = mime.lookup(nft.image);
+ if(imgType && imgType.startsWith("image/")) {
+ const imageFile = new File([convertBase64ToUint8Array(nft.image)], `cover.${mime.extension(imgType)}`, { type: imgType });
+ files.push(imageFile);
+ } else {
+ console.warn("Invalid image file type.");
+ }
+ }
+
+ // Process the audio file
+ if(nft.audio) {
+ const audioType = mime.lookup(nft.audio);
+ if(audioType && audioType.startsWith("audio/")) {
+ const audioFile = new File([convertBase64ToUint8Array(nft.audio)], `music.${mime.extension(audioType)}`, { type: audioType });
+ files.push(audioFile);
+ } else {
+ console.warn("Invalid audio file type.");
+ }
+ }
+
+ // Storing on Web3.Storage with Metadata
+ const metadata = {
+ name: nft.title,
+ description: nft.description,
+ image: nft.image ? `ipfs://${cid}/${files[0].name}` : undefined, // Ensure image exists
+ properties: {
+ artist: nft.artist,
+ genre: nft.genre,
+ royaltyPercentage: nft.royaltyPercentage,
+ },
+ animation_url: nft.audio ? `ipfs://${cid}/${files[1].name}` : undefined, // Ensure audio exists
+ // Add other relevant NFT metadata properties
+ };
+
+ const cid = await client.put(files, { wrapWithDirectory: false }); // Store files
+ metadata.image = `ipfs://${cid}/${files[0].name}`; // Construct metadata URI for image
+ metadata.animation_url = `ipfs://${cid}/${files[1].name}`; // Construct metadata URI for audio
+
+ // Store metadata separately
+ const metadataFile = new File([JSON.stringify(metadata)], 'metadata.json', { type: 'application/json' });
+ await client.put([metadataFile], { wrapWithDirectory: false });
+
+ const metadataUri = `ipfs://${cid}/metadata.json`; // Construct metadata URI
+ return metadataUri;
+ } catch (error) {
+ console.error('Error storing NFT on Web3.Storage:', error);
+ toast.error('Error storing NFT. Please try again later.');
+ throw error;
+ }
+}
+
+// Retrieve NFT metadata from Web3.Storage
+export async function retrieveNFTMetadata(metadataUri) {
+ try {
+ const res = await client.get(metadataUri.replace('ipfs://', ''));
+ if (res.ok) {
+ const files = await res.files();
+ const metadataFile = files.find(file => file.name === 'metadata.json');
+ if (!metadataFile) {
+ throw new Error('Metadata file not found in the response.');
+ }
+ const text = await metadataFile.text();
+ return JSON.parse(text);
+ } else {
+ throw new Error(`failed to get ${metadataUri}: ${res.status}`);
+ }
+ } catch (error) {
+ console.error('Error retrieving NFT metadata:', error);
+ throw error;
+ }
+}
+
+// Helper function to convert Base64 to Uint8Array
+function convertBase64ToUint8Array(base64String) {
+ const base64Data = base64String.replace(/^data:.*,/, '');
+ const binaryString = atob(base64Data);
+ const array = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ array[i] = binaryString.charCodeAt(i);
+ }
+ return array;
+}
diff --git a/contracts/fractional_ownership.move b/contracts/fractional_ownership.move
new file mode 100644
index 0000000..0a39dc9
--- /dev/null
+++ b/contracts/fractional_ownership.move
@@ -0,0 +1,85 @@
+module socialfi_music_dapp::fractional_ownership {
+ use std::signer;
+ use std::vector;
+ use aptos_framework::coin::{Coin};
+ use aptos_framework::token::{Token, TokenId};
+ use aptos_std::table::{Table};
+ use socialfi_music_dapp::music_nft::{Self, MusicNFT};
+ use socialfi_music_dapp::royalty_distribution::{Self, Royalty};
+
+
+ struct FractionalNFT has key, store {
+ token_id: TokenId,
+ original_nft_id: TokenId,
+ total_supply: u64, // Total number of fractional shares
+ fractional_royalty: Royalty,
+ shareholders: Table, // Track shares per address
+ }
+
+ struct FractionalNFTData has key {
+ fractional_nft_table: Table,
+ }
+
+ public entry fun create_fractional_nft(
+ account: &signer,
+ original_nft_id: TokenId,
+ total_supply: u64,
+ royalty_numerator: u64,
+ royalty_denominator: u64
+ ) acquires MusicNFT, FractionalNFTData {
+ let collection_name = string::utf8(b"FractionalMusicNFT");
+ let description = string::utf8(b"Fractional Music NFT collection");
+ let token_id = Token::create_collection_script(
+ account,
+ collection_name,
+ description,
+ total_supply
+ );
+
+ let fractional_nft = FractionalNFT {
+ token_id,
+ original_nft_id,
+ total_supply,
+ fractional_royalty: Royalty { numerator: royalty_numerator, denominator: royalty_denominator },
+ shareholders: table::new(),
+ };
+
+ // Update the original MusicNFT
+ let original_nft = borrow_global_mut(@socialfi_music_dapp).nft_table.borrow_mut(&original_nft_id);
+ original_nft.fractional_nft_id = Option::some(token_id);
+
+ borrow_global_mut(signer::address_of(account)).fractional_nft_table.add(token_id, fractional_nft);
+ }
+
+ // Function to buy shares of a fractional NFT
+ public entry fun buy_shares(
+ account: &signer,
+ token_id: TokenId,
+ shares: u64
+ ) acquires FractionalNFTData {
+ let fractional_nft = borrow_global_mut(signer::address_of(account)).fractional_nft_table.borrow_mut(&token_id);
+
+ // Ensure enough shares are available
+ assert!(fractional_nft.total_supply >= shares, "Not enough shares available");
+
+ // Update the fractional NFT data
+ fractional_nft.total_supply = fractional_nft.total_supply - shares;
+
+ // Update the shareholder's balance
+ let shareholders = &mut fractional_nft.shareholders;
+ if (!shareholders.contains(&signer::address_of(account))) {
+ shareholders.add(signer::address_of(account), 0);
+ };
+ let balance = shareholders.borrow_mut(&signer::address_of(account));
+ *balance = *balance + shares;
+ }
+
+ // Function to distribute royalties to shareholders
+ public entry fun distribute_royalties(
+ account: &signer,
+ token_id: TokenId,
+ amount: u64
+ ) acquires FractionalNFTData {
+ // ... (Logic similar to royalty_distribution.move but for multiple shareholders)
+ }
+}
diff --git a/contracts/music_nft.move b/contracts/music_nft.move
new file mode 100644
index 0000000..b74ab60
--- /dev/null
+++ b/contracts/music_nft.move
@@ -0,0 +1,96 @@
+module socialfi_music_dapp::music_nft {
+ use std::signer;
+ use std::string::{String};
+ use std::vector;
+ use aptos_framework::token::{Token, TokenId};
+ use aptos_std::table::{Table};
+ use socialfi_music_dapp::royalty_distribution::{Self, Royalty};
+ use socialfi_music_dapp::fractional_ownership::{Self, FractionalNFT};
+
+ // Define the MusicNFT resource
+ struct MusicNFT has key {
+ token_id: TokenId,
+ artist: address,
+ title: String,
+ metadata_uri: String,
+ royalty: Royalty,
+ fractional_nft_id: Option,
+ }
+
+ struct NFTCounter has key {
+ counter: u64,
+ }
+
+ // Storing MusicNFT
+ struct NFTData has key {
+ nft_table: Table,
+ }
+
+ // Table to store NFTCounter
+ struct NFTCounterData has key {
+ counter_table: Table,
+ }
+
+ fun init_module(account: &signer) {
+ move_to(account, NFTData { nft_table: table::new() });
+ move_to(account, NFTCounterData { counter_table: table::new() });
+ }
+
+ // Creates a new MusicNFT
+ public entry fun create_nft(
+ account: &signer,
+ artist: address,
+ title: String,
+ metadata_uri: String,
+ royalty_numerator: u64,
+ royalty_denominator: u64
+ ) acquires NFTCounterData {
+ let collection_name = string::utf8(b"MusicNFT");
+ let description = string::utf8(b"Music NFT collection");
+ let nft_counter = get_nft_counter(artist);
+ let token_id = Token::create_named_token(
+ account,
+ collection_name,
+ title,
+ description,
+ nft_counter.counter,
+ 1,
+ metadata_uri,
+ artist
+ );
+ increment_nft_counter(artist);
+
+ let nft = MusicNFT {
+ token_id,
+ artist,
+ title,
+ metadata_uri,
+ royalty: Royalty { numerator: royalty_numerator, denominator: royalty_denominator },
+ fractional_nft_id: Option::none(),
+ };
+
+ borrow_global_mut(signer::address_of(account)).nft_table.add(token_id, nft);
+ }
+
+ public entry fun set_fractional_nft_id(
+ account: &signer,
+ token_id: TokenId,
+ fractional_nft_id: TokenId
+ ) acquires NFTData {
+ let nft = borrow_global_mut(signer::address_of(account)).nft_table.borrow_mut(&token_id);
+ nft.fractional_nft_id = Option::some(fractional_nft_id);
+ }
+
+ // Helper functions to manage NFT counter
+ fun get_nft_counter(artist: address): &NFTCounter acquires NFTCounterData {
+ if (!borrow_global(@socialfi_music_dapp).counter_table.contains(&artist)) {
+ table::add(&mut borrow_global_mut(@socialfi_music_dapp).counter_table, artist, NFTCounter { counter: 0 });
+ };
+ borrow_global(@socialfi_music_dapp).counter_table.borrow(&artist)
+ }
+
+ fun increment_nft_counter(artist: address) acquires NFTCounterData {
+ let nft_counter = borrow_global_mut(@socialfi_music_dapp).counter_table.borrow_mut(&artist);
+ nft_counter.counter = nft_counter.counter + 1;
+ }
+}
diff --git a/contracts/royalty_distribution.move b/contracts/royalty_distribution.move
new file mode 100644
index 0000000..3c86fc0
--- /dev/null
+++ b/contracts/royalty_distribution.move
@@ -0,0 +1,58 @@
+module socialfi_music_dapp::royalty_distribution {
+ use std::signer;
+ use aptos_framework::coin::{Coin};
+ use aptos_std::table::{Table};
+ use aptos_framework::account::{withdraw, deposit};
+ use aptos_framework::token::{TokenId};
+ use socialfi_music_dapp::music_nft::{Self, MusicNFT};
+
+ // Define the Royalty struct
+ struct Royalty has store {
+ numerator: u64,
+ denominator: u64,
+ }
+
+ // Table to store balances for each address
+ struct BalanceData has key {
+ balance_table: Table>
+ }
+
+ struct CoinType {
+ // Place holder for your coin type
+ }
+
+ // Distribute royalties based on sales/streaming events
+ public entry fun distribute_royalties(
+ account: &signer,
+ token_id: TokenId,
+ amount: u64,
+ ) acquires MusicNFT, BalanceData {
+ let nft = borrow_global(@socialfi_music_dapp).nft_table.borrow(&token_id);
+
+ let artist = nft.artist;
+ let royalty = nft.royalty;
+
+ let artist_share = calculate_royalty_share(amount, royalty);
+
+ let artist_coin = withdraw(account, artist_share);
+
+ deposit(artist, artist_coin);
+
+ // Store the updated balance in the table
+ let balance_table = &mut borrow_global_mut(signer::address_of(account)).balance_table;
+ if (!balance_table.contains(&artist)) {
+ balance_table.add(artist, Coin::new(0));
+ };
+ let artist_balance = balance_table.borrow_mut(&artist);
+ artist_balance.value = artist_balance.value + artist_share;
+ }
+
+ // Helper function to calculate the artist's share of the royalties
+ fun calculate_royalty_share(amount: u64, royalty: Royalty): u64 {
+ (amount * royalty.numerator) / royalty.denominator
+ }
+
+ public entry fun initialize_balance_data(account: &signer) {
+ move_to(account, BalanceData { balance_table: table::new() });
+ }
+}
diff --git a/contracts/tip_jar.move b/contracts/tip_jar.move
new file mode 100644
index 0000000..6a1c32c
--- /dev/null
+++ b/contracts/tip_jar.move
@@ -0,0 +1,45 @@
+module socialfi_music_dapp::tip_jar {
+ use std::signer;
+ use aptos_framework::coin::{Coin};
+ use aptos_std::table::{Table};
+ use aptos_framework::account::{withdraw, deposit};
+
+ struct TipJar has key {
+ tips: Table>, // Artist address to tip amount
+ }
+
+ struct CoinType {
+ // Place holder for your coin type
+ }
+
+ // Tip an artist
+ public entry fun tip_artist(
+ account: &signer,
+ artist: address,
+ amount: u64,
+ ) acquires TipJar {
+ let tip_jar = borrow_global_mut(@socialfi_music_dapp);
+ if (!tip_jar.tips.contains(&artist)) {
+ tip_jar.tips.add(artist, Coin::new(0));
+ };
+ let artist_tips = tip_jar.tips.borrow_mut(&artist);
+
+ let coins = withdraw(account, amount);
+ deposit(artist, coins);
+ artist_tips.value = artist_tips.value + amount;
+ }
+
+ // Get the total amount of tips an artist has received
+ public fun get_artist_tips(artist: address): u64 acquires TipJar {
+ let tip_jar = borrow_global(@socialfi_music_dapp);
+ if (tip_jar.tips.contains(&artist)) {
+ tip_jar.tips.borrow(&artist).value
+ } else {
+ 0
+ }
+ }
+
+ public entry fun initialize_tip_jar(account: &signer) {
+ move_to(account, TipJar { tips: table::new() });
+ }
+}
diff --git a/server/src/app.js b/server/src/app.js
new file mode 100644
index 0000000..d63b144
--- /dev/null
+++ b/server/src/app.js
@@ -0,0 +1,41 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const { AptosClient, Types } = require('aptos');
+require('dotenv').config();
+
+const app = express();
+const port = process.env.PORT || 3001; // You can change this port
+
+// Middleware
+app.use(cors());
+app.use(helmet());
+app.use(express.json());
+
+// Aptos Client (connect to Aptos fullnode)
+const nodeUrl = process.env.APTOS_NODE_URL || "https://fullnode.devnet.aptoslabs.com/v1";
+const client = new AptosClient(nodeUrl);
+
+// Routes
+const artistRoutes = require('./routes/artists');
+const nftRoutes = require('./routes/nfts');
+const fanclubRoutes = require('./routes/fanclubs');
+const liveStreamRoutes = require('./routes/live-streams');
+// const authRoutes = require('./routes/auth'); // Uncomment if you have authentication
+
+app.use('/api/artists', artistRoutes(client));
+app.use('/api/nfts', nftRoutes(client));
+app.use('/api/fanclubs', fanclubRoutes(client));
+app.use('/api/live-streams', liveStreamRoutes(client));
+// app.use('/api/auth', authRoutes(client));
+
+// Error handling middleware (example)
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: 'Internal Server Error' });
+});
+
+// Start the server
+app.listen(port, () => {
+ console.log(`Server listening at http://localhost:${port}`);
+});
diff --git a/server/src/controllers/artists.js b/server/src/controllers/artists.js
new file mode 100644
index 0000000..304a57f
--- /dev/null
+++ b/server/src/controllers/artists.js
@@ -0,0 +1,88 @@
+const { AptosClient, Types } = require("aptos");
+
+// Function to get artist profile from the blockchain
+exports.getArtistProfile = async (client, artistAddress) => {
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ const profileResource = resources.find(
+ (r) => r.type === '0x1::Coin::CoinStore<0x1::aptos_coin::AptosCoin>'
+ );
+ if (!profileResource) {
+ return null; // Artist profile not found
+ }
+
+ const profileData = profileResource.data;
+
+ // Ensure the data has the required fields
+ if (!profileData.name || !profileData.bio || !profileData.profile_image_url) {
+ throw new Error("Incomplete artist profile data");
+ }
+
+ return {
+ address: artistAddress,
+ name: profileData.name,
+ bio: profileData.bio,
+ profileImage: profileData.profile_image_url,
+ // ... (Other fields from your Artist profile resource)
+ };
+
+ } catch (error) {
+ console.error("Error fetching artist profile:", error);
+ throw error; // Re-throw for the error handler middleware
+ }
+};
+
+// Function to get an artist's NFTs
+exports.getArtistNFTs = async (client, artistAddress) => {
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ const nftResources = resources.filter(
+ (r) => r.type.startsWith(`${artistAddress}::music_nft::MusicNFT`)
+ );
+
+ const nfts = await Promise.all(
+ nftResources.map(async (resource) => {
+ const token = resource.data;
+ const collectionData = await client.getTokenData(
+ token.token_id.token_data_id.creator,
+ token.token_id.token_data_id.collection,
+ token.token_id.token_data_id.name
+ );
+ const metadataUri = collectionData.uri;
+ const metadata = await client.getTableItem(
+ token.token_id.token_data_id.creator,
+ Types.MoveStructTag.fromString(`${artistAddress}::music_nft::NFTData`),
+ Types.MoveStructTag.fromString("0x1::string::String"), // Key Type
+ metadataUri // Value
+ );
+ return {
+ tokenId: token.token_id.token_data_id.name,
+ title: token.title,
+ artist: token.artist,
+ metadataUri: metadataUri,
+ royalty: token.royalty,
+ fractionalNFTId: token.fractional_nft_id,
+ ...metadata
+ };
+ })
+ );
+ return nfts;
+ } catch (error) {
+ console.error("Error fetching artist's NFTs:", error);
+ throw error;
+ }
+};
+
+// Function to check if an artist is live streaming (you'll need to implement this based on your project's requirements)
+exports.isArtistLive = async (client, artistAddress) => {
+ const resourceType = `${artistAddress}::live_stream::LiveStream`;
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ return resources.some(r => r.type === resourceType);
+ } catch (error) {
+ console.error("Error checking live status:", error);
+ throw error;
+ }
+};
+
+// ... (You can add more artist-related functions here as needed)
diff --git a/server/src/controllers/auth.js b/server/src/controllers/auth.js
new file mode 100644
index 0000000..e69de29
diff --git a/server/src/controllers/fanclubs.js b/server/src/controllers/fanclubs.js
new file mode 100644
index 0000000..9c5f6b5
--- /dev/null
+++ b/server/src/controllers/fanclubs.js
@@ -0,0 +1,156 @@
+const { AptosClient, Types } = require('aptos');
+const { retrieveNFTMetadata } = require('../utils/web3Storage');
+
+// Get fan club information
+exports.getFanClubInfo = async (client, artistAddress) => {
+ try {
+ const resources = await client.getAccountResources(artistAddress);
+ const fanClubResource = resources.find(
+ (r) => r.type === `${artistAddress}::fan_club::FanClub`
+ );
+ if (!fanClubResource) {
+ return null; // Fan club not found
+ }
+
+ const fanClubData = fanClubResource.data;
+
+ // Ensure the data has the required fields
+ if (!fanClubData.name || !fanClubData.description || !fanClubData.nft_token) {
+ throw new Error("Incomplete fan club data");
+ }
+
+ const nftInfo = await getNFTInfo(client, artistAddress, fanClubData.nft_token);
+
+ return {
+ name: fanClubData.name,
+ description: fanClubData.description,
+ nftToken: fanClubData.nft_token,
+ nftInfo: nftInfo,
+ // ... (Other fields from your FanClub resource)
+ };
+ } catch (error) {
+ console.error('Error fetching fan club info:', error);
+ throw error;
+ }
+};
+
+// Fetch NFT info by token ID and artist address
+const getNFTInfo = async (client, artistAddress, nftTokenId) => {
+ try {
+ const resourceType = `${artistAddress}::music_nft::MusicNFT`;
+ const nftResource = await client.getAccountResource(artistAddress, resourceType, {
+ token_id: {
+ token_data_id: {
+ creator: artistAddress,
+ collection: "MusicNFT", // Make sure this is your collection name
+ name: nftTokenId,
+ },
+ property_version: "0",
+ },
+ });
+ if (!nftResource) {
+ return null; // NFT not found
+ }
+
+ const token = nftResource.data;
+ const collectionData = await client.getTokenData(
+ token.token_id.token_data_id.creator,
+ token.token_id.token_data_id.collection,
+ token.token_id.token_data_id.name
+ );
+ const metadataUri = collectionData.uri;
+ const metadata = await retrieveNFTMetadata(metadataUri);
+
+ return {
+ tokenId: token.token_id.token_data_id.name,
+ title: token.title,
+ artist: token.artist,
+ metadataUri: metadataUri,
+ royalty: token.royalty,
+ fractionalNFTId: token.fractional_nft_id,
+ ...metadata
+ };
+ } catch (error) {
+ console.error("Error fetching NFT by ID:", error);
+ return null;
+ }
+};
+
+// Get exclusive content for a fan club
+exports.getExclusiveContent = async (client, artistAddress) => {
+ try {
+ const fanClubResource = await client.getAccountResource(
+ artistAddress,
+ `${artistAddress}::fan_club::FanClub`,
+ );
+
+ if (!fanClubResource) {
+ return null; // Fan club not found
+ }
+
+ const exclusiveContentIds = fanClubResource.data.exclusive_content; // Assuming a list of content IDs
+
+ // Fetch the content objects based on the IDs (replace with your implementation)
+ const exclusiveContent = exclusiveContentIds.map(contentId => {
+ // Fetch content from storage or wherever it's stored based on contentId
+ });
+
+ return exclusiveContent;
+ } catch (error) {
+ console.error('Error fetching exclusive content:', error);
+ throw error;
+ }
+};
+
+// Get messages for a fan club's chat
+exports.getFanClubMessages = async (artistAddress, client) => {
+ try {
+ const storedMessages = await client.getTableItem(
+ artistAddress,
+ Types.MoveStructTag.fromString(`${artistAddress}::message_board::MessageBoard`),
+ Types.MoveStructTag.fromString("0x1::string::String"),
+ "messages"
+ );
+ if(storedMessages) {
+ return JSON.parse(storedMessages);
+ } else {
+ return [];
+ }
+ } catch (error) {
+ console.error('Error fetching messages:', error);
+ throw error;
+ }
+};
+
+// Post a new message to a fan club's chat
+exports.postFanClubMessage = async (artistAddress, message, client, senderAddress) => {
+ try {
+ const storedMessages = await client.getTableItem(
+ artistAddress,
+ Types.MoveStructTag.fromString(`${artistAddress}::message_board::MessageBoard`),
+ Types.MoveStructTag.fromString("0x1::string::String"),
+ "messages"
+ );
+
+ let newMessages = [];
+ if (storedMessages) {
+ newMessages = JSON.parse(storedMessages);
+ }
+
+ newMessages.push({ sender: senderAddress, content: message });
+
+ const payload = {
+ type: "entry_function_payload",
+ function: `${artistAddress}::message_board::set_messages`, // Correct function name
+ type_arguments: [],
+ arguments: [JSON.stringify(newMessages)],
+ };
+
+ // Use the currently connected account
+ const txnHash = await client.signAndSubmitTransaction(senderAddress, payload);
+ await client.waitForTransaction(txnHash);
+ } catch (error) {
+ console.error('Error posting message:', error);
+ throw error;
+ }
+};
diff --git a/server/src/controllers/live-streams.js b/server/src/controllers/live-streams.js
new file mode 100644
index 0000000..e69de29
diff --git a/server/src/controllers/nfts.js b/server/src/controllers/nfts.js
new file mode 100644
index 0000000..286ce8d
--- /dev/null
+++ b/server/src/controllers/nfts.js
@@ -0,0 +1,104 @@
+const { AptosClient, Types } = require('aptos');
+const { retrieveNFTMetadata } = require('../utils/web3Storage');
+
+exports.getAllNFTs = async (client, query) => {
+ try {
+ const { creatorAddress } = query;
+
+ if (creatorAddress) {
+ const account = creatorAddress;
+ const resources = await client.getAccountResources(account);
+ const nftResources = resources.filter(
+ (r) => r.type.startsWith(`${creatorAddress}::music_nft::MusicNFT`)
+ );
+ const nfts = await Promise.all(nftResources.map(processNFTResource(client)));
+ return nfts.filter(nft => nft !== null); // Remove null (invalid) NFTs
+ } else {
+ // Implement logic to fetch ALL NFTs if creatorAddress is not provided
+ // This would likely involve querying across multiple artist addresses
+ // or using a different method to aggregate NFT data on your backend.
+ throw new Error("Fetching all NFTs without a creator address is not currently supported.");
+ }
+ } catch (error) {
+ console.error('Error fetching NFTs:', error);
+ throw new Error("Failed to fetch NFTs."); // Generic error for higher layers
+ }
+};
+
+// helper function to process an NFT resource
+async function processNFTResource(client, resource) {
+ const token = resource.data;
+ try {
+ const collectionData = await client.getTokenData(
+ token.token_id.token_data_id.creator,
+ token.token_id.token_data_id.collection,
+ token.token_id.token_data_id.name
+ );
+
+ if (!collectionData) {
+ console.error(`Token data not found for NFT: ${token.token_id.token_data_id.name}`);
+ return null;
+ }
+
+ const metadataUri = collectionData.uri;
+ const metadata = await retrieveNFTMetadata(metadataUri);
+
+ if (!metadata) {
+ console.error(`Metadata not found for NFT: ${token.token_id.token_data_id.name}`);
+ return null;
+ }
+
+ return {
+ tokenId: token.token_id.token_data_id.name,
+ title: token.title,
+ artist: token.artist,
+ metadataUri: metadataUri,
+ royalty: token.royalty,
+ fractionalNFTId: token.fractional_nft_id,
+ ...metadata,
+ };
+ } catch (error) {
+ console.error(`Error processing NFT resource: ${resource.type}`, error);
+ return null;
+ }
+}
+
+
+exports.getNFTById = async (client, nftId, artistAddress) => {
+ try {
+ const resourceType = `${artistAddress}::music_nft::MusicNFT`;
+ const nftResource = await client.getAccountResource(artistAddress, resourceType, {
+ token_id: {
+ token_data_id: {
+ creator: artistAddress,
+ collection: "MusicNFT", // collection name
+ name: nftId, // token name
+ },
+ property_version: "0",
+ },
+ });
+
+ if (!nftResource) {
+ return null; // NFT not found
+ }
+ return processNFTResource(client, nftResource);
+ } catch (error) {
+ console.error("Error fetching NFT by ID:", error);
+ throw error;
+ }
+};
+
+// Function to get buy events for an NFT (implement based on your smart contract)
+exports.getNFTBuyEvents = async (client, nftId) => {
+ try {
+ const events = await client.getEventsByEventHandle(
+ "0x1", // Replace with the address where your contract is deployed
+ "0x1::music_nft::BuyEvents",
+ nftId // Pass the token ID as the field name
+ );
+ return events;
+ } catch (error) {
+ console.error("Error getting NFT buy events:", error);
+ throw error;
+ }
+};
diff --git a/server/src/index.js b/server/src/index.js
new file mode 100644
index 0000000..e6eb22e
--- /dev/null
+++ b/server/src/index.js
@@ -0,0 +1,38 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const { AptosClient, Types } = require('aptos');
+const errorHandler = require('./middleware/errorHandler'); // Custom error handling middleware
+
+require('dotenv').config();
+
+const app = express();
+const port = process.env.PORT || 3001;
+const nodeUrl = process.env.APTOS_NODE_URL || "https://fullnode.devnet.aptoslabs.com/v1";
+const client = new AptosClient(nodeUrl);
+
+// Middleware
+app.use(cors());
+app.use(helmet());
+app.use(express.json());
+
+// Routes
+const artistRoutes = require('./routes/artists');
+const nftRoutes = require('./routes/nfts');
+const fanclubRoutes = require('./routes/fanclubs');
+const liveStreamRoutes = require('./routes/live-streams');
+// const authRoutes = require('./routes/auth'); // Uncomment if using authentication
+
+// Apply routes
+app.use('/api/artists', artistRoutes(client));
+app.use('/api/nfts', nftRoutes(client));
+app.use('/api/fanclubs', fanclubRoutes(client));
+app.use('/api/live-streams', liveStreamRoutes(client));
+// app.use('/api/auth', authRoutes(client)); // Uncomment if using authentication
+
+// Error handling middleware
+app.use(errorHandler);
+
+app.listen(port, () => {
+ console.log(`Server listening on port ${port}`);
+});
diff --git a/server/src/middleware/auth.js b/server/src/middleware/auth.js
new file mode 100644
index 0000000..e69de29
diff --git a/server/src/middleware/errorHandler.js b/server/src/middleware/errorHandler.js
new file mode 100644
index 0000000..e69de29
diff --git a/server/src/routes/artists.js b/server/src/routes/artists.js
new file mode 100644
index 0000000..8ead453
--- /dev/null
+++ b/server/src/routes/artists.js
@@ -0,0 +1,51 @@
+const express = require('express');
+const router = express.Router();
+const artistController = require('../controllers/artists'); // Assuming this file exists
+
+// Get artist profile (including basic info, NFTs, etc.)
+router.get('/:artistAddress', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const artistProfile = await artistController.getArtistProfile(req.app.locals.aptosClient, artistAddress);
+ if (!artistProfile) {
+ return res.status(404).json({ error: 'Artist not found' });
+ }
+ res.json(artistProfile);
+ } catch (error) {
+ next(error); // Pass errors to the error handling middleware
+ }
+});
+
+// Get all NFTs created by an artist
+router.get('/:artistAddress/nfts', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const artistNFTs = await artistController.getArtistNFTs(req.app.locals.aptosClient, artistAddress);
+ res.json(artistNFTs);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get live stream status of an artist (you need to implement this in the controller)
+router.get('/:artistAddress/live', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const isLive = await artistController.isArtistLive(artistAddress);
+ res.json({ isLive });
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = (client) => {
+ // Make the Aptos client available to all routes in this file
+ router.use((req, res, next) => {
+ req.app.locals.aptosClient = client;
+ next();
+ });
+ return router;
+};
diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js
new file mode 100644
index 0000000..e0172bf
--- /dev/null
+++ b/server/src/routes/auth.js
@@ -0,0 +1,33 @@
+const express = require('express');
+const router = express.Router();
+const authController = require('../controllers/auth'); // Assuming you have an auth controller
+
+// Sign up route
+router.post('/signup', async (req, res, next) => {
+ try {
+ const { address, signature } = req.body;
+ const token = await authController.signup(address, signature); // Implement in your controller
+ res.json({ token });
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Log in route
+router.post('/login', async (req, res, next) => {
+ try {
+ const { address, signature } = req.body;
+ const token = await authController.login(address, signature); // Implement in your controller
+ res.json({ token });
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = (client) => {
+ router.use((req, res, next) => {
+ req.app.locals.aptosClient = client; // Attach Aptos client
+ next();
+ });
+ return router;
+};
diff --git a/server/src/routes/fanclubs.js b/server/src/routes/fanclubs.js
new file mode 100644
index 0000000..2c586ca
--- /dev/null
+++ b/server/src/routes/fanclubs.js
@@ -0,0 +1,65 @@
+const express = require('express');
+const router = express.Router();
+const fanclubController = require('../controllers/fanclubs'); // Assuming this file exists
+const { Types } = require("aptos");
+const { authenticate, authorizeFanClubMember } = require('../middleware/auth'); // Add your auth middleware
+
+// Get fan club information by artist address
+router.get('/:artistAddress', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const fanClubInfo = await fanclubController.getFanClubInfo(req.app.locals.aptosClient, artistAddress);
+ if (!fanClubInfo) {
+ return res.status(404).json({ error: 'Fan club not found' });
+ }
+ res.json(fanClubInfo);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get exclusive content for a fan club
+router.get('/:artistAddress/exclusive-content', authenticate, authorizeFanClubMember, async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const exclusiveContent = await fanclubController.getExclusiveContent(req.app.locals.aptosClient, artistAddress);
+ res.json(exclusiveContent);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get messages for a fan club's chat
+router.get('/:artistAddress/messages', authenticate, authorizeFanClubMember, async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const messages = await fanclubController.getFanClubMessages(artistAddress, req.app.locals.aptosClient);
+ res.json(messages);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Post a new message to a fan club's chat
+router.post('/:artistAddress/messages', authenticate, authorizeFanClubMember, async (req, res, next) => {
+ const { artistAddress } = req.params;
+ const { message } = req.body;
+
+ try {
+ await fanclubController.postFanClubMessage(artistAddress, message, req.app.locals.aptosClient, req.user.address);
+ res.sendStatus(201); // Created
+ } catch (error) {
+ next(error);
+ }
+});
+
+module.exports = (client) => {
+ router.use((req, res, next) => {
+ req.app.locals.aptosClient = client;
+ next();
+ });
+ return router;
+};
diff --git a/server/src/routes/live-streams.js b/server/src/routes/live-streams.js
new file mode 100644
index 0000000..13ebe10
--- /dev/null
+++ b/server/src/routes/live-streams.js
@@ -0,0 +1,52 @@
+const express = require('express');
+const router = express.Router();
+const liveStreamController = require('../controllers/live-streams'); // Assuming this file exists
+
+// Get live stream status of an artist (could be based on blockchain events or external API)
+router.get('/:artistAddress', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const isLive = await liveStreamController.isArtistLive(req.app.locals.aptosClient, artistAddress);
+ res.json({ isLive });
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get the stream URL for an artist (if live and authorized)
+router.get('/:artistAddress/stream', async (req, res, next) => {
+ const { artistAddress } = req.params;
+
+ try {
+ const streamUrl = await liveStreamController.getStreamUrl(req.app.locals.aptosClient, artistAddress);
+ if (!streamUrl) {
+ return res.status(404).json({ error: 'Stream not found or not live' });
+ }
+ res.json({ streamUrl });
+ } catch (error) {
+ next(error);
+ }
+});
+
+
+// Get a featured live stream (e.g., most popular/recent)
+router.get('/featured', async (req, res, next) => {
+ try {
+ const featuredStream = await liveStreamController.getFeaturedLiveStream(req.app.locals.aptosClient);
+ res.json(featuredStream);
+ } catch (error) {
+ next(error);
+ }
+});
+
+
+// ... other live stream related routes (e.g., start/stop stream, etc.)
+
+module.exports = (client) => {
+ router.use((req, res, next) => {
+ req.app.locals.aptosClient = client; // Attach Aptos client
+ next();
+ });
+ return router;
+};
diff --git a/server/src/routes/nfts.js b/server/src/routes/nfts.js
new file mode 100644
index 0000000..8963cac
--- /dev/null
+++ b/server/src/routes/nfts.js
@@ -0,0 +1,92 @@
+const express = require('express');
+const router = express.Router();
+const nftController = require('../controllers/nfts');
+const { Types } = require("aptos");
+
+// Get all NFTs in the marketplace (with pagination/filtering options if needed)
+router.get('/', async (req, res, next) => {
+ try {
+ const { creatorAddress } = req.query;
+
+ if (creatorAddress) {
+ const account = creatorAddress;
+ const resources = await req.app.locals.aptosClient.getAccountResources(account);
+
+ const nftResources = resources.filter(
+ (r) => r.type.startsWith(`${creatorAddress}::music_nft::MusicNFT`)
+ );
+
+ const nfts = await Promise.all(
+ nftResources.map(async (resource) => {
+ const token = resource.data;
+ const collectionData = await req.app.locals.aptosClient.getTokenData(
+ token.token_id.token_data_id.creator,
+ token.token_id.token_data_id.collection,
+ token.token_id.token_data_id.name
+ );
+ const metadataUri = collectionData.uri;
+ const metadata = await req.app.locals.aptosClient.getTableItem(
+ token.token_id.token_data_id.creator,
+ Types.MoveStructTag.fromString(`${creatorAddress}::music_nft::NFTData`),
+ Types.MoveStructTag.fromString("0x1::string::String"), // Key Type
+ metadataUri // Value
+ );
+ return {
+ tokenId: token.token_id.token_data_id.name,
+ title: token.title,
+ artist: token.artist,
+ metadataUri: metadataUri,
+ royalty: token.royalty,
+ fractionalNFTId: token.fractional_nft_id,
+ ...metadata
+ };
+ })
+ );
+ return res.json(nfts);
+ } else {
+ // Fetch all NFTs logic when creatorAddress is not specified
+ const nfts = await nftController.getAllNFTs(req.app.locals.aptosClient, req.query);
+ res.json(nfts);
+ }
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get a specific NFT by its ID (along with its owner's address)
+router.get('/:nftId', async (req, res, next) => {
+ const { nftId } = req.params;
+
+ try {
+ const nft = await nftController.getNFTById(req.app.locals.aptosClient, nftId);
+ if (!nft) {
+ return res.status(404).json({ error: 'NFT not found' });
+ }
+ res.json(nft);
+ } catch (error) {
+ next(error);
+ }
+});
+
+// Get all buy events for an NFT
+router.get('/:nftId/buy-events', async (req, res, next) => {
+ const { nftId } = req.params;
+
+ try {
+ const buyEvents = await nftController.getNFTBuyEvents(req.app.locals.aptosClient, nftId);
+ res.json(buyEvents);
+ } catch (error) {
+ next(error);
+ }
+});
+
+
+// ... other NFT-related routes (e.g., get royalty info, etc.)
+
+module.exports = (client) => {
+ router.use((req, res, next) => {
+ req.app.locals.aptosClient = client;
+ next();
+ });
+ return router;
+};
diff --git a/server/src/utils/aptos.js b/server/src/utils/aptos.js
new file mode 100644
index 0000000..e69de29