From a2af10606e7b06abcba829071d5dbb856ff853a7 Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 00:34:19 +0530 Subject: [PATCH 01/13] new payment system --- package.json | 11 + src/app/dashboard/api/orders/route.ts | 70 ++++++ src/app/dashboard/rent/Boxes.tsx | 335 +++++++++++--------------- src/components/GpuPaymentModal.tsx | 163 +++++++++++++ src/components/Providers.tsx | 2 +- 5 files changed, 388 insertions(+), 193 deletions(-) create mode 100644 src/app/dashboard/api/orders/route.ts create mode 100644 src/components/GpuPaymentModal.tsx diff --git a/package.json b/package.json index fe49948..2598c77 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,20 @@ "dependencies": { "@geist-ui/core": "^2.3.8", "@privy-io/react-auth": "^2.0.4", + "@solana/spl-token": "^0.4.13", + "@solana/wallet-adapter-base": "^0.9.26", + "@solana/wallet-adapter-phantom": "^0.9.27", + "@solana/wallet-adapter-react": "^0.15.38", + "@solana/wallet-adapter-react-ui": "^0.9.38", + "@solana/wallet-adapter-wallets": "^0.19.36", + "@solana/web3.js": "^1.98.2", + "@supabase/supabase-js": "^2.49.4", + "axios": "^1.9.0", "chart.js": "^4.4.7", "framer-motion": "^11.18.1", "geist": "^1.3.1", "next": "^14.2.23", + "nodemailer": "^6.10.1", "react": "^18.2.0", "react-chartjs-2": "^5.3.0", "react-dom": "^18.2.0", @@ -24,6 +34,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@types/node": "^20", + "@types/nodemailer": "^6.4.17", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8.0.0", diff --git a/src/app/dashboard/api/orders/route.ts b/src/app/dashboard/api/orders/route.ts new file mode 100644 index 0000000..6df271b --- /dev/null +++ b/src/app/dashboard/api/orders/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; +import nodemailer from 'nodemailer'; + +const supabase = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); + +const emailUser = process.env.EMAIL_USER!; +const emailPass = process.env.EMAIL_PASS!; +const businessEmail = process.env.BUSINESS_EMAIL!; + +export async function POST(req: NextRequest) { + try { + const { gpuId, userEmail, txSignature, amountSol, status } = await req.json(); + + if (!gpuId || !userEmail || !amountSol || !status) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + if (!['pending', 'success', 'failed'].includes(status)) { + return NextResponse.json({ error: 'Invalid status value' }, { status: 400 }); + } + + const { error } = await supabase.from('orders').insert({ + gpu_id: gpuId, + user_email: userEmail, + amount_sol: amountSol, + sol_tx_signature: txSignature || null, + status, + }); + + if (error) { + console.error('[Supabase error]', error); + return NextResponse.json({ error: 'Database error' }, { status: 500 }); + } + + // Send confirmation email + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: emailUser, + pass: emailPass, + }, + }); + + await transporter.sendMail({ + from: `"GPU Store" <${emailUser}>`, + to: userEmail, + subject: `GPU Payment ${status === 'success' ? 'Confirmed' : 'Attempted'}`, + html: ` +

Hi,

+

Your order for GPU ID ${gpuId} has status: ${status}.

+

Amount: ${amountSol} SOL

+ ${ + txSignature + ? `

Transaction: ${txSignature}

` + : '

No transaction was recorded.

' + } +

Thank you for using our platform.

+ `, + }); + + return NextResponse.json({ message: 'Order recorded and email sent' }); + } catch (err: any) { + console.error('[Order API Error]', err); + return NextResponse.json({ error: 'Server error' }, { status: 500 }); + } +} diff --git a/src/app/dashboard/rent/Boxes.tsx b/src/app/dashboard/rent/Boxes.tsx index 2c20989..567778e 100644 --- a/src/app/dashboard/rent/Boxes.tsx +++ b/src/app/dashboard/rent/Boxes.tsx @@ -1,234 +1,185 @@ -"use client"; -import Link from "next/link"; -import { motion } from "framer-motion"; +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import GpuPaymentModal from '@/components/GpuPaymentModal' interface IGPU { + id: string; title: string; subTitle: string; price: string; + price_usd: number; isRecentlyPurchased?: boolean; - link: string; } export default function Boxes() { + const [selectedGpu, setSelectedGpu] = useState(null); + const gpus: IGPU[] = [ { - title: "NVIDIA RTX A4000", - subTitle: "16GB GDDR6 - 6144 CUDA Cores", - price: "$0.20/hour ($147/mo)", + id: 'a4000', + title: 'NVIDIA RTX A4000', + subTitle: '16GB GDDR6 - 6144 CUDA Cores', + price: '$0.20/hour ($147/mo)', + price_usd: 147, isRecentlyPurchased: true, - link: "https://nowpayments.io/payment?iid=5964763281", }, { - title: "NVIDIA RTX A5000", - subTitle: "24 GB GDDR6 - 8192 CUDA Cores", - price: "$0.26/hr ($190/month)", - link: "https://nowpayments.io/payment?iid=4873013025", + id: 'a5000', + title: 'NVIDIA RTX A5000', + subTitle: '24 GB GDDR6 - 8192 CUDA Cores', + price: '$0.26/hr ($190/month)', + price_usd: 190, }, { - title: "4x NVIDIA RTX 4090", - subTitle: "24 GB GDDR6X - 16384 GPU CUDA Cores", - price: "$0.37/hr ($268/month)", - link: "https://nowpayments.io/payment?iid=5977605820", + id: '4x4090', + title: '4x NVIDIA RTX 4090', + subTitle: '24 GB GDDR6X - 16384 GPU CUDA Cores', + price: '$0.37/hr ($268/month)', + price_usd: 268, }, { - title: "NVIDIA V100", - subTitle: "32 GB HBM2 - 5120 CUDA Cores", - price: "$0.44/hr ($317/month)", - link: "https://nowpayments.io/payment?iid=5437270588", + id: 'v100', + title: 'NVIDIA V100', + subTitle: '32 GB HBM2 - 5120 CUDA Cores', + price: '$0.44/hr ($317/month)', + price_usd: 317, }, { - title: "NVIDIA L4 Ada", - subTitle: "24 GB GDDR6 - 7680 CUDA Cores", - price: "$0.52/hr ($372/month)", - link: "https://nowpayments.io/payment?iid=6283623234", + id: 'l4ada', + title: 'NVIDIA L4 Ada', + subTitle: '24 GB GDDR6 - 7680 CUDA Cores', + price: '$0.52/hr ($372/month)', + price_usd: 372, }, { - title: "NVIDIA RTX A6000", - subTitle: "48 GB GDDR6 - 10752 CUDA Cores", - price: "$0.53/hr ($380/month)", - link: "https://nowpayments.io/payment?iid=6074730706", + id: 'a6000', + title: 'NVIDIA RTX A6000', + subTitle: '48 GB GDDR6 - 10752 CUDA Cores', + price: '$0.53/hr ($380/month)', + price_usd: 380, }, { - title: "NVIDIA L40S Ada", - subTitle: "48 GB GDDR6 - 18176 CUDA Cores", - price: "$1.24/hr ($890/month)", - link: "https://nowpayments.io/payment?iid=4570621084", + id: 'l40sada', + title: 'NVIDIA L40S Ada', + subTitle: '48 GB GDDR6 - 18176 CUDA Cores', + price: '$1.24/hr ($890/month)', + price_usd: 890, }, { - title: "NVIDIA A100 PCIe", - subTitle: "40 GB HBM2 - 6912 CUDA Cores", - price: "$1.62/hr ($1,166/month)", - link: "https://nowpayments.io/payment?iid=6381921922", + id: 'a100', + title: 'NVIDIA A100 PCIe', + subTitle: '40 GB HBM2 - 6912 CUDA Cores', + price: '$1.62/hr ($1,166/month)', + price_usd: 1166, }, { - title: "NVIDIA H100 NVL", - subTitle: "94 GB HBM3 - 14592 CUDA Cores", - price: "$3.35/hr ($2,410/month)", - link: "https://nowpayments.io/payment?iid=5319698362", + id: 'h100nvl', + title: 'NVIDIA H100 NVL', + subTitle: '94 GB HBM3 - 14592 CUDA Cores', + price: '$3.35/hr ($2,410/month)', + price_usd: 2410, }, { - title: "NVIDIA H100 SXM", - subTitle: "80 GB HBM3 - 14592 CUDA Cores", - price: "$3.60/hr ($2,592/month)", - link: "https://nowpayments.io/payment?iid=4768823499", + id: 'h100sxm', + title: 'NVIDIA H100 SXM', + subTitle: '80 GB HBM3 - 14592 CUDA Cores', + price: '$3.60/hr ($2,592/month)', + price_usd: 2592, }, ]; return ( -
- {gpus.map((gpu, index) => ( - -
-
- - - - - - - - - - - - -

{gpu.title}

+ <> +
+ {gpus.map((gpu, index) => ( + +
+
+ {/* GPU SVG ICON */} + + + + + + + + + + + +

{gpu.title}

+
+

{gpu.subTitle}

-

{gpu.subTitle}

-
- -
-
-

- {gpu.price} -

- - {gpu.isRecentlyPurchased && ( -

- - - - Recently purchased +

+
+

+ {gpu.price}

- )} + {gpu.isRecentlyPurchased && ( +

+ + + + Recently purchased +

+ )} +
+ +
- - Rent now - -
+ {/* Corner Decorations */} + {['top', 'bottom'].map((pos) => + ['left', 'right'].map((side) => ( + + + + + + + )) + )} + + ))} +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} -
+ {selectedGpu && ( + setSelectedGpu(null)} + gpu={{ + id: selectedGpu.id, + title: selectedGpu.title, + price_usd: selectedGpu.price_usd, + }} + /> + )} + ); } diff --git a/src/components/GpuPaymentModal.tsx b/src/components/GpuPaymentModal.tsx new file mode 100644 index 0000000..f9683c5 --- /dev/null +++ b/src/components/GpuPaymentModal.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Connection, + PublicKey, + Transaction, + SystemProgram, +} from '@solana/web3.js'; +import axios from 'axios'; +import { usePrivy } from '@privy-io/react-auth'; +import { + useSolanaWallets, + useSignTransaction, +} from '@privy-io/react-auth/solana'; + +interface GpuPaymentModalProps { + isOpen: boolean; + onClose: () => void; + gpu: { + id: string; + title: string; + price_usd: number; + }; +} + +const SOLANA_RPC = 'https://api.mainnet-beta.solana.com'; +const BUSINESS_WALLET = 'Y93ednSpre2XRjPTBafHU1BXPXKMPhujcYAEshS5pXm8K'; // <-- Replace this + +export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => { + const { user } = usePrivy(); + const { wallets } = useSolanaWallets(); + const { signTransaction } = useSignTransaction(); + + const [solPrice, setSolPrice] = useState(null); + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + const connection = new Connection(SOLANA_RPC); + + useEffect(() => { + const fetchSolPrice = async () => { + try { + const { data } = await axios.get( + 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd' + ); + setSolPrice(data.solana.usd); + } catch { + setErrorMsg('Failed to fetch SOL price.'); + } + }; + + if (isOpen) { + fetchSolPrice(); + setErrorMsg(null); + setSuccessMsg(null); + } + }, [isOpen]); + + const handlePayment = async () => { + setLoading(true); + setErrorMsg(null); + setSuccessMsg(null); + + try { + const solWallet = wallets[0]; + if (!solWallet || !solWallet.address) { + throw new Error('No connected Solana wallet found.'); + } + + const fromPubKey = new PublicKey(solWallet.address); + const toPubKey = new PublicKey(BUSINESS_WALLET); + const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6); + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: fromPubKey, + toPubkey: toPubKey, + lamports: Math.floor(parseFloat(solAmount) * 1e9), + }) + ); + + transaction.feePayer = fromPubKey; + const { blockhash } = await connection.getRecentBlockhash(); + transaction.recentBlockhash = blockhash; + + const signedTx = await signTransaction({ + transaction, + connection, + address: solWallet.address, + }); + + const txId = await connection.sendRawTransaction(signedTx.serialize()); + await connection.confirmTransaction(txId, 'confirmed'); + + await axios.post('/api/orders', { + gpuId: gpu.id, + userEmail: user?.email?.address, + txSignature: txId, + status: 'success', + }); + + setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); + } catch (err: any) { + setErrorMsg(err.message || 'Payment failed.'); + await axios.post('/api/orders', { + gpuId: gpu.id, + userEmail: user?.email?.address, + txSignature: null, + status: 'failed', + }); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...'; + + return ( +
+
+

{gpu.title}

+

+ Price: ${gpu.price_usd} USD (~{solAmount} SOL) +

+ + {errorMsg && ( +
+ {errorMsg} +
+ )} + + {successMsg && ( +
+ {successMsg} +
+ )} + +
+ + +
+
+
+ ); +}; + +export default GpuPaymentModal; diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index a28ffe3..a9ca767 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -11,7 +11,7 @@ const solanaConnectors = toSolanaWalletConnectors({ export default function Providers({ children }: { children: React.ReactNode }) { return ( Date: Wed, 30 Apr 2025 23:08:35 +0530 Subject: [PATCH 02/13] payment system fix --- src/app/dashboard/rent/Boxes.tsx | 17 ++-- src/components/GpuPaymentModal.tsx | 155 +++++++++++++++++++++-------- 2 files changed, 122 insertions(+), 50 deletions(-) diff --git a/src/app/dashboard/rent/Boxes.tsx b/src/app/dashboard/rent/Boxes.tsx index 567778e..453a15b 100644 --- a/src/app/dashboard/rent/Boxes.tsx +++ b/src/app/dashboard/rent/Boxes.tsx @@ -171,14 +171,15 @@ export default function Boxes() { {selectedGpu && ( setSelectedGpu(null)} - gpu={{ - id: selectedGpu.id, - title: selectedGpu.title, - price_usd: selectedGpu.price_usd, - }} - /> + isOpen={true} + onClose={() => setSelectedGpu(null)} + gpu={{ + id: selectedGpu.id, + title: selectedGpu.title, + price_usd: selectedGpu.price_usd, + price_per_hour: selectedGpu.price, // pass it here + }} + /> )} ); diff --git a/src/components/GpuPaymentModal.tsx b/src/components/GpuPaymentModal.tsx index f9683c5..8c12827 100644 --- a/src/components/GpuPaymentModal.tsx +++ b/src/components/GpuPaymentModal.tsx @@ -11,8 +11,8 @@ import axios from 'axios'; import { usePrivy } from '@privy-io/react-auth'; import { useSolanaWallets, - useSignTransaction, } from '@privy-io/react-auth/solana'; +import { createClient } from '@supabase/supabase-js'; interface GpuPaymentModalProps { isOpen: boolean; @@ -21,23 +21,35 @@ interface GpuPaymentModalProps { id: string; title: string; price_usd: number; + price_per_hour: string; // new field }; } -const SOLANA_RPC = 'https://api.mainnet-beta.solana.com'; -const BUSINESS_WALLET = 'Y93ednSpre2XRjPTBafHU1BXPXKMPhujcYAEshS5pXm8K'; // <-- Replace this + +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!; export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => { - const { user } = usePrivy(); - const { wallets } = useSolanaWallets(); - const { signTransaction } = useSignTransaction(); + const { user, connectWallet } = usePrivy(); + const { wallets: privyWallets } = useSolanaWallets(); const [solPrice, setSolPrice] = useState(null); + const [emailInput, setEmailInput] = useState(''); const [loading, setLoading] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const [successMsg, setSuccessMsg] = useState(null); const connection = new Connection(SOLANA_RPC); + const userEmail = user?.email?.address || emailInput || null; + + const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const [emailTouched, setEmailTouched] = useState(false); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); useEffect(() => { const fetchSolPrice = async () => { @@ -64,13 +76,23 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) setSuccessMsg(null); try { - const solWallet = wallets[0]; + if (!userEmail || !isValidEmail(userEmail)) { + throw new Error('Please enter a valid email address for your receipt.'); + } + + const solWallet = privyWallets[0]; + if (!solWallet || !solWallet.address) { - throw new Error('No connected Solana wallet found.'); + throw new Error('No connected Solana wallet found. Please connect a wallet via Privy.'); + } + + if (!solWallet.signTransaction) { + throw new Error('Connected wallet does not support transaction signing.'); } const fromPubKey = new PublicKey(solWallet.address); const toPubKey = new PublicKey(BUSINESS_WALLET); + const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6); const transaction = new Transaction().add( @@ -82,34 +104,41 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) ); transaction.feePayer = fromPubKey; - const { blockhash } = await connection.getRecentBlockhash(); + const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; - const signedTx = await signTransaction({ - transaction, - connection, - address: solWallet.address, - }); + const signedTx = await solWallet.signTransaction(transaction); const txId = await connection.sendRawTransaction(signedTx.serialize()); await connection.confirmTransaction(txId, 'confirmed'); - await axios.post('/api/orders', { - gpuId: gpu.id, - userEmail: user?.email?.address, - txSignature: txId, - status: 'success', - }); + // Send confirmation email + await axios.post(EMAIL_API_URL, { + email: userEmail, + product: gpu.title, + price_hour: gpu.price_per_hour, + price: gpu.price_usd.toFixed(2), + }); + + // Store order in Supabase + const { error } = await supabase.from('orders').insert([ + { + gpu_id: gpu.id, + user_email: userEmail, + amount_sol: parseFloat(solAmount), + sol_tx_signature: txId, + status: 'success', + }, + ]); + + if (error) { + throw new Error(`Failed to save order: ${error.message}`); + } + setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); } catch (err: any) { setErrorMsg(err.message || 'Payment failed.'); - await axios.post('/api/orders', { - gpuId: gpu.id, - userEmail: user?.email?.address, - txSignature: null, - status: 'failed', - }); } finally { setLoading(false); } @@ -139,22 +168,64 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
)} -
- - -
+ {/* Wallet not connected */} + {privyWallets.length === 0 ? ( +
+

+ To proceed with payment, please connect a Solana wallet. +

+ +
+ ) : ( + <> + {/* Show email prompt if not logged in with email */} + {!user?.email?.address && ( +
+ + setEmailInput(e.target.value)} + onBlur={() => setEmailTouched(true)} + className="w-full px-3 py-2 border rounded-md text-sm" + /> + {emailTouched && emailInput && !isValidEmail(emailInput) && ( +

Please enter a valid email address.

+ )} + +
+ )} + +
+ + +
+ + )}
); From 59871fc66f06c12679fcdbcd5de1c15542b60d2f Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 23:35:01 +0530 Subject: [PATCH 03/13] removed unnecessary route --- src/app/dashboard/api/orders/route.ts | 70 --------------------------- 1 file changed, 70 deletions(-) delete mode 100644 src/app/dashboard/api/orders/route.ts diff --git a/src/app/dashboard/api/orders/route.ts b/src/app/dashboard/api/orders/route.ts deleted file mode 100644 index 6df271b..0000000 --- a/src/app/dashboard/api/orders/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { createClient } from '@supabase/supabase-js'; -import nodemailer from 'nodemailer'; - -const supabase = createClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! -); - -const emailUser = process.env.EMAIL_USER!; -const emailPass = process.env.EMAIL_PASS!; -const businessEmail = process.env.BUSINESS_EMAIL!; - -export async function POST(req: NextRequest) { - try { - const { gpuId, userEmail, txSignature, amountSol, status } = await req.json(); - - if (!gpuId || !userEmail || !amountSol || !status) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); - } - - if (!['pending', 'success', 'failed'].includes(status)) { - return NextResponse.json({ error: 'Invalid status value' }, { status: 400 }); - } - - const { error } = await supabase.from('orders').insert({ - gpu_id: gpuId, - user_email: userEmail, - amount_sol: amountSol, - sol_tx_signature: txSignature || null, - status, - }); - - if (error) { - console.error('[Supabase error]', error); - return NextResponse.json({ error: 'Database error' }, { status: 500 }); - } - - // Send confirmation email - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: emailUser, - pass: emailPass, - }, - }); - - await transporter.sendMail({ - from: `"GPU Store" <${emailUser}>`, - to: userEmail, - subject: `GPU Payment ${status === 'success' ? 'Confirmed' : 'Attempted'}`, - html: ` -

Hi,

-

Your order for GPU ID ${gpuId} has status: ${status}.

-

Amount: ${amountSol} SOL

- ${ - txSignature - ? `

Transaction: ${txSignature}

` - : '

No transaction was recorded.

' - } -

Thank you for using our platform.

- `, - }); - - return NextResponse.json({ message: 'Order recorded and email sent' }); - } catch (err: any) { - console.error('[Order API Error]', err); - return NextResponse.json({ error: 'Server error' }, { status: 500 }); - } -} From 752f99af8499884e385a1e4cfff78b1de415fe4e Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:19:40 +0000 Subject: [PATCH 04/13] added nixpacks.toml --- nixpacks.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 nixpacks.toml diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 0000000..737c6e4 --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,17 @@ +NIXPACKS_NODE_VERSION[phases.setup] +nixPkgs = [ + "nodejs_20", + "npm-10_x", + "python3", + "make", + "gcc" +] + +[phases.install] +cmds = ["npm install"] + +[phases.build] +cmds = ["npm run build"] + +[start] +cmd = "npm run start" From 4a5dab296490aa1e8241bda495ce58a2b938cff1 Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:20:58 +0000 Subject: [PATCH 05/13] Update nixpacks.toml --- nixpacks.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixpacks.toml b/nixpacks.toml index 737c6e4..971b326 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,4 +1,4 @@ -NIXPACKS_NODE_VERSION[phases.setup] +[phases.setup] nixPkgs = [ "nodejs_20", "npm-10_x", From e98c02c0904a032154c158c00f870f9d6ab85c9b Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:22:36 +0000 Subject: [PATCH 06/13] Update nixpacks.toml --- nixpacks.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index 971b326..48709b8 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,9 +2,7 @@ nixPkgs = [ "nodejs_20", "npm-10_x", - "python3", - "make", - "gcc" + "python3" ] [phases.install] From 367b8c89eac09466e8771e823b6e6ef52d750981 Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:24:35 +0000 Subject: [PATCH 07/13] Update nixpacks.toml --- nixpacks.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/nixpacks.toml b/nixpacks.toml index 48709b8..5c8325e 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,7 +1,6 @@ [phases.setup] nixPkgs = [ "nodejs_20", - "npm-10_x", "python3" ] From 5fa22780bd5a37f397db66cdc146eb15696473b9 Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:29:12 +0000 Subject: [PATCH 08/13] Update nixpacks.toml --- nixpacks.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/nixpacks.toml b/nixpacks.toml index 5c8325e..33309a5 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -1,6 +1,7 @@ [phases.setup] nixPkgs = [ "nodejs_20", + "gcc", "python3" ] From cec41286341e1811b666d9d57f0637042fce1e67 Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:34:10 +0000 Subject: [PATCH 09/13] Update nixpacks.toml --- nixpacks.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nixpacks.toml b/nixpacks.toml index 33309a5..6949d3a 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,7 +2,8 @@ nixPkgs = [ "nodejs_20", "gcc", - "python3" + "python3", + "libudev" ] [phases.install] From 101be9060165b245ead7385adf8f2975dfb6700e Mon Sep 17 00:00:00 2001 From: shialoth Date: Wed, 30 Apr 2025 18:36:28 +0000 Subject: [PATCH 10/13] Update nixpacks.toml --- nixpacks.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixpacks.toml b/nixpacks.toml index 6949d3a..f7a03c1 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -3,7 +3,7 @@ nixPkgs = [ "nodejs_20", "gcc", "python3", - "libudev" + "systemd" ] [phases.install] From df220a533f25f5b49d20bd2b38ebef5a7979e46b Mon Sep 17 00:00:00 2001 From: shialoth Date: Thu, 1 May 2025 00:26:51 +0530 Subject: [PATCH 11/13] removed unnecessary module --- nixpacks.toml | 16 ---------------- package.json | 1 - 2 files changed, 17 deletions(-) delete mode 100644 nixpacks.toml diff --git a/nixpacks.toml b/nixpacks.toml deleted file mode 100644 index f7a03c1..0000000 --- a/nixpacks.toml +++ /dev/null @@ -1,16 +0,0 @@ -[phases.setup] -nixPkgs = [ - "nodejs_20", - "gcc", - "python3", - "systemd" -] - -[phases.install] -cmds = ["npm install"] - -[phases.build] -cmds = ["npm run build"] - -[start] -cmd = "npm run start" diff --git a/package.json b/package.json index 2598c77..1d6ce11 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@solana/wallet-adapter-phantom": "^0.9.27", "@solana/wallet-adapter-react": "^0.15.38", "@solana/wallet-adapter-react-ui": "^0.9.38", - "@solana/wallet-adapter-wallets": "^0.19.36", "@solana/web3.js": "^1.98.2", "@supabase/supabase-js": "^2.49.4", "axios": "^1.9.0", From 42350c9acc06f9490020fa682d97062e41a28284 Mon Sep 17 00:00:00 2001 From: shialoth Date: Sun, 4 May 2025 07:28:42 +0530 Subject: [PATCH 12/13] added new ui --- src/app/dashboard/rent/Boxes.tsx | 4 +- src/app/globals.css | 23 ++ src/components/GpuPaymentModal.tsx | 621 +++++++++++++++++++++++------ 3 files changed, 535 insertions(+), 113 deletions(-) diff --git a/src/app/dashboard/rent/Boxes.tsx b/src/app/dashboard/rent/Boxes.tsx index 453a15b..29d27c2 100644 --- a/src/app/dashboard/rent/Boxes.tsx +++ b/src/app/dashboard/rent/Boxes.tsx @@ -156,7 +156,9 @@ export default function Boxes() { ['left', 'right'].map((side) => ( diff --git a/src/app/globals.css b/src/app/globals.css index fe5d95c..9a545ec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -11,3 +11,26 @@ textarea { .hide-scrollbar::-webkit-scrollbar { display: none; } + +.token-dropdown { + width: 100%; + padding: 10px 12px; + background: #0E1618; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 3px; + color: #FFFFFF; + font-family: 'Geist Mono', monospace; + font-size: 14px; + line-height: 18px; + letter-spacing: -0.01em; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%230CE77E' stroke-width='1.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px; +} +.token-dropdown:focus { + outline: none; + border-color: #0CE77E; +} diff --git a/src/components/GpuPaymentModal.tsx b/src/components/GpuPaymentModal.tsx index 8c12827..0f9686f 100644 --- a/src/components/GpuPaymentModal.tsx +++ b/src/components/GpuPaymentModal.tsx @@ -1,18 +1,23 @@ 'use client'; import { useEffect, useState } from 'react'; +import React from "react"; import { - Connection, PublicKey, Transaction, + sendAndConfirmTransaction, + Connection, SystemProgram, -} from '@solana/web3.js'; +} from "@solana/web3.js"; import axios from 'axios'; import { usePrivy } from '@privy-io/react-auth'; -import { - useSolanaWallets, -} from '@privy-io/react-auth/solana'; +import { useSolanaWallets } from '@privy-io/react-auth/solana'; import { createClient } from '@supabase/supabase-js'; +import { + getAssociatedTokenAddress, + createTransferInstruction, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; interface GpuPaymentModalProps { isOpen: boolean; @@ -21,15 +26,197 @@ interface GpuPaymentModalProps { id: string; title: string; price_usd: number; - price_per_hour: string; // new field + price_per_hour: 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 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 = { + 'left-top': ( + <> + + + + ), + 'right-top': ( + <> + + + + ), + 'left-bottom': ( + <> + + + + ), + 'right-bottom': ( + <> + + + + ) + }; + + return ( + + {paths[position]} + + ); +}; + +// Red corner edges for error UI +const RedCornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => { + const paths = { + 'left-top': ( + <> + + + + ), + 'right-top': ( + <> + + + + ), + 'left-bottom': ( + <> + + + + ), + 'right-bottom': ( + <> + + + + ) + }; + + return ( + + {paths[position]} + + ); +}; + +const SuccessIcon = () => { + return ( +
+
+ +
+ ); +}; + +const ErrorIcon = () => { + return ( +
+
+ + + +
+ ); +}; + +const ErrorBox = () => { + return ( +
+
+
+
+ + Error + + + 404 (Payment Error) + +
+ + + + +
+ ); +}; + +interface OrderIdBoxProps { + orderId: string; +} + +const OrderIdBox = ({ orderId }: OrderIdBoxProps) => { + return ( +
+
+
+
+ + Order ID + + + {orderId} + +
+ + + + +
+ ); +}; + export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => { const { user, connectWallet } = usePrivy(); const { wallets: privyWallets } = useSolanaWallets(); @@ -39,12 +226,15 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) const [loading, setLoading] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const [successMsg, setSuccessMsg] = useState(null); + const [paymentSuccess, setPaymentSuccess] = useState(false); + const [paymentFailure, setPaymentFailure] = useState(false); + const [orderId, setOrderId] = useState(''); const connection = new Connection(SOLANA_RPC); 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 supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -67,6 +257,8 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) fetchSolPrice(); setErrorMsg(null); setSuccessMsg(null); + setPaymentSuccess(false); + setPaymentFailure(false); } }, [isOpen]); @@ -77,158 +269,363 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) try { if (!userEmail || !isValidEmail(userEmail)) { - throw new Error('Please enter a valid email address for your receipt.'); + throw new Error("Please enter a valid email address for your receipt."); } const solWallet = privyWallets[0]; - - if (!solWallet || !solWallet.address) { - throw new Error('No connected Solana wallet found. Please connect a wallet via Privy.'); - } - - if (!solWallet.signTransaction) { - throw new Error('Connected wallet does not support transaction signing.'); + 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 solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6); + const transaction = new Transaction(); + let txId = ""; + let tokenUsed = selectedToken; + let amountDisplay = ""; - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromPubKey, - toPubkey: toPubKey, - lamports: Math.floor(parseFloat(solAmount) * 1e9), - }) - ); + if (selectedToken === "SOL") { + const { data } = await axios.get( + "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" + ); + const solPrice = data.solana.usd; + const solAmount = (gpu.price_usd / solPrice).toFixed(6); + amountDisplay = `${solAmount} SOL`; + + transaction.add( + SystemProgram.transfer({ + fromPubkey: fromPubKey, + toPubkey: toPubKey, + lamports: Math.floor(parseFloat(solAmount) * 1e9), + }) + ); + } else { + const selectedMint = selectedToken === "USDC" ? USDC_MINT : JUP_MINT; + 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; + } + + const tokenAmount = (gpu.price_usd / tokenPrice).toFixed(tokenDecimals); + amountDisplay = `${tokenAmount} ${selectedToken}`; + + const fromTokenAccount = await getAssociatedTokenAddress(selectedMint, fromPubKey); + const toTokenAccount = await getAssociatedTokenAddress(selectedMint, toPubKey); + + transaction.add( + createTransferInstruction( + fromTokenAccount, + toTokenAccount, + fromPubKey, + BigInt(Math.floor(parseFloat(tokenAmount) * Math.pow(10, tokenDecimals))), + [], + TOKEN_PROGRAM_ID + ) + ); + } 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"); - const txId = await connection.sendRawTransaction(signedTx.serialize()); - await connection.confirmTransaction(txId, 'confirmed'); - - // Send confirmation email 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, + }); - // Store order in Supabase - const { error } = await supabase.from('orders').insert([ + // 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_id: gpu.id, + gpu: `${gpu.id} ${gpu.title}`, user_email: userEmail, - amount_sol: parseFloat(solAmount), - sol_tx_signature: txId, - status: 'success', + amount_usd: gpu.price_usd, + token_used: tokenUsed, + token_amount: amountDisplay, + token_tx_signature: txId, + status: "success", + order_id: generatedOrderId, }, ]); - if (error) { - throw new Error(`Failed to save order: ${error.message}`); - } - - + if (error) throw new Error(`Failed to save order: ${error.message}`); setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); + setPaymentSuccess(true); } catch (err: any) { - setErrorMsg(err.message || 'Payment failed.'); + console.error("Payment error:", err); + setErrorMsg(err.message || "Payment failed."); + setPaymentFailure(true); + + // Log the failed attempt in Supabase + const failedOrderId = `FAIL-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`; + await supabase.from("orders").insert([ + { + gpu: `${gpu.id} ${gpu.title}`, + user_email: userEmail, + amount_usd: gpu.price_usd, + token_used: selectedToken, + status: "failed", + error_message: err.message || "Unknown error", + order_id: failedOrderId, + }, + ]); } finally { setLoading(false); } }; + const handleRetry = () => { + // Reset failure state and error message + setPaymentFailure(false); + setErrorMsg(null); + // Don't reset other form values so user doesn't have to start over + }; + if (!isOpen) return null; const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...'; - return ( -
-
-

{gpu.title}

-

- Price: ${gpu.price_usd} USD (~{solAmount} SOL) -

+ // Show failure screen if payment failed + if (paymentFailure) { + return ( +
+
+ + + + + + + +
+

+ PURCHASE FAILED +

+

+ Your purchase didn't go through +

+
+ + + +
+ + + +
+
+
+ ); + } + // Show success screen if payment was successful + if (paymentSuccess) { + return ( +
+
+ + + + + + + +
+

+ PURCHASE SUCCESSFUL +

+

+ Thank you for shopping with Vertex +

+
+ + + + +
+
+ ); + } + + // Show payment form + return ( +
+
+ {/* Corner decoration elements */} + + + + + + {/* Header */} +
+

+ Selected configuration +

+

+ {gpu.title} +

+
+ + {/* Price Display */} +
+ + + + + +
+ Total Price +
+ ${gpu.price_usd} + (~{solAmount} SOL) +
+
+
+ + {/* Email Input */} + {!user?.email?.address && ( +
+ YOUR EMAIL ADDRESS +
+ + + + + + setEmailInput(e.target.value)} + onBlur={() => setEmailTouched(true)} + className="w-full p-3 bg-transparent text-[#0CE77E] placeholder-[#0CE77E]/50 focus:outline-none text-base" + /> +
+ {emailTouched && emailInput && !isValidEmail(emailInput) && ( +

Please enter a valid email address.

+ )} +
+ )} + + {/* Token Selection */} +
+ SELECT PAYMENT TOKEN +
+ + + + + + +
+
+ + {/* Error and Success Messages */} {errorMsg && ( -
+
{errorMsg}
)} - + {successMsg && ( -
+
{successMsg}
)} - - {/* Wallet not connected */} - {privyWallets.length === 0 ? ( -
-

- To proceed with payment, please connect a Solana wallet. -

- -
- ) : ( - <> - {/* Show email prompt if not logged in with email */} - {!user?.email?.address && ( -
- - setEmailInput(e.target.value)} - onBlur={() => setEmailTouched(true)} - className="w-full px-3 py-2 border rounded-md text-sm" - /> - {emailTouched && emailInput && !isValidEmail(emailInput) && ( -

Please enter a valid email address.

- )} - -
- )} - -
- - -
- - )} + + {/* Action Buttons */} +
+ {/* Confirm Button */} + + + {/* Cancel Button */} + +
); }; -export default GpuPaymentModal; +export default GpuPaymentModal; \ No newline at end of file From 5a2f5fdc6d439eb6bdbef254e116d4d27d3b1273 Mon Sep 17 00:00:00 2001 From: shialoth Date: Sun, 4 May 2025 08:34:17 +0530 Subject: [PATCH 13/13] updated payment logic --- src/app/dashboard/rent/Boxes.tsx | 74 ++++++--- src/components/GpuPaymentModal.tsx | 247 ++++++++++++++++++++++++----- 2 files changed, 258 insertions(+), 63 deletions(-) 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)