diff --git a/src/config/farcasterFrame.ts b/src/config/farcasterFrame.ts new file mode 100644 index 0000000..7d858fb --- /dev/null +++ b/src/config/farcasterFrame.ts @@ -0,0 +1,112 @@ +import { + ClientProtocolId, + FrameActionDataParsedAndHubContext, + FrameActionPayload, + FrameMessageReturnType, + getAddressesForFid, + getFrameMessage, + HubHttpUrlOptions, +} from 'frames.js'; +import { FramesMiddleware, JsonValue } from 'frames.js/core/types'; +import { MessageWithWalletAddressImplementation } from 'frames.js/middleware/walletAddressMiddleware'; + +import { InvalidFrameActionPayloadError, RequestBodyNotJSONError } from '@/constants/error'; + +type FrameMessage = Omit, 'message'> & { + state?: JsonValue; +} & MessageWithWalletAddressImplementation; +type FramesMessageContext = { + message?: FrameMessage; + clientProtocol?: ClientProtocolId; +}; + +function isValidFrameActionPayload(value: unknown): value is FrameActionPayload { + return typeof value === 'object' && value !== null && 'trustedData' in value && 'untrustedData' in value; +} + +async function decodeFrameActionPayloadFromRequest(request: Request): Promise { + try { + // use clone just in case someone wants to read body somewhere along the way + const body = (await request + .clone() + .json() + .catch(() => { + throw new RequestBodyNotJSONError(); + })) as JSON; + + if (!isValidFrameActionPayload(body)) { + throw new InvalidFrameActionPayloadError(); + } + + return body; + } catch (e) { + if (e instanceof RequestBodyNotJSONError || e instanceof InvalidFrameActionPayloadError) { + return undefined; + } + + // eslint-disable-next-line no-console -- provide feedback to the developer + console.error(e); + + return undefined; + } +} + +export function farcasterHubContext(options: HubHttpUrlOptions): FramesMiddleware { + return async (context, next) => { + if (context.request.method !== 'POST') { + return next(); + } + + const payload = await decodeFrameActionPayloadFromRequest(context.request); + if (!payload) { + return next(); + } + + try { + const message = (await getFrameMessage(payload, { + ...options, + fetchHubContext: false, + })) as FrameActionDataParsedAndHubContext; + + const requesterEthAddresses = await getAddressesForFid({ + fid: message.requesterFid, + options: { + hubHttpUrl: options.hubHttpUrl, + hubRequestOptions: options.hubRequestOptions, + }, + }); + const requesterCustodyAddress = requesterEthAddresses.find((item) => item.type === 'custody')?.address; + if (!requesterCustodyAddress) { + throw new Error('Custody address not found'); + } + + const requesterVerifiedAddresses = requesterEthAddresses + .filter((item) => item.type === 'verified') + .map((item) => item.address); + + message.requesterVerifiedAddresses = requesterVerifiedAddresses; + message.requesterCustodyAddress = requesterCustodyAddress; + + const [address] = message.requesterVerifiedAddresses; + + return next({ + message: { + ...message, + walletAddress() { + return Promise.resolve(address ?? message.requesterCustodyAddress); + }, + }, + clientProtocol: { + id: 'farcaster', + version: 'vNext', // TODO: Pass version in getFrameMessage + }, + }); + } catch (error) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.info( + 'farcasterHubContext middleware: could not decode farcaster message from payload, calling next.', + ); + return next(); + } + }; +} diff --git a/src/config/frames.ts b/src/config/frames.ts index ee85b21..6ea5fd5 100644 --- a/src/config/frames.ts +++ b/src/config/frames.ts @@ -1,7 +1,7 @@ -import { farcasterHubContext } from 'frames.js/middleware'; import { imagesWorkerMiddleware } from 'frames.js/middleware/images-worker'; import { createFrames } from 'frames.js/next'; +import { farcasterHubContext } from '@/config/farcasterFrame'; import { lensFrame } from '@/config/lensFrame'; import { IMAGE_ZOOM_SCALE } from '@/constants'; import { env } from '@/constants/env'; diff --git a/src/constants/error.ts b/src/constants/error.ts new file mode 100644 index 0000000..cb488f7 --- /dev/null +++ b/src/constants/error.ts @@ -0,0 +1,11 @@ +export class InvalidFrameActionPayloadError extends Error { + constructor(message = 'Invalid frame action payload') { + super(message); + } +} + +export class RequestBodyNotJSONError extends Error { + constructor() { + super('Invalid frame action payload, request body is not JSON'); + } +}