forked from mito-systems/sol-mem-gen
working
This commit is contained in:
parent
c4eb9fbd13
commit
a57ab292db
326
src/app/page.tsx
326
src/app/page.tsx
@ -1,82 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
|
import WalletHeader from '../components/WalletHeader'
|
||||||
import {
|
import AIServiceCard from '../components/AIServiceCard'
|
||||||
TOKEN_PROGRAM_ID,
|
import { generateWithFlux, FluxGenerationResult } from '../services/fluxService'
|
||||||
createTransferInstruction,
|
import { processMTMPayment } from '../services/paymentService'
|
||||||
getAssociatedTokenAddress,
|
|
||||||
createAssociatedTokenAccountInstruction as createATAInstruction,
|
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
||||||
} from '@solana/spl-token'
|
|
||||||
|
|
||||||
import { fal } from "@fal-ai/client"
|
|
||||||
|
|
||||||
interface WalletState {
|
interface WalletState {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
publicKey: string | null
|
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 Page: React.FC = (): React.ReactElement => {
|
||||||
const [walletState, setWalletState] = useState<WalletState>({
|
const [walletState, setWalletState] = useState<WalletState>({
|
||||||
connected: false,
|
connected: false,
|
||||||
publicKey: null,
|
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> => {
|
const connectWallet = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@ -85,7 +24,7 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await window.solflare.connect()
|
await window.solflare.connect()
|
||||||
|
|
||||||
if (!window.solflare.publicKey) {
|
if (!window.solflare.publicKey) {
|
||||||
throw new Error('Failed to connect to wallet')
|
throw new Error('Failed to connect to wallet')
|
||||||
}
|
}
|
||||||
@ -104,240 +43,57 @@ const Page: React.FC = (): React.ReactElement => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processPaymentAndGenerate = async (): Promise<void> => {
|
const handleFluxGeneration = async (prompt: string): Promise<FluxGenerationResult> => {
|
||||||
if (!walletState.connected || !walletState.publicKey || !window.solflare || !inputText) {
|
if (!walletState.connected || !walletState.publicKey || !window.solflare) {
|
||||||
return
|
return { error: 'Wallet not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
setGenerationState({
|
// First process payment
|
||||||
...generationState,
|
const paymentResult = await processMTMPayment(
|
||||||
processing: true,
|
walletState.publicKey,
|
||||||
error: null,
|
1, // 1 MTM token
|
||||||
})
|
window.solflare
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
if (!paymentResult.success) {
|
||||||
// Process payment first
|
return { error: paymentResult.error }
|
||||||
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.',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then generate image
|
||||||
|
return generateWithFlux(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="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 */}
|
{/* 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">
|
<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>
|
</h1>
|
||||||
<p className="text-gray-400 text-lg">
|
<p className="text-gray-400 text-lg mb-8">
|
||||||
Connect your Solflare wallet and pay {REQUIRED_PAYMENT_AMOUNT} MTM token per meme
|
Generate amazing content using different AI models
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<WalletHeader
|
||||||
|
isConnected={walletState.connected}
|
||||||
|
publicKey={walletState.publicKey}
|
||||||
|
onConnect={connectWallet}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* AI Services Grid */}
|
||||||
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="p-6">
|
<AIServiceCard
|
||||||
{!walletState.connected ? (
|
title="Flux Meme Generator"
|
||||||
<button
|
description="Generate creative memes using Flux AI"
|
||||||
onClick={connectWallet}
|
tokenCost={1}
|
||||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600
|
isWalletConnected={walletState.connected}
|
||||||
text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200
|
onGenerate={handleFluxGeneration}
|
||||||
shadow-lg hover:shadow-green-500/25"
|
/>
|
||||||
>
|
|
||||||
Connect Solflare Wallet
|
{/* Add more AI service cards here */}
|
||||||
</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>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
132
src/components/AIServiceCard.tsx
Normal file
132
src/components/AIServiceCard.tsx
Normal 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
|
35
src/components/WalletHeader.tsx
Normal file
35
src/components/WalletHeader.tsx
Normal 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
|
37
src/services/fluxService.ts
Normal file
37
src/services/fluxService.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
src/services/paymentService.ts
Normal file
116
src/services/paymentService.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user