This commit is contained in:
zramsay 2024-12-20 09:53:47 -05:00
parent c4eb9fbd13
commit a57ab292db
5 changed files with 361 additions and 285 deletions

View File

@ -1,82 +1,21 @@
'use client'
import React, { useState } from 'react'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import {
TOKEN_PROGRAM_ID,
createTransferInstruction,
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction as createATAInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token'
import { fal } from "@fal-ai/client"
import WalletHeader from '../components/WalletHeader'
import AIServiceCard from '../components/AIServiceCard'
import { generateWithFlux, FluxGenerationResult } from '../services/fluxService'
import { processMTMPayment } from '../services/paymentService'
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
}
// Solflare wallet type definition
interface SolflareWallet {
connect(): Promise<void>
disconnect(): Promise<void>
signAndSendTransaction(
transaction: Transaction | VersionedTransaction,
options?: { skipPreflight?: boolean }
): Promise<{ signature: string; publicKey: string }>
publicKey: PublicKey
connected: boolean
}
declare global {
interface Window {
solflare?: SolflareWallet
}
}
// Replace these with your actual addresses
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
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'
const SOLANA_WEBSOCKET_URL: string = 'wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
const connection = new Connection(
SOLANA_RPC_URL,
{
commitment: 'confirmed',
wsEndpoint: SOLANA_WEBSOCKET_URL,
confirmTransactionInitialTimeout: 30000, // 30 seconds
}
)
const Page: React.FC = (): React.ReactElement => {
const [walletState, setWalletState] = useState<WalletState>({
connected: false,
publicKey: null,
})
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
loading: false,
processing: false,
imageUrl: null,
error: null,
})
const connectWallet = async (): Promise<void> => {
try {
@ -85,7 +24,7 @@ const Page: React.FC = (): React.ReactElement => {
}
await window.solflare.connect()
if (!window.solflare.publicKey) {
throw new Error('Failed to connect to wallet')
}
@ -104,240 +43,57 @@ const Page: React.FC = (): React.ReactElement => {
}
}
const processPaymentAndGenerate = async (): Promise<void> => {
if (!walletState.connected || !walletState.publicKey || !window.solflare || !inputText) {
return
const handleFluxGeneration = async (prompt: string): Promise<FluxGenerationResult> => {
if (!walletState.connected || !walletState.publicKey || !window.solflare) {
return { error: 'Wallet not connected' }
}
setGenerationState({
...generationState,
processing: true,
error: null,
})
// First process payment
const paymentResult = await processMTMPayment(
walletState.publicKey,
1, // 1 MTM token
window.solflare
)
try {
// 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
const senderATA = await getAssociatedTokenAddress(
mintPublicKey,
senderPublicKey
)
const receiverATA = await getAssociatedTokenAddress(
mintPublicKey,
receiverPublicKey
)
// 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 ** 9))
)
transaction.add(transferInstruction)
const latestBlockhash = await connection.getLatestBlockhash('confirmed')
transaction.recentBlockhash = latestBlockhash.blockhash
transaction.feePayer = senderPublicKey
console.log('Sending transaction...')
// Sign and send transaction
const { signature } = await window.solflare.signAndSendTransaction(transaction, {
skipPreflight: false // Enable preflight checks
})
console.log('Transaction sent:', signature)
// Wait for confirmation
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('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({
prompt: inputText,
}),
})
if (!response.ok) {
throw new Error('Failed to generate image')
}
const result = await response.json()
console.log("Generation Response:", result)
if (result.data && result.data.images && result.data.images[0]?.url) {
setGenerationState({
loading: false,
processing: false,
imageUrl: result.data.images[0].url,
error: null,
})
} else {
throw new Error('No image URL in response')
}
} catch (error) {
console.error('Generation error:', error)
setGenerationState({
...generationState,
loading: false,
processing: false,
error: error instanceof Error ? error.message : 'Failed to process payment or generate meme.',
})
if (!paymentResult.success) {
return { error: paymentResult.error }
}
// Then generate image
return generateWithFlux(prompt)
}
return (
<div className="min-h-screen w-full flex flex-col items-center bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900">
<div className="container max-w-2xl mx-auto px-4 py-8 flex flex-col items-center">
<div className="container max-w-6xl mx-auto px-4 py-8">
{/* Header */}
<div className="w-full text-center mb-8">
<div className="text-center mb-8">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600">
AI Meme Generator
AI Content Generator
</h1>
<p className="text-gray-400 text-lg">
Connect your Solflare wallet and pay {REQUIRED_PAYMENT_AMOUNT} MTM token per meme
<p className="text-gray-400 text-lg mb-8">
Generate amazing content using different AI models
</p>
<WalletHeader
isConnected={walletState.connected}
publicKey={walletState.publicKey}
onConnect={connectWallet}
/>
</div>
{/* Main Content */}
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
<div className="p-6">
{!walletState.connected ? (
<button
onClick={connectWallet}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600
text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200
shadow-lg hover:shadow-green-500/25"
>
Connect Solflare Wallet
</button>
) : (
<div className="space-y-6">
{/* Wallet Info */}
<div className="flex items-center justify-between bg-gray-900/30 rounded-lg p-3">
<span className="text-gray-400">Connected Wallet</span>
<span className="px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
{walletState.publicKey?.slice(0, 8)}...
</span>
</div>
{/* Input Area */}
<div className="space-y-4">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Describe your meme idea here..."
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
focus:outline-none min-h-[120px] transition-all duration-200"
rows={4}
/>
<button
onClick={processPaymentAndGenerate}
disabled={generationState.processing || generationState.loading || !inputText}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-green-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{generationState.processing ? 'Processing Payment...' :
generationState.loading ? 'Generating Meme...' :
`Pay ${REQUIRED_PAYMENT_AMOUNT} MTM & Generate Meme`}
</button>
</div>
</div>
)}
</div>
{/* AI Services Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<AIServiceCard
title="Flux Meme Generator"
description="Generate creative memes using Flux AI"
tokenCost={1}
isWalletConnected={walletState.connected}
onGenerate={handleFluxGeneration}
/>
{/* Add more AI service cards here */}
</div>
{/* Error Message */}
{generationState.error && (
<div className="w-full mb-8">
<div className="bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
{generationState.error}
</div>
</div>
)}
{/* Loading Indicator */}
{(generationState.processing || generationState.loading) && (
<div className="flex justify-center items-center w-full mb-8">
<div className="flex space-x-2">
<div className="w-3 h-3 bg-green-400 rounded-full animate-bounce"></div>
<div className="w-3 h-3 bg-green-400 rounded-full animate-bounce delay-100"></div>
<div className="w-3 h-3 bg-green-400 rounded-full animate-bounce delay-200"></div>
</div>
</div>
)}
{/* Generated Image */}
{generationState.imageUrl && (
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 overflow-hidden">
<div className="p-4">
<img
src={generationState.imageUrl}
alt="Generated meme"
className="w-full h-auto rounded-xl shadow-2xl"
/>
</div>
</div>
)}
</div>
</div>
)

