diff --git a/src/app/api/flux/route.ts b/src/app/api/flux/route.ts index e7b1000..4e1a52d 100644 --- a/src/app/api/flux/route.ts +++ b/src/app/api/flux/route.ts @@ -3,7 +3,8 @@ import { fal } from "@fal-ai/client" import { FLUX_MODELS } from '../../../services/fluxService' import { initializeDataSource } from '../../../data-source' -import { verifyPayment, isSignatureUsed, markSignatureAsUsed } from '../../../utils/verifyPayment' +import { verifyPayment, markSignatureAsUsed } from '../../../utils/verifyPayment' +import { getUSDCToMTMQuote } from '../../../services/paymentService' if (!process.env.FAL_AI_KEY) { throw new Error('FAL_AI_KEY is not configured in environment variables') @@ -46,7 +47,13 @@ export async function POST(req: NextRequest): Promise { ) } - const expectedAmount = model.cost; + const MTMFor1USDC = await getUSDCToMTMQuote(); + + // TODO: Use bn.js for better precision + const scale = BigInt(100); + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + + const expectedAmount = (scaledCost * MTMFor1USDC) / scale; const isPaymentVerified = await verifyPayment(transactionSignature, expectedAmount) if (!isPaymentVerified) { diff --git a/src/app/page.tsx b/src/app/page.tsx index ec3d0ce..e462742 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,14 +15,13 @@ const Page: React.FC = (): React.ReactElement => { type: null }) - const [usdcPrice, setUsdcPrice] = useState(0); + const [MTMFor1USDC, setMTMFor1USDC] = useState(BigInt(0)); useEffect(() => { - const fetchPrice = async () => { try { const price = await getUSDCToMTMQuote(); - setUsdcPrice(price); + setMTMFor1USDC(price); } catch (error) { console.error('Failed to fetch price:', error); } @@ -55,38 +54,44 @@ const Page: React.FC = (): React.ReactElement => { connected: false, publicKey: null, type: null - }) + }); } - } + }; - const handleFluxGeneration = (modelId: string, cost: number) => { + const handleFluxGeneration = (modelId: string, cost: bigint) => { return async (prompt: string): Promise => { - const type = walletState.type; - if (!walletState.connected || !walletState.publicKey || !walletState.type || + const { connected, publicKey, type } = walletState; + + if (!connected || !publicKey || !type || (type === 'phantom' && !window.phantom) || (type === 'solflare' && !window.solflare)) { - return { error: 'Wallet not connected' } + return { error: 'Wallet not connected' } } - // Process payment first - const paymentResult = await processMTMPayment( - walletState.publicKey, - cost, - walletState.type - ) + try { + // Convert cost in USDC to MTM tokens using the price ratio + const paymentResult = await processMTMPayment( + publicKey, + cost, + type + ) - if (!paymentResult.success) { - return { error: paymentResult.error } + if (!paymentResult.success) { + return { error: paymentResult.error } + } + + const transactionSignature = paymentResult.transactionSignature; + + if (!transactionSignature) { + return { error: 'Transaction signature not found' } + } + + // Generate image with specified model and transaction reference + return generateWithFlux(prompt, modelId, transactionSignature) + } catch (error) { + console.error('Error in handleFluxGeneration:', error) + return { error: error instanceof Error ? error.message : 'Unknown error' } } - - const transactionSignature = paymentResult.transactionSignature; - - if (!transactionSignature) { - return { error: 'Transaction signature not found' } - } - - // Then generate image with specified model - return generateWithFlux(prompt, modelId, transactionSignature) } } @@ -116,11 +121,24 @@ const Page: React.FC = (): React.ReactElement => { key={model.modelId} title={model.name} description={model.description} - tokenCost={model.cost} isWalletConnected={walletState.connected} - onGenerate={handleFluxGeneration(model.modelId, model.cost)} - mtmPrice={model.cost * usdcPrice} - /> + // TODO: Use bn.js for better precision + onGenerate={handleFluxGeneration( + model.modelId, + (() => { + const scale = BigInt(100); // Scaling factor + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + const scaledResult = (scaledCost * MTMFor1USDC) / scale; + return scaledResult; + })() + )} + // TODO: Use bn.js for better precision + mtmPrice={(() => { + const scale = BigInt(100); + const scaledCost = BigInt(Math.round(model.cost * Number(scale))); + return (scaledCost * MTMFor1USDC) / scale; + })()} + /> ))} {/* Coming Soon Card */} diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index aaf48ca..c0910c5 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -5,10 +5,9 @@ import React, { useState } from 'react' interface AIServiceCardProps { title: string description: string - tokenCost: number isWalletConnected: boolean onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> - mtmPrice: number | null + mtmPrice: bigint } interface GenerationState { @@ -21,7 +20,6 @@ interface GenerationState { const AIServiceCard: React.FC = ({ title, description, - tokenCost, isWalletConnected, onGenerate, mtmPrice @@ -83,7 +81,7 @@ const AIServiceCard: React.FC = ({

