From e7117db99861e322fcaf0a5c6bcf981b19a0221d Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 5 Feb 2025 11:40:55 +0530 Subject: [PATCH 01/16] Setup dynamic routing for generating memes --- src/app/memes/[id]/page.tsx | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/app/memes/[id]/page.tsx diff --git a/src/app/memes/[id]/page.tsx b/src/app/memes/[id]/page.tsx new file mode 100644 index 0000000..433a357 --- /dev/null +++ b/src/app/memes/[id]/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata, ResolvingMetadata } from 'next' + +type Props = { + params: { id: string } + searchParams: { [key: string]: string | string[] | undefined } +} + +export async function generateMetadata( + { params, searchParams }: Props, + parent: ResolvingMetadata +): Promise { + const id = (await params).id + + const product = { + title: 'Generated meme', + imageUrl: 'https://jade-wonderful-barnacle-735.mypinata.cloud/ipfs/bafybeiaojxp5jkr3vc5t4ruopet4fsxgn7kjsmwi3bu6w7g7r2vpt2jj2q', + description: 'A funny meme', + } + + return { + title: product.title, + metadataBase: new URL('https://memes.markto.market'), + openGraph: { + type: "website", + url: `https://memes.markto.market/memes/${id}`, + siteName: "Mark's meme market", + images: [{ + url: product.imageUrl, + }, ], + } + + } +} + +export default async function MemePage({ params }: Props) { + const product = { + title: 'Generated meme', + imageUrl: 'https://jade-wonderful-barnacle-735.mypinata.cloud/ipfs/bafybeiaojxp5jkr3vc5t4ruopet4fsxgn7kjsmwi3bu6w7g7r2vpt2jj2q', + description: 'A funny meme', + } + + return ( +
+
+

{product.title}

+
+ {product.title} +
+

{product.description}

+
+
+ ); +} -- 2.45.2 From c7e291c19e10fb23a83bc60444f934037cc25271 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 5 Feb 2025 13:34:57 +0530 Subject: [PATCH 02/16] Derive image URL from params --- .env.example | 2 ++ src/app/memes/[id]/page.tsx | 69 +++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index 3263b51..bd04e2e 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,5 @@ NEXT_PUBLIC_USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v # Get keys from https://app.pinata.cloud/developers/api-keys PINATA_JWT= PINATA_GATEWAY= + +SITE_URL=https://memes.markto.market diff --git a/src/app/memes/[id]/page.tsx b/src/app/memes/[id]/page.tsx index 433a357..22cfcef 100644 --- a/src/app/memes/[id]/page.tsx +++ b/src/app/memes/[id]/page.tsx @@ -1,56 +1,59 @@ -import type { Metadata, ResolvingMetadata } from 'next' +import type { Metadata, ResolvingMetadata } from 'next'; -type Props = { - params: { id: string } - searchParams: { [key: string]: string | string[] | undefined } +interface Props { + params: { id: string }; +} + +interface Meme { + title: string; + imageUrl: string; + description: string; +} + +function getMeme(baseUrl: string, id: string): Meme { + return { + title: 'Generated meme', + imageUrl: `${baseUrl}/api/images/${id}`, + description: 'A funny meme', + }; } export async function generateMetadata( - { params, searchParams }: Props, + { params }: Props, parent: ResolvingMetadata ): Promise { - const id = (await params).id - - const product = { - title: 'Generated meme', - imageUrl: 'https://jade-wonderful-barnacle-735.mypinata.cloud/ipfs/bafybeiaojxp5jkr3vc5t4ruopet4fsxgn7kjsmwi3bu6w7g7r2vpt2jj2q', - description: 'A funny meme', - } + const baseUrl = process.env.SITE_URL!; + const meme = getMeme(baseUrl, params.id); return { - title: product.title, - metadataBase: new URL('https://memes.markto.market'), + title: meme.title, + description: meme.description, + metadataBase: new URL(baseUrl), openGraph: { - type: "website", - url: `https://memes.markto.market/memes/${id}`, - siteName: "Mark's meme market", - images: [{ - url: product.imageUrl, - }, ], - } - - } + type: 'website', + url: `${baseUrl}/memes/${params.id}`, + siteName: "Mark's meme market", + images: [{ url: meme.imageUrl }], + }, + }; } -export default async function MemePage({ params }: Props) { - const product = { - title: 'Generated meme', - imageUrl: 'https://jade-wonderful-barnacle-735.mypinata.cloud/ipfs/bafybeiaojxp5jkr3vc5t4ruopet4fsxgn7kjsmwi3bu6w7g7r2vpt2jj2q', - description: 'A funny meme', - } +export default function MemePage({ params }: Props) { + const baseUrl = process.env.SITE_URL!; + const meme = getMeme(baseUrl, params.id); return (
-

{product.title}

+

{meme.title}

{product.title}
-

{product.description}

+

