├── .gitignore
├── LICENSE.md
├── README.md
├── archiver-script
├── .gitignore
├── README.md
├── index.js
├── package.json
└── yarn.lock
├── cast-action
├── .gitignore
├── README.md
├── bun.lockb
├── package.json
├── src
│ ├── index.tsx
│ └── lib
│ │ └── neynarClient.ts
├── tsconfig.json
└── yarn.lock
├── fc2x
├── .env.example
├── .gitignore
├── Initial Migration.md
├── README.md
├── components.json
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── src
│ ├── ThemeProvider.tsx
│ ├── app
│ │ ├── api
│ │ │ ├── auth
│ │ │ │ ├── route.ts
│ │ │ │ └── tokens
│ │ │ │ │ └── route.ts
│ │ │ ├── callback
│ │ │ │ └── route.ts
│ │ │ ├── link-fid
│ │ │ │ └── route.ts
│ │ │ ├── logout
│ │ │ │ └── route.ts
│ │ │ ├── profile
│ │ │ │ └── route.ts
│ │ │ ├── toggle-online
│ │ │ │ └── route.ts
│ │ │ └── webhook
│ │ │ │ └── route.ts
│ │ ├── favicon.ico
│ │ ├── fonts
│ │ │ ├── GeistMonoVF.woff
│ │ │ └── GeistVF.woff
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components
│ │ └── ui
│ │ │ ├── alert.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── input.tsx
│ │ │ └── switch.tsx
│ └── lib
│ │ ├── constants.ts
│ │ ├── db.ts
│ │ ├── server.ts
│ │ ├── types.d.ts
│ │ ├── utils.ts
│ │ └── webhook.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
├── flask-app
├── .env.example
├── .gitignore
├── README.md
├── app.py
├── requirements.txt
└── templates
│ └── index.html
├── frames-bot
├── .gitignore
├── README.md
├── bun.lockb
├── index.ts
├── package.json
├── tsconfig.json
├── utils
│ └── neynarClient.ts
└── yarn.lock
├── funding.json
├── gm-bot
├── .env.example
├── .gitignore
├── README.md
├── getApprovedSigner.ts
├── package.json
├── src
│ ├── abi
│ │ ├── SignedKeyRequestMetadata.ts
│ │ └── keyGateway.ts
│ ├── app.ts
│ ├── config.ts
│ ├── neynarClient.ts
│ ├── utils.ts
│ └── viemClient.ts
├── tsconfig.json
└── yarn.lock
├── managed-signers
├── .env.example
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
│ ├── app
│ │ ├── api
│ │ │ ├── cast
│ │ │ │ └── route.ts
│ │ │ ├── signer
│ │ │ │ └── route.ts
│ │ │ └── user
│ │ │ │ └── route.ts
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── constants.ts
│ ├── lib
│ │ └── neynarClient.ts
│ └── utils
│ │ ├── getFid.ts
│ │ └── getSignedKey.ts
├── tsconfig.json
└── yarn.lock
├── neynar-webhook-kafka-consumer
├── .env.example
├── .gitignore
├── README.md
├── package.json
├── src
│ ├── index.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
├── wownar-react-native
├── .gitignore
├── README.md
├── client
│ ├── .env.example
│ ├── App.tsx
│ ├── app.config.ts
│ ├── app.json
│ ├── assets
│ │ ├── adaptive-icon.png
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ ├── splash.png
│ │ ├── wownar.png
│ │ └── wownar.svg
│ ├── babel.config.js
│ ├── constants.ts
│ ├── package.json
│ ├── src
│ │ ├── AppNavigator.tsx
│ │ ├── Context
│ │ │ └── AppContext.tsx
│ │ ├── components
│ │ │ ├── Layout.tsx
│ │ │ ├── Screens
│ │ │ │ ├── Home.tsx
│ │ │ │ ├── Loading.tsx
│ │ │ │ └── Signin.tsx
│ │ │ └── SignoutButton.tsx
│ │ └── utils.ts
│ ├── tsconfig.json
│ └── yarn.lock
└── server
│ ├── .env.example
│ ├── index.js
│ ├── package.json
│ └── yarn.lock
├── wownar-react-sdk
├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.js
├── package.json
├── postcss.config.js
├── public
│ └── logos
│ │ ├── wownar-black.svg
│ │ └── wownar.svg
├── src
│ ├── Context
│ │ ├── AppContext.tsx
│ │ └── NeynarProviderWrapper.tsx
│ ├── app
│ │ ├── Screens
│ │ │ ├── Home
│ │ │ │ ├── index.module.scss
│ │ │ │ └── index.tsx
│ │ │ ├── Signin
│ │ │ │ └── index.tsx
│ │ │ └── layout.tsx
│ │ ├── api
│ │ │ ├── cast
│ │ │ │ ├── reaction
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ └── frame
│ │ │ │ └── action
│ │ │ │ └── route.ts
│ │ ├── favicon.ico
│ │ ├── globals.scss
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── clients
│ │ └── neynar.ts
│ └── components
│ │ └── Button
│ │ ├── index.module.scss
│ │ └── index.tsx
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
└── wownar
├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.js
├── package.json
├── postcss.config.js
├── public
└── logos
│ ├── copy_clipboard.svg
│ ├── neynar.svg
│ ├── powered-by-neynar.png
│ ├── wownar-black.svg
│ ├── wownar-logo.svg
│ └── wownar.svg
├── src
├── Context
│ └── AppContext.tsx
├── app
│ ├── .well-known
│ │ └── farcaster.json
│ │ │ └── route.ts
│ ├── Screens
│ │ ├── Home
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── Signin
│ │ │ └── index.tsx
│ │ └── layout.tsx
│ ├── api
│ │ ├── cast
│ │ │ └── route.ts
│ │ └── user
│ │ │ └── [fid]
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.scss
│ ├── layout.tsx
│ ├── page.tsx
│ └── providers.tsx
├── clients
│ └── neynar.ts
├── components
│ ├── Button
│ │ ├── index.module.scss
│ │ └── index.tsx
│ └── icons
│ │ └── Signout
│ │ └── index.tsx
├── hooks
│ └── use-local-storage-state.tsx
├── types.d.ts
├── utils
│ └── helpers.ts
└── window.d.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | build/
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Neynar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # farcaster-examples
2 |
3 | A collection of Farcaster mini-apps powered by [Neynar](https://neynar.com)
4 |
5 |
6 | | Name | Description |
7 | |------------------------|-------------------------------------------------|
8 | | [archiver-script](/archiver-script) | Node.js script to fetch and archive casts of a specific user |
9 | | [cast-action](/cast-action) | A cast action made with Neynar's Node.js SDK and frog.fm |
10 | | [fc2x](/fc2x) | A Next.js app to crosspost all of your casts from Farcaster to X |
11 | | [flask-app](/flask-app) | A Flask app that grabs casts from the EVM channel |
12 | | [frames-bot](/frames-bot) | A Farcaster bot that replies to specific keywords with a frame created on the go specifically for the reply |
13 | | [gm-bot](/gm-bot) | An automated Node.js messaging bot designed to cast a 'gm 🪐' message in Warpcast every day at a scheduled time |
14 | | [managed-signers](/managed-signers) | Write casts with managed signers |
15 | | [wownar-react-native](/wownar-react-native) | An Expo app that demonstrates the integration of SIWN |
16 | | [wownar-react-sdk](/wownar-react-sdk) | A Next.js app that demonstrates the integration of `@neynar/react` and SIWN |
17 | | [wownar](/wownar) | A Next.js app that demonstrates the integration of SIWN |
18 |
--------------------------------------------------------------------------------
/archiver-script/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | data.ndjson
4 |
5 | # Logs
6 |
7 | logs
8 | _.log
9 | npm-debug.log_
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | .pnpm-debug.log*
14 |
15 | # Caches
16 |
17 | .cache
18 |
19 | # Diagnostic reports (https://nodejs.org/api/report.html)
20 |
21 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
22 |
23 | # Runtime data
24 |
25 | pids
26 | _.pid
27 | _.seed
28 | *.pid.lock
29 |
30 | # Directory for instrumented libs generated by jscoverage/JSCover
31 |
32 | lib-cov
33 |
34 | # Coverage directory used by tools like istanbul
35 |
36 | coverage
37 | *.lcov
38 |
39 | # nyc test coverage
40 |
41 | .nyc_output
42 |
43 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
44 |
45 | .grunt
46 |
47 | # Bower dependency directory (https://bower.io/)
48 |
49 | bower_components
50 |
51 | # node-waf configuration
52 |
53 | .lock-wscript
54 |
55 | # Compiled binary addons (https://nodejs.org/api/addons.html)
56 |
57 | build/Release
58 |
59 | # Dependency directories
60 |
61 | node_modules/
62 | jspm_packages/
63 |
64 | # Snowpack dependency directory (https://snowpack.dev/)
65 |
66 | web_modules/
67 |
68 | # TypeScript cache
69 |
70 | *.tsbuildinfo
71 |
72 | # Optional npm cache directory
73 |
74 | .npm
75 |
76 | # Optional eslint cache
77 |
78 | .eslintcache
79 |
80 | # Optional stylelint cache
81 |
82 | .stylelintcache
83 |
84 | # Microbundle cache
85 |
86 | .rpt2_cache/
87 | .rts2_cache_cjs/
88 | .rts2_cache_es/
89 | .rts2_cache_umd/
90 |
91 | # Optional REPL history
92 |
93 | .node_repl_history
94 |
95 | # Output of 'npm pack'
96 |
97 | *.tgz
98 |
99 | # Yarn Integrity file
100 |
101 | .yarn-integrity
102 |
103 | # dotenv environment variable files
104 |
105 | .env
106 | .env.development.local
107 | .env.test.local
108 | .env.production.local
109 | .env.local
110 |
111 | # parcel-bundler cache (https://parceljs.org/)
112 |
113 | .parcel-cache
114 |
115 | # Next.js build output
116 |
117 | .next
118 | out
119 |
120 | # Nuxt.js build / generate output
121 |
122 | .nuxt
123 | dist
124 |
125 | # Gatsby files
126 |
127 | # Comment in the public line in if your project uses Gatsby and not Next.js
128 |
129 | # https://nextjs.org/blog/next-9-1#public-directory-support
130 |
131 | # public
132 |
133 | # vuepress build output
134 |
135 | .vuepress/dist
136 |
137 | # vuepress v2.x temp and cache directory
138 |
139 | .temp
140 |
141 | # Docusaurus cache and generated files
142 |
143 | .docusaurus
144 |
145 | # Serverless directories
146 |
147 | .serverless/
148 |
149 | # FuseBox cache
150 |
151 | .fusebox/
152 |
153 | # DynamoDB Local files
154 |
155 | .dynamodb/
156 |
157 | # TernJS port file
158 |
159 | .tern-port
160 |
161 | # Stores VSCode versions used for testing VSCode extensions
162 |
163 | .vscode-test
164 |
165 | # yarn v2
166 |
167 | .yarn/cache
168 | .yarn/unplugged
169 | .yarn/build-state.yml
170 | .yarn/install-state.gz
171 | .pnp.*
172 |
173 | # IntelliJ based IDEs
174 | .idea
175 |
176 | # Finder (MacOS) folder config
177 | .DS_Store
178 |
--------------------------------------------------------------------------------
/archiver-script/README.md:
--------------------------------------------------------------------------------
1 | # Neynar Archiver Mini-App
2 |
3 | Welcome to the Neynar Mini-App! This Nodejs script uses Neynar's API to fetch and archive casts of a specific user. Follow these steps to get started:
4 |
5 | ## Steps
6 |
7 | ### Step 1: Clone the Repository
8 |
9 | Clone the Neynar Mini-App repository to your local machine:
10 |
11 | ```sh
12 | git clone https://github.com/neynarxyz/farcaster-examples
13 | ```
14 |
15 | ### Step 2: Prepare the Application Directory
16 |
17 | Move the Flask app directory to your desired location and remove the cloned repository's extra contents:
18 |
19 | ```sh
20 | mv farcaster-examples/archiver-script .
21 | rm -rf farcaster-examples
22 | cd archiver-script
23 | ```
24 |
25 | ### Step 3: Yarn install
26 |
27 | Install the required Nodejs packages:
28 |
29 | ```sh
30 | yarn install
31 | ```
32 |
33 | ### Step 4: Edit the FID and the API key
34 |
35 | Open `index.js` and replace the `FID` with your own:
36 |
37 | ```javascript
38 | // save all @rish.eth's casts in a file called data.ndjson
39 | const fid = 194;
40 | fetchAndDump(fid);
41 | ```
42 |
43 | Also don't forget to edit use your API key:
44 |
45 | ```javascript
46 | const client = new NeynarAPIClient("YOUR_NEYNAR_API_KEY");
47 | ```
48 |
49 | ### Step 5: Run the Script
50 |
51 | ```sh
52 | node index.js
53 | ```
54 |
55 | You should now be ready to run the script, saving the casts of the user with the FID you specified to a file called `data.ndjson` in the same directory as `index.js`.
56 |
--------------------------------------------------------------------------------
/archiver-script/index.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 |
3 | import { NeynarAPIClient, Configuration } from "@neynar/nodejs-sdk";
4 | const config = new Configuration({
5 | apiKey: "YOUR_NEYNAR_API_KEY"
6 | })
7 | const client = new NeynarAPIClient(config);
8 |
9 | const parser = (cast) => {
10 | return {
11 | fid: parseInt(cast.author.fid),
12 | parentFid: parseInt(cast.parent_author.fid)
13 | ? parseInt(cast.parent_author.fid)
14 | : undefined,
15 | hash: cast.hash || undefined,
16 | threadHash: cast.thread_hash || undefined,
17 | parentHash: cast.parent_hash || undefined,
18 | parentUrl: cast.parent_url || undefined,
19 | text: cast.text || undefined,
20 | };
21 | };
22 |
23 | // parse and save to file
24 | const dumpCast = (cast) => {
25 | const parsed = parser(cast);
26 | const data = `${JSON.stringify(parsed)}\n`;
27 | fs.appendFileSync("data.ndjson", data);
28 | };
29 |
30 | const fetchAndDump = async (fid, cursor) => {
31 | const response = await client.fetchCastsForUser({
32 | fid,
33 | limit: 150,
34 | cursor,
35 | });
36 | console.log(response.casts.length);
37 | response.casts.map(dumpCast);
38 |
39 | // If there is no next cursor, we are done
40 | if (response.next.cursor === null) return;
41 | await fetchAndDump(fid, response.next.cursor);
42 | };
43 |
44 | // save all @rish.eth's casts in a file called data.ndjson
45 | const fid = 194;
46 | fetchAndDump(fid);
47 |
--------------------------------------------------------------------------------
/archiver-script/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "archiver-script",
3 | "module": "index.js",
4 | "type": "module",
5 | "devDependencies": {
6 | "@biomejs/biome": "^1.4.1",
7 | "bun-types": "^1.2.4"
8 | },
9 | "peerDependencies": {
10 | "typescript": "^5.0.0"
11 | },
12 | "dependencies": {
13 | "@neynar/nodejs-sdk": "^2.0.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/cast-action/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 |
--------------------------------------------------------------------------------
/cast-action/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/cast-action/bun.lockb
--------------------------------------------------------------------------------
/cast-action/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cast-action-bun",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "frog dev",
7 | "serve": "bun run src/index.tsx",
8 | "build": "bun build src/index.tsx"
9 | },
10 | "dependencies": {
11 | "@neynar/nodejs-sdk": "^2.0.0",
12 | "dotenv": "^16.4.5",
13 | "frog": "^0.18.3",
14 | "hono": "^4"
15 | },
16 | "devDependencies": {
17 | "@types/bun": "^1.2.4",
18 | "bun": "^1.2.4"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/cast-action/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Frog, TextInput } from "frog";
2 | import { devtools } from "frog/dev";
3 | import { serveStatic } from "frog/serve-static";
4 | import neynarClient from "./lib/neynarClient.js";
5 | import { neynar } from "frog/hubs";
6 |
7 | export const app = new Frog({
8 | hub: neynar({ apiKey: "NEYNAR_FROG_FM" }),
9 | basePath: "/api",
10 | });
11 |
12 | const ADD_URL =
13 | "https://warpcast.com/~/add-cast-action?actionType=post&name=Followers&icon=person&postUrl=https%3A%2F%2F05d3-2405-201-800c-6a-70a7-56e4-516c-2d3c.ngrok-free.app%2Fapi%2Ffollowers";
14 |
15 | app.frame("/", (c) => {
16 | return c.res({
17 | image: (
18 |
32 |
44 | gm! Add cast action to view followers count
45 |
46 |
47 | ),
48 | intents: [Add Action],
49 | });
50 | });
51 |
52 | app.hono.post("/followers", async (c) => {
53 | try {
54 | const body = await c.req.json();
55 | const result = await neynarClient.validateFrameAction({
56 | messageBytesInHex: body.trustedData.messageBytes,
57 | });
58 |
59 | const user = result.action.cast.author;
60 |
61 | let message = `Count:${user.follower_count}`;
62 |
63 | return c.json({ message });
64 | } catch (e) {
65 | return c.json({ message: "Error. Try Again." }, 500);
66 | }
67 | });
68 |
69 | app.use("/*", serveStatic({ root: "./public" }));
70 | devtools(app, { serveStatic });
71 |
72 | if (typeof Bun !== "undefined") {
73 | Bun.serve({
74 | fetch: app.fetch,
75 | port: 3000,
76 | });
77 | console.log("Server is running on port 3000");
78 | }
79 |
--------------------------------------------------------------------------------
/cast-action/src/lib/neynarClient.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient, Configuration } from "@neynar/nodejs-sdk";
2 | import { config } from "dotenv";
3 |
4 | config();
5 |
6 | if (!process.env.NEYNAR_API_KEY) {
7 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file");
8 | }
9 |
10 | const neynarConfig = new Configuration({
11 | apiKey: process.env.NEYNAR_API_KEY,
12 | baseOptions: {
13 | headers: {
14 | "x-neynar-experimental": true,
15 | },
16 | },
17 | });
18 |
19 | const neynarClient = new NeynarAPIClient(neynarConfig);
20 |
21 | export default neynarClient;
22 |
--------------------------------------------------------------------------------
/cast-action/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "NodeNext",
4 | "strict": true,
5 | "jsx": "react-jsx",
6 | "jsxImportSource": "hono/jsx"
7 | }
8 | }
--------------------------------------------------------------------------------
/fc2x/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_NEYNAR_CLIENT_ID=""
2 | NEXT_PUBLIC_VERCEL_URL=""
3 | NEYNAR_API_KEY=""
4 | NEYNAR_WEBHOOK_ID=""
5 | NEYNAR_WEBHOOK_SECRET=""
6 | POSTGRES_URL=""
7 | TWITTER_CALLBACK_URL=""
8 | TWITTER_CONSUMER_KEY=""
9 | TWITTER_CONSUMER_SECRET=""
--------------------------------------------------------------------------------
/fc2x/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/fc2x/Initial Migration.md:
--------------------------------------------------------------------------------
1 | # Intial Migration
2 |
3 | Here is the initial migration you need to create the necessary users table on your own Postgres database instance:
4 |
5 | ``` sql
6 | CREATE TABLE users (
7 | id SERIAL PRIMARY KEY,
8 | twitter_user_id VARCHAR(50),
9 | farcaster_username VARCHAR(100),
10 | profile_image_url TEXT,
11 | twitter_access_token TEXT,
12 | twitter_refresh_token TEXT,
13 | twitter_token_expires_at TIMESTAMP,
14 | fid BIGINT,
15 | twitter_oauth_token VARCHAR(255),
16 | twitter_oauth_token_secret VARCHAR(255),
17 | is_online BOOLEAN DEFAULT true,
18 | signer_uuid VARCHAR(255),
19 | twitter_username VARCHAR(100),
20 | display_name VARCHAR(255),
21 | twitter_access_token_secret TEXT,
22 | UNIQUE (twitter_user_id)
23 | );
24 | ```
--------------------------------------------------------------------------------
/fc2x/README.md:
--------------------------------------------------------------------------------
1 | # FC2X
2 | Crosspost all of your casts from Farcaster to X
3 |
4 |
5 |
6 | ## Getting Started
7 |
8 | ### 1. Install Dependencies
9 |
10 | First, install local dependencies:
11 |
12 | ```bash
13 | yarn install
14 | ```
15 |
16 | ### 2. Add Environment Variables
17 |
18 | Then, copy `.env.example` to a new `.env.local` file and add the necessary environment variables. Here's some information on where you can find/create the necessary credentials:
19 |
20 | | Credential(s) | Description |
21 | |------------------------|-------------------------------------------------|
22 | | `NEXT_PUBLIC_NEYNAR_CLIENT_ID` and `NEYNAR_API_KEY` | Your Neynar Client ID and API Key can be found on the [Neynar Dev Portal](https://dev.neynar.com) on the page for your app(make sure to create an app if you haven't already). |
23 | | `NEYNAR_WEBHOOK_ID` and `NEYNAR_WEBHOOK_SECRET` | Go to the [Neynar Dev Portal](https://dev.neynar.com) and create a webhook where the target URL is `${YOUR_PROD_URL}/api/webhook`. Then grab the webhook's ID and set it as your `NEYNAR_WEBHOOK_ID` value, as well the webhook's secret value for `NEYNAR_WEBHOOK_SECRET`. |
24 | | `POSTGRES_URL` | The connection URL for your Postgres DB. To create all of the tables necessary for this project, run the SQL query in `Initial Migration.md`. |
25 | | `TWITTER_CALLBACK_URL` | The callback URL for Twitter authentication. Set this value to `${YOUR_PROD_URL}/api/callback`, and make sure to set this on the [Twitter/X Developer Portal](https://developer.x.com/en/portal) as well. |
26 | | `TWITTER_CONSUMER_KEY` and `TWITTER_CONSUMER_SECRET` | The API Key and API Key Secret for your Twitter app, which you find(or create) on the [Twitter/X Developer Portal](https://developer.x.com/en/portal). |
27 |
28 | ### 3. Run the development server
29 |
30 | Finally, run the development server:
31 |
32 | ```bash
33 | yarn dev
34 | ```
35 |
36 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
37 |
--------------------------------------------------------------------------------
/fc2x/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/fc2x/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/fc2x/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fc2x",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@neynar/react": "^0.9.5",
13 | "@pigment-css/react": "^0.0.9",
14 | "@radix-ui/react-avatar": "^1.1.0",
15 | "@radix-ui/react-checkbox": "^1.1.2",
16 | "@radix-ui/react-icons": "^1.3.0",
17 | "@radix-ui/react-slot": "^1.1.0",
18 | "@radix-ui/react-switch": "^1.1.1",
19 | "axios": "^1.7.7",
20 | "class-variance-authority": "^0.7.0",
21 | "clsx": "^2.1.1",
22 | "hls.js": "^1.5.15",
23 | "lucide-react": "^0.445.0",
24 | "next": "14.2.12",
25 | "pg": "^8.13.0",
26 | "react": "^18.3.1",
27 | "react-dom": "^18.3.1",
28 | "swr": "^2.2.5",
29 | "tailwind-merge": "^2.5.2",
30 | "tailwindcss-animate": "^1.0.7",
31 | "twitter-api-v2": "^1.17.2"
32 | },
33 | "devDependencies": {
34 | "@types/cookie": "^0.6.0",
35 | "@types/node": "^22.6.0",
36 | "@types/pg": "^8.11.10",
37 | "@types/react": "^18",
38 | "@types/react-dom": "^18",
39 | "eslint": "^8",
40 | "eslint-config-next": "14.2.12",
41 | "postcss": "^8",
42 | "tailwindcss": "^3.4.1",
43 | "typescript": "^5"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/fc2x/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/fc2x/src/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 |
3 | interface ThemeContextType {
4 | theme: 'light' | 'dark';
5 | toggleTheme: () => void;
6 | }
7 |
8 | const ThemeContext = createContext(undefined);
9 |
10 | export const ThemeProvider: React.FC<{ value: ThemeContextType; children: React.ReactNode }> = ({ value, children }) => {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export const useTheme = () => {
19 | const context = useContext(ThemeContext);
20 | if (context === undefined) {
21 | throw new Error('useTheme must be used within a ThemeProvider');
22 | }
23 | return context;
24 | };
--------------------------------------------------------------------------------
/fc2x/src/app/api/auth/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { TwitterApi } from 'twitter-api-v2';
3 |
4 | export async function GET(request: NextRequest) {
5 | const generateAuthTokens = async () => {
6 | const client = new TwitterApi({
7 | appKey: process.env.TWITTER_CONSUMER_KEY!,
8 | appSecret: process.env.TWITTER_CONSUMER_SECRET!,
9 | });
10 | return await client.generateAuthLink(process.env.TWITTER_CALLBACK_URL, {authAccessType: "write", linkMode: "authorize", forceLogin: false});
11 | };
12 |
13 | try {
14 | const { oauth_token, oauth_token_secret, url } = await generateAuthTokens();
15 | if (!oauth_token || !oauth_token_secret || !url) {
16 | console.error('Invalid authentication request', { oauth_token, oauth_token_secret, url });
17 | return new NextResponse('Invalid authentication request', { status: 400 });
18 | }
19 | const response = NextResponse.redirect(url);
20 |
21 | response.cookies.set('oauth_token', oauth_token, {
22 | httpOnly: true,
23 | secure: true,
24 | sameSite: 'lax',
25 | maxAge: 3600,
26 | path: '/',
27 | });
28 |
29 | response.cookies.set('oauth_token_secret', oauth_token_secret, {
30 | httpOnly: true,
31 | secure: true,
32 | sameSite: 'lax',
33 | maxAge: 3600,
34 | path: '/',
35 | });
36 |
37 | response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
38 | response.headers.set('Pragma', 'no-cache');
39 | response.headers.set('Expires', '0');
40 |
41 | return response;
42 | } catch (error) {
43 | console.error('Error handling OAuth token generation:', error);
44 | return new NextResponse('Error generating authentication link', { status: 500 });
45 | }
46 | }
--------------------------------------------------------------------------------
/fc2x/src/app/api/auth/tokens/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { TwitterApi } from 'twitter-api-v2';
3 |
4 | export const revalidate = 0;
5 |
6 | export async function GET(request: NextRequest) {
7 | const generateAuthTokens = async () => {
8 | const client = new TwitterApi({
9 | appKey: process.env.TWITTER_CONSUMER_KEY!,
10 | appSecret: process.env.TWITTER_CONSUMER_SECRET!,
11 | });
12 | return await client.generateAuthLink(process.env.TWITTER_CALLBACK_URL, {
13 | authAccessType: "write",
14 | linkMode: "authorize",
15 | forceLogin: false
16 | });
17 | };
18 |
19 | try {
20 | const { oauth_token, oauth_token_secret, url } = await generateAuthTokens();
21 | const response = NextResponse.json({ oauth_token, oauth_token_secret, url });
22 | response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
23 | response.headers.set('Pragma', 'no-cache');
24 | response.headers.set('Expires', '0');
25 | return response;
26 | } catch (error) {
27 | // Improved logging for error handling
28 | console.error('Error generating authentication link:', (error as Error).message);
29 | console.error('Stack trace:', (error as Error).stack);
30 |
31 | const errorResponse = NextResponse.json({ error: 'Error generating authentication link' }, { status: 500 });
32 | errorResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
33 | errorResponse.headers.set('Pragma', 'no-cache');
34 | errorResponse.headers.set('Expires', '0');
35 | return errorResponse;
36 | }
37 | }
--------------------------------------------------------------------------------
/fc2x/src/app/api/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { TwitterApi } from 'twitter-api-v2';
3 | import pool from '@/lib/db';
4 |
5 | export async function GET(request: NextRequest) {
6 | const url = request.nextUrl;
7 | const twitter_oauth_token = url.searchParams.get('oauth_token');
8 | const oauth_verifier = url.searchParams.get('oauth_verifier');
9 |
10 | const twitter_oauth_token_cookie = request.cookies.get('oauth_token')?.value;
11 | const twitter_oauth_token_secret = request.cookies.get('oauth_token_secret')?.value;
12 |
13 | if (!twitter_oauth_token || !oauth_verifier || !twitter_oauth_token_cookie || !twitter_oauth_token_secret) {
14 | console.error('Invalid authentication request', { twitter_oauth_token, oauth_verifier, twitter_oauth_token_cookie, twitter_oauth_token_secret });
15 | return new NextResponse('Invalid authentication request', { status: 400 });
16 | }
17 |
18 | try {
19 | const client = new TwitterApi({
20 | appKey: process.env.TWITTER_CONSUMER_KEY!,
21 | appSecret: process.env.TWITTER_CONSUMER_SECRET!,
22 | accessToken: twitter_oauth_token,
23 | accessSecret: twitter_oauth_token_secret,
24 | });
25 |
26 | const { accessToken, accessSecret, userId } = await client.login(oauth_verifier);
27 |
28 | const userClient = new TwitterApi({
29 | appKey: process.env.TWITTER_CONSUMER_KEY!,
30 | appSecret: process.env.TWITTER_CONSUMER_SECRET!,
31 | accessToken: accessToken,
32 | accessSecret: accessSecret,
33 | });
34 |
35 | console.log("user id before verifying credentials", userId);
36 | const userData = await userClient.v1.verifyCredentials();
37 | console.log("user details after verifying credentails", userData);
38 |
39 | const insertQuery = `
40 | INSERT INTO users (
41 | twitter_user_id, display_name, twitter_username, profile_image_url, twitter_access_token
42 | ) VALUES ($1, $2, $3, $4, $5)
43 | ON CONFLICT (twitter_user_id)
44 | DO UPDATE SET
45 | display_name = EXCLUDED.display_name,
46 | twitter_username = EXCLUDED.twitter_username,
47 | profile_image_url = EXCLUDED.profile_image_url,
48 | twitter_access_token = EXCLUDED.twitter_access_token
49 | `;
50 |
51 | const result = await pool.query(insertQuery, [
52 | userId,
53 | userData.name,
54 | userData.screen_name,
55 | userData.profile_image_url_https,
56 | accessToken
57 | ]);
58 |
59 | if(result){
60 | const updateSecretQuery = `
61 | UPDATE users
62 | SET twitter_access_token_secret = $2::text
63 | WHERE twitter_user_id = $1
64 | `;
65 | const updateResult = await pool.query(updateSecretQuery, [userId, accessSecret]);
66 | }
67 |
68 | const response = NextResponse.redirect(new URL('/', request.url));
69 |
70 | response.cookies.set('twitter_user_id', userId, {
71 | httpOnly: true,
72 | secure: true,
73 | sameSite: 'lax',
74 | maxAge: 30 * 24 * 60 * 60, // 30 days
75 | path: '/',
76 | });
77 |
78 | response.cookies.delete('oauth_token');
79 | response.cookies.delete('oauth_token_secret');
80 |
81 | response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
82 | response.headers.set('Pragma', 'no-cache');
83 | response.headers.set('Expires', '0');
84 |
85 | return response;
86 | } catch (error) {
87 | console.error('Error during callback:', error);
88 | return new NextResponse('Error during authentication callback', { status: 500 });
89 | }
90 | }
--------------------------------------------------------------------------------
/fc2x/src/app/api/link-fid/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import pool from '@/lib/db';
3 | import { updateNeynarWebhook } from '@/lib/webhook';
4 |
5 | export async function POST(request: NextRequest) {
6 |
7 | const twitterUserId = request.cookies.get('twitter_user_id')?.value;
8 | if (!twitterUserId) {
9 | console.error('Twitter user not authenticated');
10 | return NextResponse.json({ error: 'Twitter user not authenticated' }, { status: 401 });
11 | }
12 |
13 | const { fid } = await request.json();
14 | if (!fid) {
15 | console.error('Farcaster user not authenticated');
16 | return NextResponse.json({ error: 'Farcaster user not authenticated' }, { status: 401 });
17 | }
18 |
19 | try {
20 | const { rows: fcUserRows } = await pool.query('SELECT * FROM users WHERE fid = $1', [fid]);
21 | const { rows: twitterUserRows } = await pool.query('SELECT * FROM users WHERE twitter_user_id = $1', [twitterUserId]);
22 |
23 | // if the FID is already linked to the twitterUserId, return this object
24 | if (twitterUserRows.length > 0 && twitterUserRows[0].fid !== null) {
25 | console.error('Fid already linked');
26 | return NextResponse.json({ message: "fid already linked" }, { status: 200 });
27 | }
28 |
29 | // if this FID is already linked to *another row in the table*, delete this current row and merge its contents with the existing row
30 | if(fcUserRows.length > 0 && fcUserRows[0].fid !== null){
31 | await pool.query('DELETE FROM users WHERE id = $1', [twitterUserRows[0].id]);
32 | await pool.query(
33 | 'UPDATE users SET twitter_user_id = $1, profile_image_url = $2, twitter_access_token = $3, twitter_oauth_token = $4, twitter_oauth_token_secret = $5, twitter_username = $6 WHERE fid = $7 AND id = $8',
34 | [
35 | twitterUserId,
36 | twitterUserRows[0].profile_image_url,
37 | twitterUserRows[0].twitter_access_token,
38 | twitterUserRows[0].twitter_oauth_token,
39 | twitterUserRows[0].twitter_oauth_token_secret,
40 | twitterUserRows[0].twitter_username,
41 | fid,
42 | fcUserRows[0].id
43 | ]
44 | );
45 | } else{
46 | await pool.query(
47 | 'UPDATE users SET fid = $1 WHERE twitter_user_id = $2',
48 | [fid, twitterUserId]
49 | );
50 | }
51 |
52 | const { rows } = await pool.query('SELECT fid FROM users WHERE fid IS NOT NULL AND twitter_user_id IS NOT NULL');
53 | const authorFids = Array.from(new Set(rows.map((row: { fid: number }) => row.fid)));
54 |
55 | await updateNeynarWebhook(authorFids);
56 |
57 | return NextResponse.json({ message: 'Fid linked successfully' }, { status: 200 });
58 | } catch (error) {
59 | console.error('Error linking fid:', error);
60 | return NextResponse.json({ error: 'Error linking fid' }, { status: 500 });
61 | }
62 | }
--------------------------------------------------------------------------------
/fc2x/src/app/api/logout/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | export async function GET(request: NextRequest) {
4 | const response = new NextResponse('Logged out');
5 | response.cookies.set({
6 | name: 'twitter_user_id',
7 | value: '',
8 | path: '/',
9 | httpOnly: true,
10 | secure: true,
11 | sameSite: 'lax',
12 | maxAge: 0,
13 | });
14 | return response;
15 | }
16 |
--------------------------------------------------------------------------------
/fc2x/src/app/api/profile/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { TwitterApi } from 'twitter-api-v2';
3 | import pool from '@/lib/db';
4 |
5 | export async function GET(request: NextRequest) {
6 | const twitterUserId = request.cookies.get('twitter_user_id')?.value;
7 | if (!twitterUserId) {
8 | return new NextResponse('User not authenticated', { status: 401 });
9 | }
10 |
11 | const { rows } = await pool.query(
12 | 'SELECT * FROM users WHERE twitter_user_id = $1',
13 | [twitterUserId]
14 | );
15 |
16 | if (rows.length === 0) {
17 | return new NextResponse('User not authenticated', { status: 401 });
18 | }
19 |
20 | const userProfile = rows[0];
21 |
22 | return NextResponse.json(userProfile);
23 | }
--------------------------------------------------------------------------------
/fc2x/src/app/api/toggle-online/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import pool from '@/lib/db';
3 |
4 | export async function POST(request: NextRequest) {
5 | const twitter_user_id = request.cookies.get('twitter_user_id')?.value;
6 |
7 | if (!twitter_user_id) {
8 | return new NextResponse('Unauthorized', { status: 401 });
9 | }
10 |
11 | const body = await request.json();
12 | const { is_online } = body;
13 |
14 | try {
15 | await pool.query(
16 | `UPDATE users SET is_online = $1 WHERE twitter_user_id = $2`,
17 | [is_online, twitter_user_id]
18 | );
19 |
20 | return new NextResponse(JSON.stringify({ success: true, is_online }), { status: 200 });
21 | } catch (error) {
22 | console.error('Error updating online status:', error);
23 | return new NextResponse('Failed to update status', { status: 500 });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/fc2x/src/app/api/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { TwitterApi } from 'twitter-api-v2';
3 | import pool from '@/lib/db';
4 | import axios from 'axios';
5 | import { verifyWebhookSignature } from '@/lib/webhook';
6 |
7 | export async function POST(request: NextRequest) {
8 | try {
9 | const body = await verifyWebhookSignature(request);
10 | const payload = JSON.parse(body);
11 | const type = payload.type;
12 | const data = payload.data;
13 |
14 | if (type !== 'cast.created' || !data) {
15 | console.error('Invalid webhook payload:', payload);
16 | return new NextResponse('Invalid webhook payload', { status: 400 });
17 | }
18 |
19 | const text = data.text || '';
20 | const linkEmbeds = (data.embeds || []).map((embed: any) => embed.url).filter((url: string) => url);
21 | const castEmbeds = (data.embeds || [])
22 | .filter((embed: any) => embed.cast_id &&
23 | typeof embed.cast_id.fid === 'number' &&
24 | embed.cast_id.hash &&
25 | embed.cast_id.hash.length > 1 &&
26 | embed.cast_id.hash.startsWith('0x'))
27 | .map((embed: any) => `https://client.warpcast.com/v2/cast-image?castHash=${embed.cast_id.hash}`);
28 |
29 | const embeds = castEmbeds.concat(linkEmbeds);
30 |
31 | const fid = data.author?.fid;
32 | const parent_hash = data.parent_hash;
33 |
34 | if (parent_hash !== null) {
35 | return new NextResponse('Parent hash is not null', { status: 400 });
36 | }
37 |
38 | if (!fid) {
39 | return new NextResponse('Farcaster user ID (fid) not found in payload', { status: 400 });
40 | }
41 |
42 | const { rows } = await pool.query(
43 | 'SELECT is_online, twitter_access_token, twitter_access_token_secret FROM users WHERE fid = $1',
44 | [fid]
45 | );
46 |
47 | if (rows.length === 0) {
48 | return new NextResponse('No linked Twitter account for this Farcaster user', { status: 404 });
49 | }
50 |
51 | const { is_online, twitter_access_token, twitter_access_token_secret } = rows[0];
52 |
53 | console.log("is online?", is_online);
54 | if(!is_online){
55 | return new NextResponse('User is not online', { status: 401 })
56 | }
57 |
58 | if (!twitter_access_token || !twitter_access_token_secret) {
59 | console.error('Twitter OAuth 1.0a tokens not found:', { twitter_access_token, twitter_access_token_secret });
60 | return new NextResponse('Twitter OAuth 1.0a tokens not found', { status: 401 });
61 | }
62 |
63 | const client = new TwitterApi({
64 | appKey: process.env.TWITTER_CONSUMER_KEY!,
65 | appSecret: process.env.TWITTER_CONSUMER_SECRET!,
66 | accessToken: twitter_access_token,
67 | accessSecret: twitter_access_token_secret
68 | });
69 |
70 | console.log('raw cast text before process', text);
71 |
72 | let tweetContent = text;
73 | const maxTweetLength = 280;
74 | let warpcastUrl = '';
75 |
76 | if (tweetContent.length > maxTweetLength) {
77 | tweetContent = tweetContent.substring(0, maxTweetLength - 3) + '...';
78 | }
79 |
80 | let mediaIds: string[] = [];
81 | if (embeds.length > 0) {
82 | for (const embedUrl of embeds) {
83 | try {
84 | const response = await axios.get(embedUrl, { responseType: 'arraybuffer' });
85 | const mediaType = response.headers['content-type'];
86 | const mediaId = await client.v1.uploadMedia(Buffer.from(response.data), {
87 | mimeType: mediaType,
88 | });
89 | mediaIds.push(mediaId);
90 | console.log('Uploaded media ID:', mediaId);
91 | } catch (error) {
92 | console.error('Error uploading media:', error);
93 | }
94 | }
95 | }
96 |
97 | const tweetParams: any = { text: tweetContent };
98 | if (mediaIds.length > 0) {
99 | tweetParams.media = {
100 | media_ids: mediaIds
101 | };
102 | }
103 | console.log('Tweet params:', tweetParams);
104 |
105 | const tweet = await client.v2.tweet(tweetContent, tweetParams)
106 | console.log('Tweet response:', tweet);
107 |
108 | return NextResponse.json({ message: 'Tweet posted successfully', tweet });
109 | } catch (error) {
110 | console.error('Error handling webhook:', error);
111 | return new NextResponse('Error handling webhook', { status: 500 });
112 | }
113 | }
--------------------------------------------------------------------------------
/fc2x/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/fc2x/src/app/favicon.ico
--------------------------------------------------------------------------------
/fc2x/src/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/fc2x/src/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/fc2x/src/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/fc2x/src/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/fc2x/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
23 | @layer utilities {
24 | .text-balance {
25 | text-wrap: balance;
26 | }
27 | }
28 |
29 | @layer base {
30 | :root {
31 | --background: 0 0% 100%;
32 | --foreground: 222.2 84% 4.9%;
33 | --card: 0 0% 100%;
34 | --card-foreground: 222.2 84% 4.9%;
35 | --popover: 0 0% 100%;
36 | --popover-foreground: 222.2 84% 4.9%;
37 | --primary: 222.2 47.4% 11.2%;
38 | --primary-foreground: 210 40% 98%;
39 | --secondary: 210 40% 96.1%;
40 | --secondary-foreground: 222.2 47.4% 11.2%;
41 | --muted: 210 40% 96.1%;
42 | --muted-foreground: 215.4 16.3% 46.9%;
43 | --accent: 210 40% 96.1%;
44 | --accent-foreground: 222.2 47.4% 11.2%;
45 | --destructive: 0 84.2% 60.2%;
46 | --destructive-foreground: 210 40% 98%;
47 | --border: 214.3 31.8% 91.4%;
48 | --input: 214.3 31.8% 91.4%;
49 | --ring: 222.2 84% 4.9%;
50 | --chart-1: 12 76% 61%;
51 | --chart-2: 173 58% 39%;
52 | --chart-3: 197 37% 24%;
53 | --chart-4: 43 74% 66%;
54 | --chart-5: 27 87% 67%;
55 | --radius: 0.5rem;
56 | }
57 | .dark {
58 | --background: 222.2 84% 4.9%;
59 | --foreground: 210 40% 98%;
60 | --card: 222.2 84% 4.9%;
61 | --card-foreground: 210 40% 98%;
62 | --popover: 222.2 84% 4.9%;
63 | --popover-foreground: 210 40% 98%;
64 | --primary: 210 40% 98%;
65 | --primary-foreground: 222.2 47.4% 11.2%;
66 | --secondary: 217.2 32.6% 17.5%;
67 | --secondary-foreground: 210 40% 98%;
68 | --muted: 217.2 32.6% 17.5%;
69 | --muted-foreground: 215 20.2% 65.1%;
70 | --accent: 217.2 32.6% 17.5%;
71 | --accent-foreground: 210 40% 98%;
72 | --destructive: 0 62.8% 30.6%;
73 | --destructive-foreground: 210 40% 98%;
74 | --border: 217.2 32.6% 17.5%;
75 | --input: 217.2 32.6% 17.5%;
76 | --ring: 212.7 26.8% 83.9%;
77 | --chart-1: 220 70% 50%;
78 | --chart-2: 160 60% 45%;
79 | --chart-3: 30 80% 55%;
80 | --chart-4: 280 65% 60%;
81 | --chart-5: 340 75% 55%;
82 | }
83 | }
84 |
85 | @layer base {
86 | * {
87 | @apply border-border;
88 | }
89 | body {
90 | @apply bg-background text-foreground;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/fc2x/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useState, useEffect } from 'react';
3 | import "@neynar/react/dist/style.css";
4 | import "./globals.css";
5 | import { NeynarContextProvider, Theme } from "@neynar/react";
6 | import { ThemeProvider } from "@/ThemeProvider";
7 |
8 | export default function RootLayout({
9 | children,
10 | }: Readonly<{
11 | children: React.ReactNode;
12 | }>) {
13 | const [theme, setTheme] = useState<'light' | 'dark'>('light');
14 |
15 | useEffect(() => {
16 | const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
17 | if (savedTheme) {
18 | setTheme(savedTheme);
19 | document.documentElement.classList.toggle('dark', savedTheme === 'dark');
20 | } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
21 | setTheme('dark');
22 | document.documentElement.classList.add('dark');
23 | }
24 | }, []);
25 |
26 | const toggleTheme = () => {
27 | setTheme(prevTheme => {
28 | const newTheme = prevTheme === 'light' ? 'dark' : 'light';
29 | localStorage.setItem('theme', newTheme);
30 | document.documentElement.classList.toggle('dark', newTheme === 'dark');
31 | return newTheme;
32 | });
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 |
46 | {children}
47 |
48 |
49 |
50 |
51 | );
52 | }
--------------------------------------------------------------------------------
/fc2x/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { CheckIcon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/fc2x/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/fc2x/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const NEYNAR_API_BASE_URL = 'https://api.neynar.com';
2 | export const VERCEL_URL = "https://x-crosspost.vercel.app"
3 | // note: replace VERCEL_URL with your production URL
--------------------------------------------------------------------------------
/fc2x/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { Pool } from 'pg';
2 |
3 | const pool = new Pool({
4 | connectionString: process.env.POSTGRES_URL,
5 | ssl: {
6 | rejectUnauthorized: false,
7 | },
8 | });
9 |
10 | export default pool;
11 |
--------------------------------------------------------------------------------
/fc2x/src/lib/server.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { redirect } from "next/navigation";
4 |
5 | export async function navigate(url: string){
6 | redirect(url);
7 | }
--------------------------------------------------------------------------------
/fc2x/src/lib/types.d.ts:
--------------------------------------------------------------------------------
1 | export type User = {
2 | id: number;
3 | twitter_user_id: string | null;
4 | farcaster_username: string | null;
5 | profile_image_url: string | null;
6 | twitter_access_token: string | null;
7 | twitter_refresh_token: string | null;
8 | twitter_token_expires_at: string | null;
9 | fid: number | null;
10 | twitter_oauth_token: string | null;
11 | twitter_oauth_token_secret: string | null;
12 | is_online: boolean | null;
13 | slack_access_token: string | null;
14 | slack_user_id: string | null;
15 | slack_team_id: string | null;
16 | slack_token_expires_at: string | null;
17 | signer_uuid: string | null;
18 | twitter_username: string | null;
19 | display_name: string | null;
20 | };
--------------------------------------------------------------------------------
/fc2x/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/fc2x/src/lib/webhook.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 | import { createHmac } from 'crypto';
3 | import axios from 'axios';
4 | import { NEYNAR_API_BASE_URL, VERCEL_URL } from './constants';
5 |
6 | export async function verifyWebhookSignature(req: NextRequest): Promise {
7 | const body = await req.text();
8 |
9 | const sig = req.headers.get("X-Neynar-Signature");
10 | if (!sig) {
11 | throw new Error("Neynar signature missing from request headers");
12 | }
13 |
14 | const webhookSecret = process.env.NEYNAR_WEBHOOK_SECRET;
15 | if (!webhookSecret) {
16 | throw new Error("Make sure you set NEYNAR_WEBHOOK_SECRET in your .env file");
17 | }
18 |
19 | const hmac = createHmac("sha512", webhookSecret);
20 | hmac.update(body);
21 |
22 | const generatedSignature = hmac.digest("hex");
23 |
24 | const isValid = generatedSignature === sig;
25 | if (!isValid) {
26 | throw new Error("Invalid Neynar webhook signature");
27 | }
28 | return body
29 | }
30 |
31 | export async function updateNeynarWebhook(author_fids: (string | number)[]) {
32 | const webhookId = process.env.NEYNAR_WEBHOOK_ID;
33 | const neynarApiKey = process.env.NEYNAR_API_KEY;
34 |
35 | if (!webhookId || !neynarApiKey) {
36 | console.error('Neynar webhook ID or API key not set in environment variables');
37 | throw new Error('Neynar webhook ID or API key not set in environment variables');
38 | }
39 |
40 | const numericAuthorFids = author_fids.map(fid => typeof fid === 'string' ? parseInt(fid, 10) : fid);
41 |
42 | const subscription = {
43 | "cast.created": {
44 | author_fids: numericAuthorFids
45 | },
46 | };
47 |
48 | try {
49 | const response = await axios.put(
50 | `${NEYNAR_API_BASE_URL}/v2/farcaster/webhook`,
51 | {
52 | name: 'Neynar Twitter Cross-Post Webhook',
53 | webhook_id: webhookId,
54 | subscription,
55 | url: `${process.env.NEXT_PUBLIC_VERCEL_URL ?? VERCEL_URL}/api/webhook`,
56 | },
57 | {
58 | headers: {
59 | 'Content-Type': 'application/json',
60 | 'api_key': `${neynarApiKey}`,
61 | },
62 | }
63 | );
64 |
65 | } catch (error: any) {
66 | console.log("Update neynar webhook error", error);
67 | console.error('Error updating Neynar webhook:', error.response?.data || error.message);
68 | throw new Error('Failed to update Neynar webhook');
69 | }
70 | }
--------------------------------------------------------------------------------
/fc2x/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: "media",
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: 'var(--radius)',
56 | md: 'calc(var(--radius) - 2px)',
57 | sm: 'calc(var(--radius) - 4px)'
58 | }
59 | }
60 | },
61 | plugins: [require("tailwindcss-animate")],
62 | };
63 | export default config;
64 |
--------------------------------------------------------------------------------
/fc2x/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/flask-app/.env.example:
--------------------------------------------------------------------------------
1 | NEYNAR_API_KEY=YOUR_API_KEY_HERE
--------------------------------------------------------------------------------
/flask-app/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | *.py,cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 | db.sqlite3-journal
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 | build/docs/
72 |
73 | # PyBuilder
74 | target/
75 |
76 | # Jupyter Notebook
77 | .ipynb_checkpoints
78 |
79 | # pyenv
80 | .python-version
81 |
82 | # pipenv
83 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
84 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
85 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
86 | # install all needed dependencies.
87 | #Pipfile.lock
88 |
89 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
90 | __pypackages__/
91 |
92 | # Celery stuff
93 | celerybeat-schedule
94 | celerybeat.pid
95 |
96 | # SageMath parsed files
97 | *.sage.py
98 |
99 | # Environments
100 | .env
101 | .envrc
102 | .venv
103 | venv/
104 | ENV/
105 | env/
106 | env.bak/
107 | env.tmp/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 |
113 | # Rope project settings
114 | .ropeproject
115 |
116 | # mkdocs documentation
117 | /site
118 |
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 |
124 | # Pyre type checker
125 | .pyre/
126 |
127 | # pytype static type analyzer
128 | .pytype/
129 |
130 | # Cython debug symbols
131 | cython_debug/
132 |
--------------------------------------------------------------------------------
/flask-app/README.md:
--------------------------------------------------------------------------------
1 | # Neynar Flask Mini-App
2 |
3 | Welcome to the Neynar Mini-App! This Flask application uses Neynar's API to fetch and display casts from the EVM channel. Follow these steps to get started:
4 |
5 | ## Steps
6 |
7 | ### Step 1: Clone the Repository
8 |
9 | Clone the Neynar Mini-App repository to your local machine:
10 |
11 | ```sh
12 | git clone https://github.com/neynarxyz/farcaster-examples
13 | ```
14 |
15 | ### Step 2: Prepare the Application Directory
16 |
17 | Move the Flask app directory to your desired location and remove the cloned repository's extra contents:
18 |
19 | ```sh
20 | mv farcaster-examples/flask-app .
21 | rm -rf farcaster-examples
22 | cd flask-app
23 | ```
24 |
25 | ### Step 3: Set Up a Virtual Environment
26 |
27 | Create and activate a virtual environment for the application:
28 |
29 | ```sh
30 | python -m venv venv
31 | # Activate the environment:
32 | # On macOS/Linux:
33 | source venv/bin/activate
34 | # On Windows:
35 | .\venv\Scripts\activate
36 | ```
37 |
38 | ### Step 4: Install Dependencies
39 |
40 | Install the required Python packages:
41 |
42 | ```sh
43 | pip install -r requirements.txt
44 | ```
45 |
46 | ### Step 5: Edit the FID and the API key
47 |
48 | Open `app.py` and replace the `api_key` with your own:
49 |
50 | ```python
51 | api_key = "YOUR_API_KEY_HERE"
52 | ```
53 |
54 | ### Step 6: Run the Application
55 |
56 | Start the Flask app:
57 |
58 | ```sh
59 | python app.py
60 | # Your app will be running on port 5000
61 | ```
62 |
63 | You should now see the Neynar mini-app running and displaying casts from the EVM channel!
64 |
--------------------------------------------------------------------------------
/flask-app/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, render_template
2 | import requests
3 | import os
4 |
5 | # Initialize the Flask application
6 | app = Flask(__name__)
7 |
8 |
9 | @app.route("/")
10 | def main_app():
11 | api_key = "YOUR_API_KEY_HERE"
12 | evm_channel = {
13 | "name": "EVM",
14 | "parent_url": "chain://eip155:1/erc721:0x37fb80ef28008704288087831464058a4a3940ae",
15 | "image": "https://warpcast.com/~/channel-images/evm.png",
16 | "channel_id": "evm",
17 | "lead_fid": 3621,
18 | }
19 |
20 | url = "https://api.neynar.com/v2/"
21 | url += "farcaster/feed?feed_type=filter&filter_type=parent_url"
22 | url += f"&parent_url={evm_channel['parent_url']}&limit=25"
23 | headers = {"accept": "application/json", "api_key": api_key}
24 |
25 | # get data from Neynar API, parse, then render template
26 | response = requests.get(url, headers=headers)
27 | data = response.json()
28 |
29 | # parse data
30 | parser = lambda cast: {"fid": cast["author"]["fid"], "text": cast["text"]}
31 | parsed_data = list(map(parser, data["casts"]))
32 | parsed_data = [cast for cast in parsed_data if cast["text"] not in ["", None]]
33 |
34 | return render_template("index.html", data=parsed_data)
35 |
36 |
37 | # Run the application
38 | if __name__ == "__main__":
39 | app.run(debug=True)
40 |
--------------------------------------------------------------------------------
/flask-app/requirements.txt:
--------------------------------------------------------------------------------
1 | blinker==1.7.0
2 | certifi==2024.7.4
3 | charset-normalizer==3.3.2
4 | click==8.1.7
5 | Flask==3.0.0
6 | idna==3.7
7 | itsdangerous==2.1.2
8 | Jinja2==3.1.5
9 | MarkupSafe==2.1.3
10 | requests==2.32.2
11 | urllib3==2.2.2
12 | Werkzeug==3.0.6
13 |
--------------------------------------------------------------------------------
/flask-app/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | My app!
8 |
49 |
50 |
51 |
52 |
53 |
Casts in EVM channel!
54 | {% for item in data %}
55 |
56 |
57 |
{{ item.text }}
58 |
59 | {% endfor %}
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/frames-bot/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 |
15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
16 |
17 | # Runtime data
18 |
19 | pids
20 | _.pid
21 | _.seed
22 | \*.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 |
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 |
30 | coverage
31 | \*.lcov
32 |
33 | # nyc test coverage
34 |
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 |
39 | .grunt
40 |
41 | # Bower dependency directory (https://bower.io/)
42 |
43 | bower_components
44 |
45 | # node-waf configuration
46 |
47 | .lock-wscript
48 |
49 | # Compiled binary addons (https://nodejs.org/api/addons.html)
50 |
51 | build/Release
52 |
53 | # Dependency directories
54 |
55 | node_modules/
56 | jspm_packages/
57 |
58 | # Snowpack dependency directory (https://snowpack.dev/)
59 |
60 | web_modules/
61 |
62 | # TypeScript cache
63 |
64 | \*.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 |
68 | .npm
69 |
70 | # Optional eslint cache
71 |
72 | .eslintcache
73 |
74 | # Optional stylelint cache
75 |
76 | .stylelintcache
77 |
78 | # Microbundle cache
79 |
80 | .rpt2_cache/
81 | .rts2_cache_cjs/
82 | .rts2_cache_es/
83 | .rts2_cache_umd/
84 |
85 | # Optional REPL history
86 |
87 | .node_repl_history
88 |
89 | # Output of 'npm pack'
90 |
91 | \*.tgz
92 |
93 | # Yarn Integrity file
94 |
95 | .yarn-integrity
96 |
97 | # dotenv environment variable files
98 |
99 | .env
100 | .env.development.local
101 | .env.test.local
102 | .env.production.local
103 | .env.local
104 |
105 | # parcel-bundler cache (https://parceljs.org/)
106 |
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 |
112 | .next
113 | out
114 |
115 | # Nuxt.js build / generate output
116 |
117 | .nuxt
118 | dist
119 |
120 | # Gatsby files
121 |
122 | .cache/
123 |
124 | # Comment in the public line in if your project uses Gatsby and not Next.js
125 |
126 | # https://nextjs.org/blog/next-9-1#public-directory-support
127 |
128 | # public
129 |
130 | # vuepress build output
131 |
132 | .vuepress/dist
133 |
134 | # vuepress v2.x temp and cache directory
135 |
136 | .temp
137 | .cache
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.\*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
177 |
--------------------------------------------------------------------------------
/frames-bot/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/frames-bot/bun.lockb
--------------------------------------------------------------------------------
/frames-bot/index.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "./utils/neynarClient";
2 | import { NeynarFrameCreationRequest } from "@neynar/nodejs-sdk/build/neynar-api/v2";
3 |
4 | const server = Bun.serve({
5 | port: 3000,
6 | async fetch(req) {
7 | try {
8 | const body = await req.text();
9 | const hookData = JSON.parse(body);
10 |
11 | const creationRequest: NeynarFrameCreationRequest = {
12 | name: `gm ${hookData.data.author.username}`,
13 | pages: [
14 | {
15 | image: {
16 | url: "https://moralis.io/wp-content/uploads/web3wiki/638-gm/637aeda23eca28502f6d3eae_61QOyzDqTfxekyfVuvH7dO5qeRpU50X-Hs46PiZFReI.jpeg",
17 | aspect_ratio: "1:1",
18 | },
19 | title: "Page title",
20 | buttons: [],
21 | input: {
22 | text: {
23 | enabled: false,
24 | },
25 | },
26 | uuid: "gm",
27 | version: "vNext",
28 | },
29 | ],
30 | };
31 | const frame = await neynarClient.publishNeynarFrame(creationRequest);
32 |
33 | if (!process.env.SIGNER_UUID) {
34 | throw new Error("Make sure you set SIGNER_UUID in your .env file");
35 | }
36 |
37 | const reply = await neynarClient.publishCast(
38 | process.env.SIGNER_UUID,
39 | `gm ${hookData.data.author.username}`,
40 | {
41 | embeds: [
42 | {
43 | url: frame.link,
44 | },
45 | ],
46 | replyTo: hookData.data.hash,
47 | }
48 | );
49 |
50 | return new Response(`Replied to the cast with hash: ${reply.hash}`);
51 | } catch (e: any) {
52 | return new Response(e.message, { status: 500 });
53 | }
54 | },
55 | });
56 |
57 | console.log(`Listening on localhost:${server.port}`);
58 |
--------------------------------------------------------------------------------
/frames-bot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "farcaster-bot",
3 | "module": "index.ts",
4 | "type": "module",
5 | "scripts": {
6 | "start": "bun run index.ts"
7 | },
8 | "devDependencies": {
9 | "bun-types": "latest"
10 | },
11 | "peerDependencies": {
12 | "typescript": "^5.0.0"
13 | },
14 | "dependencies": {
15 | "@neynar/nodejs-sdk": "^1.69.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/frames-bot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext"],
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "bundler",
7 | "moduleDetection": "force",
8 | "allowImportingTsExtensions": true,
9 | "noEmit": true,
10 | "composite": true,
11 | "strict": true,
12 | "downlevelIteration": true,
13 | "skipLibCheck": true,
14 | "jsx": "react-jsx",
15 | "allowSyntheticDefaultImports": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "allowJs": true,
18 | "types": [
19 | "bun-types" // add Bun global
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/frames-bot/utils/neynarClient.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk";
2 |
3 | if (!process.env.NEYNAR_API_KEY) {
4 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file");
5 | }
6 |
7 | const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY);
8 |
9 | export default neynarClient;
10 |
--------------------------------------------------------------------------------
/funding.json:
--------------------------------------------------------------------------------
1 | {
2 | "opRetro": {
3 | "projectId": "0x6ad4cefe7290c830541e5477c95554c29d6b95f16e0942bc9d95472052da5802"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/gm-bot/.env.example:
--------------------------------------------------------------------------------
1 | PUBLISH_CAST_TIME # example 08:30 (24 hour time). If not provided will default to 09:00
2 | TIME_ZONE # example "America/New_York". If not provided will default to "UTC"
3 | NEYNAR_API_KEY="key" # Get API Key -> https://neynar.com/.
4 | FARCASTER_BOT_MNEMONIC="MNEMONIC"
5 |
--------------------------------------------------------------------------------
/gm-bot/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.local
3 | node_modules
4 | dist
5 |
--------------------------------------------------------------------------------
/gm-bot/README.md:
--------------------------------------------------------------------------------
1 | # gm_bot
2 |
3 | ## Introduction
4 |
5 | `gm_bot` is an automated messaging bot designed to cast a 'gm 🪐' message in Warpcast every day at a scheduled time. The bot operates continuously as long as the system remains online. It leverages [Neynar API](https://docs.neynar.com/) and is built using [@neynar/nodejs-sdk](https://www.npmjs.com/package/@neynar/nodejs-sdk).
6 |
7 | ## Prerequisites
8 |
9 | - [Node.js](https://nodejs.org/en/): A JavaScript runtime built on Chrome's V8 JavaScript engine. Ensure you have Node.js installed on your system.
10 |
11 | ## Installation
12 |
13 | ### Setting Up the Environment
14 |
15 | 1. **Install Project Dependencies**: Navigate to the project directory and run one of the following commands to install all required dependencies:
16 |
17 | ```bash
18 | yarn install
19 | # or
20 | npm install
21 | ```
22 |
23 | 2. **Configure Environment Variables**:
24 | - Copy the example environment file:
25 | ```bash
26 | cp .env.example .env
27 | ```
28 | - Edit `.env` to add your `NEYNAR_API_KEY` and `FARCASTER_DEVELOPER_MNEMONIC`. Optionally, you can also specify `PUBLISH_CAST_TIME` and `TIME_ZONE` for custom scheduling.
29 |
30 | ### Generating a Signer
31 |
32 | Before running the bot, you need to generate a signer and get it approved via an onchain transaction. To execute the transaction, you'll need a browser extension wallet with funded roughly $2 worth of OP ETH on the Optimism mainnet. This is crucial for the bot's operation. Run the following command:
33 |
34 | ```bash
35 | yarn get-approved-signer
36 | ```
37 |
38 | ### Approving a signer
39 |
40 | In order to get an approved signer you need to do an on-chain transaction on OP mainnet.
41 | Go to Farcaster KeyGateway optimism explorer
42 | https://optimistic.etherscan.io/address/0x00000000fc56947c7e7183f8ca4b62398caadf0b#writeContract
43 |
44 | Connect to Web3.
45 |
46 | Navigate to `addFor` function and add following values inside the respective placeholders. You will see values for fidOwner, keyType, key, metadataType, metadata, deadline, sig in your terminal logs.
47 |
48 | Press "Write" to execute the transaction. This will create a signer for your mnemonic on the OP mainnet.
49 |
50 | ## Running the Bot
51 |
52 | 1. **Watch the bot**: To run the bot in watch mode, use the following command:
53 |
54 | ```bash
55 | yarn watch
56 | # or
57 | npm run watch
58 | ```
59 |
60 | 2. **Start the Bot**: Launch the bot using the following command:
61 |
62 | ```bash
63 | yarn start
64 | # or
65 | npm run start
66 | ```
67 |
68 | 3. **Stopping the Bot**: If you need to stop the bot, kill the instance.
69 |
70 | ## License
71 |
72 | `gm_bot` is released under the MIT License. This license permits free use, modification, and distribution of the software, with the requirement that the original copyright and license notice are included in any substantial portion of the work.
73 |
74 | ## FAQs/Troubleshooting
75 |
76 | - **Q1**: What if `gm_bot` stops sending messages?
77 | - **A1**: Check the logs for any errors and ensure your system's time settings align with the specified `TIME_ZONE`, also ensure that the process is running.
78 |
--------------------------------------------------------------------------------
/gm-bot/getApprovedSigner.ts:
--------------------------------------------------------------------------------
1 | import { getApprovedSigner } from "./src/utils";
2 | getApprovedSigner();
--------------------------------------------------------------------------------
/gm-bot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gm_bot",
3 | "version": "1.0.0",
4 | "description": "A bot that will cast a 'gm 🪐' message in Warpcast at scheduled time everyday (As long as system is online)",
5 | "main": "./dist/app.js",
6 | "scripts": {
7 | "watch": "tsc --watch",
8 | "build": "rm -rf dist && tsc",
9 | "start": "npm run build && node dist/app.js",
10 | "get-approved-signer": "ts-node getApprovedSigner.ts"
11 | },
12 | "author": "Neynar",
13 | "license": "MIT",
14 | "dependencies": {
15 | "@neynar/nodejs-sdk": "^2.7.0",
16 | "dotenv": "^16.4.7",
17 | "node-cron": "^3.0.3",
18 | "typescript": "^5.7.2",
19 | "viem": "^2.21.57"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^22.10.2",
23 | "@types/node-cron": "^3.0.11",
24 | "ts-node": "^10.9.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gm-bot/src/abi/SignedKeyRequestMetadata.ts:
--------------------------------------------------------------------------------
1 | export const SignedKeyRequestMetadataABI = {
2 | inputs: [
3 | {
4 | components: [
5 | {
6 | internalType: "uint256",
7 | name: "requestFid",
8 | type: "uint256",
9 | },
10 | {
11 | internalType: "address",
12 | name: "requestSigner",
13 | type: "address",
14 | },
15 | {
16 | internalType: "bytes",
17 | name: "signature",
18 | type: "bytes",
19 | },
20 | {
21 | internalType: "uint256",
22 | name: "deadline",
23 | type: "uint256",
24 | },
25 | ],
26 | internalType: "struct SignedKeyRequestValidator.SignedKeyRequestMetadata",
27 | name: "metadata",
28 | type: "tuple",
29 | },
30 | ],
31 | name: "encodeMetadata",
32 | outputs: [
33 | {
34 | internalType: "bytes",
35 | name: "",
36 | type: "bytes",
37 | },
38 | ],
39 | stateMutability: "pure",
40 | type: "function",
41 | };
42 |
--------------------------------------------------------------------------------
/gm-bot/src/app.ts:
--------------------------------------------------------------------------------
1 | import cron from "node-cron";
2 | import { MESSAGE } from "./utils";
3 | import neynarClient from "./neynarClient";
4 | import {
5 | PUBLISH_CAST_TIME,
6 | SIGNER_UUID,
7 | TIME_ZONE,
8 | NEYNAR_API_KEY,
9 | } from "./config";
10 | import { isApiErrorResponse } from "@neynar/nodejs-sdk";
11 |
12 | // Validating necessary environment variables or configurations.
13 | if (!SIGNER_UUID) {
14 | throw new Error("SIGNER_UUID is not defined");
15 | }
16 |
17 | if (!NEYNAR_API_KEY) {
18 | throw new Error("NEYNAR_API_KEY is not defined");
19 | }
20 |
21 | /**
22 | * Function to publish a message (cast) using neynarClient.
23 | * @param msg - The message to be published.
24 | */
25 | const publishCast = async (msg: string) => {
26 | try {
27 | // Using the neynarClient to publish the cast.
28 | await neynarClient.publishCast({ signerUuid: SIGNER_UUID, text: msg });
29 | console.log("Cast published successfully");
30 | } catch (err) {
31 | // Error handling, checking if it's an API response error.
32 | if (isApiErrorResponse(err)) {
33 | console.log(err.response.data);
34 | } else console.log(err);
35 | }
36 | };
37 |
38 | // Initial call to publish a motivational message.
39 | publishCast(
40 | `gm! I'm here to brighten your day with daily cheer. Look forward to a warm 'gm' everyday!`
41 | );
42 |
43 | // Extracting hour and minute from the PUBLISH_CAST_TIME configuration.
44 | const [hour, minute] = PUBLISH_CAST_TIME.split(":");
45 |
46 | // Scheduling a cron job to publish a message at a specific time every day.
47 | cron.schedule(
48 | `${minute} ${hour} * * *`, // Cron time format
49 | function () {
50 | publishCast(MESSAGE); // Function to execute at the scheduled time.
51 | },
52 | {
53 | scheduled: true, // Ensure the job is scheduled.
54 | timezone: TIME_ZONE, // Set the timezone for the schedule.
55 | }
56 | );
57 |
58 | // Logging to inform that the cron job is scheduled.
59 | console.log(
60 | `Cron job scheduled at ${PUBLISH_CAST_TIME} ${TIME_ZONE}, please don't restart your system before the scheduled time.`
61 | );
62 |
--------------------------------------------------------------------------------
/gm-bot/src/config.ts:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | dotenv.config();
3 |
4 | export const FARCASTER_BOT_MNEMONIC = process.env.FARCASTER_BOT_MNEMONIC!;
5 | export const SIGNER_UUID = process.env.SIGNER_UUID!;
6 | export const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY!;
7 | export const PUBLISH_CAST_TIME = process.env.PUBLISH_CAST_TIME || "09:00";
8 | export const TIME_ZONE = process.env.TIME_ZONE || "UTC";
9 |
--------------------------------------------------------------------------------
/gm-bot/src/neynarClient.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient, Configuration } from "@neynar/nodejs-sdk";
2 | import { NEYNAR_API_KEY } from "./config";
3 |
4 | const config = new Configuration({
5 | apiKey: NEYNAR_API_KEY!,
6 | });
7 |
8 | const neynarClient = new NeynarAPIClient(config);
9 |
10 | export default neynarClient;
11 |
--------------------------------------------------------------------------------
/gm-bot/src/viemClient.ts:
--------------------------------------------------------------------------------
1 | import { createPublicClient, http } from "viem";
2 | import { optimism } from "viem/chains";
3 |
4 | export const viemPublicClient = createPublicClient({
5 | chain: optimism,
6 | transport: http(),
7 | });
8 |
--------------------------------------------------------------------------------
/gm-bot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "allowJs": true,
5 | "checkJs": false,
6 | "outDir": "dist",
7 | "sourceMap": true,
8 | "strict": true,
9 | "alwaysStrict": true,
10 | "target": "es5",
11 | "module": "CommonJS",
12 | "esModuleInterop": true,
13 | "moduleResolution": "node",
14 | "skipLibCheck": true
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/managed-signers/.env.example:
--------------------------------------------------------------------------------
1 | NEYNAR_API_KEY=
2 | FARCASTER_DEVELOPER_MNEMONIC=
3 |
--------------------------------------------------------------------------------
/managed-signers/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/managed-signers/README.md:
--------------------------------------------------------------------------------
1 | # Write casts with managed signers
2 |
3 | ## Prerequisites
4 |
5 | - Make sure you have Node.js and yarn installed on your machine.
6 |
7 | ## Setup
8 |
9 | 1. **Install Dependencies**:
10 | To ensure that all the required libraries and modules are installed, run:
11 |
12 | ```bash
13 | yarn install
14 | ```
15 |
16 | 2. **Environment Configuration**:
17 | We use environment variables for various configurations. Start by copying the example environment variables file:
18 |
19 | ```bash
20 | cp .env.example .env.local
21 | ```
22 |
23 | 3. **Update Environment Variables**:
24 | Open the `.env.local` file in your favorite editor and make sure to replace placeholders with the correct values.
25 |
26 | ```bash
27 | open .env.local
28 | ```
29 |
30 | 🔔 Notes:
31 |
32 | - NEYNAR_API_KEY: If you need one, sign up on [https://neynar.com](https://neynar.com)
33 | - FARCASTER_DEVELOPER_MNEMONIC: The 12 or 24 word recovery phrase for the Farcaster account. Make sure the value is within single quotes.
34 | - A farcaster developer account is the same as any farcaster account. You can use your personal account as a farcaster developer account e.g. https://warpcast.com/manan or your company / branded developer account like https://warpcast.com/neynar
35 |
36 | 4. **Start the App**:
37 | To start the Sample Farcaster app in development mode, run:
38 |
39 | ```bash
40 | yarn run dev
41 | ```
42 |
43 | Your app should now be running on `http://localhost:3000` (or a specified port in your `.env`).
44 |
45 | ## Troubleshooting
46 |
47 | - If you run into issues with missing packages, make sure to run `yarn install` again.
48 | - Ensure all environment variables in `.env.local` are correctly set.
49 |
50 | ## Feedback
51 |
52 | If you have any feedback or run into issues, please reach out to our team or create an issue on the repository.
53 |
54 | ---
55 |
56 | Happy coding! 🚀
57 |
--------------------------------------------------------------------------------
/managed-signers/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | images: {
5 | remotePatterns: [
6 | {
7 | hostname: "*",
8 | protocol: "http",
9 | },
10 | {
11 | hostname: "*",
12 | protocol: "https",
13 | },
14 | ],
15 | },
16 | };
17 |
18 | export default nextConfig;
19 |
--------------------------------------------------------------------------------
/managed-signers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "managed-signers",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@farcaster/hub-nodejs": "^0.11.24",
13 | "@neynar/nodejs-sdk": "^2.17.0",
14 | "axios": "^1.7.6",
15 | "next": "14.2.10",
16 | "qrcode.react": "^3.1.0",
17 | "react": "^18.3.1",
18 | "react-dom": "^18.3.1",
19 | "viem": "^2.19.4"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^22.2.0",
23 | "@types/react": "^18.3.3",
24 | "@types/react-dom": "^18.3.0",
25 | "typescript": "^5.5.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/managed-signers/src/app/api/cast/route.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "@/lib/neynarClient";
2 | import { NextResponse } from "next/server";
3 |
4 | export async function POST(req: Request) {
5 | const body = await req.json();
6 |
7 | try {
8 | const cast = await neynarClient.publishCast({
9 | signerUuid: body.signer_uuid,
10 | text: body.text,
11 | });
12 |
13 | return NextResponse.json(cast, { status: 200 });
14 | } catch (error) {
15 | console.error(error);
16 | return NextResponse.json({ error: "An error occurred" }, { status: 500 });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/managed-signers/src/app/api/signer/route.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "@/lib/neynarClient";
2 | import { getSignedKey } from "@/utils/getSignedKey";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function POST() {
6 | try {
7 | const signedKey = await getSignedKey(false);
8 |
9 | return NextResponse.json(signedKey, {
10 | status: 200,
11 | });
12 | } catch (error) {
13 | return NextResponse.json({ error: "An error occurred" }, { status: 500 });
14 | }
15 | }
16 |
17 | export async function GET(req: Request) {
18 | const { searchParams } = new URL(req.url);
19 | const signer_uuid = searchParams.get("signer_uuid");
20 |
21 | if (!signer_uuid) {
22 | return NextResponse.json(
23 | { error: "signer_uuid is required" },
24 | { status: 400 }
25 | );
26 | }
27 |
28 | try {
29 | const signer = await neynarClient.lookupSigner({ signerUuid: signer_uuid });
30 |
31 | return NextResponse.json(signer, { status: 200 });
32 | } catch (error) {
33 | return NextResponse.json({ error: "An error occurred" }, { status: 500 });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/managed-signers/src/app/api/user/route.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "@/lib/neynarClient";
2 | import { NextResponse } from "next/server";
3 |
4 | export async function GET(req: Request) {
5 | const { searchParams } = new URL(req.url);
6 | const fid = searchParams.get("fid");
7 |
8 | if (!fid) {
9 | return NextResponse.json({ error: "fid is required" }, { status: 400 });
10 | }
11 |
12 | try {
13 | const user = await neynarClient.fetchBulkUsers({ fids: [Number(fid)] });
14 |
15 | return NextResponse.json(user.users[0], { status: 200 });
16 | } catch (error) {
17 | return NextResponse.json({ error: "An error occurred" }, { status: 500 });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/managed-signers/src/app/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | html,
8 | body {
9 | max-width: 100vw;
10 | overflow-x: hidden;
11 | }
12 |
13 | body {
14 | color: #fff;
15 | background: #111111;
16 | }
17 |
18 | a {
19 | color: inherit;
20 | text-decoration: none;
21 | }
22 |
--------------------------------------------------------------------------------
/managed-signers/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/managed-signers/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | text-align: center;
9 | margin-top: 50px;
10 | }
11 |
12 | .btn {
13 | border: none;
14 | border-radius: 5px;
15 | transition: opacity 0.3s ease;
16 | padding: 10px 20px;
17 | font-size: 16px;
18 | }
19 |
20 | .btn:disabled {
21 | opacity: 0.5;
22 | }
23 |
24 | .btn:hover:not(:disabled) {
25 | opacity: 0.8;
26 | }
27 |
28 | .qrContainer {
29 | margin-top: 30px;
30 | display: flex;
31 | flex-direction: column;
32 | align-items: center;
33 | }
34 |
35 | .qr-label {
36 | margin-top: 20px;
37 | font-size: 16px;
38 | }
39 |
40 | .or {
41 | margin-top: 20px;
42 | font-size: 16px;
43 | }
44 |
45 | .castSection {
46 | margin-top: 30px;
47 | display: flex;
48 | flex-direction: column;
49 | align-items: center;
50 | width: 70%;
51 | }
52 |
53 | .link {
54 | color: #007bff;
55 | text-decoration: none;
56 | }
57 |
58 | .toast {
59 | position: fixed;
60 | bottom: 10px;
61 | left: 50%;
62 | transform: translateX(-50%);
63 | background-color: rgba(0, 0, 0, 0.7);
64 | color: white;
65 | padding: 10px 20px;
66 | border-radius: 5px;
67 | }
68 |
69 | .userInfo {
70 | margin-bottom: 30px;
71 | font-size: 20px;
72 | font-weight: bold;
73 | display: flex;
74 | justify-content: center;
75 | align-items: center;
76 | }
77 |
78 | .castContainer {
79 | display: flex;
80 | justify-content: center;
81 | align-items: start;
82 | gap: 10px;
83 | flex-direction: column;
84 | outline: none;
85 | width: 100%;
86 | }
87 |
88 | .castContainer:focus {
89 | outline: none;
90 | border: none;
91 | }
92 |
93 | .castTextarea {
94 | width: 100%;
95 | padding-right: 10px;
96 | }
97 |
98 | .profilePic {
99 | width: 60px;
100 | height: 60px;
101 | border-radius: 50%;
102 | margin-right: 10px;
103 | object-fit: cover;
104 | }
105 |
--------------------------------------------------------------------------------
/managed-signers/src/constants.ts:
--------------------------------------------------------------------------------
1 | // constants.ts
2 | export const LOCAL_STORAGE_KEYS = {
3 | FARCASTER_USER: "farcasterUser",
4 | };
5 |
6 | export const DEFAULT_CAST = `gm Farcaster!
7 |
8 |
9 | - Sent from my Neynar App`;
10 |
--------------------------------------------------------------------------------
/managed-signers/src/lib/neynarClient.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk";
2 |
3 | if (!process.env.NEYNAR_API_KEY) {
4 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file");
5 | }
6 |
7 | const neynarClient = new NeynarAPIClient({
8 | apiKey: process.env.NEYNAR_API_KEY
9 | });
10 |
11 | export default neynarClient;
12 |
--------------------------------------------------------------------------------
/managed-signers/src/utils/getFid.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "@/lib/neynarClient";
2 | import { mnemonicToAccount } from "viem/accounts";
3 |
4 | export const getFid = async () => {
5 | if (!process.env.FARCASTER_DEVELOPER_MNEMONIC) {
6 | throw new Error("FARCASTER_DEVELOPER_MNEMONIC is not set.");
7 | }
8 |
9 | const account = mnemonicToAccount(process.env.FARCASTER_DEVELOPER_MNEMONIC);
10 |
11 | // Lookup user details using the custody address.
12 | const { user: farcasterDeveloper } =
13 | await neynarClient.lookupUserByCustodyAddress({
14 | custodyAddress: account.address,
15 | });
16 |
17 | return Number(farcasterDeveloper.fid);
18 | };
19 |
--------------------------------------------------------------------------------
/managed-signers/src/utils/getSignedKey.ts:
--------------------------------------------------------------------------------
1 | import neynarClient from "@/lib/neynarClient";
2 | import { ViemLocalEip712Signer } from "@farcaster/hub-nodejs";
3 | import { bytesToHex, hexToBytes } from "viem";
4 | import { mnemonicToAccount } from "viem/accounts";
5 | import { getFid } from "./getFid";
6 |
7 | export const getSignedKey = async (is_sponsored: boolean) => {
8 | const createSigner = await neynarClient.createSigner();
9 | const { deadline, signature, sponsor } = await generate_signature(
10 | createSigner.public_key,
11 | is_sponsored
12 | );
13 |
14 | if (deadline === 0 || signature === "") {
15 | throw new Error("Failed to generate signature");
16 | }
17 |
18 | const fid = await getFid();
19 |
20 | const signedKey = await neynarClient.registerSignedKey({
21 | signerUuid: createSigner.signer_uuid,
22 | appFid: fid,
23 | deadline,
24 | signature,
25 | sponsor,
26 | });
27 |
28 | return signedKey;
29 | };
30 |
31 | const generate_signature = async function (
32 | public_key: string,
33 | is_sponsored = false
34 | ) {
35 | if (typeof process.env.FARCASTER_DEVELOPER_MNEMONIC === "undefined") {
36 | throw new Error("FARCASTER_DEVELOPER_MNEMONIC is not defined");
37 | }
38 |
39 | const FARCASTER_DEVELOPER_MNEMONIC = process.env.FARCASTER_DEVELOPER_MNEMONIC;
40 | const FID = await getFid();
41 |
42 | const account = mnemonicToAccount(FARCASTER_DEVELOPER_MNEMONIC);
43 | const appAccountKey = new ViemLocalEip712Signer(account as any);
44 |
45 | // Generates an expiration date for the signature (24 hours from now).
46 | const deadline = Math.floor(Date.now() / 1000) + 86400;
47 |
48 | const uintAddress = hexToBytes(public_key as `0x${string}`);
49 |
50 | const signature = await appAccountKey.signKeyRequest({
51 | requestFid: BigInt(FID),
52 | key: uintAddress,
53 | deadline: BigInt(deadline),
54 | });
55 |
56 | if (signature.isErr()) {
57 | return {
58 | deadline,
59 | signature: "",
60 | };
61 | }
62 |
63 | const sigHex = bytesToHex(signature.value);
64 |
65 | let sponsor;
66 |
67 | if (is_sponsored) {
68 | const sponsorSignature = await account.signMessage({
69 | message: { raw: sigHex },
70 | });
71 |
72 | sponsor = {
73 | signature: sponsorSignature,
74 | fid: FID,
75 | };
76 | }
77 |
78 | return { deadline, signature: sigHex, sponsor };
79 | };
80 |
--------------------------------------------------------------------------------
/managed-signers/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=
2 |
3 | KAFKA_BROKERS=
4 |
5 | # Contact us to get the following credentials
6 | KAFKA_USERNAME=
7 | KAFKA_PASSWORD=
8 | KAFKA_CONSUMER_GROUP=
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | .env*
4 | dump.rdb
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/README.md:
--------------------------------------------------------------------------------
1 | # Neynar Webhook Kafka Consumer Example
2 |
3 | This repository provides an example of a Kafka consumer that interacts with Neynar's Kafka stream. With Neynar's Kafka stream, developers can ingest hydrated events from a hosted Kafka stream (as compared to dehydrated events from gRPC hub)
4 |
5 | With Kafka, you can subscribe to the same data that we use for sending webhook notifications
6 |
7 | ## Documentation
8 |
9 | For detailed information on how the Kafka stream works, please refer to the [Neynar Kafka Stream Documentation](https://docs.neynar.com/docs/from-kafka-stream).
10 |
11 | ## Project Setup
12 |
13 | ### Prerequisites
14 |
15 | Make sure you have the following installed:
16 |
17 | - [Node.js](https://nodejs.org/) (v14 or later)
18 | - [Redis](https://redis.io/downloads/) (Redis is used only for this example, you can you any other storage solution)
19 |
20 | ### Step 1: Clone the Repository
21 |
22 | Clone the repository to your local machine:
23 |
24 | ```sh
25 | git clone https://github.com/neynarxyz/farcaster-examples.git && cd farcaster-examples/neynar-webhook-kafka-consumer
26 | ```
27 |
28 | ### Step 2: Configure Environment Variables
29 |
30 | Copy the contents of the `.env.example` file into a new `.env` file:
31 |
32 | ```sh
33 | cp .env.example .env
34 | ```
35 |
36 | Then, open the `.env` file and update the placeholders with your actual configuration:
37 |
38 | ```env
39 | NODE_ENV=production
40 |
41 | KAFKA_BROKERS= // Please checkout docs
42 |
43 | # Contact Neynar to get the following credentials
44 |
45 | KAFKA_USERNAME=
46 | KAFKA_PASSWORD=
47 | KAFKA_CONSUMER_GROUP=
48 | ```
49 |
50 | ### Step 3: Install Dependencies
51 |
52 | For yarn
53 |
54 | ```bash
55 | yarn install
56 | ```
57 |
58 | For npm
59 |
60 | ```bash
61 | npm install
62 | ```
63 |
64 | ### Step 4: Start the Kafka Consumer
65 |
66 | For yarn
67 |
68 | ```bash
69 | yarn start
70 | ```
71 |
72 | For npm
73 |
74 | ```bash
75 | npm run start
76 | ```
77 |
78 | ## License
79 |
80 | `kafka_consumer_example` is released under the MIT License. This license permits free use, modification, and distribution of the software, with the requirement that the original copyright and license notice are included in any substantial portion of the work.
81 |
82 | ## Author
83 |
84 | Developed by [Neynar](https://neynar.com/).
85 |
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Neynar",
3 | "license": "MIT",
4 | "main": "./dist/index.js",
5 | "scripts": {
6 | "build": "rm -rf dist && tsc",
7 | "start": "npm run build && node ./dist/index.js"
8 | },
9 | "dependencies": {
10 | "dotenv": "^16.4.5",
11 | "kafkajs": "^2.2.4",
12 | "redis": "^4.7.0",
13 | "uuid": "^10.0.0"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^22.5.0",
17 | "@types/uuid": "^10.0.0",
18 | "typescript": "^5.5.4"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/src/types.ts:
--------------------------------------------------------------------------------
1 | interface Bio {
2 | text: string;
3 | }
4 |
5 | interface Profile {
6 | bio: Bio;
7 | }
8 |
9 | interface VerifiedAddresses {
10 | eth_addresses: string[];
11 | sol_addresses: string[];
12 | }
13 |
14 | interface UserHydrated {
15 | object: "user";
16 | fid: number;
17 | custody_address: string;
18 | username: string;
19 | display_name: string | null;
20 | pfp_url: string | null;
21 | profile: Profile;
22 | follower_count: number;
23 | following_count: number;
24 | verifications: string[];
25 | verified_addresses: VerifiedAddresses;
26 | active_status: "inactive" | "active";
27 | power_badge: boolean;
28 | event_timestamp: string; // ISO 8601 format
29 | }
30 |
31 | interface UserDehydrated {
32 | object: "user_dehydrated";
33 | fid: number;
34 | username: string;
35 | }
36 |
37 | interface CustomHeaders {
38 | "x-convoy-message-type": "broadcast";
39 | }
40 |
41 | interface EmbedUrlMetadata {
42 | content_type?: string | null;
43 | content_length?: number | null;
44 | }
45 |
46 | interface EmbedUrl {
47 | url: string;
48 | metadata?: EmbedUrlMetadata;
49 | }
50 |
51 | interface CastId {
52 | fid: number;
53 | hash: string;
54 | }
55 |
56 | interface EmbedCastId {
57 | cast_id: CastId;
58 | }
59 |
60 | type EmbeddedCast = EmbedUrl | EmbedCastId;
61 |
62 | interface CastDehydrated {
63 | object: "cast_dehydrated";
64 | hash: string;
65 | author: UserDehydrated;
66 | }
67 |
68 | interface CastCreatedEventData {
69 | object: "cast";
70 | hash: string;
71 | parent_hash?: string | null;
72 | parent_url?: string | null;
73 | root_parent_url?: string | null;
74 | parent_author?: {
75 | fid?: number | null;
76 | };
77 | author: UserHydrated;
78 | mentioned_profiles?: UserHydrated[];
79 | text: string;
80 | timestamp: string; // ISO 8601 format
81 | embeds: EmbeddedCast[];
82 | }
83 |
84 | interface FollowCreatedEventData {
85 | object: "follow";
86 | event_timestamp: string; // ISO 8601 format
87 | timestamp: string; // ISO 8601 format with timezone
88 | user: UserDehydrated;
89 | target_user: UserDehydrated;
90 | }
91 |
92 | interface FollowDeletedEventData {
93 | object: "unfollow";
94 | event_timestamp: string; // ISO 8601 format
95 | timestamp: string; // ISO 8601 format with timezone
96 | user: UserDehydrated;
97 | target_user: UserDehydrated;
98 | }
99 |
100 | interface ReactionCreatedEventData {
101 | object: "reaction";
102 | event_timestamp: string; // ISO 8601 format
103 | timestamp: string; // ISO 8601 format with timezone
104 | reaction_type: number;
105 | user: UserDehydrated;
106 | cast: CastDehydrated;
107 | }
108 |
109 | interface ReactionDeletedEventData {
110 | object: "reaction";
111 | event_timestamp: string; // ISO 8601 format
112 | timestamp: string; // ISO 8601 format with timezone
113 | reaction_type: number;
114 | user: UserDehydrated;
115 | cast: CastDehydrated;
116 | }
117 |
118 | interface UserCreatedEvent {
119 | event_type: "user.created";
120 | data: UserHydrated;
121 | custom_headers: CustomHeaders;
122 | idempotency_key?: string;
123 | }
124 |
125 | interface UserUpdatedEvent {
126 | event_type: "user.updated";
127 | data: UserHydrated;
128 | custom_headers: CustomHeaders;
129 | idempotency_key?: string;
130 | }
131 |
132 | interface CastCreatedEvent {
133 | event_type: "cast.created";
134 | data: CastCreatedEventData;
135 | custom_headers: CustomHeaders;
136 | idempotency_key?: string;
137 | }
138 |
139 | interface FollowCreatedEvent {
140 | event_type: "follow.created";
141 | data: FollowCreatedEventData;
142 | custom_headers: CustomHeaders;
143 | idempotency_key?: string;
144 | }
145 |
146 | interface FollowDeletedEvent {
147 | event_type: "follow.deleted";
148 | data: FollowDeletedEventData;
149 | custom_headers: CustomHeaders;
150 | idempotency_key?: string;
151 | }
152 |
153 | interface ReactionCreatedEvent {
154 | event_type: "reaction.created";
155 | data: ReactionCreatedEventData;
156 | custom_headers: CustomHeaders;
157 | idempotency_key?: string;
158 | }
159 | interface ReactionDeletedEvent {
160 | event_type: "reaction.deleted";
161 | data: ReactionDeletedEventData;
162 | custom_headers: CustomHeaders;
163 | idempotency_key?: string;
164 | }
165 |
166 | export type FarcasterEvent =
167 | | UserCreatedEvent
168 | | UserUpdatedEvent
169 | | CastCreatedEvent
170 | | FollowCreatedEvent
171 | | FollowDeletedEvent
172 | | ReactionCreatedEvent
173 | | ReactionDeletedEvent;
174 |
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "allowJs": true,
5 | "checkJs": false,
6 | "outDir": "dist",
7 | "sourceMap": true,
8 | "strict": true,
9 | "alwaysStrict": true,
10 | "target": "es2017",
11 | "module": "CommonJS",
12 | "esModuleInterop": true,
13 | "moduleResolution": "node"
14 | },
15 | "include": ["src/**/*"],
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------
/neynar-webhook-kafka-consumer/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@redis/bloom@1.2.0":
6 | version "1.2.0"
7 | resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
8 | integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
9 |
10 | "@redis/client@1.6.0":
11 | version "1.6.0"
12 | resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.6.0.tgz#dcf4ae1319763db6fdddd6de7f0af68a352c30ea"
13 | integrity sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==
14 | dependencies:
15 | cluster-key-slot "1.1.2"
16 | generic-pool "3.9.0"
17 | yallist "4.0.0"
18 |
19 | "@redis/graph@1.1.1":
20 | version "1.1.1"
21 | resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea"
22 | integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==
23 |
24 | "@redis/json@1.0.7":
25 | version "1.0.7"
26 | resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.7.tgz#016257fcd933c4cbcb9c49cde8a0961375c6893b"
27 | integrity sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==
28 |
29 | "@redis/search@1.2.0":
30 | version "1.2.0"
31 | resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.2.0.tgz#50976fd3f31168f585666f7922dde111c74567b8"
32 | integrity sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==
33 |
34 | "@redis/time-series@1.1.0":
35 | version "1.1.0"
36 | resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.1.0.tgz#cba454c05ec201bd5547aaf55286d44682ac8eb5"
37 | integrity sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==
38 |
39 | "@types/node@^22.5.0":
40 | version "22.5.0"
41 | resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958"
42 | integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==
43 | dependencies:
44 | undici-types "~6.19.2"
45 |
46 | "@types/uuid@^10.0.0":
47 | version "10.0.0"
48 | resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
49 | integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
50 |
51 | cluster-key-slot@1.1.2:
52 | version "1.1.2"
53 | resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
54 | integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
55 |
56 | dotenv@^16.4.5:
57 | version "16.4.5"
58 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
59 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
60 |
61 | generic-pool@3.9.0:
62 | version "3.9.0"
63 | resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
64 | integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
65 |
66 | kafkajs@^2.2.4:
67 | version "2.2.4"
68 | resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.4.tgz#59e6e16459d87fdf8b64be73970ed5aa42370a5b"
69 | integrity sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==
70 |
71 | redis@^4.7.0:
72 | version "4.7.0"
73 | resolved "https://registry.yarnpkg.com/redis/-/redis-4.7.0.tgz#b401787514d25dd0cfc22406d767937ba3be55d6"
74 | integrity sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==
75 | dependencies:
76 | "@redis/bloom" "1.2.0"
77 | "@redis/client" "1.6.0"
78 | "@redis/graph" "1.1.1"
79 | "@redis/json" "1.0.7"
80 | "@redis/search" "1.2.0"
81 | "@redis/time-series" "1.1.0"
82 |
83 | typescript@^5.5.4:
84 | version "5.5.4"
85 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
86 | integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
87 |
88 | undici-types@~6.19.2:
89 | version "6.19.8"
90 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
91 | integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
92 |
93 | uuid@^10.0.0:
94 | version "10.0.0"
95 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
96 | integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
97 |
98 | yallist@4.0.0:
99 | version "4.0.0"
100 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
101 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
102 |
--------------------------------------------------------------------------------
/wownar-react-native/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 | .env
34 |
35 | # typescript
36 | *.tsbuildinfo
--------------------------------------------------------------------------------
/wownar-react-native/README.md:
--------------------------------------------------------------------------------
1 | # wownar-react-native
2 |
3 | ## Introduction
4 |
5 | `wownar-react-native` is an expo app that demonstrates the integration of [SIWN](https://docs.neynar.com/docs/how-to-let-users-connect-farcaster-accounts-with-write-access-for-free-using-sign-in-with-neynar-siwn).
6 |
7 | ## Prerequisites
8 |
9 | - [Node.js](https://nodejs.org/en/): A JavaScript runtime built on Chrome's V8 JavaScript engine. Ensure you have Node.js installed on your system.
10 | - [Expo Go](https://expo.dev/client): Install Expo Go on your phone
11 |
12 | ## Installation and Setup Environment
13 |
14 | ### Server
15 |
16 | 1. **Navigate to server directory**: Navigate to the server directory
17 |
18 | ```bash
19 | cd server
20 | ```
21 |
22 | 2. **Install Project Dependencies**: Based on the package manager run one of the following commands to install all required dependencies:
23 |
24 | For yarn
25 |
26 | ```bash
27 | yarn install
28 | ```
29 |
30 | For npm
31 |
32 | ```bash
33 | npm install
34 | ```
35 |
36 | 3. **Configure Environment Variables**:
37 |
38 | - Copy the example environment file:
39 | ```bash
40 | cp .env.example .env
41 | ```
42 | - Edit `.env` to add your `NEYNAR_API_KEY` and `NEYNAR_CLIENT_ID`.
43 |
44 | 4. **Start the server**:
45 | For yarn
46 |
47 | ```bash
48 | yarn start
49 | ```
50 |
51 | For npm
52 |
53 | ```bash
54 | npm run start
55 | ```
56 |
57 | ### Client
58 |
59 | Open new terminal
60 |
61 | 1. **Navigate to server directory**: Navigate to the client directory
62 |
63 | ```bash
64 | cd client
65 | ```
66 |
67 | 2. **Install Project Dependencies**: Based on the package manager run one of the following commands to install all required dependencies:
68 |
69 | For yarn
70 |
71 | ```bash
72 | yarn install
73 | ```
74 |
75 | For npm
76 |
77 | ```bash
78 | npm install
79 | ```
80 |
81 | 3. **Configure Environment Variables**:
82 |
83 | - Copy the example environment file:
84 | ```bash
85 | cp .env.example .env
86 | ```
87 | - Edit `.env` to add your `COMPUTER_IP_ADDRESS`. Refer [find-IP-address article](https://www.avg.com/en/signal/find-ip-address) to get IP address of your Computer
88 |
89 | 4. **Start the app**: (Make sure your phone and computer is connected to the same network)
90 |
91 | For yarn
92 |
93 | ```bash
94 | yarn start
95 | ```
96 |
97 | For npm
98 |
99 | ```bash
100 | npm run start
101 | ```
102 |
103 | you'll see a QR Code
104 |
105 | 5. **Run App**:
106 |
107 | Open Expo Go app on your phone and scan the QR Code
108 |
109 | ## License
110 |
111 | `wownar-react-native` is released under the MIT License. This license permits free use, modification, and distribution of the software, with the requirement that the original copyright and license notice are included in any substantial portion of the work.
112 |
--------------------------------------------------------------------------------
/wownar-react-native/client/.env.example:
--------------------------------------------------------------------------------
1 | COMPUTER_IP_ADDRESS=192.168.x.x # => Refer https://www.avg.com/en/signal/find-ip-address to find your computer's IP address
--------------------------------------------------------------------------------
/wownar-react-native/client/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import {
3 | CommonActions,
4 | NavigationContainer,
5 | NavigationContainerRef,
6 | } from "@react-navigation/native";
7 | import { AppProvider, useApp } from "./src/Context/AppContext";
8 | import AppNavigator, { RootStackParamList } from "./src/AppNavigator";
9 |
10 | const AuthNavigation: React.FC = () => {
11 | const { isAuthenticated } = useApp();
12 | const navigationRef =
13 | useRef>(null);
14 |
15 | useEffect(() => {
16 | if (isAuthenticated === null) {
17 | navigationRef.current?.navigate("Loading");
18 | return;
19 | }
20 |
21 | if (isAuthenticated) {
22 | navigationRef.current?.dispatch(
23 | CommonActions.reset({
24 | index: 0,
25 | routes: [{ name: "Home" }],
26 | })
27 | );
28 | } else {
29 | navigationRef.current?.navigate("Signin");
30 | }
31 | }, [isAuthenticated]);
32 |
33 | return (
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const App: React.FC = () => {
41 | return (
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default App;
49 |
--------------------------------------------------------------------------------
/wownar-react-native/client/app.config.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 |
3 | export default ({ config }: any) => {
4 | return {
5 | ...config,
6 | extra: {
7 | COMPUTER_IP_ADDRESS: process.env.COMPUTER_IP_ADDRESS,
8 | },
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/wownar-react-native/client/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "wownar-native",
4 | "slug": "wownar-native",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true
19 | },
20 | "android": {
21 | "adaptiveIcon": {
22 | "foregroundImage": "./assets/adaptive-icon.png",
23 | "backgroundColor": "#ffffff"
24 | }
25 | },
26 | "web": {
27 | "favicon": "./assets/favicon.png"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-native/client/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-native/client/assets/favicon.png
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-native/client/assets/icon.png
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-native/client/assets/splash.png
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/wownar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-native/client/assets/wownar.png
--------------------------------------------------------------------------------
/wownar-react-native/client/assets/wownar.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
59 |
--------------------------------------------------------------------------------
/wownar-react-native/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/wownar-react-native/client/constants.ts:
--------------------------------------------------------------------------------
1 | import Constants from "expo-constants";
2 |
3 | export const COMPUTER_IP_ADDRESS = Constants.expoConfig!.extra!.COMPUTER_IP_ADDRESS;
4 |
5 | export const API_URL = `http://${COMPUTER_IP_ADDRESS}:5500`;
6 |
7 |
--------------------------------------------------------------------------------
/wownar-react-native/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wownar-native",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@neynar/react-native-signin": "^1.3.0",
13 | "@react-navigation/native": "^6.1.9",
14 | "@react-navigation/native-stack": "^6.9.17",
15 | "@types/ip": "^1.1.3",
16 | "dotenv": "^16.3.1",
17 | "expo": "~49.0.15",
18 | "expo-constants": "~14.4.2",
19 | "expo-secure-store": "~12.3.1",
20 | "expo-status-bar": "~1.6.0",
21 | "react": "18.2.0",
22 | "react-native": "0.72.6",
23 | "react-native-keyboard-aware-scroll-view": "^0.9.5",
24 | "react-native-paper": "^5.12.1",
25 | "react-native-safe-area-context": "4.6.3",
26 | "react-native-screens": "^3.29.0",
27 | "react-native-svg": "^15.2.0",
28 | "react-native-webview": "^13.6.4"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.20.0",
32 | "@types/react": "~18.2.14",
33 | "typescript": "^5.1.3"
34 | },
35 | "private": true
36 | }
37 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/AppNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createNativeStackNavigator } from "@react-navigation/native-stack";
3 | import Signin from "./components/Screens/Signin";
4 | import Home from "./components/Screens/Home";
5 | import Loading from "./components/Screens/Loading";
6 |
7 | export type RootStackParamList = {
8 | Home: undefined;
9 | Signin: undefined;
10 | Loading: undefined;
11 | };
12 |
13 | const Stack = createNativeStackNavigator();
14 |
15 | const AppNavigator: React.FC = () => {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default AppNavigator;
26 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/Context/AppContext.tsx:
--------------------------------------------------------------------------------
1 | import { ISuccessMessage } from "@neynar/react-native-signin";
2 | import {
3 | useContext,
4 | createContext,
5 | useMemo,
6 | useState,
7 | FC,
8 | ReactNode,
9 | useEffect,
10 | } from "react";
11 | import { API_URL } from "../../constants";
12 | import { retrieveUser, storeUser } from "../utils";
13 |
14 | type SetState = React.Dispatch>;
15 |
16 | interface Props {
17 | children: ReactNode;
18 | }
19 |
20 | interface AppContextInterface {
21 | displayName: string | null;
22 | setDisplayName: SetState;
23 | pfp: string | null;
24 | setPfp: SetState;
25 | signerUuid: string | null;
26 | setSignerUuid: SetState;
27 | fid: string | null;
28 | setFid: SetState;
29 | isAuthenticated: boolean | null;
30 | setIsAuthenticated: SetState;
31 | handleSignin: (data: ISuccessMessage) => void;
32 | }
33 |
34 | const AppContext = createContext(null);
35 |
36 | export const AppProvider: FC = ({ children }) => {
37 | const [displayName, setDisplayName] = useState(null);
38 | const [pfp, setPfp] = useState(null);
39 | const [signerUuid, setSignerUuid] = useState(null);
40 | const [fid, setFid] = useState(null);
41 | const [isAuthenticated, setIsAuthenticated] = useState(null);
42 |
43 | const retrieveUserFromStorage = async () => {
44 | const user = await retrieveUser();
45 | if (!user) {
46 | setIsAuthenticated(false);
47 | return;
48 | }
49 | await fetchUserAndSetUser(parseInt(user.fid));
50 | setSignerUuid(user.signer_uuid);
51 | setFid(user.fid);
52 | setIsAuthenticated(user.is_authenticated);
53 | };
54 |
55 | useEffect(() => {
56 | retrieveUserFromStorage();
57 | }, []);
58 |
59 | const fetchUserAndSetUser = async (fid: number) => {
60 | try {
61 | const response = await fetch(`${API_URL}/user?fid=${fid}`);
62 | if (!response.ok) {
63 | throw new Error("Failed to fetch user");
64 | }
65 | const { display_name, pfp_url } = await response.json();
66 | setDisplayName(display_name);
67 | setPfp(pfp_url);
68 | } catch (err) {
69 | console.log(err);
70 | }
71 | };
72 |
73 | const handleSignin = async (data: ISuccessMessage) => {
74 | setIsAuthenticated(null);
75 | storeUser(data);
76 | await fetchUserAndSetUser(parseInt(data.fid));
77 | setIsAuthenticated(data.is_authenticated);
78 | setFid(data.fid);
79 | setSignerUuid(data.signer_uuid);
80 | };
81 |
82 | const value: AppContextInterface | null = useMemo(
83 | () => ({
84 | displayName,
85 | setDisplayName,
86 | pfp,
87 | setPfp,
88 | signerUuid,
89 | setSignerUuid,
90 | fid,
91 | setFid,
92 | isAuthenticated,
93 | setIsAuthenticated,
94 | handleSignin,
95 | }),
96 | [displayName, pfp, signerUuid, fid, isAuthenticated]
97 | );
98 |
99 | return {children};
100 | };
101 |
102 | export const useApp = (): AppContextInterface => {
103 | const context = useContext(AppContext);
104 | if (!context) {
105 | throw new Error("AppContext must be used within AppProvider");
106 | }
107 | return context;
108 | };
109 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | StyleSheet,
4 | View,
5 | Text,
6 | SafeAreaView,
7 | ScrollView,
8 | Linking,
9 | StatusBar,
10 | Image,
11 | } from "react-native";
12 | import SignOutButton from "./SignoutButton";
13 | import { useApp } from "../Context/AppContext";
14 | import { removeUser } from "../utils";
15 |
16 | interface LayoutProps {
17 | children: React.ReactNode;
18 | }
19 |
20 | const Layout: React.FC = ({ children }) => {
21 | const handleSignIn = () => {};
22 | const { isAuthenticated, setIsAuthenticated } = useApp();
23 |
24 | const handleOpenGithub = () => {
25 | const githubRepoUrl = "https://www.google.com";
26 | Linking.openURL(githubRepoUrl);
27 | };
28 |
29 | const handleSignOut = () => {
30 | setIsAuthenticated(false);
31 | removeUser();
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
43 | Wownar
44 |
45 | {isAuthenticated && }
46 |
47 |
48 | {children}
49 |
50 |
51 |
52 | Connect Farcaster accounts for free using{" "}
53 |
54 | Sign in with Neynar
55 |
56 |
57 |
58 | Github Repo -> Wownar
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | const styles = StyleSheet.create({
66 | logo: {
67 | width: 60,
68 | height: 48,
69 | marginRight: 10,
70 | },
71 | container: {
72 | flex: 1,
73 | // flexDirection: "column",
74 | backgroundColor: "#000",
75 | },
76 | header: {
77 | flexDirection: "row",
78 | justifyContent: "space-between",
79 | padding: 10,
80 | },
81 | headerLeft: {
82 | flexDirection: "row",
83 | alignItems: "center",
84 | },
85 | headerText: {
86 | fontSize: 20,
87 | fontWeight: "200",
88 | color: "#fff",
89 | },
90 | contentContainer: {
91 | flexGrow: 1,
92 | justifyContent: "center",
93 | alignItems: "center",
94 | },
95 | content: {
96 | justifyContent: "center",
97 | alignItems: "center",
98 | },
99 | footer: {
100 | padding: 16,
101 | alignItems: "center",
102 | rowGap: 20,
103 | },
104 | connectText: {
105 | fontSize: 16,
106 | color: "#fff",
107 | },
108 | githubText: {
109 | fontSize: 16,
110 | color: "#fff",
111 | },
112 | boldText: {
113 | fontWeight: "700",
114 | },
115 | });
116 |
117 | export default Layout;
118 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/components/Screens/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | StyleSheet,
4 | View,
5 | Text,
6 | TextInput,
7 | TouchableOpacity,
8 | Image,
9 | Keyboard,
10 | } from "react-native";
11 | import Layout from "../Layout";
12 | import { useApp } from "../../Context/AppContext";
13 | import { API_URL } from "../../../constants";
14 | import { Snackbar } from "react-native-paper";
15 |
16 | const Home: React.FC = () => {
17 | const [inputValue, setInputValue] = useState("");
18 | const { displayName, pfp, signerUuid } = useApp();
19 | const [snackbarVisible, setSnackbarVisible] = useState(false);
20 | const [snackbarMessage, setSnackbarMessage] = useState("");
21 |
22 | const handleCastPress = async () => {
23 | Keyboard.dismiss();
24 | if (inputValue === "") return;
25 | try {
26 |
27 | const response = await fetch(`${API_URL}/cast`, {
28 | method: "POST",
29 | headers: {
30 | Accept: "application/json",
31 | "Content-Type": "application/json",
32 | },
33 | body: JSON.stringify({
34 | signerUuid,
35 | text: inputValue,
36 | }),
37 | });
38 |
39 | if (!response.ok) {
40 | throw new Error("Failed to cast");
41 | }
42 | const { hash } = await response.json();
43 | setSnackbarMessage(`Cast successful! Hash: ${hash}`);
44 | setSnackbarVisible(true);
45 | setInputValue("");
46 | } catch (error) {
47 | console.error("Error:", error);
48 | }
49 | };
50 |
51 | return displayName && pfp ? (
52 |
53 |
54 |
55 | Hello {displayName}... 👋
56 |
57 |
58 |
64 |
74 |
75 |
76 | Cast
77 |
78 | setSnackbarVisible(false)}
81 | style={{ backgroundColor: "#08bd0eff" }}
82 | >
83 | {snackbarMessage}
84 |
85 |
86 |
87 | ) : (
88 |
89 | Loading...
90 |
91 | );
92 | };
93 |
94 | const styles = StyleSheet.create({
95 | container: {
96 | flex: 1,
97 | backgroundColor: "black",
98 | alignItems: "center",
99 | justifyContent: "center",
100 | padding: 20,
101 | },
102 | greeting: {
103 | fontSize: 30,
104 | color: "white",
105 | marginBottom: 16,
106 | flexDirection: "row",
107 | alignItems: "center",
108 | },
109 | inputContainer: {
110 | flexDirection: "row",
111 | alignItems: "flex-start",
112 | marginBottom: 16,
113 | borderWidth: 1,
114 | borderColor: "white",
115 | padding: 10,
116 | borderRadius: 5,
117 | },
118 | avatar: {
119 | width: 50,
120 | height: 50,
121 | borderRadius: 25,
122 | marginRight: 10,
123 | },
124 | input: {
125 | flex: 1,
126 | color: "white",
127 | paddingTop: 10,
128 | },
129 | castButton: {
130 | backgroundColor: "#000",
131 | paddingVertical: 10,
132 | paddingHorizontal: 20,
133 | borderRadius: 5,
134 | borderColor: "white",
135 | borderWidth: 1,
136 | },
137 | castButtonText: {
138 | color: "white",
139 | fontSize: 16,
140 | },
141 | username: {
142 | fontWeight: "500",
143 | fontSize: 30,
144 | },
145 | loadingContainer: {
146 | flex: 1,
147 | backgroundColor: "black",
148 | alignItems: "center",
149 | justifyContent: "center",
150 | padding: 20,
151 | },
152 | });
153 |
154 | export default Home;
155 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/components/Screens/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View, Text } from "react-native";
2 |
3 | const Loading: React.FC = () => {
4 | return (
5 |
6 | Loading...
7 |
8 | );
9 | };
10 |
11 | const styles = StyleSheet.create({
12 | text: {
13 | fontSize: 20,
14 | color: "white",
15 | marginBottom: 16,
16 | flexDirection: "row",
17 | alignItems: "center",
18 | },
19 | loadingContainer: {
20 | flex: 1,
21 | backgroundColor: "black",
22 | alignItems: "center",
23 | justifyContent: "center",
24 | padding: 20,
25 | },
26 | });
27 |
28 | export default Loading;
29 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/components/Screens/Signin.tsx:
--------------------------------------------------------------------------------
1 | import { Text, StyleSheet } from "react-native";
2 | import { NeynarSigninButton } from "@neynar/react-native-signin";
3 | import { useApp } from "../../Context/AppContext";
4 | import Layout from "../Layout";
5 | import { API_URL, COMPUTER_IP_ADDRESS } from "../../../constants";
6 |
7 | const Signin = () => {
8 | const { handleSignin } = useApp();
9 |
10 | // This function should be an API call to your server where NEYNAR_API_KEY and NEYNAR_CLIENT_ID are stored securely
11 | const fetchAuthorizationUrl = async () => {
12 | const res = await fetch(`${API_URL}/get-auth-url`);
13 | if (!res.ok) {
14 | throw new Error("Failed to fetch auth url");
15 | }
16 | const { authorization_url } = (await res.json()) as {
17 | authorization_url: string;
18 | };
19 | return authorization_url;
20 | };
21 |
22 | const handleError = (error: Error) => {
23 | console.error(error);
24 | };
25 |
26 | return (
27 |
28 | Wowow Farcaster
29 |
59 |
60 | );
61 | };
62 |
63 | export default Signin;
64 |
65 | const styles = StyleSheet.create({
66 | title: {
67 | fontSize: 36,
68 | color: "#fff",
69 | marginBottom: 24,
70 | fontWeight: "100",
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/components/SignoutButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { StyleSheet, Text, TouchableOpacity } from "react-native";
3 | import { AntDesign } from "@expo/vector-icons";
4 |
5 | type SignOutButtonProps = {
6 | onPress: () => void;
7 | };
8 |
9 | const SignOutButton: React.FC = ({ onPress }) => {
10 | return (
11 |
12 | Sign Out
13 |
14 |
15 | );
16 | };
17 |
18 | const styles = StyleSheet.create({
19 | button: {
20 | flexDirection: "row",
21 | justifyContent: "center",
22 | alignItems: "center",
23 | paddingVertical: 10,
24 | paddingHorizontal: 20,
25 | borderWidth: 1,
26 | borderColor: "white",
27 | borderRadius: 5,
28 | backgroundColor: "transparent",
29 | },
30 | text: {
31 | color: "white",
32 | marginRight: 10,
33 | fontSize: 16,
34 | },
35 | });
36 |
37 | export default SignOutButton;
38 |
--------------------------------------------------------------------------------
/wownar-react-native/client/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { ISuccessMessage } from "@neynar/react-native-signin";
2 | import * as SecureStore from "expo-secure-store";
3 |
4 | const KEY = "userInfo";
5 |
6 | export const storeUser = async (data: ISuccessMessage) => {
7 | try {
8 | await SecureStore.setItemAsync(KEY, JSON.stringify(data));
9 | } catch (error) {
10 | console.log("Error storing credentials", error);
11 | }
12 | };
13 |
14 | export const retrieveUser = async () => {
15 | try {
16 | const user = await SecureStore.getItemAsync(KEY);
17 | if (!user) return null;
18 | return JSON.parse(user) as ISuccessMessage;
19 | } catch (error) {
20 | console.error(`Error retrieving ${KEY}`, error);
21 | return null;
22 | }
23 | };
24 |
25 | export const removeUser = async () => {
26 | try {
27 | await SecureStore.deleteItemAsync(KEY);
28 | } catch (error) {
29 | console.error(`Error deleting ${KEY}`, error);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/wownar-react-native/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/wownar-react-native/server/.env.example:
--------------------------------------------------------------------------------
1 | NEYNAR_API_KEY="key" # Get API Key -> https://neynar.com/.
2 | NEYNAR_CLIENT_ID="" # Get Client ID -> Neynar Developer Portal -> https://dev.neynar.com
--------------------------------------------------------------------------------
/wownar-react-native/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | NeynarAPIClient,
4 | AuthorizationUrlResponseType,
5 | } = require("@neynar/nodejs-sdk");
6 | var { json } = require("body-parser");
7 | require("dotenv").config({ path: ".env" });
8 |
9 | const app = express();
10 |
11 | app.use(json());
12 |
13 | const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY;
14 | const NEYNAR_CLIENT_ID = process.env.NEYNAR_CLIENT_ID;
15 |
16 | const client = new NeynarAPIClient(NEYNAR_API_KEY);
17 |
18 | app.get("/get-auth-url", async (_, res) => {
19 | try {
20 | const { authorization_url } = await client.fetchAuthorizationUrl(
21 | NEYNAR_CLIENT_ID,
22 | AuthorizationUrlResponseType.Code
23 | );
24 | res.json({ authorization_url });
25 | } catch (error) {
26 | if (error.isAxiosError) {
27 | console.error("Error:", error);
28 | res.status(error.response.status).json({ error });
29 | } else {
30 | console.error("Error:", error);
31 | res.status(500).json({ error: "Server error" });
32 | }
33 | }
34 | });
35 |
36 | app.get("/user", async (req, res) => {
37 | const { fid } = req.query;
38 |
39 | try {
40 | const { users } = await client.fetchBulkUsers([fid]);
41 | const user = users[0];
42 | const { display_name, pfp_url } = user;
43 | res.json({ display_name, pfp_url });
44 | } catch (error) {
45 | if (error.isAxiosError) {
46 | console.error("Error:", error);
47 | res.status(error.response.status).json({ error });
48 | } else {
49 | console.error("Error:", error);
50 | res.status(500).json({ error: "Server error" });
51 | }
52 | }
53 | });
54 |
55 | app.post("/cast", async (req, res) => {
56 | const { signerUuid, text } = req.body;
57 |
58 | try {
59 | const { hash } = await client.publishCast(signerUuid, text);
60 | res.json({ hash });
61 | } catch (error) {
62 | if (error.isAxiosError) {
63 | console.error("Error:", error);
64 | res.status(error.response.status).json({ error });
65 | } else {
66 | console.error("Error:", error);
67 | res.status(500).json({ error: "Server error" });
68 | }
69 | }
70 | });
71 |
72 | const PORT = 5500;
73 | app.listen(PORT, () => {
74 | console.log(`Server listening on port ${PORT}`);
75 | });
76 |
--------------------------------------------------------------------------------
/wownar-react-native/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon -w index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@neynar/nodejs-sdk": "^1.69.1",
14 | "body-parser": "^1.20.2",
15 | "dotenv": "^16.4.5",
16 | "express": "^4.19.2"
17 | },
18 | "devDependencies": {
19 | "nodemon": "^3.1.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/wownar-react-sdk/.env.example:
--------------------------------------------------------------------------------
1 | NEYNAR_API_KEY="key" # Get API Key -> https://neynar.com/.
2 | NEXT_PUBLIC_NEYNAR_CLIENT_ID="" # Get Client ID -> Neynar Developer Portal -> https://dev.neynar.com
--------------------------------------------------------------------------------
/wownar-react-sdk/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/wownar-react-sdk/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/wownar-react-sdk/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "**",
8 | },
9 | ],
10 | },
11 | };
12 |
13 | module.exports = nextConfig;
14 |
--------------------------------------------------------------------------------
/wownar-react-sdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wownar_react-sdk-implementation",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 4500",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@neynar/nodejs-sdk": "^1.69.1",
13 | "@neynar/react": "^0.9.1",
14 | "@pigment-css/react": "^0.0.9",
15 | "next": "14.2.10",
16 | "react": "^18",
17 | "react-dom": "^18",
18 | "react-toastify": "^9.1.3",
19 | "sass": "^1.69.5",
20 | "viem": "^2.19.4"
21 | },
22 | "devDependencies": {
23 | "@types/node": "^20",
24 | "@types/react": "^18",
25 | "@types/react-dom": "^18",
26 | "autoprefixer": "^10.0.1",
27 | "eslint": "^8",
28 | "eslint-config-next": "14.0.3",
29 | "postcss": "^8",
30 | "tailwindcss": "^3.3.0",
31 | "typescript": "^5"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/wownar-react-sdk/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/wownar-react-sdk/public/logos/wownar-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
59 |
--------------------------------------------------------------------------------
/wownar-react-sdk/public/logos/wownar.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
59 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/Context/AppContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | useContext,
5 | createContext,
6 | useMemo,
7 | useState,
8 | FC,
9 | ReactNode,
10 | } from "react";
11 |
12 | type SetState = React.Dispatch>;
13 |
14 | export enum ScreenState {
15 | Signin = "signin",
16 | Home = "home",
17 | }
18 |
19 | interface Props {
20 | children: ReactNode;
21 | }
22 |
23 | interface AppContextInterface {
24 | screen: ScreenState;
25 | setScreen: SetState;
26 | }
27 |
28 | const AppContext = createContext(null);
29 |
30 | export const AppProvider: FC = ({ children }) => {
31 | const [screen, setScreen] = useState(ScreenState.Signin);
32 |
33 | const value: AppContextInterface | null = useMemo(
34 | () => ({
35 | screen,
36 | setScreen,
37 | }),
38 | [screen]
39 | );
40 |
41 | return {children};
42 | };
43 |
44 | export const useApp = (): AppContextInterface => {
45 | const context = useContext(AppContext);
46 | if (!context) {
47 | throw new Error("AppContext must be used within AppProvider");
48 | }
49 | return context;
50 | };
51 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/Context/NeynarProviderWrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, ReactNode } from "react";
4 | import { ScreenState, useApp } from "./AppContext";
5 | import { NeynarContextProvider, Theme } from "@neynar/react";
6 |
7 | interface Props {
8 | children: ReactNode;
9 | }
10 |
11 | const NeynarProviderWrapper: FC = ({ children }) => {
12 | const { setScreen } = useApp();
13 |
14 | return (
15 | {
21 | setScreen(ScreenState.Home);
22 | },
23 | onSignout() {
24 | setScreen(ScreenState.Signin);
25 | },
26 | },
27 | }}
28 | >
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export default NeynarProviderWrapper;
35 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/Screens/Home/index.module.scss:
--------------------------------------------------------------------------------
1 | .inputContainer {
2 | width: 100%;
3 | max-width: 500px; // Adjust as needed
4 | margin: 0 auto;
5 | position: relative;
6 | text-align: center;
7 | padding: 0px 20px;
8 | }
9 |
10 | .userInput {
11 | width: 100%;
12 | padding: 20px 15px 15px 65px;
13 | margin-bottom: 16px;
14 | background-color: transparent;
15 | border: 1px solid white;
16 | border-radius: 5px;
17 | color: white;
18 | outline: none;
19 |
20 | &::placeholder {
21 | color: white;
22 | opacity: 0.7; // Adjust as needed
23 | }
24 | }
25 |
26 | .profilePic {
27 | position: relative;
28 | top: 50px;
29 | left: 15px;
30 | }
31 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/Screens/Signin/index.tsx:
--------------------------------------------------------------------------------
1 | import ScreenLayout from "../layout";
2 | import { NeynarAuthButton, SIWN_variant } from "@neynar/react";
3 |
4 | const Signin = () => {
5 | return (
6 |
7 |
8 |
9 |
Wowow Farcaster
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default Signin;
18 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/Screens/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ScreenState, useApp } from "@/Context/AppContext";
2 | import { NeynarAuthButton, useNeynarContext } from "@neynar/react";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { ReactNode, useEffect } from "react";
6 |
7 | interface Props {
8 | children: ReactNode;
9 | }
10 |
11 | const ScreenLayout = ({ children }: Props) => {
12 | const { setScreen } = useApp();
13 | const { isAuthenticated } = useNeynarContext();
14 |
15 | useEffect(() => {
16 | if (isAuthenticated) {
17 | setScreen(ScreenState.Home);
18 | } else {
19 | setScreen(ScreenState.Signin);
20 | }
21 | }, [isAuthenticated, setScreen]);
22 |
23 | return (
24 |
25 |
26 |
27 |
33 |
Wownar
34 |
35 | {isAuthenticated && }
36 |
37 | {children}
38 |
53 |
54 | );
55 | };
56 |
57 | export default ScreenLayout;
58 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/api/cast/reaction/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import neynarClient from "@/clients/neynar";
3 | import { ReactionType, isApiErrorResponse } from "@neynar/nodejs-sdk";
4 | import { Cast } from "@neynar/nodejs-sdk/build/neynar-api/v2";
5 |
6 | export async function POST(request: NextRequest) {
7 | const { signerUuid, reaction, castOrCastHash } = (await request.json()) as {
8 | signerUuid: string;
9 | reaction: ReactionType
10 | castOrCastHash: string | Cast;
11 | };
12 |
13 | try {
14 | const { success, message } = await neynarClient.publishReactionToCast(signerUuid, reaction, castOrCastHash);
15 | return NextResponse.json(
16 | { message: `Cast ${reaction} with hash ${castOrCastHash} published successfully` },
17 | { status: 200 }
18 | );
19 | } catch (err) {
20 | console.log("/api/cast/reaction", err);
21 | if (isApiErrorResponse(err)) {
22 | return NextResponse.json(
23 | { ...err.response.data },
24 | { status: err.response.status }
25 | );
26 | } else
27 | return NextResponse.json(
28 | { message: "Something went wrong" },
29 | { status: 500 }
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/api/cast/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import neynarClient from "@/clients/neynar";
3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk";
4 |
5 | export async function GET(request: NextRequest) {
6 | const fid = (await await request.json()) as { fid: number };
7 | return NextResponse.json({ fid }, { status: 200 });
8 | }
9 |
10 | export async function POST(request: NextRequest) {
11 | const { signerUuid, text } = (await request.json()) as {
12 | signerUuid: string;
13 | text: string;
14 | };
15 |
16 | try {
17 | const { hash } = await neynarClient.publishCast(signerUuid, text);
18 | return NextResponse.json(
19 | { message: `Cast with hash ${hash} published successfully` },
20 | { status: 200 }
21 | );
22 | } catch (err) {
23 | console.log("/api/cast", err);
24 | if (isApiErrorResponse(err)) {
25 | return NextResponse.json(
26 | { ...err.response.data },
27 | { status: err.response.status }
28 | );
29 | } else
30 | return NextResponse.json(
31 | { message: "Something went wrong" },
32 | { status: 500 }
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/api/frame/action/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import neynarClient from "@/clients/neynar";
3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk";
4 |
5 | export async function POST(request: NextRequest) {
6 | const { signer_uuid, castHash, action} = (await request.json()) as {
7 | signer_uuid: string;
8 | castHash: string;
9 | action: any;
10 | };
11 |
12 | try {
13 | const response = await neynarClient.postFrameAction(signer_uuid, castHash, action);
14 |
15 | if (response) {
16 | return NextResponse.json(response, { status: 200 });
17 | } else {
18 | return NextResponse.json(response, { status: 500 });
19 | }
20 | } catch (err) {
21 | console.log("/api/frame/action", err);
22 | if (isApiErrorResponse(err)) {
23 | return NextResponse.json(
24 | { ...err.response.data },
25 | { status: err.response.status }
26 | );
27 | } else {
28 | return NextResponse.json(
29 | { message: "Something went wrong" },
30 | { status: 500 }
31 | );
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar-react-sdk/src/app/favicon.ico
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/globals.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-weight: 200;
7 | background-color: #ebebeb;
8 | color: #000;
9 | }
10 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Inter } from "next/font/google";
4 | import "./globals.scss";
5 | import { AppProvider } from "@/Context/AppContext";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import { ToastContainer } from "react-toastify";
8 | import "@neynar/react/dist/style.css";
9 | import NeynarProviderWrapper from "@/Context/NeynarProviderWrapper";
10 | import { base } from "viem/chains";
11 |
12 | const inter = Inter({ subsets: ["latin"] });
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) {
19 | return (
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ScreenState, useApp } from "@/Context/AppContext";
4 | import Signin from "./Screens/Signin";
5 | import Home from "./Screens/Home";
6 |
7 | export default function Index() {
8 | const { screen } = useApp();
9 |
10 | if (screen === ScreenState.Signin) {
11 | return ;
12 | }
13 |
14 | if (screen === ScreenState.Home) {
15 | return ;
16 | }
17 |
18 | return <>>;
19 | }
20 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/clients/neynar.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk";
2 |
3 | const client = new NeynarAPIClient(process.env.NEYNAR_API_KEY!);
4 |
5 | export default client;
6 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/components/Button/index.module.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | column-gap: 10px;
6 | padding: 12px 20px;
7 | background-color: transparent;
8 | border: 1px solid white;
9 | border-radius: 5px;
10 | color: white;
11 | cursor: pointer;
12 | transition: background-color 0.3s ease, color 0.3s ease;
13 |
14 | &:hover {
15 | background-color: white;
16 | color: black;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/wownar-react-sdk/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | interface Props extends React.ButtonHTMLAttributes {
4 | title: string;
5 | rightIcon?: React.ReactNode;
6 | }
7 |
8 | const Button = (props: Props) => {
9 | const { title, rightIcon } = props;
10 | return (
11 |
15 | );
16 | };
17 |
18 | export default Button;
19 |
--------------------------------------------------------------------------------
/wownar-react-sdk/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/wownar-react-sdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/wownar/.env.example:
--------------------------------------------------------------------------------
1 | NEYNAR_API_KEY="key" # Get API Key -> https://neynar.com/.
2 | NEXT_PUBLIC_NEYNAR_CLIENT_ID="" # Get Client ID -> Neynar Developer Portal -> https://dev.neynar.com
3 | NEXT_PUBLIC_URL="" # Your app's URL e.g. http://localhost:3000
--------------------------------------------------------------------------------
/wownar/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/wownar/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/wownar/README.md:
--------------------------------------------------------------------------------
1 | # wownar-react-sdk
2 |
3 | ## Introduction
4 |
5 | `wownar` is a nextjs app that demonstrates the integration of [SIWN](https://docs.neynar.com/docs/how-to-let-users-connect-farcaster-accounts-with-write-access-for-free-using-sign-in-with-neynar-siwn).
6 |
7 | ## Prerequisites
8 |
9 | - [Node.js](https://nodejs.org/en/): A JavaScript runtime built on Chrome's V8 JavaScript engine. Ensure you have Node.js installed on your system.
10 |
11 | ## Installation and Setup Environment
12 |
13 | 1. **Install Project Dependencies**: Based on the package manager run one of the following commands to install all required dependencies:
14 |
15 | For yarn
16 |
17 | ```bash
18 | yarn install
19 | ```
20 |
21 | 2. **Configure Environment Variables**
22 |
23 | - Copy the example environment file:
24 |
25 | ```bash
26 | cp .env.example .env.local
27 | ```
28 |
29 | - Edit `.env` to add your `NEYNAR_API_KEY` and `NEXT_PUBLIC_NEYNAR_CLIENT_ID`.
30 |
31 | ## Run Application
32 |
33 | - For yarn
34 |
35 | ```bash
36 | yarn dev
37 | ```
38 |
39 | ## License
40 |
41 | `wownar-react-sdk` is released under the MIT License. This license permits free use, modification, and distribution of the software, with the requirement that the original copyright and license notice are included in any substantial portion of the work.
--------------------------------------------------------------------------------
/wownar/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "**",
8 | },
9 | ],
10 | },
11 | };
12 |
13 | module.exports = nextConfig;
14 |
--------------------------------------------------------------------------------
/wownar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Wownar",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 4500",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@farcaster/frame-sdk": "^0.0.31",
13 | "@neynar/nodejs-sdk": "^2.0.3",
14 | "mipd": "^0.0.7",
15 | "next": "14.2.10",
16 | "react": "^18",
17 | "react-dom": "^18",
18 | "react-toastify": "^9.1.3",
19 | "sass": "^1.69.5"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^20",
23 | "@types/react": "^18",
24 | "@types/react-dom": "^18",
25 | "autoprefixer": "^10.0.1",
26 | "eslint": "^8",
27 | "eslint-config-next": "14.0.3",
28 | "postcss": "^8",
29 | "tailwindcss": "^3.3.0",
30 | "typescript": "^5"
31 | },
32 | "packageManager": "yarn@4.5.3+sha512.3003a14012e2987072d244c720506549c1aab73ee728208f1b2580a9fd67b92d61ba6b08fe93f6dce68fd771e3af1e59a0afa28dd242dd0940d73b95fedd4e90"
33 | }
34 |
--------------------------------------------------------------------------------
/wownar/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/wownar/public/logos/copy_clipboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/wownar/public/logos/powered-by-neynar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar/public/logos/powered-by-neynar.png
--------------------------------------------------------------------------------
/wownar/public/logos/wownar-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
59 |
--------------------------------------------------------------------------------
/wownar/public/logos/wownar-logo.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/wownar/public/logos/wownar.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
59 |
--------------------------------------------------------------------------------
/wownar/src/Context/AppContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | useContext,
4 | createContext,
5 | useMemo,
6 | useState,
7 | FC,
8 | ReactNode,
9 | useEffect,
10 | useCallback,
11 | } from "react";
12 | import axios, { AxiosError } from "axios";
13 | import useLocalStorage from "@/hooks/use-local-storage-state";
14 | import { UserInfo } from "@/types";
15 | import { toast } from "react-toastify";
16 | import { ErrorRes } from "@neynar/nodejs-sdk/build/api/models";
17 | import { User } from "@neynar/nodejs-sdk/build/api/models";
18 |
19 | type SetState = React.Dispatch>;
20 |
21 | export enum ScreenState {
22 | Signin = "signin",
23 | Home = "home",
24 | }
25 |
26 | interface Props {
27 | children: ReactNode;
28 | }
29 |
30 | interface AppContextInterface {
31 | screen: ScreenState;
32 | setScreen: SetState;
33 | displayName: string | null;
34 | setDisplayName: SetState;
35 | pfp: string | null;
36 | setPfp: SetState;
37 | signerUuid: string | null;
38 | setSignerUuid: SetState;
39 | fid: string | null;
40 | setFid: SetState;
41 | }
42 |
43 | const AppContext = createContext(null);
44 |
45 | export const AppProvider: FC = ({ children }) => {
46 | const [screen, setScreen] = useState(ScreenState.Signin);
47 | const [displayName, setDisplayName] = useState(null);
48 | const [pfp, setPfp] = useState(null);
49 | const [signerUuid, setSignerUuid] = useState(null);
50 | const [fid, setFid] = useState(null);
51 | const [user, setUser, removeUser] = useLocalStorage(
52 | "user",
53 | null
54 | );
55 |
56 | const lookupUser = useCallback(async () => {
57 | if (user && user.fid) {
58 | try {
59 | const { data } = await axios.get<{ user: User }>(
60 | `/api/user/${user.fid}`
61 | );
62 | setDisplayName(data.user.display_name ?? "");
63 | setPfp(data.user.pfp_url ?? "");
64 | } catch (err) {
65 | const axiosError = err as AxiosError;
66 | toast(axiosError.response?.data.message || "An error occurred", {
67 | type: "error",
68 | theme: "dark",
69 | autoClose: 3000,
70 | position: "bottom-right",
71 | pauseOnHover: true,
72 | });
73 | }
74 | }
75 | }, [user]);
76 |
77 | useEffect(() => {
78 | // Read from URL query params if we need to support old flow
79 | // if (searchParams.get("signer_uuid") && searchParams.get("fid")) {
80 | // setSignerUuid(searchParams.get("signer_uuid"));
81 | // setFid(searchParams.get("fid"));
82 | // }
83 |
84 | lookupUser();
85 | }, [lookupUser]);
86 |
87 | const isUserLoggedIn = useCallback(async () => {
88 | if (user) {
89 | setScreen(ScreenState.Home);
90 | } else {
91 | if (signerUuid && fid) {
92 | setUser({ signerUuid, fid });
93 | setScreen(ScreenState.Home);
94 | } else {
95 | setScreen(ScreenState.Signin);
96 | }
97 | }
98 | }, [user, signerUuid, fid, setUser, removeUser]);
99 |
100 | useEffect(() => {
101 | isUserLoggedIn();
102 | }, [isUserLoggedIn]);
103 |
104 | const value: AppContextInterface | null = useMemo(
105 | () => ({
106 | screen,
107 | setScreen,
108 | displayName,
109 | setDisplayName,
110 | pfp,
111 | setPfp,
112 | signerUuid,
113 | setSignerUuid,
114 | fid,
115 | setFid,
116 | }),
117 | [screen, displayName, pfp, signerUuid, fid]
118 | );
119 |
120 | return {children};
121 | };
122 |
123 | export const useApp = (): AppContextInterface => {
124 | const context = useContext(AppContext);
125 | if (!context) {
126 | throw new Error("AppContext must be used within AppProvider");
127 | }
128 | return context;
129 | };
130 |
--------------------------------------------------------------------------------
/wownar/src/app/.well-known/farcaster.json/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | const appUrl = process.env.NEXT_PUBLIC_URL;
4 |
5 | const config = {
6 | "accountAssociation": {
7 | "header": "eyJmaWQiOjU0OTEzOSwidHlwZSI6ImN1c3RvZHkiLCJrZXkiOiIweDIxRDgyMkQ4QkUwNERGRTYwN0FmMTY3MzE5OTZBYzBGZmI5M0QxY2EifQ",
8 | "payload": "eyJkb21haW4iOiJkZW1vLm5leW5hci5jb20ifQ",
9 | "signature": "MHg3MzIwNmEwOGI5ZWMyMjUzODY5ZWUyNjNlZDE0NjA0N2I4Njc1YTVhMTAxNzZmYWI5OTE5OTU2MjBmYmQ0NWIyN2JjYTZmZjU5ZTk0MGY1MWY3M2Y2ZmUwYjlmZDU2YjRjMmE2Y2VjOTBiYWJlN2U2MTg2YjQ5NjNkYTMyZWNjYjFj"
10 | },
11 | "frame": {
12 | "name": "Wownar",
13 | "version": "1",
14 | "iconUrl": `${appUrl}/logos/neynar.svg`,
15 | "homeUrl": `${appUrl}`,
16 | "imageUrl": `${appUrl}/logos/powered-by-neynar.png`,
17 | "buttonTitle": "Launch Wownar",
18 | "splashImageUrl": `${appUrl}/logos/powered-by-neynar.png`,
19 | "splashBackgroundColor": "#000000",
20 | "webhookUrl": "https://api.neynar.com/f/app/a1092b41-629f-45e0-b196-b3ff3a8f193f/event"
21 | }
22 | };
23 |
24 | export async function GET() {
25 | try {
26 | return NextResponse.json(config);
27 | } catch (error) {
28 | console.error('Error generating metadata:', error);
29 | return NextResponse.json({ error: error.message }, { status: 500 });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/wownar/src/app/Screens/Home/index.module.scss:
--------------------------------------------------------------------------------
1 | .inputContainer {
2 | width: 100%;
3 | max-width: 500px; // Adjust as needed
4 | margin: 0 auto;
5 | position: relative;
6 | text-align: center;
7 | padding: 0px 20px;
8 | }
9 |
10 | .userInput {
11 | width: 100%;
12 | padding: 20px 15px 15px 65px;
13 | margin-bottom: 16px;
14 | background-color: transparent;
15 | border: 1px solid white;
16 | border-radius: 5px;
17 | color: white;
18 | outline: none;
19 |
20 | &::placeholder {
21 | color: white;
22 | opacity: 0.7; // Adjust as needed
23 | }
24 | }
25 |
26 | .profilePic {
27 | position: relative;
28 | top: 50px;
29 | left: 15px;
30 | }
31 |
--------------------------------------------------------------------------------
/wownar/src/app/Screens/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ScreenState, useApp } from "@/Context/AppContext";
4 | import Button from "@/components/Button";
5 | import Signout from "@/components/icons/Signout";
6 | import useLocalStorage from "@/hooks/use-local-storage-state";
7 | import { UserInfo } from "@/types";
8 | import Image from "next/image";
9 | import Link from "next/link";
10 | import { ReactNode } from "react";
11 |
12 | interface Props {
13 | children: ReactNode;
14 | }
15 |
16 | const ScreenLayout = ({ children }: Props) => {
17 | const { screen } = useApp();
18 | const [_, _1, removeItem] = useLocalStorage("user");
19 |
20 | const handleSignout = () => {
21 | removeItem();
22 | window.location.reload();
23 | };
24 |
25 | return (
26 |
27 |
28 |
29 |
35 |
Wownar
36 |
37 | {screen !== ScreenState.Signin && (
38 |
39 | }
43 | />
44 |
45 | )}
46 |
47 | {children}
48 |
63 |
64 | );
65 | };
66 |
67 | export default ScreenLayout;
68 |
--------------------------------------------------------------------------------
/wownar/src/app/api/cast/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import neynarClient from "@/clients/neynar";
3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk";
4 |
5 | export async function GET(request: NextRequest) {
6 | const fid = (await await request.json()) as { fid: number };
7 | return NextResponse.json({ fid }, { status: 200 });
8 | }
9 |
10 | export async function POST(request: NextRequest) {
11 | const { signerUuid, text } = (await request.json()) as {
12 | signerUuid: string;
13 | text: string;
14 | };
15 |
16 | try {
17 | const { cast } = await neynarClient.publishCast({ signerUuid, text });
18 | return NextResponse.json(
19 | { message: `Cast with hash ${cast.hash} published successfully` },
20 | { status: 200 }
21 | );
22 | } catch (err) {
23 | console.log("/api/cast", err);
24 | if (isApiErrorResponse(err)) {
25 | return NextResponse.json(
26 | { ...err.response.data },
27 | { status: err.response.status }
28 | );
29 | } else
30 | return NextResponse.json(
31 | { message: "Something went wrong" },
32 | { status: 500 }
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/wownar/src/app/api/user/[fid]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import neynarClient from "@/clients/neynar";
3 | import { isApiErrorResponse } from "@neynar/nodejs-sdk";
4 |
5 | export async function GET(
6 | request: NextRequest,
7 | { params }: { params: { fid: string } }
8 | ) {
9 | try {
10 | const fid = parseInt(params.fid);
11 | const {
12 | users
13 | } = await neynarClient.fetchBulkUsers({ fids: [fid] });
14 | return NextResponse.json({ user: users[0] }, { status: 200 });
15 | } catch (err) {
16 | console.log("/api/user/[fid]", err);
17 | if (isApiErrorResponse(err)) {
18 | return NextResponse.json(
19 | { ...err.response.data },
20 | { status: err.response.status }
21 | );
22 | } else
23 | return NextResponse.json(
24 | { message: "Something went wrong" },
25 | { status: 500 }
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/wownar/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neynarxyz/farcaster-examples/870360d7c2aa7deb533cc585e578421415418740/wownar/src/app/favicon.ico
--------------------------------------------------------------------------------
/wownar/src/app/globals.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-weight: 200;
7 | background-color: #111111;
8 | color: #fff;
9 | }
10 |
--------------------------------------------------------------------------------
/wownar/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.scss";
4 | import { AppProvider } from "@/Context/AppContext";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import { ToastContainer } from "react-toastify";
7 | import { FrameProvider } from "@/app/providers";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | const appUrl = process.env.NEXT_PUBLIC_URL;
12 |
13 | // frame preview metadata
14 | const appName = 'Wownar';
15 | const splashImageUrl = `${appUrl}/logos/powered-by-neynar.png`;
16 | const iconUrl = `${appUrl}/logos/neynar.svg`;
17 |
18 | const framePreviewMetadata = {
19 | version: "next",
20 | imageUrl: splashImageUrl,
21 | button: {
22 | title: 'Launch Wownar',
23 | action: {
24 | type: "launch_frame",
25 | name: appName,
26 | url: appUrl,
27 | splashImageUrl,
28 | iconUrl,
29 | splashBackgroundColor: "#000000",
30 | },
31 | },
32 | };
33 |
34 | export async function generateMetadata(): Promise {
35 | return {
36 | title: appName,
37 | openGraph: {
38 | title: appName,
39 | description: "A demo app (powered by Neynar) that will help user to cast",
40 | },
41 | other: {
42 | "fc:frame": JSON.stringify(framePreviewMetadata),
43 | },
44 | };
45 | }
46 |
47 | export default function RootLayout({
48 | children,
49 | }: {
50 | children: React.ReactNode;
51 | }) {
52 | return (
53 |
54 |
55 | {/* Start of Neynar Frame */}
56 | {appName}
57 |
58 |
62 |
63 |
64 |
65 | {/* End of Neynar Frame */}
66 |
67 |
68 |
69 |
70 | {children}
71 |
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/wownar/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ScreenState, useApp } from "@/Context/AppContext";
4 | import Signin from "./Screens/Signin";
5 | import Home from "./Screens/Home";
6 |
7 | export default function Index() {
8 | const { screen } = useApp();
9 |
10 | if (screen === ScreenState.Signin) {
11 | return ;
12 | }
13 |
14 | if (screen === ScreenState.Home) {
15 | return ;
16 | }
17 |
18 | return <>>;
19 | }
20 |
--------------------------------------------------------------------------------
/wownar/src/app/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState, useCallback } from "react";
4 | import sdk, { type Context, type FrameNotificationDetails, AddFrame } from "@farcaster/frame-sdk";
5 | import { createStore } from "mipd";
6 | import React from "react";
7 |
8 | interface FrameContextType {
9 | isSDKLoaded: boolean;
10 | context: Context.FrameContext | undefined;
11 | }
12 |
13 | const FrameContext = React.createContext(undefined);
14 |
15 | export function useFrame() {
16 | const [isSDKLoaded, setIsSDKLoaded] = useState(false);
17 | const [context, setContext] = useState();
18 | const [added, setAdded] = useState(false);
19 | const [notificationDetails, setNotificationDetails] = useState(null);
20 | const [lastEvent, setLastEvent] = useState("");
21 | const [addFrameResult, setAddFrameResult] = useState("");
22 |
23 | const addFrame = useCallback(async () => {
24 | try {
25 | setNotificationDetails(null);
26 |
27 | const result = await sdk.actions.addFrame();
28 |
29 | if (result.notificationDetails) {
30 | setNotificationDetails(result.notificationDetails);
31 | }
32 | setAddFrameResult(
33 | result.notificationDetails
34 | ? `Added, got notificaton token ${result.notificationDetails.token} and url ${result.notificationDetails.url}`
35 | : "Added, got no notification details"
36 | );
37 | } catch (error) {
38 | if (error instanceof AddFrame.RejectedByUser) {
39 | setAddFrameResult(`Not added: ${error.message}`);
40 | }
41 |
42 | if (error instanceof AddFrame.InvalidDomainManifest) {
43 | setAddFrameResult(`Not added: ${error.message}`);
44 | }
45 |
46 | setAddFrameResult(`Error: ${error}`);
47 | }
48 | }, []);
49 |
50 | useEffect(() => {
51 | const load = async () => {
52 | const context = await sdk.context;
53 | setContext(context);
54 | setIsSDKLoaded(true);
55 |
56 | // Set up event listeners
57 | sdk.on("frameAdded", ({ notificationDetails }) => {
58 | console.log("Frame added", notificationDetails);
59 | setAdded(true);
60 | setNotificationDetails(notificationDetails ?? null);
61 | setLastEvent("Frame added");
62 | });
63 |
64 | sdk.on("frameAddRejected", ({ reason }) => {
65 | console.log("Frame add rejected", reason);
66 | setAdded(false);
67 | setLastEvent(`Frame add rejected: ${reason}`);
68 | });
69 |
70 | sdk.on("frameRemoved", () => {
71 | console.log("Frame removed");
72 | setAdded(false);
73 | setLastEvent("Frame removed");
74 | });
75 |
76 | sdk.on("notificationsEnabled", ({ notificationDetails }) => {
77 | console.log("Notifications enabled", notificationDetails);
78 | setNotificationDetails(notificationDetails ?? null);
79 | setLastEvent("Notifications enabled");
80 | });
81 |
82 | sdk.on("notificationsDisabled", () => {
83 | console.log("Notifications disabled");
84 | setNotificationDetails(null);
85 | setLastEvent("Notifications disabled");
86 | });
87 |
88 | sdk.on("primaryButtonClicked", () => {
89 | console.log("Primary button clicked");
90 | setLastEvent("Primary button clicked");
91 | });
92 |
93 | // Call ready action
94 | console.log("Calling ready");
95 | sdk.actions.ready({});
96 |
97 | // Set up MIPD Store
98 | const store = createStore();
99 | store.subscribe((providerDetails) => {
100 | console.log("PROVIDER DETAILS", providerDetails);
101 | });
102 | };
103 |
104 | if (sdk && !isSDKLoaded) {
105 | console.log("Calling load");
106 | setIsSDKLoaded(true);
107 | load();
108 | return () => {
109 | sdk.removeAllListeners();
110 | };
111 | }
112 | }, [isSDKLoaded]);
113 |
114 | return { isSDKLoaded, context, added, notificationDetails, lastEvent, addFrame, addFrameResult };
115 | }
116 |
117 | export function FrameProvider({ children }: { children: React.ReactNode }) {
118 | const { isSDKLoaded, context } = useFrame();
119 |
120 | if (!isSDKLoaded) {
121 | return Loading...
;
122 | }
123 |
124 | return (
125 |
126 | {children}
127 |
128 | );
129 | }
--------------------------------------------------------------------------------
/wownar/src/clients/neynar.ts:
--------------------------------------------------------------------------------
1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk";
2 |
3 | const client = new NeynarAPIClient({
4 | apiKey: process.env.NEYNAR_API_KEY!
5 | });
6 |
7 | export default client;
8 |
--------------------------------------------------------------------------------
/wownar/src/components/Button/index.module.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | column-gap: 10px;
6 | padding: 12px 20px;
7 | background-color: transparent;
8 | border: 1px solid white;
9 | border-radius: 5px;
10 | color: white;
11 | cursor: pointer;
12 | transition: background-color 0.3s ease, color 0.3s ease;
13 |
14 | &:hover {
15 | background-color: white;
16 | color: black;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/wownar/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | interface Props extends React.ButtonHTMLAttributes {
4 | title: string;
5 | rightIcon?: React.ReactNode;
6 | }
7 |
8 | const Button = (props: Props) => {
9 | const { title, rightIcon } = props;
10 | return (
11 |
15 | );
16 | };
17 |
18 | export default Button;
19 |
--------------------------------------------------------------------------------
/wownar/src/components/icons/Signout/index.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | height?: string;
3 | width?: string;
4 | }
5 |
6 | const Signout = ({ height = "512px", width = "512px" }: Props) => {
7 | return (
8 |
19 | );
20 | };
21 |
22 | export default Signout;
23 |
--------------------------------------------------------------------------------
/wownar/src/hooks/use-local-storage-state.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react";
2 |
3 | type DeserializeFunction = (value: string) => T;
4 | type SerializeFunction = (value: T) => string;
5 |
6 | interface UseLocalStorageStateOptions {
7 | serialize?: SerializeFunction;
8 | deserialize?: DeserializeFunction;
9 | }
10 |
11 | function useLocalStorage(
12 | key: string,
13 | defaultValue: T | (() => T) = "" as T,
14 | {
15 | serialize = JSON.stringify,
16 | deserialize = JSON.parse,
17 | }: UseLocalStorageStateOptions = {}
18 | ): [T, React.Dispatch>, () => void] {
19 | const [state, setState] = useState(() => {
20 | if (typeof window !== "undefined") {
21 | try {
22 | const valueInLocalStorage = window.localStorage.getItem(key);
23 | return valueInLocalStorage
24 | ? deserialize(valueInLocalStorage)
25 | : defaultValue instanceof Function
26 | ? defaultValue()
27 | : defaultValue;
28 | } catch (error) {
29 | console.error("Error reading from localStorage:", error);
30 | return defaultValue instanceof Function ? defaultValue() : defaultValue;
31 | }
32 | }
33 | return defaultValue instanceof Function ? defaultValue() : defaultValue;
34 | });
35 |
36 | const prevKeyRef = useRef(key);
37 |
38 | useEffect(() => {
39 | const prevKey = prevKeyRef.current;
40 | if (prevKey !== key && typeof window !== "undefined") {
41 | window.localStorage.removeItem(prevKey);
42 | }
43 | prevKeyRef.current = key;
44 | try {
45 | window.localStorage.setItem(key, serialize(state));
46 | } catch (error) {
47 | console.error("Error writing to localStorage:", error);
48 | }
49 | }, [key, state, serialize]);
50 |
51 | const removeItem = () => {
52 | window.localStorage.removeItem(key);
53 | };
54 |
55 | return [state, setState, removeItem];
56 | }
57 |
58 | export default useLocalStorage;
59 |
--------------------------------------------------------------------------------
/wownar/src/types.d.ts:
--------------------------------------------------------------------------------
1 | export interface UserInfo {
2 | signerUuid: string;
3 | fid: string;
4 | }
5 |
--------------------------------------------------------------------------------
/wownar/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export const welcomeMessages = [
2 | "Wowow Farcaster",
3 | // "Join the conversation. Sign in to share your story on Warpcast.",
4 | // "Ready to make your mark? Sign in to start casting on Warpcast.",
5 | // "Sign in to cast your thoughts and connect with the Warpcast community.",
6 | // "Be part of the decentralized dialogue. Sign in to cast your first post now.",
7 | // "Let's get your ideas out there. Sign in to start casting your unique perspective.",
8 | // "Elevate your voice. Sign in and amplify your message.",
9 | // "Connect, engage, and influence. Sign in to begin your Warpcast journey.",
10 | // "Make waves with your words. Sign in and cast away!",
11 | // "Sign in and join a new era of social networking.",
12 | ];
13 |
14 | export const getMessage = (messagesList: string[]) => {
15 | return messagesList[Math.floor(Math.random() * messagesList.length)];
16 | };
--------------------------------------------------------------------------------
/wownar/src/window.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | onSignInSuccess?: (data: any) => void; // Replace 'any' with a more specific type if known
3 | }
4 |
--------------------------------------------------------------------------------
/wownar/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/wownar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------