{description}

- Cost: {mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM + Cost: {mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM
@@ -107,7 +105,7 @@ const AIServiceCard: React.FC = ({ transition-all duration-200 shadow-lg hover:shadow-green-500/25 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none" > - {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? mtmPrice.toFixed(2) : '...'} MTM & Generate`} + {generationState.loading ? 'Processing...' : `Pay ${mtmPrice ? (mtmPrice / BigInt(10 ** 6)).toString() : '...'} MTM & Generate`} diff --git a/src/services/fluxService.ts b/src/services/fluxService.ts index e7698d3..a947fb0 100644 --- a/src/services/fluxService.ts +++ b/src/services/fluxService.ts @@ -16,19 +16,19 @@ export const FLUX_MODELS: FluxModelConfig[] = [ modelId: "fal-ai/flux/schnell", name: "Schnell", description: "Fast meme generator", - cost: 0.1 + cost: 0.05 }, { modelId: "fal-ai/recraft-v3", name: "Recraft", description: "Advanced meme generator", - cost: 0.15 + cost: 0.10 }, { modelId: "fal-ai/stable-diffusion-v35-large", name: "Marquee", description: "Best meme generator", - cost: 0.2 + cost: 0.15 } ] diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 49956e1..dd12f4a 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -60,8 +60,10 @@ export async function getUSDCToMTMQuote() { } const quoteResponse = await response.json(); - const price = quoteResponse['data'][USDC_MINT]['price'] + // TODO: Use bn.js for better precision + const priceFromAPI = Number(quoteResponse['data'][USDC_MINT]['price'])* (10 ** 6); + const price = BigInt(Number(priceFromAPI.toFixed(0))); return price; } @@ -71,7 +73,7 @@ interface WalletAdapter { export async function processMTMPayment( walletPublicKey: string, - tokenAmount: number, + tokenAmount: bigint, walletType: WalletType ): Promise { try { @@ -143,12 +145,14 @@ export async function processMTMPayment( ) } + const tokens = tokenAmount / BigInt(10 ** 6); + transaction.add( createTransferInstruction( senderATA, receiverATA, senderPublicKey, - BigInt(tokenAmount * (10 ** 6)) + tokens * BigInt(10 ** 6) ) ) @@ -157,7 +161,7 @@ export async function processMTMPayment( transaction.feePayer = senderPublicKey console.log('Sending transaction...') - const { signature } = await wallet.signAndSendTransaction(transaction) + const { signature } = await wallet.signAndSendTransaction(transaction); console.log('Transaction sent:', signature) const confirmation = await connection.confirmTransaction({ diff --git a/src/utils/verifyPayment.ts b/src/utils/verifyPayment.ts index 770a984..0caf559 100644 --- a/src/utils/verifyPayment.ts +++ b/src/utils/verifyPayment.ts @@ -1,9 +1,13 @@ +import assert from 'assert'; + import { Connection } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AppDataSource } from '../data-source'; import { Payment } from '../entity/Payment'; +assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); + const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_WEBSOCKET_URL = process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL; @@ -42,7 +46,7 @@ export async function markSignatureAsUsed(transactionSignature: string): Promise export async function verifyPayment( transactionSignature: string, - expectedAmount: number, + expectedAmount: bigint, ): Promise { try { // Check if the signature is already used @@ -67,7 +71,14 @@ export async function verifyPayment( const { info } = parsed; const { amount } = info; - if (BigInt(amount) === BigInt(expectedAmount * (10 ** 6))) { + const transactionAmount = BigInt(amount); + const expectedAmountInMicroTokens = expectedAmount; + + const lowerBound = expectedAmountInMicroTokens * BigInt(9) / BigInt(10); + const upperBound = expectedAmountInMicroTokens * BigInt(11) / BigInt(10); + + // transaction within the appropriate range + if (transactionAmount >= lowerBound && transactionAmount <= upperBound) { return true; }