├── .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 | Deploy with Vercel 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 |
fid:{{ item.fid }}
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 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 29 | 57 | 58 | 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 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 29 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /wownar-react-sdk/public/logos/wownar.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 29 | 57 | 58 | 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 | SimpleCaster Logo 33 |

Wownar

34 |
35 | {isAuthenticated && } 36 |
37 | {children} 38 |
39 | {/* 43 | Connect Farcaster accounts for free using  44 | Sign in with Neynar 45 | */} 46 | 50 | Github Repo -> Wownar 51 | 52 |
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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 29 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /wownar/public/logos/wownar-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /wownar/public/logos/wownar.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 29 | 57 | 58 | 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 | SimpleCaster Logo 35 |

Wownar

36 |
37 | {screen !== ScreenState.Signin && ( 38 |
39 |
45 | )} 46 |
47 | {children} 48 |
49 | 53 | Connect Farcaster accounts for free using  54 | Sign in with Neynar 55 | 56 | 60 | Github Repo -> Wownar 61 | 62 |
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 | 16 | 17 | 18 | 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 | --------------------------------------------------------------------------------