Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/config/farcasterFrame.ts
Original file line number Diff line number Diff line change
@@ -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<FrameMessageReturnType<{ fetchHubContext: true }>, '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<FrameActionPayload | undefined> {
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<any, FramesMessageContext> {
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();
}
};
}
2 changes: 1 addition & 1 deletion src/config/frames.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
11 changes: 11 additions & 0 deletions src/constants/error.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading