updated payment logic

This commit is contained in:
shialoth 2025-05-04 08:34:17 +05:30
parent 42350c9acc
commit 5a2f5fdc6d
2 changed files with 258 additions and 63 deletions

View File

@ -17,79 +17,101 @@ export default function Boxes() {
const [selectedGpu, setSelectedGpu] = useState<IGPU | null>(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 (
<>
<div className="grid grid-cols-2 gap-4">

View File

@ -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 (
<div className="relative h-[54px] w-[54px]">
<div className="absolute bg-red-600 rounded-xl h-[54px] w-[54px]" />
<div className="absolute top-0 left-0 bg-red-600 rounded-xl h-[54px] w-[54px]" />
<svg
width="39"
width="38"
height="38"
viewBox="0 0 39 38"
viewBox="0 0 38 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute w-[38px] h-[38px] left-[9px] top-[8px]"
className="absolute top-2 left-2"
aria-hidden="true"
>
<path
d="M29 9.5L10 28.5M10 9.5L29 28.5"
@ -171,6 +179,7 @@ const ErrorIcon = () => {
);
};
const ErrorBox = () => {
return (
<div className="relative w-full max-w-[357px] h-[60px]">
@ -222,6 +231,9 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
const { wallets: privyWallets } = useSolanaWallets();
const [solPrice, setSolPrice] = useState<number | null>(null);
const [customToken, setCustomToken] = useState<CustomToken | null>(null);
const [customTokenPrice, setCustomTokenPrice] = useState<number | null>(null);
const [customTokenName, setCustomTokenName] = useState<string>('VERTEX');
const [emailInput, setEmailInput] = useState('');
const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(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<string>('');
useEffect(() => {
const fetchSolPrice = async () => {
try {
@ -248,19 +263,160 @@ 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);
@ -300,15 +456,16 @@ 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);
@ -317,6 +474,25 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
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,
@ -341,8 +517,7 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
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)
@ -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)
<span className="text-base font-medium text-white">Total Price</span>
<div className="flex gap-3 items-center">
<span className="text-base font-bold text-[#0CE77E]">${gpu.price_usd}</span>
<span className="text-base text-[#0CE77E]">(~{solAmount} SOL)</span>
<span className="text-base text-[#0CE77E]">(~{currentTokenAmount} {selectedToken})</span>
</div>
</div>
</div>
@ -572,12 +745,12 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
<select
value={selectedToken}
onChange={(e) => setSelectedToken(e.target.value as 'SOL' | 'USDC' | 'JUP')}
className="w-full p-3 bg-transparent text-[#0CE77E] focus:outline-none text-base cursor-pointer"
onChange={(e) => setSelectedToken(e.target.value as 'SOL' | 'USDC' | 'VERTEX')}
className="w-full p-3 bg-transparent text-[#0CE77E] focus:outline-none text-base cursor-pointer"
>
<option value="SOL" className="bg-[#0E1618] text-[#0CE77E]">SOL</option>
<option value="USDC" className="bg-[#0E1618] text-[#0CE77E]">USDC</option>
<option value="JUP" className="bg-[#0E1618] text-[#0CE77E]">JUP</option>
<option value="VERTEX" className="bg-[#0E1618] text-[#0CE77E]">{customTokenName}</option>
</select>
</div>
</div>