{meme.description}

); -- 2.45.2 From 1976cfbbe68c901b10b85d75a344d9638f53e787 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 5 Feb 2025 14:19:32 +0530 Subject: [PATCH 03/16] Add button to share generated meme on twitter --- src/app/memes/[id]/page.tsx | 32 ++++++++++---------------------- src/components/AIServiceCard.tsx | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/app/memes/[id]/page.tsx b/src/app/memes/[id]/page.tsx index 22cfcef..da6942d 100644 --- a/src/app/memes/[id]/page.tsx +++ b/src/app/memes/[id]/page.tsx @@ -4,18 +4,9 @@ interface Props { params: { id: string }; } -interface Meme { - title: string; - imageUrl: string; - description: string; -} - -function getMeme(baseUrl: string, id: string): Meme { - return { - title: 'Generated meme', - imageUrl: `${baseUrl}/api/images/${id}`, - description: 'A funny meme', - }; +function getMeme(id: string): string { + const pinataImageUrl = `https://${process.env.PINATA_GATEWAY}/ipfs/${id}` + return pinataImageUrl; } export async function generateMetadata( @@ -23,37 +14,34 @@ export async function generateMetadata( parent: ResolvingMetadata ): Promise { const baseUrl = process.env.SITE_URL!; - const meme = getMeme(baseUrl, params.id); + const meme = getMeme(params.id); return { - title: meme.title, - description: meme.description, + title: '', + description: '', metadataBase: new URL(baseUrl), openGraph: { type: 'website', url: `${baseUrl}/memes/${params.id}`, siteName: "Mark's meme market", - images: [{ url: meme.imageUrl }], + images: [{ url: meme }], }, }; } export default function MemePage({ params }: Props) { - const baseUrl = process.env.SITE_URL!; - const meme = getMeme(baseUrl, params.id); + const meme = getMeme(params.id); return (
-

{meme.title}

{meme.title}
-

{meme.description}

); diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 12ef008..9f1010d 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -86,6 +86,14 @@ const AIServiceCard: React.FC = ({ } } + const generateTwitterShareUrl = (imageUrl: string): string => { + const baseUrl = window.location.href; + const ipfsImageUrl = imageUrl.split("/ipfs/")[1]; + const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`; + + return `https://twitter.com/intent/tweet?text=Check%20out%20this%20generated%20image!&url=${encodeURIComponent(memeUrl)}`; + }; + return (
@@ -136,6 +144,16 @@ const AIServiceCard: React.FC = ({ alt="Generated content" className="w-full h-auto rounded-xl shadow-2xl" /> +
)}
-- 2.45.2 From 297a4c7ed8b5770ebf9fb90ca6071f8e11366630 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 5 Feb 2025 15:57:22 +0530 Subject: [PATCH 04/16] Generate twitter metadata tags --- .env.example | 2 ++ src/app/memes/[id]/page.tsx | 6 +++++- src/components/AIServiceCard.tsx | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index bd04e2e..da43a76 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,6 @@ NEXT_PUBLIC_USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v PINATA_JWT= PINATA_GATEWAY= +# Change to your website URL +# For development: set to http://localhost:3000 SITE_URL=https://memes.markto.market diff --git a/src/app/memes/[id]/page.tsx b/src/app/memes/[id]/page.tsx index da6942d..7e20528 100644 --- a/src/app/memes/[id]/page.tsx +++ b/src/app/memes/[id]/page.tsx @@ -22,8 +22,12 @@ export async function generateMetadata( metadataBase: new URL(baseUrl), openGraph: { type: 'website', + title: 'Generated Meme', url: `${baseUrl}/memes/${params.id}`, - siteName: "Mark's meme market", + images: [{ url: meme }], + }, + twitter: { + title: 'Generated Meme', images: [{ url: meme }], }, }; diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 9f1010d..12f99e1 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -91,7 +91,7 @@ const AIServiceCard: React.FC = ({ const ipfsImageUrl = imageUrl.split("/ipfs/")[1]; const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`; - return `https://twitter.com/intent/tweet?text=Check%20out%20this%20generated%20image!&url=${encodeURIComponent(memeUrl)}`; + return `https://twitter.com/intent/tweet?text=Check%20out%20this%20meme%20I%20generated!&url=${encodeURIComponent(memeUrl)}`; }; return ( -- 2.45.2 From a7f255a33db390953c7ca6a28db368d54feb5668 Mon Sep 17 00:00:00 2001 From: IshaVenikar Date: Wed, 5 Feb 2025 17:43:25 +0530 Subject: [PATCH 05/16] Implement API to verify tweet data --- .env.example | 1 + src/app/api/tweet/route.ts | 47 ++++++++++++++++++++++++ src/components/AIServiceCard.tsx | 26 +++++++++++--- src/components/TweetForm.tsx | 61 ++++++++++++++++++++++++++++++++ src/data-source.ts | 3 +- src/entity/Tweets.ts | 13 +++++++ src/services/fluxService.ts | 3 +- src/utils/verifyTweet.ts | 25 +++++++++++++ 8 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/app/api/tweet/route.ts create mode 100644 src/components/TweetForm.tsx create mode 100644 src/entity/Tweets.ts create mode 100644 src/utils/verifyTweet.ts diff --git a/.env.example b/.env.example index da43a76..0b88a49 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,4 @@ PINATA_GATEWAY= # Change to your website URL # For development: set to http://localhost:3000 SITE_URL=https://memes.markto.market +ACCOUNT_HANDLE=mark_2_market1 diff --git a/src/app/api/tweet/route.ts b/src/app/api/tweet/route.ts new file mode 100644 index 0000000..b44d0ae --- /dev/null +++ b/src/app/api/tweet/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import { verifySignatureInTweet } from '../../../utils/verifyTweet'; + +export async function POST(req: NextRequest): Promise { + try { + const { tweetUrl } = await req.json(); + + const url = `https://publish.twitter.com/oembed?url=${tweetUrl}&maxwidth=600`; + + const response = await fetch(url); + const data = await response.json(); + const { handle, memeUrl, txHash } = extractData(data.html); + if (!handle || !memeUrl || !txHash) { + return NextResponse.json( + { error: 'Verification failed' }, + { status: 500 } + ) + } + + const isVerified = await verifySignatureInTweet(txHash); + const isHandleCorrect = handle === process.env.ACCOUNT_HANDLE; + + return NextResponse.json({isVerified: isVerified && isHandleCorrect}) + } catch (error) { + console.error('Error while verifying tweet:', error) + return NextResponse.json( + { error: 'Failed to verify tweet' }, + { status: 500 } + ) + } +} + +const extractData = (tweet: string | object) => { + const tweetText = typeof tweet === 'string' ? tweet : JSON.stringify(tweet); + + const decodedTweet = tweetText.replace(/'/g, "'").replace(/"/g, '"'); + + const urlMatch = decodedTweet.match(//); + const txHashMatch = decodedTweet.match(/TX Hash: '([^']+)'/); + const handleMatch = decodedTweet.match(/@([A-Za-z0-9_]+)/); + + return { + memeUrl: urlMatch ? urlMatch[1] : null, + txHash: txHashMatch ? txHashMatch[1].trim() : null, + handle: handleMatch ? handleMatch[1] : null, + }; +}; diff --git a/src/components/AIServiceCard.tsx b/src/components/AIServiceCard.tsx index 12f99e1..8658c03 100644 --- a/src/components/AIServiceCard.tsx +++ b/src/components/AIServiceCard.tsx @@ -4,11 +4,13 @@ import React, { useState } from 'react' import BN from 'bn.js'; import Big from 'big.js'; +import TweetUrlForm from './TweetForm'; + interface AIServiceCardProps { title: string description: string isWalletConnected: boolean - onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }> + onGenerate: (prompt: string) => Promise<{ imageUrl?: string, transactionSignature?: string, error?: string }> priceMTM: BN } @@ -16,6 +18,7 @@ interface GenerationState { loading: boolean processing: boolean imageUrl: string | null + transactionSignature: string | null error: string | null } @@ -38,6 +41,7 @@ const AIServiceCard: React.FC = ({ loading: false, processing: false, imageUrl: null, + transactionSignature: null, error: null, }) @@ -67,11 +71,12 @@ const AIServiceCard: React.FC = ({ return } - if (result.imageUrl) { + if (result.imageUrl && result.transactionSignature) { setGenerationState({ loading: false, processing: false, imageUrl: result.imageUrl, + transactionSignature: result.transactionSignature, error: null, }) } else { @@ -86,7 +91,7 @@ const AIServiceCard: React.FC = ({ } } - const generateTwitterShareUrl = (imageUrl: string): string => { + const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => { const baseUrl = window.location.href; const ipfsImageUrl = imageUrl.split("/ipfs/")[1]; const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`; @@ -94,6 +99,17 @@ const AIServiceCard: React.FC = ({ return `https://twitter.com/intent/tweet?text=Check%20out%20this%20meme%20I%20generated!&url=${encodeURIComponent(memeUrl)}`; }; + // const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => { + // const baseUrl = window.location.href; + // const ipfsImageUrl = imageUrl.split("/ipfs/")[1]; + // const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`; + + // // Ensure the entire tweet text is properly URL-encoded + // const tweetText = `Check out this meme that I generated! TX Hash: ${transactionSignature} @mark_2_market1`; + + // return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(memeUrl)}`; + // }; + return (
@@ -137,7 +153,7 @@ const AIServiceCard: React.FC = ({
)} - {generationState.imageUrl && ( + {generationState.imageUrl && generationState.transactionSignature && (
= ({ />
{ + const [inputText, setInputText] = useState('') + + const handleGenerate = async (): Promise => { + try { + const response = await fetch('/api/tweet', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ tweetUrl: inputText }), + }) + + if (!response.ok) { + throw new Error(`Failed to verify tweet: ${response.statusText}`); + } + + } catch (error) { + console.error('Failed to fetch price:', error); + } + } + + return ( +
+
+
+ +
+ +
+