diff --git a/.changeset/good-cougars-boil.md b/.changeset/good-cougars-boil.md new file mode 100644 index 000000000..e9f405dec --- /dev/null +++ b/.changeset/good-cougars-boil.md @@ -0,0 +1,61 @@ +--- +'@coinbase/onchainkit': minor +--- + +- **docs**: Polished README for `getFrameMessage()`. By @zizzamia #38 +- **fix**: Refactor Farcaster typing to be explicit, and added a Farcaster message verification integration test. By @robpolak @cnasc @zizzamia #37 +- **feat**: Added a concept of integration tests where we can assert the actual values coming back from `neynar`. We decoupled these from unit tests as we should not commingle. By @robpolak #35 +- **feat**: Refactored `neynar` client out of the `./src/core` code-path, for better composability and testability. By @robpolak #35 + +BREAKING CHANGES + +We made the `getFrameValidatedMessage` method more type-safe and renamed it to `getFrameMessage`. + +Before + +```ts +import { getFrameValidatedMessage } from '@coinbase/onchainkit'; + +... + +const validatedMessage = await getFrameValidatedMessage(body); +``` + +**@Returns** + +```ts +type Promise +``` + +After + +```ts +import { getFrameMessage } from '@coinbase/onchainkit'; + +... + +const { isValid, message } = await getFrameMessage(body); +``` + +**@Returns** + +```ts +type Promise; + +type FrameValidationResponse = + | { isValid: true; message: FrameData } + | { isValid: false; message: undefined }; + +interface FrameData { + fid: number; + url: string; + messageHash: string; + timestamp: number; + network: number; + buttonIndex: number; + castId: { + fid: number; + hash: string; + }; +} +``` diff --git a/README.md b/README.md index 18e3c2076..210d43265 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -OnchainKit +OnchainKit # [OnchainKit](https://github.com/coinbase/onchainkit/) -> OnchainKit is a collection of tools to build world-class onchain apps with CSS, React, and typescript. +> OnchainKit is a collection of tools to build world-class onchain apps with CSS, React, and Typescript. ## Getting Started Add OnchainKit to your project, install the required packages. +
+ ```bash # Use Yarn yarn add @coinbase/onchainkit @@ -31,10 +33,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#getframeaccountaddress) +- [getFrameMessage()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getFrameMessage) - [getFrameMetadata()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getFrameMetadata) -- [getFrameValidatedMessage()](https://github.com/coinbase/onchainkit?tab=readme-ov-file#getFrameValidatedMessage) -### getFrameAccountAddress() +
+ +### getFrameAccountAddress(body, options) When a user interacts with your Frame, you will receive a JSON message called `Frame Signature Packet`. From this message, you can extract the Account Address using the `getFrameAccountAddress()` function. @@ -83,12 +87,79 @@ export async function POST(req: NextRequest): Promise { export const dynamic = 'force-dynamic'; ``` -`getFrameAccountAddress` params +**@Param** - `body`: The Frame Signature Packet body - `options`: - `NEYNAR_API_KEY`: The NEYNAR_API_KEY used to access [Neynar Farcaster Indexer](https://docs.neynar.com/reference/user-bulk) +**@Returns** + +```ts +type AccountAddressResponse = Promise; +``` + +
+ +### getFrameMessage() + +When a user interacts with your Frame, you receive a JSON message called the "Frame Signature Packet". Decode and validate this message using the `getFrameMessage` function. + +It returns undefined if the message is not valid. + +```ts +// Steps 1. import getFrameMessage from @coinbase/onchainkit +import { getFrameMessage } from '@coinbase/onchainkit'; +import { NextRequest, NextResponse } from 'next/server'; + +async function getResponse(req: NextRequest): Promise { + // Step 2. Read the body from the Next Request + const body = 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) { + // the message is valid + } 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** + +- `body`: The Frame Signature Packet body + +**@Returns** + +```ts +type FrameValidationResponse = + | { isValid: true; message: FrameData } + | { isValid: false; message: undefined }; + +interface FrameData { + fid: number; + url: string; + messageHash: string; + timestamp: number; + network: number; + buttonIndex: number; + castId: { + fid: number; + hash: string; + }; +} +``` +
### getFrameMetadata() @@ -121,45 +192,20 @@ export default function Page() { } ``` -`getFrameMetadata` params +**@Param** - `buttons`: A list of strings which are the label for the buttons in the frame (max 4 buttons). - `image`: An image which must be smaller than 10MB and should have an aspect ratio of 1.91:1 - `post_url`: A valid POST URL to send the Signature Packet to. -
- -### getFrameValidatedMessage() - -When a user interacts with your Frame, you receive a JSON message called the "Frame Signature Packet". Decode and validate this message using the `getFrameValidatedMessage` function. It returns undefined if the message is not valid. +**@Returns** ```ts -// Steps 1. import getFrameValidatedMessage from @coinbase/onchainkit -import { getFrameValidatedMessage } from '@coinbase/onchainkit'; -import { NextRequest, NextResponse } from 'next/server'; - -async function getResponse(req: NextRequest): Promise { - try { - // Step 2. Read the body from the Next Request - const body = await req.json(); - // Step 3. Validate the message - validatedMessage = await getFrameValidatedMessage(body); - - // Step 4. Determine the Frame experience based on the validity of the message - if (validatedMessage) { - // the message is valid - } else { - // sorry, the message is not valid - } - - ... -} - -export async function POST(req: NextRequest): Promise { - return getResponse(req); -} - -export const dynamic = 'force-dynamic'; +type FrameMetadataResponse = { + buttons: string[]; + image: string; + post_url: string; +}; ```
diff --git a/docs/logo-v-0-2.png b/docs/logo-v-0-2.png new file mode 100644 index 000000000..e911d0d04 Binary files /dev/null and b/docs/logo-v-0-2.png differ diff --git a/src/core/getFrameAccountAddress.ts b/src/core/getFrameAccountAddress.ts index 0ebac7098..7017d1448 100644 --- a/src/core/getFrameAccountAddress.ts +++ b/src/core/getFrameAccountAddress.ts @@ -6,6 +6,8 @@ type FidResponse = { verifications: string[]; }; +type AccountAddressResponse = Promise; + /** * Get the Account Address from the Farcaster ID using the Frame. * This uses a Neynar api to get verified addresses belonging @@ -13,14 +15,14 @@ type FidResponse = { * * This is using a demo api key so please register * on through https://neynar.com/. - * @param body - * @param param1 - * @returns + * @param body The JSON received by server on frame callback + * @param NEYNAR_API_KEY The api key for the Neynar API + * @returns The account address or undefined */ async function getFrameAccountAddress( body: FrameRequest, { NEYNAR_API_KEY = 'NEYNAR_API_DOCS' }, -): Promise { +): AccountAddressResponse { const validatedMessage = await getFrameMessage(body); if (!validatedMessage?.isValid) { return; @@ -28,7 +30,7 @@ async function getFrameAccountAddress( // Get the Farcaster ID from the message const farcasterID = validatedMessage?.message?.fid ?? 0; // Get the user verifications from the Farcaster Indexer - const bulkUserLookupResponse = await neynarBulkUserLookup([farcasterID]); + const bulkUserLookupResponse = await neynarBulkUserLookup([farcasterID], NEYNAR_API_KEY); if (bulkUserLookupResponse?.users) { const userVerifications = bulkUserLookupResponse?.users[0] as FidResponse; if (userVerifications.verifications) { diff --git a/src/core/getFrameValidatedMessage.test.ts b/src/core/getFrameMessage.test.ts similarity index 100% rename from src/core/getFrameValidatedMessage.test.ts rename to src/core/getFrameMessage.test.ts diff --git a/src/core/getFrameMessage.ts b/src/core/getFrameMessage.ts index 1f8d3893f..248b18e49 100644 --- a/src/core/getFrameMessage.ts +++ b/src/core/getFrameMessage.ts @@ -19,7 +19,7 @@ export function getHubClient(): HubRpcClient { * * @param body The JSON received by server on frame callback */ -async function getFrameMessage(body: FrameRequest): Promise { +async function getFrameMessage(body: FrameRequest): Promise { // Get the message from the request body const frameMessage: Message = Message.decode( Buffer.from(body?.trustedData?.messageBytes ?? '', 'hex'), diff --git a/src/core/getFrameMetadata.ts b/src/core/getFrameMetadata.ts index 316707b60..eccfc72e4 100644 --- a/src/core/getFrameMetadata.ts +++ b/src/core/getFrameMetadata.ts @@ -1,3 +1,9 @@ +type FrameMetadataResponse = { + buttons: string[]; + image: string; + post_url: string; +}; + /** * This function generates the metadata for a Farcaster Frame. * @param buttons: An array of button names. @@ -5,15 +11,7 @@ * @param post_url: The URL to post the frame to. * @returns The metadata for the frame. */ -export const getFrameMetadata = function ({ - buttons, - image, - post_url, -}: { - buttons: string[]; - image: string; - post_url: string; -}) { +export const getFrameMetadata = function ({ buttons, image, post_url }: FrameMetadataResponse) { const metadata: Record = { 'fc:frame': 'vNext', }; diff --git a/src/index.ts b/src/index.ts index 8753eba9c..24d1abcf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // 🌲 -const version = '0.1.6'; +const version = '0.2.0'; export { version }; export { getFrameAccountAddress } from './core/getFrameAccountAddress';