feat: auto-generate manifest

This commit is contained in:
lucas-neynar 2025-03-13 12:39:59 -07:00
parent 51f8c92eb3
commit b168e34521
No known key found for this signature in database
14 changed files with 289 additions and 29 deletions

View File

@ -2,5 +2,4 @@ KV_REST_API_TOKEN=
KV_REST_API_URL= KV_REST_API_URL=
NEXT_PUBLIC_URL= NEXT_PUBLIC_URL=
NEXTAUTH_URL= NEXTAUTH_URL=
NEXTAUTH_SECERT=
NEYNAR_API_KEY=FARCASTER_V2_FRAMES_DEMO NEYNAR_API_KEY=FARCASTER_V2_FRAMES_DEMO

View File

@ -10,3 +10,7 @@ To create a new frames project, run:
```{bash} ```{bash}
npx frames-v2-quickstart npx frames-v2-quickstart
``` ```
## TODO
* try ngrok locally, then integrate with localtunnel if possible
* ask for seed phrase in setup and generate manifest

View File

@ -6,25 +6,59 @@ import { dirname } from 'path';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { generateManifest } from './manifest.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const REPO_URL = 'https://github.com/lucas-neynar/frames-v2-quickstart.git'; const REPO_URL = 'https://github.com/lucas-neynar/frames-v2-quickstart.git';
const SCRIPT_VERSION = '0.1.0'; const SCRIPT_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version;
async function init() { async function init() {
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 project?', message: 'What is the name of your frame?',
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Project name cannot be empty'; return 'Project name cannot be empty';
} }
return true; return true;
} }
},
{
type: 'input',
name: 'description',
message: 'Give a one-line description of your frame:',
validate: (input) => {
if (input.trim() === '') {
return 'Description cannot be empty';
}
return true;
}
},
{
type: 'input',
name: 'fid',
message: 'Enter your Farcaster FID:',
validate: (input) => {
if (input.trim() === '') {
return 'FID cannot be empty';
}
return true;
}
},
{
type: 'password',
name: 'seedPhrase',
message: 'Enter your Farcaster account seed phrase:',
validate: (input) => {
if (input.trim() === '') {
return 'Seed phrase cannot be empty';
}
return true;
}
} }
]); ]);
@ -40,6 +74,14 @@ async function init() {
console.log('\nRemoving .git directory...'); console.log('\nRemoving .git directory...');
fs.rmSync(path.join(projectPath, '.git'), { recursive: true, force: true }); fs.rmSync(path.join(projectPath, '.git'), { recursive: true, force: true });
// Generate manifest and write to public folder
console.log('\nGenerating manifest...');
const manifest = await generateManifest(answers.fid, answers.seedPhrase);
fs.writeFileSync(
path.join(projectPath, 'public/manifest.json'),
JSON.stringify(manifest)
);
// Update package.json // Update package.json
console.log('\nUpdating package.json...'); console.log('\nUpdating package.json...');
const packageJsonPath = path.join(projectPath, 'package.json'); const packageJsonPath = path.join(projectPath, 'package.json');
@ -68,6 +110,9 @@ async function init() {
if (fs.existsSync(envExamplePath)) { if (fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath); fs.copyFileSync(envExamplePath, envPath);
fs.unlinkSync(envExamplePath); fs.unlinkSync(envExamplePath);
// Append project name and description to .env
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`);
console.log('\nCreated .env file from .env.example'); console.log('\nCreated .env file from .env.example');
} else { } else {
console.log('\n.env.example does not exist, skipping copy and remove operations'); console.log('\n.env.example does not exist, skipping copy and remove operations');

43
bin/manifest.js Normal file
View File

@ -0,0 +1,43 @@
// utils to generate a manifest.json file for a frames v2 app
const { mnemonicToAccount } = require('viem/accounts');
async function generateManifest(fid, seedPhrase) {
if (!Number.isInteger(fid) || fid <= 0) {
throw new Error('FID must be a positive integer');
}
let account;
try {
account = mnemonicToAccount(seedPhrase);
} catch (error) {
throw new Error('Invalid seed phrase');
}
const custodyAddress = account.address;
const header = {
fid,
type: 'custody', // question: do we want to support type of 'app_key', which indicates the signature is from a registered App Key for the FID
key: custodyAddress,
};
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64');
const payload = {
domain: 'warpcast.com'
};
const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url');
const signature = await account.signMessage({
message: `${encodedHeader}.${encodedPayload}`
});
const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url');
const jsonJfs = {
header: encodedHeader,
payload: encodedPayload,
signature: encodedSignature
};
return jsonJfs;
}
module.exports = { generateManifest };

158
package-lock.json generated
View File

@ -19,6 +19,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"inquirer": "^12.4.3", "inquirer": "^12.4.3",
"localtunnel": "^2.0.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"next": "15.0.3", "next": "15.0.3",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
@ -31,7 +32,7 @@
"wagmi": "^2.14.12" "wagmi": "^2.14.12"
}, },
"bin": { "bin": {
"frames-v2-quickstart": "bin/index.js" "test-frames-v2-quickstart": "bin/index.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@ -3859,6 +3860,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -4929,6 +4939,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -5582,9 +5601,9 @@
} }
}, },
"node_modules/ethers": { "node_modules/ethers": {
"version": "6.13.4", "version": "6.13.5",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.4.tgz", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz",
"integrity": "sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==", "integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -5872,6 +5891,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -7247,6 +7286,111 @@
"@types/trusted-types": "^2.0.2" "@types/trusted-types": "^2.0.2"
} }
}, },
"node_modules/localtunnel": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz",
"integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==",
"license": "MIT",
"dependencies": {
"axios": "0.21.4",
"debug": "4.3.2",
"openurl": "1.1.1",
"yargs": "17.1.1"
},
"bin": {
"lt": "bin/lt.js"
},
"engines": {
"node": ">=8.3.0"
}
},
"node_modules/localtunnel/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/localtunnel/node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/localtunnel/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/localtunnel/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/localtunnel/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/localtunnel/node_modules/yargs": {
"version": "17.1.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz",
"integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/localtunnel/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -7995,6 +8139,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/openurl": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
"integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==",
"license": "MIT"
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",

View File

@ -2,7 +2,9 @@
"name": "frames-v2-demo", "name": "frames-v2-demo",
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"files": ["bin/index.js"], "files": [
"bin/index.js"
],
"keywords": [ "keywords": [
"farcaster", "farcaster",
"frames", "frames",
@ -12,13 +14,13 @@
"web3" "web3"
], ],
"scripts": { "scripts": {
"dev": "next dev", "dev": "lt --port 3000 & next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
}, },
"bin": { "bin": {
"testing-frames-v2-quickstart": "./bin/index.js" "test-frames-v2-quickstart": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@farcaster/auth-kit": "^0.6.0", "@farcaster/auth-kit": "^0.6.0",
@ -32,6 +34,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"inquirer": "^12.4.3", "inquirer": "^12.4.3",
"localtunnel": "^2.0.2",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"next": "15.0.3", "next": "15.0.3",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",

View File

@ -1,17 +1,24 @@
import { readFileSync } from 'fs';
import { join } from 'path';
export async function GET() { export async function GET() {
const appUrl = process.env.NEXT_PUBLIC_URL; const appUrl = process.env.NEXT_PUBLIC_URL;
let accountAssociation; // TODO: add type
try {
const manifestPath = join(process.cwd(), 'public/manifest.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
accountAssociation = manifest;
} catch (error) {
console.warn('Warning: manifest.json not found or invalid. Frame will not be associated with an account.');
accountAssociation = null;
}
const config = { const config = {
accountAssociation: { ...(accountAssociation && { accountAssociation }),
header:
"eyJmaWQiOjM2MjEsInR5cGUiOiJjdXN0b2R5Iiwia2V5IjoiMHgyY2Q4NWEwOTMyNjFmNTkyNzA4MDRBNkVBNjk3Q2VBNENlQkVjYWZFIn0",
payload: "eyJkb21haW4iOiJmcmFtZXMtdjIudmVyY2VsLmFwcCJ9",
signature:
"MHhiNDIwMzQ1MGZkNzgzYTExZjRiOTllZTFlYjA3NmMwOTdjM2JkOTY1NGM2ODZjYjkyZTAyMzk2Y2Q0YjU2MWY1MjY5NjI5ZGQ5NTliYjU0YzEwOGI4OGVmNjdjMTVlZTdjZDc2YTRiMGU5NzkzNzA3YzkxYzFkOWFjNTg0YmQzNjFi",
},
frame: { frame: {
version: "1", version: "1",
name: "Frames v2 Demo", name: process.env.NEXT_PUBLIC_FRAME_NAME || "Frames v2 Demo",
iconUrl: `${appUrl}/icon.png`, iconUrl: `${appUrl}/icon.png`,
homeUrl: appUrl, homeUrl: appUrl,
imageUrl: `${appUrl}/frames/hello/opengraph-image`, imageUrl: `${appUrl}/frames/hello/opengraph-image`,

View File

@ -2,6 +2,8 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
// note: dynamic import is required for components that use the Frame SDK
const Demo = dynamic(() => import("~/components/Demo"), { const Demo = dynamic(() => import("~/components/Demo"), {
ssr: false, ssr: false,
}); });

View File

@ -19,7 +19,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
title: "Launch Frame", title: "Launch Frame",
action: { action: {
type: "launch_frame", type: "launch_frame",
name: "Farcaster Frames v2 Demo", name: process.env.NEXT_PUBLIC_FRAME_NAME || "Frames v2 Demo",
url: `${appUrl}/frames/hello/${name}/`, url: `${appUrl}/frames/hello/${name}/`,
splashImageUrl: `${appUrl}/splash.png`, splashImageUrl: `${appUrl}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",

View File

@ -10,7 +10,7 @@ const frame = {
title: "Launch Frame", title: "Launch Frame",
action: { action: {
type: "launch_frame", type: "launch_frame",
name: "Farcaster Frames v2 Demo", name: process.env.NEXT_PUBLIC_FRAME_NAME || "Frames v2 Demo",
url: `${appUrl}/frames/hello/`, url: `${appUrl}/frames/hello/`,
splashImageUrl: `${appUrl}/splash.png`, splashImageUrl: `${appUrl}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",

View File

@ -5,8 +5,8 @@ import "~/app/globals.css";
import { Providers } from "~/app/providers"; import { Providers } from "~/app/providers";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Farcaster Frames v2 Demo", title: process.env.NEXT_PUBLIC_FRAME_NAME || "Frames v2 Demo",
description: "A Farcaster Frames v2 demo app", description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION || "A Farcaster Frames v2 demo app",
}; };
export default async function RootLayout({ export default async function RootLayout({

View File

@ -1,6 +1,6 @@
import { ImageResponse } from "next/og"; import { ImageResponse } from "next/og";
export const alt = "Farcaster Frames V2 Demo"; export const alt = process.env.NEXT_PUBLIC_FRAME_NAME || "Frames V2 Demo";
export const size = { export const size = {
width: 600, width: 600,
height: 400, height: 400,
@ -8,11 +8,13 @@ export const size = {
export const contentType = "image/png"; export const contentType = "image/png";
// dynamically generated OG image for frame preview
// TODO: make this dynamic with user info (like robin's example)
export default async function Image() { export default async function Image() {
return new ImageResponse( return new ImageResponse(
( (
<div tw="h-full w-full flex flex-col justify-center items-center relative bg-white"> <div tw="h-full w-full flex flex-col justify-center items-center relative bg-white">
<h1 tw="text-6xl">Frames v2 Demo</h1> <h1 tw="text-6xl">{alt}</h1>
</div> </div>
), ),
{ {

View File

@ -3,6 +3,9 @@ import App from "./app";
const appUrl = process.env.NEXT_PUBLIC_URL; const appUrl = process.env.NEXT_PUBLIC_URL;
// frame preview metadata
const appName = process.env.NEXT_PUBLIC_FRAME_NAME || "Frames v2 Demo";
const frame = { const frame = {
version: "next", version: "next",
imageUrl: `${appUrl}/opengraph-image`, imageUrl: `${appUrl}/opengraph-image`,
@ -10,7 +13,7 @@ const frame = {
title: "Launch Frame", title: "Launch Frame",
action: { action: {
type: "launch_frame", type: "launch_frame",
name: "Farcaster Frames v2 Demo", name: appName,
url: appUrl, url: appUrl,
splashImageUrl: `${appUrl}/splash.png`, splashImageUrl: `${appUrl}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",
@ -22,10 +25,10 @@ export const revalidate = 300;
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
return { return {
title: "Farcaster Frames v2 Demo", title: appName,
openGraph: { openGraph: {
title: "Farcaster Frames v2 Demo", title: appName,
description: "A Farcaster Frames v2 demo app.", description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION || "A Farcaster Frames v2 demo app.",
}, },
other: { other: {
"fc:frame": JSON.stringify(frame), "fc:frame": JSON.stringify(frame),

View File

@ -29,6 +29,7 @@ export const authOptions: AuthOptions = {
// In a production app with a server, these should be fetched from // In a production app with a server, these should be fetched from
// your Farcaster data indexer rather than have them accepted as part // your Farcaster data indexer rather than have them accepted as part
// of credentials. // of credentials.
// question: should these natively use the Neynar API?
name: { name: {
label: "Name", label: "Name",
type: "text", type: "text",
@ -49,6 +50,7 @@ export const authOptions: AuthOptions = {
const verifyResponse = await appClient.verifySignInMessage({ const verifyResponse = await appClient.verifySignInMessage({
message: credentials?.message as string, message: credentials?.message as string,
signature: credentials?.signature as `0x${string}`, signature: credentials?.signature as `0x${string}`,
// question: what domain should this be?
domain: new URL(process.env.NEXTAUTH_URL ?? '').hostname, domain: new URL(process.env.NEXTAUTH_URL ?? '').hostname,
nonce: csrfToken, nonce: csrfToken,
}); });