Skip to content

Commit

Permalink
feat: add frame validate using neynar for neynar hubs (#292)
Browse files Browse the repository at this point in the history
* feat: add frame validate using neynar for neynar hubs

* fix: remove unnecessary async

* feat: use hub api key instead

* refactor: improve from feedback

* chore: remove debugging console log

* fix: comment;

* chore: format

* chore: changeset

---------

Co-authored-by: Tom Meagher <tom@meagher.co>
  • Loading branch information
avneesh0612 and tmm committed May 3, 2024
1 parent c6a209e commit 0017052
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-parents-smile.md
@@ -0,0 +1,5 @@
---
"frog": patch
---

Added `verifyFrame` option to hub definition.
15 changes: 15 additions & 0 deletions src/hubs/neynar.ts
@@ -1,3 +1,4 @@
import type { TrustedData } from '../types/frame.js'
import { createHub } from './utils.js'

export type NeynarHubParameters = {
Expand All @@ -6,12 +7,26 @@ export type NeynarHubParameters = {

export const neynar = createHub((parameters: NeynarHubParameters) => {
const { apiKey } = parameters

return {
apiUrl: 'https://hub-api.neynar.com',
fetchOptions: {
headers: {
api_key: apiKey,
},
},
verifyFrame: async ({ trustedData }: { trustedData: TrustedData }) => {
return await fetch('https://api.neynar.com/v2/farcaster/frame/validate', {
method: 'POST',
headers: {
accept: 'application json',
api_key: apiKey,
'content-type': 'application/json',
},
body: JSON.stringify({
message_bytes_in_hex: `0x${trustedData.messageBytes}`,
}),
}).then(async (res) => res.json())
},
}
})
4 changes: 4 additions & 0 deletions src/types/hub.ts
@@ -1,6 +1,10 @@
import type { TrustedData } from './frame.js'

export type Hub = {
/** Hub API URL. */
apiUrl: string
/** Options to pass to `fetch`. */
fetchOptions?: RequestInit
/** Verify frame override. */
verifyFrame?: (parameters: { trustedData: TrustedData }) => Promise<void>
}
48 changes: 48 additions & 0 deletions src/utils/verifyFrame.test.ts
@@ -1,5 +1,6 @@
import { expect, test } from 'vitest'
import { frog } from '../hubs/frog.js'
import { neynar } from '../hubs/neynar.js'
import { verifyFrame } from './verifyFrame.js'

test('valid', async () => {
Expand Down Expand Up @@ -42,3 +43,50 @@ test('invalid url', async () => {
'[Error: Invalid frame url: https://test-farc7.vercel.app/api. Expected: https://test-farc6.vercel.app/foo.]',
)
})

test('valid neynar', async () => {
const messageBytes =
'0a4f080d10ff2f18c1a6802f20018201400a2168747470733a2f2f746573742d66617263362e76657263656c2e6170702f61706910011a1908ff2f1214000000000000000000000000000000000000000112141de03010b0ce4f39ba4b8ff29851d0d610dc5ddd180122404aab47af096150fe7193713722bcdd6ddcd6cd35c1e84cc42e7713624916a97568fa8232e2ffd70ce5eeafb0391c7bbcdf6c5ba15a9a02834102b016058e7d0128013220daa3f0a5335900f542a266e4b837309aeac52d736f4cf9b2eff0d4c4f4c7e58f'
await verifyFrame({
frameUrl: 'https://test-farc6.vercel.app/api/foo',
hub: neynar({
apiKey: 'NEYNAR_FROG_FM',
}),
trustedData: { messageBytes },
url: 'https://test-farc6.vercel.app/api',
})
})

test('invalid hash neynar', async () => {
const messageBytes =
'0a4d080d10ff2f18c1a6802f20018201400a2168747470733a2a2f746573742d66617263362e76657263656c2e6170702f61706910011a1908ff2f1214000000000000000000000000000000000000000112141de03010b0ce4f39ba4b8ff29851d0d610dc5ddd180122404aab47af096150fe7193713722bcdd6ddcd6cd35c1e84cc42e7713624916a97568fa8232e2ffd70ce5eeafb0391c7bbcdf6c5ba15a9a02834102b016058e7d0128013220daa3f0a5335900f542a266e4b837309aeac52d736f4cf9b2eff0d4c4f4c7e58f'
await expect(() =>
verifyFrame({
frameUrl: 'https://test-farc6.vercel.app/api',
hub: neynar({
apiKey: 'NEYNAR_FROG_FM',
}),
trustedData: { messageBytes },
url: 'https://test-farc6.vercel.app/api',
}),
).rejects.toMatchInlineSnapshot(
'[Error: message is invalid. No data in message.]',
)
})

test('invalid url neynar', async () => {
const messageBytes =
'0a49080d1085940118f6a6a32e20018201390a1a86db69b3ffdf6ab8acb6872b69ccbe7eb6a67af7ab71e95aa69f10021a1908ef011214237025b322fd03a9ddc7ec6c078fb9c56d1a72111214e3d88aeb2d0af356024e0c693f31c11b42c76b721801224043cb2f3fcbfb5dafce110e934b9369267cf3d1aef06f51ce653dc01700fc7b778522eb7873fd60dda4611376200076caf26d40a736d3919ce14e78a684e4d30b280132203a66717c82d728beb3511b05975c6603275c7f6a0600370bf637b9ecd2bd231e'
await expect(() =>
verifyFrame({
frameUrl: 'https://test-farc7.vercel.app/api',
hub: neynar({
apiKey: 'NEYNAR_FROG_FM',
}),
trustedData: { messageBytes },
url: 'https://test-farc6.vercel.app/foo',
}),
).rejects.toMatchInlineSnapshot(
'[Error: Invalid frame url: https://test-farc7.vercel.app/api. Expected: https://test-farc6.vercel.app/foo.]',
)
})
25 changes: 15 additions & 10 deletions src/utils/verifyFrame.ts
Expand Up @@ -21,18 +21,23 @@ export async function verifyFrame({
url,
}: VerifyFrameParameters): Promise<VerifyFrameReturnType> {
const body = hexToBytes(`0x${trustedData.messageBytes}`)
const response = await fetch(`${hub.apiUrl}/v1/validateMessage`, {
...hub.fetchOptions,
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
...hub.fetchOptions?.headers,
},
body,
}).then((res) => res.json())

const response = hub.verifyFrame
? await hub.verifyFrame({ trustedData })
: await fetch(`${hub.apiUrl}/v1/validateMessage`, {
...hub.fetchOptions,
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
...hub.fetchOptions?.headers,
},
body,
}).then((res) => res.json())

if (!response.valid)
throw new Error(`message is invalid. ${response.details}`)
throw new Error(
`message is invalid. ${response.details || response.message}`,
)

if (new URL(url).origin !== new URL(frameUrl).origin)
throw new Error(`Invalid frame url: ${frameUrl}. Expected: ${url}.`)
Expand Down

0 comments on commit 0017052

Please sign in to comment.