sol-mem-gen/src/app/page.tsx
2024-12-19 15:27:45 -05:00

311 lines
9.5 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import { TOKEN_PROGRAM_ID, createTransferInstruction, getAssociatedTokenAddress } from '@solana/spl-token'
interface WalletState {
connected: boolean
publicKey: string | null
}
interface PaymentStatus {
paid: boolean
processing: boolean
error: string | null
}
interface GenerationState {
loading: 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 = 'JB8YCqKBKNtS4ZHcGPJXSJokcvdgdeeyWRxGqgyX5EwP'
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 [paymentStatus, setPaymentStatus] = useState<PaymentStatus>({
paid: false,
processing: false,
error: null,
})
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
loading: false,
imageUrl: null,
error: null,
})
const connectWallet = async (): Promise<void> => {
try {
if (typeof window === 'undefined' || !window.solflare) {
throw new Error('Solflare wallet not found! Please install it first.')
}
await window.solflare.connect()
if (!window.solflare.publicKey) {
throw new Error('Failed to connect to wallet')
}
const publicKey: string = window.solflare.publicKey.toString()
setWalletState({
connected: true,
publicKey,
})
} catch (error) {
console.error('Wallet connection error:', error)
setWalletState({
connected: false,
publicKey: null,
})
}
}
const processPayment = async (): Promise<void> => {
if (!walletState.connected || !walletState.publicKey || !window.solflare) {
return
}
setPaymentStatus({ ...paymentStatus, processing: true, error: null })
try {
// Convert string addresses to PublicKeys
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
const senderATA = await getAssociatedTokenAddress(
mintPublicKey,
senderPublicKey
)
const receiverATA = await getAssociatedTokenAddress(
mintPublicKey,
receiverPublicKey
)
// Create transfer instruction
const transferInstruction = createTransferInstruction(
senderATA,
receiverATA,
senderPublicKey,
BigInt(REQUIRED_PAYMENT_AMOUNT * (10 ** 6)) // Convert to proper format
)
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.',
})
}
}
const generateMeme = async (): Promise<void> => {
if (!inputText || !paymentStatus.paid) {
return
}
setGenerationState({
...generationState,
loading: true,
error: null,
})
try {
const response = await fetch('/api/proxy', {
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',
}),
}),
})
const data = await response.json()
if (data.data && data.data[0].url) {
setGenerationState({
loading: false,
imageUrl: data.data[0].url,
error: null,
})
} else {
throw new Error('Failed to generate image')
}
} catch (error) {
setGenerationState({
...generationState,
loading: false,
error: 'Failed to generate meme. Please try again.',
})
}
}
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto space-y-8">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
AI Meme Generator
</h1>
<p className="text-lg text-gray-600">
Connect your Solflare wallet and pay with MTM tokens to generate custom memes!
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
{!walletState.connected ? (
<button
onClick={connectWallet}
className="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 transition-colors"
>
Connect Solflare Wallet
</button>
) : !paymentStatus.paid ? (
<div className="space-y-4">
<p className="text-green-600">
Wallet Connected: {walletState.publicKey?.slice(0, 8)}...
</p>
<button
onClick={processPayment}
disabled={paymentStatus.processing}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors disabled:bg-blue-300"
>
{paymentStatus.processing ? 'Processing...' : `Pay ${REQUIRED_PAYMENT_AMOUNT} MTM Tokens`}
</button>
{paymentStatus.error && (
<p className="text-red-600">{paymentStatus.error}</p>
)}
</div>
) : (
<div className="space-y-4">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter your meme text here..."
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={3}
/>
<button
onClick={generateMeme}
disabled={generationState.loading || !inputText}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors disabled:bg-green-300"
>
{generationState.loading ? 'Generating...' : 'Generate Meme'}
</button>
</div>
)}
</div>
{generationState.error && (
<div className="text-red-600 text-center">
{generationState.error}
</div>
)}
{generationState.imageUrl && (
<div className="bg-white p-6 rounded-lg shadow-md">
<img
src={generationState.imageUrl}
alt="Generated meme"
className="w-full h-auto rounded-md"
/>
</div>
)}
</div>
</div>
)
}
export default Page