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) => ( +
+

{message.content}

+
+ ))} +
+
+ 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 +
+ ); +}; + +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} +

{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

+ +
+ +
+

Featured Artists

+
+ {featuredArtists.map((artist) => ( + + {/* Display artist card/thumbnail here */} + {artist.name} +

{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

+ +
+ +
+

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