This commit is contained in:
zramsay 2024-12-19 15:22:30 -05:00
commit 01d95feb48
7 changed files with 6359 additions and 0 deletions

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

5971
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "solana-meme-generator",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.4",
"@types/node": "20.5.7",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"next": "13.4.19",
"postcss": "8.4.28",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19"
}
}

16
src/app/layout.tsx Normal file
View File

@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

286
src/app/page.tsx Normal file
View File

@ -0,0 +1,286 @@
'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
}
// Replace these with your actual addresses
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
const PAYMENT_RECEIVER_ADDRESS: string = 'JB8YCqKBKNtS4ZHcGPJXSJokcvdgdeeyWRxGqgyX5EwP'
const REQUIRED_PAYMENT_AMOUNT: number = 1
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) {
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 ** 9)) // Convert to proper format
)
const latestBlockhash = await connection.getLatestBlockhash()
// Create transaction
const transaction = new Transaction()
transaction.add(transferInstruction)
transaction.recentBlockhash = latestBlockhash.blockhash
transaction.feePayer = senderPublicKey
try {
// Request signature and send transaction
const signedTransaction = await window.solflare.signAndSendTransaction(transaction)
const signature = await connection.sendRawTransaction(signedTransaction.serialize())
// Wait for confirmation
const confirmation = await connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
})
if (confirmation.value.err) {
throw new Error('Transaction failed to confirm')
}
setPaymentStatus({
paid: true,
processing: false,
error: null,
})
} catch (error) {
throw new Error('Failed to sign or send transaction')
}
} catch (error) {
console.error('Payment error:', error)
setPaymentStatus({
...paymentStatus,
processing: false,
error: '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

16
src/types/solflare.ts Normal file
View File

@ -0,0 +1,16 @@
export interface SolflareWallet {
connect(): Promise<void>
disconnect(): Promise<void>
signAndSendTransaction(transaction: any): Promise<any>
publicKey: {
toString(): string
}
}
declare global {
interface Window {
solflare?: SolflareWallet
}
}
export {}

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}