diff --git a/.changeset/new-dragons-serve.md b/.changeset/new-dragons-serve.md new file mode 100644 index 0000000000..4a6ad30b9c --- /dev/null +++ b/.changeset/new-dragons-serve.md @@ -0,0 +1,11 @@ +--- +'@coinbase/onchainkit': minor +--- + +- **feat**: deprecated `getFrameAccountAddress` as now `getFrameMessage` returns also the Account Address. #60 +- **feat**: integrated with Neynars api to get validated messages + additional context like recast/follow/etc. By @robpolak #59 +- **fix**: removed farcaster references as they were generating build errors and compatibility issues. By @robpolak #59 + +BREAKING CHANGES + +I will write the breaking changes in the next PR diff --git a/README.md b/README.md index 3d2d725bfa..e9e152a5e9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -OnchainKit +OnchainKit # [OnchainKit](https://github.com/coinbase/onchainkit/) @@ -32,73 +32,12 @@ Creating a frame is easy: select an image and add clickable buttons. When a butt Utilities: -- [getFrameAccountAddress()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getframeaccountaddressmessage-options): Retrieves the user **Account Address** from a Frame message. - [getFrameHtmlResponse()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getframehtmlresponseframemetadata): Retrieves the **Frame HTML** for your HTTP responses. - [getFrameMessage()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getframemessageframerequest): Retrieves a valid **Frame message** from the Frame Signature Packet. - [getFrameMetadata()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getframeframemetadata): Retrieves valid **Frame metadata** for your initial HTML page.
-### getFrameAccountAddress(message, options) - -When a user interacts with your Frame, you will receive a JSON message called the "Frame Signature Packet." Once you validate this `message`, you can extract the Account Address by using the `getFrameAccountAddress(message)` function. - -This Account Address can then be utilized for subsequent operations, enhancing the personalized experience of each individual using the Frame. - -Note: To utilize this function, we rely on [Neynar APIs](https://docs.neynar.com/reference/user-bulk). In order to avoid rate limiting, please ensure that you have your own API KEY. Sign up [here](https://neynar.com). - -```ts -// Steps 1. import getFrameAccountAddress from @coinbase/onchainkit -import { FrameRequest, getFrameAccountAddress, getFrameMessage } from '@coinbase/onchainkit'; -import { NextRequest, NextResponse } from 'next/server'; - -async function getResponse(req: NextRequest): Promise { - let accountAddress = ''; - // Step 2. Read the body from the Next Request - const body: FrameRequest = await req.json(); - // Step 3. Validate the message - const { isValid, message } = await getFrameMessage(body); - - // Step 4. Determine the experience based on the validity of the message - if (isValid) { - // Step 5. Get from the message the Account Address of the user using the Frame - accountAddress = await getFrameAccountAddress(message, { NEYNAR_API_KEY: 'NEYNAR_ONCHAIN_KIT' }); - } else { - // sorry, the message is not valid and it will be undefined - } - - ... -} - -export async function POST(req: NextRequest): Promise { - return getResponse(req); -} - -export const dynamic = 'force-dynamic'; -``` - -**@Param** - -```ts -type AccountAddressRequest = { - // The validated message from the getFrameMessage() function - message: FrameData; - options: { - // The NEYNAR_API_KEY used to access Neynar Farcaster Indexer - // https://docs.neynar.com/reference/user-bulk - NEYNAR_API_KEY: string; - }; -}; -``` - -**@Returns** - -```ts -type AccountAddressResponse = Promise; -``` - -
- ### getFrameHtmlResponse(frameMetadata) When you need to send an HTML Frame Response, the `getFrameHtmlResponse` method is here to assist you. @@ -108,7 +47,6 @@ It generates a valid HTML string response with a frame and utilizes `FrameMetada ```ts import { FrameRequest, - getFrameAccountAddress, getFrameMessage, getFrameHtmlResponse, } from '@coinbase/onchainkit'; diff --git a/docs/logo-v-0-4.png b/docs/logo-v-0-4.png new file mode 100644 index 0000000000..19049f15af Binary files /dev/null and b/docs/logo-v-0-4.png differ diff --git a/src/core/farcasterTypes.ts b/src/core/farcasterTypes.ts index abfb20f747..c525723f09 100644 --- a/src/core/farcasterTypes.ts +++ b/src/core/farcasterTypes.ts @@ -1,15 +1,4 @@ -import { NeynarFrameValidationResponse } from '../utils/neynar/frame/neynarFrameModels'; - -export interface FrameRequest { - untrustedData: FrameData; - trustedData: { - messageBytes: string; - }; -} - -export type FrameValidationResponse = - | { isValid: true; message: NeynarFrameValidationResponse } - | { isValid: false; message: undefined }; +import { NeynarFrameValidationInternalModel } from '../utils/neynar/frame/types'; export interface FrameData { fid: number; @@ -24,6 +13,34 @@ export interface FrameData { }; } +export interface FrameRequest { + untrustedData: FrameData; + trustedData: { + messageBytes: string; + }; +} + +/** + * Simplified Object model with the raw Neynar data if-needed. + */ +export interface FrameValidationData { + valid: boolean; + button: number; + liked: boolean; + recasted: boolean; + following: boolean; + interactor: { + fid: number; + custody_address: string; + verified_accounts: string[]; + }; + raw: NeynarFrameValidationInternalModel; +} + +export type FrameValidationResponse = + | { isValid: true; message: FrameValidationData } + | { isValid: false; message: undefined }; + export function convertToFrame(json: any) { return { fid: json.fid, diff --git a/src/core/getFrameAccountAddress.test.ts b/src/core/getFrameAccountAddress.test.ts deleted file mode 100644 index e27a31a762..0000000000 --- a/src/core/getFrameAccountAddress.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getFrameAccountAddress } from './getFrameAccountAddress'; -import { mockNeynarResponse } from './mock'; -import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; -import { FrameData } from './farcasterTypes'; - -jest.mock('../utils/neynar/user/neynarUserFunctions', () => { - return { - neynarBulkUserLookup: jest.fn(), - }; -}); - -describe('getFrameAccountAddress', () => { - const fakeMessage = { - fid: 1234, - }; - const fakeApiKey = { - NEYNAR_API_KEY: '1234', - }; - - it('should return the first verification for valid input', async () => { - const addresses = ['0xaddr1']; - mockNeynarResponse(fakeMessage.fid, addresses, neynarBulkUserLookup as jest.Mock); - - const response = await getFrameAccountAddress(fakeMessage as FrameData, fakeApiKey); - expect(response).toEqual(addresses[0]); - }); - - it('should return undefined for invalid input', async () => { - mockNeynarResponse(fakeMessage.fid, undefined, neynarBulkUserLookup as jest.Mock); - - const response = await getFrameAccountAddress(fakeMessage as FrameData, fakeApiKey); - expect(response).toBeUndefined(); - }); -}); diff --git a/src/core/getFrameAccountAddress.ts b/src/core/getFrameAccountAddress.ts deleted file mode 100644 index b68ad287a1..0000000000 --- a/src/core/getFrameAccountAddress.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; -import { FrameData } from './farcasterTypes'; - -type AccountAddressResponse = Promise; - -type FidResponse = { - verifications: string[]; -}; - -/** - * Get the Account Address from the Farcaster ID using the Frame. - * This uses a Neynar api to get verified addresses belonging - * to the user wht that FID. - * - * This is using a demo api key so please register - * on through https://neynar.com/. - * @param message The validated message from the Frame - * @param NEYNAR_API_KEY The api key for the Neynar API - * @returns The account address or undefined - */ -async function getFrameAccountAddress( - message: FrameData, - { NEYNAR_API_KEY = 'NEYNAR_ONCHAIN_KIT' }, -): AccountAddressResponse { - // Get the Farcaster ID from the message - const farcasterID = message.fid ?? 0; - // Get the user verifications from the Farcaster Indexer - const bulkUserLookupResponse = await neynarBulkUserLookup([farcasterID], NEYNAR_API_KEY); - if (bulkUserLookupResponse?.users) { - const userVerifications = bulkUserLookupResponse?.users[0] as FidResponse; - if (userVerifications.verifications) { - return userVerifications.verifications[0]; - } - } - return; -} - -export { getFrameAccountAddress }; diff --git a/src/index.ts b/src/index.ts index 6245bb8b20..787820fac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,6 @@ -// 🌲 -const version = '0.3.1'; - -export { version }; +// 🌲☀️🌲 +export { version } from './version'; export { getFrameHtmlResponse } from './core/getFrameHtmlResponse'; -export { getFrameAccountAddress } from './core/getFrameAccountAddress'; export { getFrameMetadata } from './core/getFrameMetadata'; export { getFrameMessage } from './core/getFrameMessage'; -export type { FrameRequest, FrameData } from './core/farcasterTypes'; +export type { FrameRequest, FrameValidationData } from './core/farcasterTypes'; diff --git a/src/utils/neynar/frame/neynarFrameFunctions.ts b/src/utils/neynar/frame/neynarFrameFunctions.ts index 82b0e24b4c..1af22c2bf2 100644 --- a/src/utils/neynar/frame/neynarFrameFunctions.ts +++ b/src/utils/neynar/frame/neynarFrameFunctions.ts @@ -1,5 +1,7 @@ +import { version } from '../../../version'; +import { FrameValidationData } from '../../../core/farcasterTypes'; import { FetchError } from '../exceptions/FetchError'; -import { convertToNeynarResponseModel, NeynarFrameValidationResponse } from './neynarFrameModels'; +import { convertToNeynarResponseModel } from './neynarFrameModels'; export const NEYNAR_DEFAULT_API_KEY = 'NEYNAR_ONCHAIN_KIT'; @@ -8,11 +10,16 @@ export async function neynarFrameValidation( apiKey: string = NEYNAR_DEFAULT_API_KEY, castReactionContext = true, followContext = true, -): Promise { +): Promise { const options = { method: 'POST', url: `https://api.neynar.com/v2/farcaster/frame/validate`, - headers: { accept: 'application/json', api_key: apiKey, 'content-type': 'application/json' }, + headers: { + accept: 'application/json', + api_key: apiKey, + 'content-type': 'application/json', + onchainkit_version: version, + }, body: JSON.stringify({ message_bytes_in_hex: messageBytes, cast_reaction_context: castReactionContext, // Returns if the user has liked/recasted diff --git a/src/utils/neynar/frame/neynarFrameModels.ts b/src/utils/neynar/frame/neynarFrameModels.ts index 5b042d385d..006ab419ff 100644 --- a/src/utils/neynar/frame/neynarFrameModels.ts +++ b/src/utils/neynar/frame/neynarFrameModels.ts @@ -1,140 +1,7 @@ -/** - * Simplified Object model with the raw Neynar data if-needed. - */ -export interface NeynarFrameValidationResponse { - valid: boolean; - button: number; - liked: boolean; - recasted: boolean; - following: boolean; - interactor: { - fid: number; - custody_address: string; - verified_accounts: string[]; - }; - raw: NeynarFrameValidationInternalModel; -} -/** - * Raw Response from Neynar - */ -export interface NeynarFrameValidationInternalModel { - valid: boolean; - action: { - object: string; - interactor: { - object: string; - fid: number; - custody_address: string; - username: null | string; - display_name: string; - pfp_url: string; - profile: { - bio: { - text: string; - mentioned_profiles?: any[]; - }; - }; - follower_count: number; - following_count: number; - verifications: any[]; - active_status: string; - viewer_context: { - following: boolean; - followed_by: boolean; - }; - }; - tapped_button: { - index: number; - }; - cast: { - object: string; - hash: string; - thread_hash: string; - parent_hash: null | string; - parent_url: string; - root_parent_url: string; - parent_author: { - fid: null | number; - }; - author: { - object: string; - fid: number; - custody_address: string; - username: string; - display_name: string; - pfp_url: string; - profile: { - bio: { - text: string; - mentioned_profiles?: any[]; - }; - }; - follower_count: number; - following_count: number; - verifications: any[]; - active_status: string; - viewer_context: { - liked: boolean; - recasted: boolean; - }; - }; - text: string; - timestamp: string; - embeds: { - url: string; - }[]; - frames: { - version: string; - title: string; - image: string; - buttons: { - index: number; - title: string; - action_type: string; - }[]; - post_url: string; - frames_url: string; - }[]; - reactions: { - likes: { - fid: number; - fname: string; - }[]; - recasts: { - fid: number; - fname: string; - }[]; - }; - replies: { - count: number; - }; - mentioned_profiles: { - object: string; - fid: number; - custody_address: string; - username: string; - display_name: string; - pfp_url: string; - profile: { - bio: { - text: string; - mentioned_profiles?: any[]; - }; - }; - follower_count: number; - following_count: number; - verifications: any[]; - active_status: string; - }[]; - viewer_context: { - liked: boolean; - recasted: boolean; - }; - }; - }; -} +import { FrameValidationData } from '../../../core/farcasterTypes'; +import { NeynarFrameValidationInternalModel } from './types'; -export function convertToNeynarResponseModel(data: any): NeynarFrameValidationResponse | undefined { +export function convertToNeynarResponseModel(data: any): FrameValidationData | undefined { if (!data) { return; } diff --git a/src/utils/neynar/frame/types.ts b/src/utils/neynar/frame/types.ts new file mode 100644 index 0000000000..f45edbcf36 --- /dev/null +++ b/src/utils/neynar/frame/types.ts @@ -0,0 +1,119 @@ +/** + * Raw Response from Neynar + */ +export interface NeynarFrameValidationInternalModel { + valid: boolean; + action: { + object: string; + interactor: { + object: string; + fid: number; + custody_address: string; + username: null | string; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + mentioned_profiles?: any[]; + }; + }; + follower_count: number; + following_count: number; + verifications: any[]; + active_status: string; + viewer_context: { + following: boolean; + followed_by: boolean; + }; + }; + tapped_button: { + index: number; + }; + cast: { + object: string; + hash: string; + thread_hash: string; + parent_hash: null | string; + parent_url: string; + root_parent_url: string; + parent_author: { + fid: null | number; + }; + author: { + object: string; + fid: number; + custody_address: string; + username: string; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + mentioned_profiles?: any[]; + }; + }; + follower_count: number; + following_count: number; + verifications: any[]; + active_status: string; + viewer_context: { + liked: boolean; + recasted: boolean; + }; + }; + text: string; + timestamp: string; + embeds: { + url: string; + }[]; + frames: { + version: string; + title: string; + image: string; + buttons: { + index: number; + title: string; + action_type: string; + }[]; + post_url: string; + frames_url: string; + }[]; + reactions: { + likes: { + fid: number; + fname: string; + }[]; + recasts: { + fid: number; + fname: string; + }[]; + }; + replies: { + count: number; + }; + mentioned_profiles: { + object: string; + fid: number; + custody_address: string; + username: string; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + mentioned_profiles?: any[]; + }; + }; + follower_count: number; + following_count: number; + verifications: any[]; + active_status: string; + }[]; + viewer_context: { + liked: boolean; + recasted: boolean; + }; + }; + }; +} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000000..038b22ba29 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const version = '0.4.0';