View File

@ -0,0 +1,132 @@
'use client'
import React, { useState } from 'react'
interface AIServiceCardProps {
title: string
description: string
tokenCost: number
isWalletConnected: boolean
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
}
interface GenerationState {
loading: boolean
processing: boolean
imageUrl: string | null
error: string | null
}
const AIServiceCard: React.FC<AIServiceCardProps> = ({
title,
description,
tokenCost,
isWalletConnected,
onGenerate
}) => {
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
loading: false,
processing: false,
imageUrl: null,
error: null,
})
const handleGenerate = async (): Promise<void> => {
if (!inputText || !isWalletConnected) return
setGenerationState({
...generationState,
loading: true,
error: null,
})
try {
const result = await onGenerate(inputText)
if (result.error) {
setGenerationState({
...generationState,
loading: false,
error: result.error,
})
return
}
if (result.imageUrl) {
setGenerationState({
loading: false,
processing: false,
imageUrl: result.imageUrl,
error: null,
})
} else {
throw new Error('No image URL received')
}
} catch (error) {
setGenerationState({
...generationState,
loading: false,
error: error instanceof Error ? error.message : 'Generation failed',
})
}
}
return (
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
<div className="p-6">
<div className="mb-4">
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600">
{title}
</h2>
<p className="text-gray-400 mt-2">{description}</p>
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
Cost: {tokenCost} MTM
</div>
</div>
<div className="space-y-4">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter your prompt here..."
disabled={!isWalletConnected}
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
focus:outline-none min-h-[120px] transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed"
rows={4}
/>
<button
onClick={handleGenerate}
disabled={!isWalletConnected || generationState.loading || !inputText}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
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 ${tokenCost} MTM & Generate`}
</button>
</div>
{generationState.error && (
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
{generationState.error}
</div>
)}
{generationState.imageUrl && (
<div className="mt-4">
<img
src={generationState.imageUrl}
alt="Generated content"
className="w-full h-auto rounded-xl shadow-2xl"
/>
</div>
)}
</div>
</div>
)
}
export default AIServiceCard

View File

@ -0,0 +1,35 @@
'use client'
import React from 'react'
interface WalletHeaderProps {
isConnected: boolean
publicKey: string | null
onConnect: () => Promise<void>
}
const WalletHeader: React.FC<WalletHeaderProps> = ({ isConnected, publicKey, onConnect }) => {
return (
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-xl shadow-lg border border-gray-700/50 mb-8 p-4">
{!isConnected ? (
<button
onClick={onConnect}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600
text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200
shadow-lg hover:shadow-green-500/25"
>
Connect Solflare Wallet
</button>
) : (
<div className="flex items-center justify-between">
<span className="text-gray-400">Connected Wallet</span>
<span className="px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
{publicKey?.slice(0, 8)}...
</span>
</div>
)}
</div>
)
}
export default WalletHeader

View File

@ -0,0 +1,37 @@
import { fal } from "@fal-ai/client"
export interface FluxGenerationResult {
imageUrl?: string
error?: string
}
export async function generateWithFlux(prompt: string): Promise<FluxGenerationResult> {
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt }),
})
if (!response.ok) {
throw new Error('Failed to generate image')
}
const data = await response.json()
console.log('Raw Flux response:', data)
if (data.data?.images?.[0]?.url) {
return { imageUrl: data.data.images[0].url }
} else {
console.error('Unexpected response structure:', data)
throw new Error('Invalid response format from Flux API')
}
} catch (error) {
console.error('Generation error:', error)
return {
error: error instanceof Error ? error.message : 'Generation failed'
}
}
}

View File

@ -0,0 +1,116 @@
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import {
TOKEN_PROGRAM_ID,
createTransferInstruction,
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction as createATAInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token'
// Constants
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
const PAYMENT_RECEIVER_ADDRESS: string = '9B3mGyeJTUN7ZTqyLWHLL37zL92eif239hH2pYSkvq8J'
// RPC Configuration
const SOLANA_RPC_URL: string = 'https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
const SOLANA_WEBSOCKET_URL: string = 'wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
// Initialize connection with WebSocket support
const connection = new Connection(
SOLANA_RPC_URL,
{
commitment: 'confirmed',
wsEndpoint: SOLANA_WEBSOCKET_URL,
confirmTransactionInitialTimeout: 30000, // 30 seconds
}
)
export interface PaymentResult {
success: boolean
error?: string
}
export async function processMTMPayment(
walletPublicKey: string,
tokenAmount: number,
solflareWallet: any
): Promise<PaymentResult> {
try {
const senderPublicKey = new PublicKey(walletPublicKey)
const mintPublicKey = new PublicKey(MTM_TOKEN_MINT)
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS)
const senderATA = await getAssociatedTokenAddress(
mintPublicKey,
senderPublicKey
)
const receiverATA = await getAssociatedTokenAddress(
mintPublicKey,
receiverPublicKey
)
const transaction = new Transaction()
// Check and create ATAs if needed
const receiverATAInfo = await connection.getAccountInfo(receiverATA)
if (!receiverATAInfo) {
transaction.add(
createATAInstruction(
senderPublicKey,
receiverATA,
receiverPublicKey,
mintPublicKey,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
)
)
}
const senderATAInfo = await connection.getAccountInfo(senderATA)
if (!senderATAInfo) {
transaction.add(
createATAInstruction(
senderPublicKey,
senderATA,
senderPublicKey,
mintPublicKey,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
)
)
}
// Add transfer instruction
const transferInstruction = createTransferInstruction(
senderATA,
receiverATA,
senderPublicKey,
BigInt(tokenAmount * (10 ** 6))
)
transaction.add(transferInstruction)
const latestBlockhash = await connection.getLatestBlockhash('confirmed')
transaction.recentBlockhash = latestBlockhash.blockhash
transaction.feePayer = senderPublicKey
const { signature } = await solflareWallet.signAndSendTransaction(transaction)
const confirmation = await connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
})
if (confirmation.value.err) {
return { success: false, error: 'Payment failed to confirm' }
}
return { success: true }
} catch (error) {
console.error('Payment error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Payment failed'
}
}
}