forked from mito-systems/sol-mem-gen
init
This commit is contained in:
commit
01d95feb48
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal 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
5971
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal 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
16
src/app/layout.tsx
Normal 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
286
src/app/page.tsx
Normal 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
16
src/types/solflare.ts
Normal 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
34
tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user