diff --git a/src/app/dashboard/rent/Boxes.tsx b/src/app/dashboard/rent/Boxes.tsx index 29d27c2..0fab187 100644 --- a/src/app/dashboard/rent/Boxes.tsx +++ b/src/app/dashboard/rent/Boxes.tsx @@ -17,78 +17,100 @@ export default function Boxes() { const [selectedGpu, setSelectedGpu] = useState(null); const gpus: IGPU[] = [ + { + id: 'k80', + title: 'NVIDIA Tesla K80', + subTitle: '24 GB GDDR5 - 4992 CUDA Cores', + price: '$0.15/hour ($110/mo)', + price_usd: 110, + }, + { + id: 't1000', + title: 'NVIDIA T1000', + subTitle: '4 GB GDDR6 - 896 CUDA Cores', + price: '$0.16/hr ($119/month)', + price_usd: 119, + }, { id: 'a4000', title: 'NVIDIA RTX A4000', - subTitle: '16GB GDDR6 - 6144 CUDA Cores', - price: '$0.20/hour ($147/mo)', - price_usd: 147, + subTitle: '16 GB GDDR6 - 6144 CUDA Cores', + price: '$0.17/hour ($125/mo)', + price_usd: 125, isRecentlyPurchased: true, }, { id: 'a5000', title: 'NVIDIA RTX A5000', subTitle: '24 GB GDDR6 - 8192 CUDA Cores', - price: '$0.26/hr ($190/month)', - price_usd: 190, - }, - { - id: '4x4090', - title: '4x NVIDIA RTX 4090', - subTitle: '24 GB GDDR6X - 16384 GPU CUDA Cores', - price: '$0.37/hr ($268/month)', - price_usd: 268, + price: '$0.22/hr ($162/month)', + price_usd: 162, }, { id: 'v100', title: 'NVIDIA V100', subTitle: '32 GB HBM2 - 5120 CUDA Cores', - price: '$0.44/hr ($317/month)', - price_usd: 317, + price: '$0.23/hr ($170/month)', + price_usd: 170, + }, + { + id: 'p100', + title: 'NVIDIA Tesla P100', + subTitle: '16 GB HBM2 - 3584 CUDA Cores', + price: '$0.27/hour ($196/mo)', + price_usd: 196, + }, + { + id: '4x4090', + title: '4x NVIDIA RTX 4090', + subTitle: '24 GB GDDR6X - 16384 GPU CUDA Cores', + price: '$0.32/hr ($228/month)', + price_usd: 228, }, { id: 'l4ada', title: 'NVIDIA L4 Ada', subTitle: '24 GB GDDR6 - 7680 CUDA Cores', - price: '$0.52/hr ($372/month)', - price_usd: 372, + price: '$0.44/hr ($316/month)', + price_usd: 316, }, { id: 'a6000', title: 'NVIDIA RTX A6000', subTitle: '48 GB GDDR6 - 10752 CUDA Cores', - price: '$0.53/hr ($380/month)', - price_usd: 380, + price: '$0.45/hr ($323/month)', + price_usd: 323, }, { id: 'l40sada', title: 'NVIDIA L40S Ada', subTitle: '48 GB GDDR6 - 18176 CUDA Cores', - price: '$1.24/hr ($890/month)', - price_usd: 890, + price: '$1.04/hr ($756/month)', + price_usd: 756, }, { id: 'a100', title: 'NVIDIA A100 PCIe', subTitle: '40 GB HBM2 - 6912 CUDA Cores', - price: '$1.62/hr ($1,166/month)', - price_usd: 1166, + price: '$1.36/hr ($991/month)', + price_usd: 991, }, { id: 'h100nvl', title: 'NVIDIA H100 NVL', subTitle: '94 GB HBM3 - 14592 CUDA Cores', - price: '$3.35/hr ($2,410/month)', - price_usd: 2410, + price: '$2.85/hr ($2,048/month)', + price_usd: 2048, }, { id: 'h100sxm', title: 'NVIDIA H100 SXM', subTitle: '80 GB HBM3 - 14592 CUDA Cores', - price: '$3.60/hr ($2,592/month)', - price_usd: 2592, + price: '$3.06/hr ($2,203/month)', + price_usd: 2203, }, ]; + return ( <> diff --git a/src/components/GpuPaymentModal.tsx b/src/components/GpuPaymentModal.tsx index 0f9686f..6856a62 100644 --- a/src/components/GpuPaymentModal.tsx +++ b/src/components/GpuPaymentModal.tsx @@ -17,6 +17,7 @@ import { getAssociatedTokenAddress, createTransferInstruction, TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, } from "@solana/spl-token"; interface GpuPaymentModalProps { @@ -30,12 +31,18 @@ interface GpuPaymentModalProps { }; } +interface CustomToken { + address: string; + id: number; + project_name: string; +} + const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!; const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!; const EMAIL_API_URL = process.env.NEXT_PUBLIC_EMAIL_API_URL!; +const CUSTOM_TOKEN_API_URL = "https://catools.dev3vds1.link/get/vertex"; const USDC_MINT = new PublicKey("Es9vMFrzaCERJ8gLhEvX5yQceQ2uKcXfUrx2Wcikgqay"); -const JUP_MINT = new PublicKey("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"); const CornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => { const paths = { @@ -150,14 +157,15 @@ const SuccessIcon = () => { const ErrorIcon = () => { return (
-
+
@@ -222,6 +231,9 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) const { wallets: privyWallets } = useSolanaWallets(); const [solPrice, setSolPrice] = useState(null); + const [customToken, setCustomToken] = useState(null); + const [customTokenPrice, setCustomTokenPrice] = useState(null); + const [customTokenName, setCustomTokenName] = useState('VERTEX'); const [emailInput, setEmailInput] = useState(''); const [loading, setLoading] = useState(false); const [errorMsg, setErrorMsg] = useState(null); @@ -234,13 +246,16 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) const userEmail = user?.email?.address || emailInput || null; const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); const [emailTouched, setEmailTouched] = useState(false); - const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'JUP'>('SOL'); + const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'VERTEX'>('SOL'); const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); + // Calculate token amounts based on selected token + const [currentTokenAmount, setCurrentTokenAmount] = useState(''); + useEffect(() => { const fetchSolPrice = async () => { try { @@ -248,42 +263,183 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd' ); setSolPrice(data.solana.usd); - } catch { + } catch (error) { + console.error("Failed to fetch SOL price:", error); setErrorMsg('Failed to fetch SOL price.'); } }; + const fetchCustomToken = async () => { + try { + const { data } = await axios.get(CUSTOM_TOKEN_API_URL); + if (data && Array.isArray(data) && data.length > 0) { + setCustomToken(data[0]); + + // Fetch the actual token price from CoinGecko or Jupiter Aggregator + await fetchTokenPrice(data[0].address); + + // Try to get token metadata using the address + fetchTokenMetadata(data[0].address); + } + } catch (error) { + console.error("Failed to fetch custom token:", error); + } + }; + + const fetchTokenPrice = async (tokenAddress: string) => { + try { + // First attempt: Try using CoinGecko if the token is listed there by contract address + // Convert Solana address to CoinGecko platform format + const platformId = 'solana'; + const contractAddress = tokenAddress; + + try { + const coinGeckoUrl = `https://api.coingecko.com/api/v3/simple/token_price/${platformId}?contract_addresses=${contractAddress}&vs_currencies=usd`; + const response = await axios.get(coinGeckoUrl); + + // Check if we got a valid price + if (response.data && response.data[contractAddress.toLowerCase()] && response.data[contractAddress.toLowerCase()].usd) { + setCustomTokenPrice(response.data[contractAddress.toLowerCase()].usd); + return; + } + } catch (cgError) { + console.log("CoinGecko lookup failed, trying alternative source"); + } + + // Second attempt: Try using Jupiter Aggregator API for Solana tokens + try { + // Jupiter provides price data for most Solana tokens + const jupiterUrl = `https://price.jup.ag/v4/price?ids=${tokenAddress}`; + const jupResponse = await axios.get(jupiterUrl); + + if (jupResponse.data && + jupResponse.data.data && + jupResponse.data.data[tokenAddress] && + jupResponse.data.data[tokenAddress].price) { + setCustomTokenPrice(jupResponse.data.data[tokenAddress].price); + return; + } + } catch (jupError) { + console.log("Jupiter API lookup failed, trying another alternative"); + } + + // Third attempt: Try using Raydium API (another Solana DEX) + try { + const raydiumUrl = `https://api.raydium.io/v2/main/price?tokens=${tokenAddress}`; + const raydiumResponse = await axios.get(raydiumUrl); + + if (raydiumResponse.data && raydiumResponse.data[tokenAddress]) { + setCustomTokenPrice(raydiumResponse.data[tokenAddress]); + return; + } + } catch (raydiumError) { + console.log("Raydium API lookup failed"); + } + + // Fallback: If all API calls fail, use a fallback price or calculate from an existing pool + console.warn("Could not fetch price from any API, using fallback price"); + setCustomTokenPrice(0.5); // Fallback price as last resort + } catch (error) { + console.error("Failed to fetch token price:", error); + // Default price as absolute fallback + setCustomTokenPrice(0.5); + } + }; + + const fetchTokenMetadata = async (address: string) => { + try { + // First try to get metadata from Solana token registry + const tokenRegistryUrl = `https://cdn.jsdelivr.net/gh/solana-labs/token-list@main/src/tokens/solana.tokenlist.json`; + const registryResponse = await axios.get(tokenRegistryUrl); + const tokenList = registryResponse.data.tokens; + + // Find token in the registry by address + const tokenInfo = tokenList.find((token: any) => token.address === address); + + if (tokenInfo) { + // Use token info from the registry + setCustomTokenName(tokenInfo.symbol.toUpperCase()); + return; + } + + // If not found in registry, try to get on-chain metadata + const tokenPublicKey = new PublicKey(address); + const connection = new Connection(SOLANA_RPC); + + // Attempt to get token account info + const tokenMint = await connection.getTokenSupply(tokenPublicKey); + + if (tokenMint) { + // If we can't get a name from on-chain data, use project_name from our API + // or fall back to default name + if (customToken?.project_name) { + setCustomTokenName(customToken.project_name.toUpperCase()); + } else { + // If all else fails, use the default name + setCustomTokenName('VERTEX'); + } + return; + } + + // If we couldn't get metadata, use the default name + setCustomTokenName('VERTEX'); + } catch (error) { + console.error("Failed to fetch token metadata:", error); + // Default to VERTEX if we can't get the name + setCustomTokenName('VERTEX'); + } + }; + if (isOpen) { fetchSolPrice(); + fetchCustomToken(); setErrorMsg(null); setSuccessMsg(null); setPaymentSuccess(false); setPaymentFailure(false); } - }, [isOpen]); + }, [isOpen, customToken?.project_name]); + + // Update token amount when price or selected token changes + useEffect(() => { + updateTokenAmount(); + }, [selectedToken, solPrice, customTokenPrice, gpu.price_usd]); + + const updateTokenAmount = () => { + if (selectedToken === 'SOL' && solPrice) { + const amount = (gpu.price_usd / solPrice).toFixed(6); + setCurrentTokenAmount(amount); + } else if (selectedToken === 'USDC') { + // USDC is a stablecoin, so 1 USDC ≈ 1 USD + setCurrentTokenAmount(gpu.price_usd.toFixed(2)); + } else if (selectedToken === 'VERTEX' && customTokenPrice) { + const amount = (gpu.price_usd / customTokenPrice).toFixed(2); + setCurrentTokenAmount(amount); + } + }; const handlePayment = async () => { setLoading(true); setErrorMsg(null); setSuccessMsg(null); - + try { if (!userEmail || !isValidEmail(userEmail)) { throw new Error("Please enter a valid email address for your receipt."); } - + const solWallet = privyWallets[0]; if (!solWallet?.address || !solWallet.signTransaction) { throw new Error("No connected Solana wallet found or wallet can't sign."); } - + const fromPubKey = new PublicKey(solWallet.address); const toPubKey = new PublicKey(BUSINESS_WALLET); const transaction = new Transaction(); let txId = ""; let tokenUsed = selectedToken; let amountDisplay = ""; - + if (selectedToken === "SOL") { const { data } = await axios.get( "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" @@ -291,7 +447,7 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) const solPrice = data.solana.usd; const solAmount = (gpu.price_usd / solPrice).toFixed(6); amountDisplay = `${solAmount} SOL`; - + transaction.add( SystemProgram.transfer({ fromPubkey: fromPubKey, @@ -300,23 +456,43 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) }) ); } else { - const selectedMint = selectedToken === "USDC" ? USDC_MINT : JUP_MINT; + // For both USDC and VERTEX tokens + const selectedMint = selectedToken === "USDC" + ? USDC_MINT + : new PublicKey(customToken?.address || ""); + const tokenDecimals = 6; - + let tokenPrice = 1; - if (selectedToken === "JUP") { - const { data } = await axios.get( - "https://api.coingecko.com/api/v3/simple/price?ids=jupiter-exchange&vs_currencies=usd" - ); - tokenPrice = data["jupiter-exchange"]?.usd ?? 0.5; + if (selectedToken === "VERTEX") { + tokenPrice = customTokenPrice || 0.5; } - + const tokenAmount = (gpu.price_usd / tokenPrice).toFixed(tokenDecimals); amountDisplay = `${tokenAmount} ${selectedToken}`; - + const fromTokenAccount = await getAssociatedTokenAddress(selectedMint, fromPubKey); const toTokenAccount = await getAssociatedTokenAddress(selectedMint, toPubKey); - + + // Check if destination token account exists, if not create it + const toTokenAccountInfo = await connection.getAccountInfo(toTokenAccount); + + if (!toTokenAccountInfo) { + // Import the necessary function + const { createAssociatedTokenAccountInstruction } = await import("@solana/spl-token"); + + // Add instruction to create the associated token account for the business wallet + transaction.add( + createAssociatedTokenAccountInstruction( + fromPubKey, // payer + toTokenAccount, // associated token account address + toPubKey, // owner of the new account + selectedMint // token mint + ) + ); + } + + // Add the transfer instruction after ensuring the account exists transaction.add( createTransferInstruction( fromTokenAccount, @@ -328,27 +504,26 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) ) ); } - + transaction.feePayer = fromPubKey; const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; - + const signedTx = await solWallet.signTransaction(transaction); txId = await connection.sendRawTransaction(signedTx.serialize()); await connection.confirmTransaction(txId, "confirmed"); - + await axios.post(EMAIL_API_URL, { email: userEmail, product: gpu.title, price_hour: gpu.price_per_hour, - price: gpu.price_usd.toFixed(2), - token: selectedToken, + price: gpu.price_usd.toFixed(2) }); - + // Generate a random order ID (in production this would come from the backend) const generatedOrderId = `${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`; setOrderId(generatedOrderId); - + const { error } = await supabase.from("orders").insert([ { gpu: `${gpu.id} ${gpu.title}`, @@ -361,7 +536,7 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) order_id: generatedOrderId, }, ]); - + if (error) throw new Error(`Failed to save order: ${error.message}`); setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); setPaymentSuccess(true); @@ -397,8 +572,6 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) if (!isOpen) return null; - const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...'; - // Show failure screen if payment failed if (paymentFailure) { return ( @@ -531,7 +704,7 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) Total Price
${gpu.price_usd} - (~{solAmount} SOL) + (~{currentTokenAmount} {selectedToken})
@@ -572,12 +745,12 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)