diff --git a/package-lock.json b/package-lock.json index e9b0a69..1887b3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "solana-meme-generator", "version": "0.1.0", "dependencies": { + "@fal-ai/client": "^1.2.1", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", "@types/node": "20.5.7", @@ -131,6 +132,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fal-ai/client": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fal-ai/client/-/client-1.2.1.tgz", + "integrity": "sha512-Z/bztRqOeduuUYl74VPQRDS12vjXGGebROJM7NcJXCmkV4GSZllA68ES1GwY3Q+31puukBoOppC0iBmY6us5MQ==", + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "eventsource-parser": "^1.1.2", + "robot3": "^0.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -272,6 +286,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpack/msgpack": { + "version": "3.0.0-beta2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.0.0-beta2.tgz", + "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/@next/env": { "version": "13.4.19", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz", @@ -2609,6 +2631,14 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "engines": { + "node": ">=14.18" + } + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -4723,6 +4753,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==" + }, "node_modules/rpc-websockets": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.4.tgz", diff --git a/package.json b/package.json index ed96fec..29ba1f7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@fal-ai/client": "^1.2.1", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.4", "@types/node": "20.5.7", diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts new file mode 100644 index 0000000..7180f22 --- /dev/null +++ b/src/app/api/generate/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' +import { fal } from "@fal-ai/client" + +// Add debug logging +const FAL_KEY = process.env.FAL_AI_KEY + +if (!FAL_KEY) { + throw new Error('FAL_AI_KEY is not configured in environment variables') +} + +// Configure fal client server-side only +fal.config({ + credentials: FAL_KEY +}) + +export async function POST(req: NextRequest): Promise { + try { + const { prompt } = await req.json() + + if (!prompt) { + return NextResponse.json( + { error: 'Prompt is required' }, + { status: 400 } + ) + } + + console.log('Attempting Fal.ai request with prompt:', prompt) + + const result = await fal.subscribe("fal-ai/flux-pro/v1.1", { + input: { + prompt + }, + logs: true, + onQueueUpdate: (update) => { + if (update.status === "IN_PROGRESS") { + console.log('Generation progress:', update.logs.map((log) => log.message)) + } + }, + }) + + console.log('Fal.ai response received:', result ? 'Success' : 'Empty response') + return NextResponse.json(result) + } catch (error) { + console.error('Image generation error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to generate image' }, + { status: 500 } + ) + } +} + +export const dynamic = 'force-dynamic' diff --git a/src/app/page.tsx b/src/app/page.tsx index c63ef53..73c1121 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,21 +2,31 @@ import React, { useState } from 'react' import { Connection, PublicKey, Transaction } from '@solana/web3.js' -import { TOKEN_PROGRAM_ID, createTransferInstruction, getAssociatedTokenAddress } from '@solana/spl-token' +import { + TOKEN_PROGRAM_ID, + createTransferInstruction, + getAssociatedTokenAddress, + createAssociatedTokenAccountInstruction as createATAInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID +} from '@solana/spl-token' + +import { fal } from "@fal-ai/client" interface WalletState { connected: boolean publicKey: string | null } - +/* interface PaymentStatus { paid: boolean processing: boolean error: string | null } +*/ interface GenerationState { loading: boolean + processing: boolean imageUrl: string | null error: string | null } @@ -41,7 +51,7 @@ declare global { // Replace these with your actual addresses const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump' -const PAYMENT_RECEIVER_ADDRESS: string = 'JB8YCqKBKNtS4ZHcGPJXSJokcvdgdeeyWRxGqgyX5EwP' +const PAYMENT_RECEIVER_ADDRESS: string = '9B3mGyeJTUN7ZTqyLWHLL37zL92eif239hH2pYSkvq8J' const REQUIRED_PAYMENT_AMOUNT: number = 2 const SOLANA_NETWORK: string = 'mainnet' const SOLANA_RPC_URL: string = 'https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4' @@ -60,14 +70,10 @@ const Page: React.FC = (): React.ReactElement => { connected: false, publicKey: null, }) - const [paymentStatus, setPaymentStatus] = useState({ - paid: false, - processing: false, - error: null, - }) const [inputText, setInputText] = useState('') const [generationState, setGenerationState] = useState({ loading: false, + processing: false, imageUrl: null, error: null, }) @@ -98,20 +104,24 @@ const Page: React.FC = (): React.ReactElement => { } } - const processPayment = async (): Promise => { - if (!walletState.connected || !walletState.publicKey || !window.solflare) { +const processPaymentAndGenerate = async (): Promise => { + if (!walletState.connected || !walletState.publicKey || !window.solflare || !inputText) { return } - setPaymentStatus({ ...paymentStatus, processing: true, error: null }) + setGenerationState({ + ...generationState, + processing: true, + error: null, + }) try { - // Convert string addresses to PublicKeys + // Process payment first const senderPublicKey = new PublicKey(walletState.publicKey) const mintPublicKey = new PublicKey(MTM_TOKEN_MINT) const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS) - // Get the associated token accounts for sender and receiver + // Get the associated token accounts const senderATA = await getAssociatedTokenAddress( mintPublicKey, senderPublicKey @@ -121,112 +131,113 @@ const Page: React.FC = (): React.ReactElement => { receiverPublicKey ) - // Create transfer instruction + // Create transaction + const transaction = new Transaction() + + // Check if receiver's ATA exists + const receiverATAInfo = await connection.getAccountInfo(receiverATA) + if (!receiverATAInfo) { + console.log('Creating receiver ATA...') + transaction.add( + createATAInstruction( + senderPublicKey, // payer + receiverATA, // ata + receiverPublicKey, // owner + mintPublicKey, // mint + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ) + ) + } + + // Check if sender's ATA exists + const senderATAInfo = await connection.getAccountInfo(senderATA) + if (!senderATAInfo) { + console.log('Creating sender ATA...') + transaction.add( + createATAInstruction( + senderPublicKey, // payer + senderATA, // ata + senderPublicKey, // owner + mintPublicKey, // mint + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ) + ) + } + + // Add transfer instruction const transferInstruction = createTransferInstruction( senderATA, receiverATA, senderPublicKey, - BigInt(REQUIRED_PAYMENT_AMOUNT * (10 ** 6)) // Convert to proper format + BigInt(REQUIRED_PAYMENT_AMOUNT * (10 ** 9)) ) + transaction.add(transferInstruction) const latestBlockhash = await connection.getLatestBlockhash('confirmed') - - // Create transaction - const transaction = new Transaction() - transaction.add(transferInstruction) transaction.recentBlockhash = latestBlockhash.blockhash transaction.feePayer = senderPublicKey - try { - // Sign and send transaction with preflight disabled - const { signature } = await window.solflare.signAndSendTransaction(transaction, { - skipPreflight: true - }) - - console.log('Transaction sent:', signature) - - // Wait for confirmation with WebSocket support - const confirmation = await connection.confirmTransaction({ - signature, - blockhash: latestBlockhash.blockhash, - lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, - }, 'confirmed') - - console.log('Transaction confirmed:', confirmation) - - if (confirmation.value.err) { - throw new Error('Transaction failed to confirm') - } - - setPaymentStatus({ - paid: true, - processing: false, - error: null, - }) - } catch (error) { - console.error('Transaction error:', error) - throw new Error(`Failed to sign or send transaction: ${error instanceof Error ? error.message : 'Unknown error'}`) - } - } catch (error) { - console.error('Payment error:', error) - setPaymentStatus({ - ...paymentStatus, - processing: false, - error: error instanceof Error ? error.message : 'Payment failed. Please try again.', + console.log('Sending transaction...') + // Sign and send transaction + const { signature } = await window.solflare.signAndSendTransaction(transaction, { + skipPreflight: false // Enable preflight checks }) - } - } - const generateMeme = async (): Promise => { - if (!inputText || !paymentStatus.paid) { - return - } + console.log('Transaction sent:', signature) - setGenerationState({ - ...generationState, - loading: true, - error: null, - }) + // Wait for confirmation + const confirmation = await connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, 'confirmed') - try { - const response = await fetch('/api/proxy', { + console.log('Transaction confirmed:', confirmation) + + if (confirmation.value.err) { + throw new Error('Payment failed to confirm') + } + + // After payment is confirmed, generate the meme + setGenerationState(prev => ({ ...prev, loading: true })) + + // Call our secure API endpoint instead of direct Fal.ai call + const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - protocol: 'https', - origin: 'api.openai.com', - path: '/v1/images/generations', - headers: { - 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - prompt: `Generate a meme with text: ${inputText}`, - n: 1, - size: '512x512', - }), + prompt: inputText, }), }) - const data = await response.json() + if (!response.ok) { + throw new Error('Failed to generate image') + } - if (data.data && data.data[0].url) { + const result = await response.json() + console.log("Generation Response:", result) + + if (result.data && result.data.images && result.data.images[0]?.url) { setGenerationState({ loading: false, - imageUrl: data.data[0].url, + processing: false, + imageUrl: result.data.images[0].url, error: null, }) } else { - throw new Error('Failed to generate image') + throw new Error('No image URL in response') } } catch (error) { + console.error('Generation error:', error) setGenerationState({ ...generationState, loading: false, - error: 'Failed to generate meme. Please try again.', + processing: false, + error: error instanceof Error ? error.message : 'Failed to process payment or generate meme.', }) } } @@ -239,7 +250,7 @@ const Page: React.FC = (): React.ReactElement => { AI Meme Generator

- Connect your Solflare wallet and pay with MTM tokens to generate custom memes! + Connect your Solflare wallet and pay {REQUIRED_PAYMENT_AMOUNT} MTM token per meme generation!

@@ -251,24 +262,11 @@ const Page: React.FC = (): React.ReactElement => { > Connect Solflare Wallet - ) : !paymentStatus.paid ? ( + ) : (

Wallet Connected: {walletState.publicKey?.slice(0, 8)}...

- - {paymentStatus.error && ( -

{paymentStatus.error}

- )} -
- ) : ( -