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 XMTP Support #96

Closed
wants to merge 2 commits into from
Closed
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@
"type": "commonjs",
"main": "./dist/lib/index.js",
"typings": "./dist/types/index.d.ts",
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"@xmtp/frames-validator": "^0.3.1"
}
}
3 changes: 3 additions & 0 deletions src/core/farcaster.integ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ describe('getFrameValidatedMessage integration tests', () => {
},
};
const response = await getFrameMessage(body);
if (!('clientType' in response) || response.clientType !== 'farcaster') {
throw new Error('Not a farcaster response');
}
expect(response?.isValid).toEqual(true);
expect(response?.message?.button).toEqual(body.untrustedData.buttonIndex);
expect(response?.message?.interactor.fid).toEqual(body.untrustedData.fid);
Expand Down
49 changes: 48 additions & 1 deletion src/core/getFrameMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getFrameMessage } from './getFrameMessage';
import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions';
import { FrameRequest } from './types';
import { neynarFrameValidation } from '../utils/neynar/frame/neynarFrameFunctions';
import { validateXmtpFrameResponse } from '../utils/xmtp/validation';

jest.mock('../utils/neynar/user/neynarUserFunctions', () => {
return {
Expand All @@ -12,10 +13,15 @@ jest.mock('../utils/neynar/user/neynarUserFunctions', () => {

jest.mock('../utils/neynar/frame/neynarFrameFunctions', () => {
return {
isXmtpFrameResponse: jest.requireActual('../utils/xmtp/validation').isXmtpFrameResponse,
neynarFrameValidation: jest.fn(),
};
});

jest.mock('../utils/xmtp/validation', () => ({
validateXmtpFrameResponse: jest.fn(),
}));

describe('getFrameValidatedMessage', () => {
it('should return undefined if the message is invalid', async () => {
const result = await getFrameMessage({
Expand All @@ -37,6 +43,47 @@ describe('getFrameValidatedMessage', () => {
trustedData: {},
};
const result = await getFrameMessage(fakeFrameData as FrameRequest);
expect(result?.message?.interactor.fid).toEqual(fid);
if ('clientType' in result && result.clientType === 'farcaster') {
expect(result?.message.interactor.fid).toEqual(fid);
} else {
fail("Expected clientType to be 'farcaster' but it wasn't");
}
});
});

describe('getFrameValidatedMessageXmtp', () => {
afterEach(() => {
(validateXmtpFrameResponse as jest.Mock).mockReset();
});

it('should return a valid response if opaqueConversationIdentifier is present', async () => {
const payload = {
untrustedData: {
opaqueConversationIdentifier: 'opaqueConversationIdentifier',
},
trustedData: {
messageBytes: 'messageBytes',
},
};
(validateXmtpFrameResponse as jest.Mock).mockResolvedValue({
clientType: 'xmtp',
isValid: true,
message: {
verifiedWalletAddress: 'verifiedWalletAddress',
buttonIndex: 1,
frameUrl: 'http://frames.com/foo',
opaqueConversationIdentifier: 'opaqueConversationIdentifier',
},
});

const result = await getFrameMessage(payload as FrameRequest);
expect(result.isValid).toEqual(true);
if (!result.message || result.clientType !== 'xmtp') {
throw new Error('Invalid response type');
}
expect(result.message.buttonIndex).toEqual(1);
expect(result.message.frameUrl).toEqual('http://frames.com/foo');
expect(result.message.opaqueConversationIdentifier).toEqual('opaqueConversationIdentifier');
expect(result.message.verifiedWalletAddress).toEqual('verifiedWalletAddress');
});
});
5 changes: 5 additions & 0 deletions src/core/getFrameMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
NEYNAR_DEFAULT_API_KEY,
neynarFrameValidation,
} from '../utils/neynar/frame/neynarFrameFunctions';
import { isXmtpFrameResponse, validateXmtpFrameResponse } from '../utils/xmtp/validation';

type FrameMessageOptions =
| {
Expand All @@ -23,6 +24,9 @@ async function getFrameMessage(
body: FrameRequest,
messageOptions?: FrameMessageOptions,
): Promise<FrameValidationResponse> {
if (isXmtpFrameResponse(body)) {
return await validateXmtpFrameResponse(body);
}
// Validate the message
const response = await neynarFrameValidation(
body?.trustedData?.messageBytes,
Expand All @@ -34,6 +38,7 @@ async function getFrameMessage(
return {
isValid: true,
message: response,
clientType: 'farcaster',
};
} else {
// Security best practice, don't return anything if we can't validate the frame.
Expand Down
15 changes: 12 additions & 3 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { FramePostUntrustedData as XmtpUntrustedData } from '@xmtp/frames-validator';
import { NeynarFrameValidationInternalModel } from '../utils/neynar/frame/types';
import { validateFramesPost } from '@xmtp/frames-validator';

/**
* Frame Data
Expand All @@ -25,7 +27,7 @@ export interface FrameData {
* Note: exported as public Type
*/
export interface FrameRequest {
untrustedData: FrameData;
untrustedData: FrameData | XmtpUntrustedData;
trustedData: {
messageBytes: string;
};
Expand All @@ -34,7 +36,7 @@ export interface FrameRequest {
/**
* Simplified Object model with the raw Neynar data if-needed.
*/
export interface FrameValidationData {
export interface FarcasterValidationData {
button: number; // Number of the button clicked
following: boolean; // Indicates if the viewer clicking the frame follows the cast author
input: string; // Text input from the viewer typing in the frame
Expand All @@ -49,8 +51,15 @@ export interface FrameValidationData {
valid: boolean; // Indicates if the frame is valid
}

export type XmtpValidationData = Awaited<ReturnType<typeof validateFramesPost>>['actionBody'] & {
verifiedWalletAddress: string;
};

export type FrameValidationData = XmtpValidationData | FarcasterValidationData;

export type FrameValidationResponse =
| { isValid: true; message: FrameValidationData }
| { isValid: true; message: XmtpValidationData; clientType: 'xmtp' }
| { isValid: true; message: FarcasterValidationData; clientType: 'farcaster' }
| { isValid: false; message: undefined };

export function convertToFrame(json: any) {
Expand Down
4 changes: 2 additions & 2 deletions src/utils/neynar/frame/neynarFrameFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { version } from '../../../version';
import { FrameValidationData } from '../../../core/types';
import { FarcasterValidationData } from '../../../core/types';
import { FetchError } from '../exceptions/FetchError';
import { convertToNeynarResponseModel } from './neynarFrameModels';

Expand All @@ -10,7 +10,7 @@ export async function neynarFrameValidation(
apiKey: string = NEYNAR_DEFAULT_API_KEY,
castReactionContext = true,
followContext = true,
): Promise<FrameValidationData | undefined> {
): Promise<FarcasterValidationData | undefined> {
const options = {
method: 'POST',
url: `https://api.neynar.com/v2/farcaster/frame/validate`,
Expand Down
4 changes: 2 additions & 2 deletions src/utils/neynar/frame/neynarFrameModels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FrameValidationData } from '../../../core/types';
import { FarcasterValidationData } from '../../../core/types';
import { NeynarFrameValidationInternalModel } from './types';

export function convertToNeynarResponseModel(data: any): FrameValidationData | undefined {
export function convertToNeynarResponseModel(data: any): FarcasterValidationData | undefined {
if (!data) {
return;
}
Expand Down
28 changes: 28 additions & 0 deletions src/utils/xmtp/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FramePostPayload } from '@xmtp/frames-validator';
import { FrameValidationResponse } from '../../core/types';
import { validateFramesPost } from '@xmtp/frames-validator';
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if this can be implemented independently.

I am a bit worry of how big is @xmtp/frames-validator dependency for OnchainKit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Frames validator itself is very small, but it relies on xmtp-js. The good news is that we have someone already working on removing our dependency on ethers, which is the biggest dependency of that library.

Just for my own understanding, is there a plan to run onchainkit in browsers? Or is this going to remain purely for servers? If we want browser support, there are a few other changes I'd need to make to remove dependencies on node:crypto.

Copy link
Contributor

Choose a reason for hiding this comment

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

For context, any feature in OnchainKit has to meet three bars:

  • being able to run on Web client
  • being able to run on latest Backend Node.js
  • being able to run on React Native

Copy link
Contributor

Choose a reason for hiding this comment

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

As OnchainKit has to work out of the box for all Coinbase products using these three case scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. That's very good to know. We can work with those requirements. RN is the most challenging, since it doesn't have a built in crypto module or wasm support, but we have a RN SDK so none of this is new to us.

I'll get back to you on a new timeline with those requirements in mind.

Copy link
Contributor

Choose a reason for hiding this comment

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

All the current functionality only makes sense on a server, and a lot of it is specifically designed around Next.js

Yeah that's fair. I think we did a lot of simplification with our Frames approach by using a third-party provider for validating messages.

Copy link
Contributor

Choose a reason for hiding this comment

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

Part of me is like, we should create a https://github.com/Zizzamia/a-frame-in-100-lines version that showcase how to use your library in backend for node.js and just call it the day.

After all OnchainKit is a client library, and it avoids the backend part as much as possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or in some way, OnchainKit knows how to recognize if is XMTP, an that's it, if that's the case we advice to use your library to complete other parts.

Copy link
Contributor

Choose a reason for hiding this comment

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

You see what I mean, I think it's fair to find a way to make the two working well together, instead of trying to force OnchainKit to be something is not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are good points. I'm going to take a step back and try again with a more abstract approach, where the validation logic and cryptographic libraries don't have to be embedded in onchainkit. That avoids the dependency issues and library bloat, plus it lets you potentially support more than just XMTP Frames.


export function isXmtpFrameResponse(json: any): json is FramePostPayload {
return (
!!json?.untrustedData?.opaqueConversationIdentifier && !!json.trustedData?.messageBytes.length
);
}

export async function validateXmtpFrameResponse(
data: FramePostPayload,
): Promise<FrameValidationResponse> {
try {
const { verifiedWalletAddress, actionBody } = await validateFramesPost(data);
return {
isValid: true,
message: {
...actionBody,
verifiedWalletAddress,
},
clientType: 'xmtp',
};
} catch (e) {
console.error(e);
return { isValid: false, message: undefined };
}
}
Loading