Skip to content

Instantly share code, notes, and snippets.

@kirtirajsinh
Last active April 7, 2025 10:52
Show Gist options
  • Select an option

  • Save kirtirajsinh/75f779ca1aa97bd73209c8b06c94daba to your computer and use it in GitHub Desktop.

Select an option

Save kirtirajsinh/75f779ca1aa97bd73209c8b06c94daba to your computer and use it in GitHub Desktop.
Token CreAtion from Farcaster Cast webhook.
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