Last active
April 7, 2025 10:52
-
-
Save kirtirajsinh/75f779ca1aa97bd73209c8b06c94daba to your computer and use it in GitHub Desktop.
Token CreAtion from Farcaster Cast webhook.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { createCoin } from '@zoralabs/coins-sdk'; | |
| import { createWalletClient, createPublicClient, http } from 'viem'; | |
| import { baseSepolia } from 'viem/chains'; | |
| import { privateKeyToAccount } from 'viem/accounts'; // Import for private key handling | |
| import { uploadFileToR2 } from '@/lib/file'; | |
| import { v4 as uuidv4 } from "uuid"; | |
| import { validateMetadata } from '@/lib/utils'; | |
| import { ExtractDataFromText } from '../actions/ai'; | |
| import { cast } from '@/lib/cast'; | |
| interface FarcasterWebhookData { | |
| data: { | |
| text: string; | |
| author?: { | |
| custody_address: `0x${string}`; | |
| }; | |
| embeds?: { url: string }[]; | |
| hash: string; // Assuming the cast hash is needed for the reply | |
| }; | |
| // Add other potential fields from the webhook if needed | |
| } | |
| // Separate function to handle the heavy processing | |
| async function processWebhookData(castData: FarcasterWebhookData['data']) { | |
| try { | |
| console.log("Starting asynchronous processing for cast:", castData.hash); | |
| const text = castData.text; | |
| const payoutRecipient = castData.author?.custody_address; | |
| const imageUrl = castData?.embeds?.[0]?.url; | |
| // --- Validations --- | |
| if (!payoutRecipient) { | |
| console.error('Missing payoutRecipient (author.custody_address)'); | |
| // Decide how to handle this: log, notify, etc. Cannot proceed without it. | |
| return; | |
| } | |
| if (!imageUrl) { | |
| console.error('Missing imageUrl in embeds'); | |
| // Decide how to handle this. | |
| return; | |
| } | |
| if (!process.env.PRIVATE_KEY) { | |
| console.error('Missing PRIVATE_KEY environment variable'); | |
| // Cannot proceed without it. | |
| return; | |
| } | |
| if (!process.env.RPC_URL) { | |
| console.error('Missing RPC_URL environment variable'); | |
| return; | |
| } | |
| console.log("Extracted Data:", { text, payoutRecipient, imageUrl }); | |
| // --- AI Metadata Extraction --- | |
| const generatedMetadata = await ExtractDataFromText(text); | |
| console.log("Generated Metadata from AI:", generatedMetadata); | |
| const { name, description, symbol } = generatedMetadata; | |
| if (!name || !symbol) { | |
| console.error('AI failed to extract required metadata (name or symbol)'); | |
| // Handle fallback or failure | |
| return; | |
| } | |
| console.log("Metadata Fields:", { name, description, symbol }); | |
| // --- Blockchain Setup --- | |
| const serverAccount = privateKeyToAccount(`0x${process.env.PRIVATE_KEY.replace('0x', '')}`); | |
| const publicClient = createPublicClient({ | |
| chain: baseSepolia, | |
| transport: http(process.env.RPC_URL), | |
| }); | |
| const walletClient = createWalletClient({ | |
| account: serverAccount, | |
| chain: baseSepolia, | |
| transport: http(process.env.RPC_URL), | |
| }); | |
| // --- Metadata Preparation & Validation --- | |
| const metadata = { | |
| name: name, | |
| description: description || `Coin created from Farcaster cast ${castData.hash}`, // Add fallback description | |
| symbol: symbol, | |
| image: imageUrl, | |
| properties: { | |
| "category": "social", | |
| "sourceCastHash": castData.hash | |
| } | |
| }; | |
| const isValidMetadata = await validateMetadata(metadata); | |
| console.log("Metadata Validation Result:", isValidMetadata); | |
| if (!isValidMetadata) { | |
| console.error('Generated metadata failed validation:', metadata); | |
| return; // Stop processing if metadata is invalid | |
| } | |
| // --- Metadata Upload --- | |
| const metadataBlob = new Blob([JSON.stringify(metadata)], { type: 'application/json' }); | |
| const fileKey = `${uuidv4()}`; // Generate unique key for the file | |
| const metadataFile = new File([metadataBlob], `${fileKey}.json`, { type: 'application/json' }); // Use key in filename | |
| const uploadResult = await uploadFileToR2(metadataFile, 'zora-farcaster', fileKey); // Ensure bucket name is correct | |
| console.log("Upload to R2 Result:", uploadResult); | |
| if (!uploadResult) { // Check if upload was successful and returned a URL | |
| console.error('Failed to upload metadata to R2'); | |
| return; | |
| } | |
| // Construct URI carefully based on your R2 setup and uploadFileToR2 return value | |
| // const uri = `ipfs://${uploadResult.cid}` // If it returns an IPFS CID | |
| const uri = `https://pub-c09dd5628d6f44dfb783aa3657780ed1.r2.dev/${fileKey}` as const; // Replace with the actual URI of the uploaded metadata | |
| console.log("Metadata URI:", uri); | |
| // --- Coin Creation --- | |
| const PlatformReferrer = (process.env.PROJECT_WALLET_ADDRESS?.startsWith('0x') | |
| ? process.env.PROJECT_WALLET_ADDRESS | |
| : "0xEbaEbc9baB52157936975D7bF121e3A8a2c09a7f") as `0x${string}`; | |
| const coinParams = { | |
| name: name, | |
| symbol: symbol, | |
| uri: uri, | |
| payoutRecipient: payoutRecipient, | |
| platformReferrer: PlatformReferrer | |
| }; | |
| console.log("Creating coin with params:", coinParams); | |
| const result = await createCoin(coinParams, walletClient, publicClient); | |
| console.log("Coin Creation Result:", result); | |
| // --- Post-Creation Actions (e.g., Farcaster Cast) --- | |
| if (result?.deployment?.coin) { | |
| const coinAddress = result.deployment.coin; | |
| console.log("Successfully created coin:", coinAddress); | |
| const coinPage = `https://zora.co/coin/base:${coinAddress.toLowerCase()}?referrer=${PlatformReferrer.toLowerCase()}`; // Use correct Zora URL structure | |
| console.log("Casting reply with coin page:", coinPage); | |
| const castResponse = await cast({ coinPage: coinPage, parentId: castData.hash }); | |
| console.log("Farcaster Cast Response:", castResponse); | |
| } else { | |
| console.error("Coin creation did not return expected deployment data:", result); | |
| } | |
| console.log("Finished asynchronous processing for cast:", castData.hash); | |
| } catch (error) { | |
| // Log detailed error for debugging | |
| console.error(`Error during asynchronous processing for cast ${castData?.hash || 'unknown'}:`, error); | |
| // Optional: Send notification to an error tracking service (Sentry, etc.) | |
| } | |
| } | |
| export async function POST(req: Request) { | |
| try { | |
| const body = await req.json(); | |
| console.log("Webhook received:", body); | |
| // --- Basic Validation --- | |
| // Perform only the *absolute minimum* validation needed | |
| // to ensure you have the data to start processing. | |
| if (!body || !body.data || !body.data.hash) { | |
| console.warn('Webhook received invalid or incomplete data.'); | |
| // Still send 200, but log the issue. Or send 400 if you MUST reject it. | |
| // Sending 200 is generally preferred for webhooks unless data is totally unusable. | |
| return new Response(JSON.stringify({ message: 'Webhook received but data incomplete.' }), { status: 200 }); | |
| } | |
| const castData = body.data; | |
| // --- Acknowledge Immediately --- | |
| // Send the 200 OK response *before* starting heavy processing. | |
| console.log(`Acknowledging webhook for cast ${castData.hash} immediately.`); | |
| // Fire-and-forget (use with caution, see note above) | |
| processWebhookData(castData) | |
| // Return the success response to the webhook provider *now*. | |
| return new Response(JSON.stringify({ message: 'Webhook received successfully. Processing started.' }), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| } catch (error) { | |
| // This catch block now primarily handles errors during initial request parsing/validation | |
| console.error('Error processing initial webhook request:', error); | |
| // Send an error response back to the webhook provider for critical initial failures | |
| return new Response(JSON.stringify({ error: 'Failed to process webhook request' }), { status: 500 }); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment