From ee72476e618d4c68b557f2763e31f856de0554df Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Mon, 19 Feb 2024 22:28:39 -0500 Subject: [PATCH] feat: `getFrameMessage` can now handle mock frame messages. (#149) --- .changeset/cuddly-frogs-flow.md | 5 +++ src/core/getFrameMessage.test.ts | 63 ++++++++++++++++++++++++++++++++ src/core/getFrameMessage.ts | 15 +++++++- src/core/getMockFrameRequest.ts | 37 +++++++++++++++++++ src/core/types.ts | 23 ++++++++++++ src/index.ts | 3 ++ 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 .changeset/cuddly-frogs-flow.md create mode 100644 src/core/getMockFrameRequest.ts diff --git a/.changeset/cuddly-frogs-flow.md b/.changeset/cuddly-frogs-flow.md new file mode 100644 index 0000000000..0c9628e6d0 --- /dev/null +++ b/.changeset/cuddly-frogs-flow.md @@ -0,0 +1,5 @@ +--- +'@coinbase/onchainkit': minor +--- + +- **feat**: `getFrameMessage` can now handle mock frame messages. When `allowFramegear` is passed as an option (defaults to `false`), it will skip validating which facilitates testing locally running apps with future releases of `framegear`. diff --git a/src/core/getFrameMessage.test.ts b/src/core/getFrameMessage.test.ts index bfa2a90cd6..acc90f1f58 100644 --- a/src/core/getFrameMessage.test.ts +++ b/src/core/getFrameMessage.test.ts @@ -1,5 +1,6 @@ import { mockNeynarResponse } from './mock'; import { getFrameMessage } from './getFrameMessage'; +import { getMockFrameRequest } from './getMockFrameRequest'; import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; import { FrameRequest } from './types'; import { neynarFrameValidation } from '../utils/neynar/frame/neynarFrameFunctions'; @@ -24,6 +25,68 @@ describe('getFrameValidatedMessage', () => { expect(result?.isValid).toEqual(false); }); + it('should consider invalid non-mock requests as invalid, even if mock requests are allowed', async () => { + const result = await getFrameMessage( + { + trustedData: { messageBytes: 'invalid' }, + } as FrameRequest, + { allowFramegear: true }, + ); + expect(result?.isValid).toEqual(false); + expect(result.message).toBeUndefined(); + }); + + it('should consider mock messages valid, if allowed', async () => { + const result = await getFrameMessage( + getMockFrameRequest({ + untrustedData: { + buttonIndex: 1, + castId: { + fid: 0, + hash: '0xthisisnotreal', + }, + inputText: '', + fid: 0, + network: 0, + messageHash: '0xthisisnotreal', + timestamp: 0, + url: 'https://localhost:3000', + }, + trustedData: { + messageBytes: '0xthisisnotreal', + }, + }), + { allowFramegear: true }, + ); + expect(result?.isValid).toEqual(true); + expect(result.message?.button).toEqual(1); + }); + + it('should consider mock messages invalid, if not allowed (default)', async () => { + const result = await getFrameMessage( + getMockFrameRequest({ + untrustedData: { + buttonIndex: 1, + castId: { + fid: 0, + hash: '0xthisisnotreal', + }, + inputText: '', + fid: 0, + network: 0, + messageHash: '0xthisisnotreal', + timestamp: 0, + url: 'https://localhost:3000', + }, + trustedData: { + messageBytes: '0xthisisnotreal', + }, + }), + ); + expect(result?.isValid).toEqual(false); + expect(result.message).toBeUndefined(); + }); + it('should return the message if the message is valid', async () => { const fid = 1234; const addresses = ['0xaddr1']; diff --git a/src/core/getFrameMessage.ts b/src/core/getFrameMessage.ts index 8450fd55a1..a0f6d73692 100644 --- a/src/core/getFrameMessage.ts +++ b/src/core/getFrameMessage.ts @@ -1,4 +1,4 @@ -import { FrameRequest, FrameValidationResponse } from './types'; +import { FrameRequest, FrameValidationResponse, MockFrameRequest } from './types'; import { NEYNAR_DEFAULT_API_KEY, neynarFrameValidation, @@ -9,6 +9,7 @@ type FrameMessageOptions = neynarApiKey?: string; castReactionContext?: boolean; followContext?: boolean; + allowFramegear?: boolean; } | undefined; @@ -20,9 +21,19 @@ type FrameMessageOptions = * @param body The JSON received by server on frame callback */ async function getFrameMessage( - body: FrameRequest, + body: FrameRequest | MockFrameRequest, messageOptions?: FrameMessageOptions, ): Promise { + // Skip validation only when allowed and when receiving a request from framegear + if (messageOptions?.allowFramegear) { + if ((body as MockFrameRequest).mockFrameData) { + return { + isValid: true, + message: (body as MockFrameRequest).mockFrameData, + }; + } + } + // Validate the message const response = await neynarFrameValidation( body?.trustedData?.messageBytes, diff --git a/src/core/getMockFrameRequest.ts b/src/core/getMockFrameRequest.ts new file mode 100644 index 0000000000..93c2acbb11 --- /dev/null +++ b/src/core/getMockFrameRequest.ts @@ -0,0 +1,37 @@ +import { FrameRequest, MockFrameRequest, MockFrameRequestOptions } from './types'; + +/** + * Modify a standard frame request to include simulated values (e.g., indicate the viewer + * follows the cast author) for development/debugging purposes. + * @param request A standard frame request. + * @param options An object containing values we will pretend are real for the purposes of debugging. + * @returns + */ +function getMockFrameRequest( + request: FrameRequest, + options?: MockFrameRequestOptions, +): MockFrameRequest { + return { + ...request, + mockFrameData: { + button: request.untrustedData.buttonIndex, + input: request.untrustedData.inputText, + following: !!options?.following, + interactor: { + fid: options?.interactor?.fid || 0, + custody_address: options?.interactor?.custody_address || '0xnotarealaddress', + verified_accounts: options?.interactor?.verified_accounts || [], + }, + liked: !!options?.liked, + recasted: !!options?.recasted, + valid: true, + raw: { + valid: true, + // TODO: unjank + action: {} as any, + }, + }, + }; +} + +export { getMockFrameRequest }; diff --git a/src/core/types.ts b/src/core/types.ts index f87d1864a4..2cde916435 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -175,3 +175,26 @@ export type EASChainDefinition = { id: number; // blockchain source id schemaUids: EASSchemaUid[]; // Array of EAS Schema UIDs }; + +/** + * Settings to simulate statuses on mock frames. + * + * Note: exported as public Type + */ +export type MockFrameRequestOptions = { + following?: boolean; // Indicates if the viewer clicking the frame follows the cast author + interactor?: { + fid?: number; // Viewer Farcaster ID + custody_address?: string; // Viewer custody address + verified_accounts?: string[]; // Viewer account addresses + }; + liked?: boolean; // Indicates if the viewer clicking the frame liked the cast + recasted?: boolean; // Indicates if the viewer clicking the frame recasted the cast +}; + +/** + * A mock frame request payload + * + * Note: exported as public Type + */ +export type MockFrameRequest = FrameRequest & { mockFrameData: Required }; diff --git a/src/index.ts b/src/index.ts index b4aaf07920..b66026d08b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { getEASAttestations } from './core/getEASAttestations'; export { getFrameHtmlResponse } from './core/getFrameHtmlResponse'; export { getFrameMetadata } from './core/getFrameMetadata'; export { getFrameMessage } from './core/getFrameMessage'; +export { getMockFrameRequest } from './core/getMockFrameRequest'; export { FrameMetadata } from './components/FrameMetadata'; export { Avatar } from './components/Avatar'; export { Name } from './components/Name'; @@ -17,4 +18,6 @@ export type { FrameMetadataType, FrameRequest, FrameValidationData, + MockFrameRequest, + MockFrameRequestOptions, } from './core/types';