Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add Open Frames Neynar validator #116

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
"@open-frames/types": "^0.0.5",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Veeeery interesting

"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^14.2.0",
"@types/jest": "^29.5.11",
Expand Down
41 changes: 29 additions & 12 deletions src/core/getFrameMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import {
NEYNAR_DEFAULT_API_KEY,
neynarFrameValidation,
} from '../utils/neynar/frame/neynarFrameFunctions';
import type {
GetValidationResponse,
OpenFramesRequest,
RequestValidator,
} from '@open-frames/types';
import { NeynarValidator } from '../utils/neynar/openframes/neynarValidator';

type FrameMessageOptions =
| {
Expand All @@ -23,18 +29,13 @@ async function getFrameMessage(
body: FrameRequest,
messageOptions?: FrameMessageOptions,
): Promise<FrameValidationResponse> {
const neynarValidator = new NeynarValidator(messageOptions);
// Validate the message
const response = await neynarFrameValidation(
body?.trustedData?.messageBytes,
messageOptions?.neynarApiKey || NEYNAR_DEFAULT_API_KEY,
messageOptions?.castReactionContext || true,
messageOptions?.followContext || true,
);
if (response?.valid) {
return {
isValid: true,
message: response,
};
const response = await getOpenFramesMessage({ clientProtocol: 'farcaster@VNext', ...body }, [
neynarValidator,
]);
if (response.isValid) {
return response;
} else {
// Security best practice, don't return anything if we can't validate the frame.
return {
Expand All @@ -44,4 +45,20 @@ async function getFrameMessage(
}
}

export { getFrameMessage };
async function getOpenFramesMessage<ClientProtocols extends Array<RequestValidator<any, any, any>>>(
body: OpenFramesRequest,
clientProtocols: ClientProtocols,
): Promise<GetValidationResponse<ClientProtocols[number]>> {
for (const protocol of clientProtocols) {
if (protocol.isSupported(body)) {
const validationResponse = await protocol.validate(body as any);
return validationResponse as GetValidationResponse<ClientProtocols[number]>;
}
}

return {
isValid: false,
} as GetValidationResponse<ClientProtocols[number]>;
}

export { getFrameMessage, getOpenFramesMessage };
54 changes: 54 additions & 0 deletions src/utils/neynar/openframes/neynarValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { OpenFramesRequest, RequestValidator, ValidationResponse } from '@open-frames/types';
import { FarcasterOpenFramesRequest, FrameRequest, FrameValidationData } from '../../../core/types';
import { NEYNAR_DEFAULT_API_KEY, neynarFrameValidation } from '../frame/neynarFrameFunctions';
import { Options } from './types';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh side note, we prefer Functional code vs Object Oriented, as it is much lighter in terms of memory and also easier to write test for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. All I really care about is having something that satisfies a common interface so that developers can build their own request validators and have confidence they will be able to integrate.

Whether the implementation of the interface is a module, an object, or a class is up to the implementer.

export class NeynarValidator
implements RequestValidator<FarcasterOpenFramesRequest, FrameValidationData, 'farcaster'>
{
readonly protocolIdentifier = 'farcaster';
private options: Required<Options>;

constructor(options?: Options) {
this.options = {
neynarApiKey: options?.neynarApiKey || NEYNAR_DEFAULT_API_KEY,
castReactionContext: options?.castReactionContext || true,
followContext: options?.followContext || true,
};
}

minProtocolVersion(): string {
return `${this.protocolIdentifier}@VNext`;
}

isSupported(payload: OpenFramesRequest): payload is FarcasterOpenFramesRequest {
const isCorrectClientProtocol =
!!payload.clientProtocol && payload.clientProtocol.startsWith('farcaster@');
const isTrustedDataValid = typeof payload.trustedData?.messageBytes === 'string';

return isCorrectClientProtocol && isTrustedDataValid;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I see, isSupported is where the magic happens.

And ideally we can have as many client as we want.

There are some implementation details that we need to align, but I like where your head is @neekolas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. Here is an example of the XMTP RequestValidator that can be safely passed in to getOpenFramesMessage as an argument and will allow for processing of button clicks coming from XMTP.


async validate(
payload: FarcasterOpenFramesRequest,
): Promise<ValidationResponse<FrameValidationData, typeof this.protocolIdentifier>> {
const response = await neynarFrameValidation(
payload.trustedData?.messageBytes,
this.options.neynarApiKey,
this.options.castReactionContext,
this.options.followContext,
);

if (response?.valid) {
return {
isValid: true,
clientProtocol: payload.clientProtocol,
message: response,
};
}

return {
isValid: false,
};
}
}
5 changes: 5 additions & 0 deletions src/utils/neynar/openframes/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Options = {
neynarApiKey?: string;
castReactionContext?: boolean;
followContext?: boolean;
};
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,7 @@ __metadata:
dependencies:
"@changesets/changelog-github": "npm:^0.4.8"
"@changesets/cli": "npm:^2.26.2"
"@open-frames/types": "npm:^0.0.5"
"@testing-library/jest-dom": "npm:^6.4.0"
"@testing-library/react": "npm:^14.2.0"
"@types/jest": "npm:^29.5.11"
Expand Down Expand Up @@ -1508,6 +1509,13 @@ __metadata:
languageName: node
linkType: hard

"@open-frames/types@npm:^0.0.5":
version: 0.0.5
resolution: "@open-frames/types@npm:0.0.5"
checksum: 971153e1ba5af69f54df0ebe65ce6dcc15163675abe097d3d9454a0552d9181207d89bf64ff1e30859b49f77cdac286baac6bbc42b8df112b1977fc35b8e4ac5
languageName: node
linkType: hard

"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
Expand Down
Loading