updated payment logic
This commit is contained in:
parent
42350c9acc
commit
5a2f5fdc6d
@ -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">
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user