feat: update to query neynar client id from neynar

This commit is contained in:
lucas-neynar 2025-03-19 12:54:58 -07:00
parent 1cc837ca86
commit b18ff0da95
No known key found for this signature in database
2 changed files with 189 additions and 108 deletions

View File

@ -36,6 +36,28 @@ Let's create your Frame! 🚀
`); `);
} }
async function queryNeynarApp(apiKey) {
if (!apiKey) {
return null;
}
try {
const response = await fetch(
`https://api.neynar.com/portal/app_by_api_key`,
{
headers: {
'x-api-key': apiKey
}
}
);
const data = await response.json();
console.log('Neynar app data:', data);
return data;
} catch (error) {
console.error('Error querying Neynar app data:', error);
return null;
}
}
async function lookupFidByCustodyAddress(custodyAddress, apiKey) { async function lookupFidByCustodyAddress(custodyAddress, apiKey) {
if (!apiKey) { if (!apiKey) {
throw new Error('Neynar API key is required'); throw new Error('Neynar API key is required');
@ -66,11 +88,101 @@ async function lookupFidByCustodyAddress(custodyAddress, apiKey) {
async function init() { async function init() {
printWelcomeMessage(); printWelcomeMessage();
// Ask about Neynar usage
let useNeynar = true;
let neynarApiKey = null;
let neynarClientId = null;
let neynarAppName = null;
let neynarAppLogoUrl = null;
while (useNeynar) {
const neynarAnswers = await inquirer.prompt([
{
type: 'confirm',
name: 'useNeynar',
message: '🪐 Neynar is an API that makes it easy to build on Farcaster.\n\nBenefits of using Neynar in your frame:\n- Pre-configured webhook handling (no setup required)\n- Automatic frame analytics in your dev portal\n- Send manual notifications from dev.neynar.com\n- Built-in rate limiting and error handling\n\nWould you like to use Neynar in your frame?',
default: true
}
]);
if (!neynarAnswers.useNeynar) {
useNeynar = false;
break;
}
const neynarKeyAnswer = await inquirer.prompt([
{
type: 'password',
name: 'neynarApiKey',
message: 'Enter your Neynar API key (or press enter to skip):',
default: null
}
]);
if (neynarKeyAnswer.neynarApiKey) {
neynarApiKey = neynarKeyAnswer.neynarApiKey;
} else {
const useDemoKey = await inquirer.prompt([
{
type: 'confirm',
name: 'useDemo',
message: 'Would you like to try the demo Neynar API key?',
default: true
}
]);
neynarApiKey = useDemoKey.useDemo ? 'FARCASTER_V2_FRAMES_DEMO' : null;
}
if (!neynarApiKey) {
console.log('\n⚠ No valid API key provided. Would you like to try again?');
const { retry } = await inquirer.prompt([
{
type: 'confirm',
name: 'retry',
message: 'Try configuring Neynar again?',
default: true
}
]);
if (!retry) {
useNeynar = false;
break;
}
continue;
}
const appInfo = await queryNeynarApp(neynarApiKey);
if (appInfo) {
neynarClientId = appInfo.app_uuid;
neynarAppName = appInfo.app_name;
neynarAppLogoUrl = appInfo.logo_url;
}
if (!neynarClientId) {
const { retry } = await inquirer.prompt([
{
type: 'confirm',
name: 'retry',
message: '⚠️ Could not find a client ID for this API key. Would you like to try configuring Neynar again?',
default: true
}
]);
if (!retry) {
useNeynar = false;
break;
}
continue;
}
// If we get here, we have both API key and client ID
break;
}
const answers = await inquirer.prompt([ const answers = await inquirer.prompt([
{ {
type: 'input', type: 'input',
name: 'projectName', name: 'projectName',
message: 'What is the name of your frame?', message: 'What is the name of your frame?',
default: neynarAppName || undefined,
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Project name cannot be empty'; return 'Project name cannot be empty';
@ -105,76 +217,16 @@ async function init() {
type: 'input', type: 'input',
name: 'splashImageUrl', name: 'splashImageUrl',
message: 'Enter the URL for your splash image\n(optional -- leave blank to use the default public/splash.png image or replace public/splash.png with your own)\n\nExternal splash image URL:', message: 'Enter the URL for your splash image\n(optional -- leave blank to use the default public/splash.png image or replace public/splash.png with your own)\n\nExternal splash image URL:',
default: null default: neynarAppLogoUrl || undefined
}, },
{ {
type: 'input', type: 'input',
name: 'iconImageUrl', name: 'iconImageUrl',
message: 'Enter the URL for your app icon\n(optional -- leave blank to use the default public/icon.png image or replace public/icon.png with your own)\n\nExternal app icon URL:', message: 'Enter the URL for your app icon\n(optional -- leave blank to use the default public/icon.png image or replace public/icon.png with your own)\n\nExternal app icon URL:',
default: null default: neynarAppLogoUrl || undefined
} }
]); ]);
// Handle Neynar API key
const neynarFlow = await inquirer.prompt([
{
type: 'confirm',
name: 'useNeynar',
message: '🪐 Neynar is an API that makes it easy to build on Farcaster.\n\nBenefits of using Neynar in your frame:\n- Pre-configured webhook handling (no setup required)\n- Automatic frame analytics in your dev portal\n- Send manual notifications from dev.neynar.com\n- Built-in rate limiting and error handling\n\nWould you like to use Neynar in your frame?',
default: true
}
]);
if (neynarFlow.useNeynar) {
const neynarKeyAnswer = await inquirer.prompt([
{
type: 'password',
name: 'neynarApiKey',
message: 'Enter your Neynar API key (or press enter to skip):',
default: null
}
]);
if (!neynarKeyAnswer.neynarApiKey) {
const useDemoKey = await inquirer.prompt([
{
type: 'confirm',
name: 'useDemo',
message: 'Would you like to try the demo Neynar API key?',
default: true
}
]);
answers.useNeynar = useDemoKey.useDemo;
answers.neynarApiKey = useDemoKey.useDemo ? 'FARCASTER_V2_FRAMES_DEMO' : null;
} else {
answers.useNeynar = true;
answers.neynarApiKey = neynarKeyAnswer.neynarApiKey;
// Get Neynar client ID if using Neynar
if (answers.useNeynar) {
const neynarClientIdAnswer = await inquirer.prompt([
{
type: 'input',
name: 'neynarClientId',
message: 'Enter your Neynar client ID:',
validate: (input) => {
if (input && !/^[a-zA-Z0-9-]+$/.test(input)) {
return 'Invalid Neynar client ID format';
}
return true;
}
}
]);
answers.neynarClientId = neynarClientIdAnswer.neynarClientId;
}
}
} else {
answers.useNeynar = false;
answers.neynarApiKey = null;
answers.neynarClientId = null;
}
// Ask about localhost vs tunnel // Ask about localhost vs tunnel
const hostingAnswer = await inquirer.prompt([ const hostingAnswer = await inquirer.prompt([
{ {
@ -221,8 +273,7 @@ async function init() {
// Look up FID using custody address // Look up FID using custody address
console.log('\nUsing seed phrase to look up FID by custody address...'); console.log('\nUsing seed phrase to look up FID by custody address...');
const neynarApiKey = answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO'; fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO');
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey);
if (!fid) { if (!fid) {
throw new Error('No FID found for this custody address'); throw new Error('No FID found for this custody address');
@ -339,7 +390,7 @@ async function init() {
}; };
// Add Neynar dependencies if selected // Add Neynar dependencies if selected
if (answers.useNeynar) { if (useNeynar) {
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0'; packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
packageJson.dependencies['@neynar/react'] = '^0.9.7'; packageJson.dependencies['@neynar/react'] = '^0.9.7';
} }
@ -364,8 +415,7 @@ async function init() {
// Look up FID using custody address // Look up FID using custody address
if (!fid) { if (!fid) {
console.log('\nLooking up FID...'); console.log('\nLooking up FID...');
const neynarApiKey = answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO'; fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO');
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey);
} }
// Write seed phrase and FID to .env.local for manifest signature generation // Write seed phrase and FID to .env.local for manifest signature generation
@ -385,9 +435,9 @@ async function init() {
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`);
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO'}"`); if (useNeynar) {
if (answers.neynarClientId) { fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${answers.neynarClientId}"`); fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
} }
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`); fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);

View File

@ -28,11 +28,32 @@ async function validateDomain(domain) {
return cleanDomain; return cleanDomain;
} }
async function queryNeynarApp(apiKey) {
if (!apiKey) {
return null;
}
try {
const response = await fetch(
`https://api.neynar.com/portal/app_by_api_key`,
{
headers: {
'x-api-key': apiKey
}
}
);
const data = await response.json();
console.log('Neynar app data:', data);
return data;
} catch (error) {
console.error('Error querying Neynar app data:', error);
return null;
}
}
async function validateSeedPhrase(seedPhrase) { async function validateSeedPhrase(seedPhrase) {
try { try {
// Try to create an account from the seed phrase // Try to create an account from the seed phrase
const account = mnemonicToAccount(seedPhrase); const account = mnemonicToAccount(seedPhrase);
console.log('✅ Seed phrase validated successfully');
return account.address; return account.address;
} catch (error) { } catch (error) {
throw new Error('Invalid seed phrase'); throw new Error('Invalid seed phrase');
@ -102,7 +123,7 @@ async function main() {
type: 'input', type: 'input',
name: 'frameName', name: 'frameName',
message: 'Enter the name for your frame (e.g., My Cool Frame):', message: 'Enter the name for your frame (e.g., My Cool Frame):',
default: process.env.NEXT_PUBLIC_FRAME_NAME || 'Frames v2 Demo', default: process.env.NEXT_PUBLIC_FRAME_NAME,
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Frame name cannot be empty'; return 'Frame name cannot be empty';
@ -128,10 +149,12 @@ async function main() {
} }
]); ]);
// Get Neynar API key from user if not already in .env.local // Get Neynar configuration
let neynarApiKey = process.env.NEYNAR_API_KEY; let neynarApiKey = process.env.NEYNAR_API_KEY;
let neynarClientId = null; let neynarClientId = process.env.NEYNAR_CLIENT_ID;
let useNeynar = true;
while (useNeynar) {
if (!neynarApiKey) { if (!neynarApiKey) {
const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([ const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([
{ {
@ -143,32 +166,40 @@ async function main() {
]); ]);
neynarApiKey = inputNeynarApiKey; neynarApiKey = inputNeynarApiKey;
} else { } else {
console.log('Using existing Neynar API key from .env') console.log('Using existing Neynar API key from .env');
} }
// Only ask for client ID if we have an API key if (!neynarApiKey) {
if (neynarApiKey) { useNeynar = false;
neynarClientId = process.env.NEYNAR_CLIENT_ID; break;
if (!neynarClientId) { }
const { neynarClientId: inputNeynarClientId } = await inquirer.prompt([
// Try to get client ID from API
const appInfo = await queryNeynarApp(neynarApiKey);
if (appInfo) {
neynarClientId = appInfo.app_uuid;
console.log('✅ Fetched Neynar app client ID');
break;
}
// If we get here, the API key was invalid
console.log('\n⚠ Could not find Neynar app information. The API key may be incorrect.');
const { retry } = await inquirer.prompt([
{ {
type: 'input', type: 'confirm',
name: 'neynarClientId', name: 'retry',
message: 'Enter your Neynar client ID (required for Neynar webhook):', message: 'Would you like to try a different API key?',
validate: (input) => { default: true
if (!input) {
return 'Client ID is required when using Neynar API key';
}
if (!/^[a-zA-Z0-9-]+$/.test(input)) {
return 'Invalid Neynar client ID format';
}
return true;
}
} }
]); ]);
neynarClientId = inputNeynarClientId;
} else { // Reset for retry
console.log('Using existing Neynar client ID from .env'); neynarApiKey = null;
neynarClientId = null;
if (!retry) {
useNeynar = false;
break;
} }
} }
@ -197,7 +228,7 @@ async function main() {
// Validate seed phrase and get account address // Validate seed phrase and get account address
const accountAddress = await validateSeedPhrase(seedPhrase); const accountAddress = await validateSeedPhrase(seedPhrase);
console.log('✅ Seed phrase validated successfully'); console.log('✅ Generated account address from seed phrase');
// Generate and sign manifest // Generate and sign manifest
console.log('\n🔨 Generating frame manifest...'); console.log('\n🔨 Generating frame manifest...');