Skip to content

Commit

Permalink
Merge pull request #202 from FTCHD/rjs-openframes
Browse files Browse the repository at this point in the history
[FH-2] Open Frames Implementation (#163)
  • Loading branch information
FTCHD authored Oct 21, 2024
2 parents ef10efe + 5113f6d commit 2e12c96
Show file tree
Hide file tree
Showing 8 changed files with 2,263 additions and 270 deletions.
53 changes: 29 additions & 24 deletions app/(app)/f/[frameId]/[handler]/[[...query]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { client } from '@/db/client'
import { frameTable, interactionTable } from '@/db/schema'
import type { BuildFrameData, FramePayload } from '@/lib/farcaster'
import type { BuildFrameData, FarcasterFramePayload } from '@/lib/farcaster'
import { updateFrameStorage } from '@/lib/frame'
import { buildFramePage, validatePayload, validatePayloadAirstack } from '@/lib/serve'
import { buildFramePage } from '@/lib/serve'
import { isFarcasterFrameActionPayload, validatePayload, validatePayloadAirstack, type FramePayload } from '@/lib/validate'
import type { BaseConfig, BaseStorage } from '@/lib/types'
import { FrameError } from '@/sdk/error'
import templates from '@/templates'
Expand Down Expand Up @@ -166,26 +167,30 @@ async function processFrame(
}
}

const airstackKey = frame.config?.airstackKey || process.env.AIRSTACK_API_KEY

const airstackPayloadValidated = await validatePayloadAirstack(payload, airstackKey)

console.log(JSON.stringify(airstackPayloadValidated, null, 2))

await client
.insert(interactionTable)
.values({
frame: frame.id,
fid: airstackPayloadValidated.message.data.fid.toString(),
buttonIndex:
airstackPayloadValidated.message.data.frameActionBody.buttonIndex.toString(),
inputText: airstackPayloadValidated.message.data.frameActionBody.inputText || undefined,
state: airstackPayloadValidated.message.data.frameActionBody.state || undefined,
transactionHash:
airstackPayloadValidated.message.data.frameActionBody.transactionId || undefined,
castFid: airstackPayloadValidated.message.data.frameActionBody.castId.fid.toString(),
castHash: airstackPayloadValidated.message.data.frameActionBody.castId.hash,
createdAt: new Date(),
})
.run()
// TODO Do we want to support interaction logging for non-Farcaster frames?
if (isFarcasterFrameActionPayload(payload)) {
const airstackKey = frame.config?.airstackKey || process.env.AIRSTACK_API_KEY

const airstackPayloadValidated = await validatePayloadAirstack(payload as FarcasterFramePayload, airstackKey)

console.log(JSON.stringify(airstackPayloadValidated, null, 2))

await client
.insert(interactionTable)
.values({
frame: frame.id,
fid: airstackPayloadValidated.message.data.fid.toString(),
buttonIndex:
airstackPayloadValidated.message.data.frameActionBody.buttonIndex.toString(),
inputText: airstackPayloadValidated.message.data.frameActionBody.inputText || undefined,
state: airstackPayloadValidated.message.data.frameActionBody.state || undefined,
transactionHash:
airstackPayloadValidated.message.data.frameActionBody.transactionId || undefined,
castFid: airstackPayloadValidated.message.data.frameActionBody.castId.fid.toString(),
castHash: airstackPayloadValidated.message.data.frameActionBody.castId.hash,
createdAt: new Date(),
})
.run()
}

}
4 changes: 2 additions & 2 deletions app/api/compose/[templateId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { client } from '@/db/client'
import { frameTable } from '@/db/schema'
import { validatePayload } from '@/lib/serve'
import { validatePayloadFarcaster } from '@/lib/farcaster'
import templates from '@/templates'
import type { InferInsertModel } from 'drizzle-orm'
import { encode } from 'next-auth/jwt'
Expand Down Expand Up @@ -35,7 +35,7 @@ export async function POST(
) {
const body = await request.json()

const validatedPayload = await validatePayload(body)
const validatedPayload = await validatePayloadFarcaster(body)

const templateId = params.templateId

Expand Down
6 changes: 4 additions & 2 deletions lib/farcaster.d.ts → lib/farcaster.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use server'

import type {
Channel as NeynarChannel,
User as NeynarUser,
Expand Down Expand Up @@ -27,8 +29,8 @@ export type FrameButtonMetadata =
callback?: string
}

export type FramePayload = FramesJSFrameActionPayload
export type FramePayloadValidated = NeynarValidatedFrameActionResponse['action']
export type FarcasterFramePayload = FramesJSFrameActionPayload
export type FarcasterFramePayloadValidated = NeynarValidatedFrameActionResponse['action']

export type FarcasterUserInfo = NeynarUser
export type FarcasterChannel = NeynarChannel
Expand Down
98 changes: 42 additions & 56 deletions lib/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import sharp from 'sharp'
import type {
BuildFrameData,
FrameButtonMetadata,
FramePayload,
FramePayloadValidated,
} from './farcaster'

export async function buildFramePage({
Expand Down Expand Up @@ -241,61 +239,49 @@ export async function buildFrame({
metadata['fc:frame:refresh_period'] = refreshPeriod.toString()
}

return metadata
}

export async function validatePayload(body: FramePayload): Promise<FramePayloadValidated> {
const options = {
method: 'POST',
headers: {
accept: 'application/json',
api_key: process.env.NEYNAR_API_KEY!,
'content-type': 'application/json',
},
body: JSON.stringify({
cast_reaction_context: true,
follow_context: true,
signer_context: true,
message_bytes_in_hex: body.trustedData.messageBytes,
}),
}

const r = await fetch('https://api.neynar.com/v2/farcaster/frame/validate', options)
.then((response) => response.json())
.catch((err) => {
console.error(err)
throw new Error('PAYLOAD_COULD_NOT_BE_VALIDATED')
})

if (!r.valid) {
throw new Error('PAYLOAD_NOT_VALID')
// Open Frames version the handler supports
metadata['of:version'] = 'vNext'

// Lens Protocol Open Frames version the handler supports
metadata['of:accepts:lens'] = '1.0.0'

// XMTP Open Frames version the handler supports
metadata['of:accepts:xmtp'] = 'vNext'

// Map of equivalent Farcaster Frame tags to Open Frame tags
// For clarity and to aid searchability of the codebase,
// we use explicit keys here rather than e.g. search and replace
// See: https://www.openframes.xyz/#farcaster-compatibility
const openFrameEquivalentTags: Record<string, string> = {
'fc:frame:state': 'of:state',
'fc:frame:image': 'of:image',
'fc:frame:image:aspect_ratio': 'of:image:aspect_ratio',
'fc:frame:post_url': 'of:post_url',
'fc:frame:input:text': 'of:input:text',
'fc:frame:button:1': 'of:button:1',
'fc:frame:button:1:action': 'of:button:1:action',
'fc:frame:button:1:target': 'of:button:1:target',
'fc:frame:button:1:post_url': 'of:button:1:post_url',
'fc:frame:button:2': 'of:button:2',
'fc:frame:button:2:action': 'of:button:2:action',
'fc:frame:button:2:target': 'of:button:2:target',
'fc:frame:button:2:post_url': 'of:button:2:post_url',
'fc:frame:button:3': 'of:button:3',
'fc:frame:button:3:action': 'of:button:3:action',
'fc:frame:button:3:target': 'of:button:3:target',
'fc:frame:button:3:post_url': 'of:button:3:post_url',
'fc:frame:button:4': 'of:button:4',
'fc:frame:button:4:action': 'of:button:4:action',
'fc:frame:button:4:target': 'of:button:4:target',
'fc:frame:button:4:post_url': 'of:button:4:post_url',
}

console.log(r.action)

return r.action
}
export async function validatePayloadAirstack(
body: FramePayload,
airstackKey: string
): Promise<any> {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'x-airstack-hubs': airstackKey,
},
body: new Uint8Array(
body.trustedData.messageBytes.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16))
),
for (const [farcasterFramesTag, value] of Object.entries(metadata)) {
const openFramesTag = openFrameEquivalentTags[farcasterFramesTag]
if (openFramesTag) {
metadata[openFramesTag] = value
}
}

const r = await fetch('https://hubs.airstack.xyz/v1/validateMessage', options)
.then((response) => response.json())
.catch((err) => {
console.error(err)
throw new Error('AIRSTACK_PAYLOAD_COULD_NOT_BE_VALIDATED')
})

return r
}
return metadata
}
Loading

0 comments on commit 2e12c96

Please sign in to comment.