From 9dede3ccfb682f186c14f8be7fddc6b82050656c Mon Sep 17 00:00:00 2001 From: Rob Polak Date: Mon, 29 Jan 2024 10:07:05 -0600 Subject: [PATCH 1/3] fix: handle neynar responses + add additional information --- jest.config.js | 2 +- package.json | 5 +- src/core/getFrameAccountAddress.test.ts | 19 ++++- src/core/getFrameAccountAddress.ts | 15 ++-- src/core/getFrameValidatedMessage.test.ts | 19 ++++- src/core/mock.ts | 10 +-- src/internal/neynar/exceptions/FetchError.ts | 6 ++ src/internal/neynar/neynar.integ.ts | 14 ++++ src/internal/neynar/neynarClient.ts | 14 ++++ .../neynar/user/neynarUserFunctions.test.ts | 33 ++++++++ .../neynar/user/neynarUserFunctions.ts | 78 +++++++++++++++++++ 11 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 src/internal/neynar/exceptions/FetchError.ts create mode 100644 src/internal/neynar/neynar.integ.ts create mode 100644 src/internal/neynar/neynarClient.ts create mode 100644 src/internal/neynar/user/neynarUserFunctions.test.ts create mode 100644 src/internal/neynar/user/neynarUserFunctions.ts diff --git a/jest.config.js b/jest.config.js index 4430c2f448..69ea173c28 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,5 +3,5 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, - testMatch: ['**/?(*.)+(spec|test).ts'], + testMatch: ['**/?(*.)+(spec|test|integ).ts'], }; diff --git a/package.json b/package.json index 5c049f8a6b..fd08248cc0 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "format": "prettier --log-level warn --write .", "format:check": "prettier --check .", "prebuild": "rimraf dist", - "test": "jest .", - "test:coverage": "jest . --coverage", + "test": "jest --testPathIgnorePatterns=\\.integ\\.", + "test:integration": "jest --testPathIgnorePatterns=\\.test\\.", + "test:coverage": "jest . --coverage ", "release:check": "changeset status --verbose --since=origin/main", "release:publish": "yarn install && yarn build && changeset publish", "release:version": "changeset version && yarn install --immutable" diff --git a/src/core/getFrameAccountAddress.test.ts b/src/core/getFrameAccountAddress.test.ts index 01863e98ab..79754d5f73 100644 --- a/src/core/getFrameAccountAddress.test.ts +++ b/src/core/getFrameAccountAddress.test.ts @@ -1,6 +1,21 @@ import { getFrameAccountAddress } from './getFrameAccountAddress'; import { mockNeynarResponse } from './mock'; +const bulkUserLookupMock = jest.fn(); +jest.mock('../internal/neynar/neynarClient', () => { + return { + NeynarClient: jest.fn().mockImplementation(() => { + return { + user: { + bulkUserLookup: bulkUserLookupMock, + // other user functions can be mocked here + }, + // other properties and methods of NeynarClient can be mocked here + }; + }), + }; +}); + jest.mock('@farcaster/hub-nodejs', () => { return { getSSLHubRpcClient: jest.fn().mockReturnValue({ @@ -23,7 +38,7 @@ describe('getFrameAccountAddress', () => { it('should return the first verification for valid input', async () => { const fid = 1234; const addresses = ['0xaddr1']; - mockNeynarResponse(fid, addresses); + mockNeynarResponse(fid, addresses, bulkUserLookupMock); const response = await getFrameAccountAddress(fakeFrameData, fakeApiKey); expect(response).toEqual(addresses[0]); @@ -32,7 +47,7 @@ describe('getFrameAccountAddress', () => { it('when the call from farcaster fails we should return undefined', async () => { const fid = 1234; const addresses = ['0xaddr1']; - const { validateMock } = mockNeynarResponse(fid, addresses); + const { validateMock } = mockNeynarResponse(fid, addresses, bulkUserLookupMock); validateMock.mockClear(); validateMock.mockResolvedValue({ isOk: () => { diff --git a/src/core/getFrameAccountAddress.ts b/src/core/getFrameAccountAddress.ts index e77d10be27..f674ec7d74 100644 --- a/src/core/getFrameAccountAddress.ts +++ b/src/core/getFrameAccountAddress.ts @@ -1,4 +1,5 @@ import { getFrameValidatedMessage } from './getFrameValidatedMessage'; +import { NeynarClient } from '../internal/neynar/neynarClient'; type FidResponse = { verifications: string[]; @@ -26,16 +27,10 @@ async function getFrameAccountAddress( // Get the Farcaster ID from the message const farcasterID = validatedMessage?.data?.fid ?? 0; // Get the user verifications from the Farcaster Indexer - const options = { - method: 'GET', - url: `https://api.neynar.com/v2/farcaster/user/bulk?fids=${farcasterID}`, - headers: { accept: 'application/json', api_key: NEYNAR_API_KEY }, - }; - const resp = await fetch(options.url, { headers: options.headers }); - const responseBody = await resp.json(); - // Get the user verifications from the response - if (responseBody.users) { - const userVerifications = responseBody.users[0] as FidResponse; + const neynarClient = new NeynarClient(); + const bulkUserLookupResponse = await neynarClient.user.bulkUserLookup([farcasterID]); + if (bulkUserLookupResponse?.users) { + const userVerifications = bulkUserLookupResponse?.users[0] as FidResponse; if (userVerifications.verifications) { return userVerifications.verifications[0]; } diff --git a/src/core/getFrameValidatedMessage.test.ts b/src/core/getFrameValidatedMessage.test.ts index 93f17d51d0..98b3d2f4c1 100644 --- a/src/core/getFrameValidatedMessage.test.ts +++ b/src/core/getFrameValidatedMessage.test.ts @@ -1,6 +1,21 @@ import { mockNeynarResponse } from './mock'; import { getFrameValidatedMessage } from './getFrameValidatedMessage'; +const bulkUserLookupMock = jest.fn(); +jest.mock('../internal/neynar/neynarClient', () => { + return { + NeynarClient: jest.fn().mockImplementation(() => { + return { + user: { + bulkUserLookup: bulkUserLookupMock, + // other user functions can be mocked here + }, + // other properties and methods of NeynarClient can be mocked here + }; + }), + }; +}); + jest.mock('@farcaster/hub-nodejs', () => { return { getSSLHubRpcClient: jest.fn().mockReturnValue({ @@ -16,7 +31,7 @@ describe('getFrameValidatedMessage', () => { it('should return undefined if the message is invalid', async () => { const fid = 1234; const addresses = ['0xaddr1']; - const { validateMock } = mockNeynarResponse(fid, addresses); + const { validateMock } = mockNeynarResponse(fid, addresses, bulkUserLookupMock); validateMock.mockClear(); validateMock.mockResolvedValue({ isOk: () => { @@ -32,7 +47,7 @@ describe('getFrameValidatedMessage', () => { it('should return the message if the message is valid', async () => { const fid = 1234; const addresses = ['0xaddr1']; - mockNeynarResponse(fid, addresses); + mockNeynarResponse(fid, addresses, bulkUserLookupMock); const fakeFrameData = { trustedData: {}, }; diff --git a/src/core/mock.ts b/src/core/mock.ts index f419c2f93b..9b5e6835f9 100644 --- a/src/core/mock.ts +++ b/src/core/mock.ts @@ -14,7 +14,7 @@ export function buildFarcasterResponse(fid: number) { }; } -export function mockNeynarResponse(fid: number, addresses: string[]) { +export function mockNeynarResponse(fid: number, addresses: string[], lookupMock: jest.Mock) { const neynarResponse = { users: [ { @@ -22,6 +22,7 @@ export function mockNeynarResponse(fid: number, addresses: string[]) { }, ], }; + lookupMock.mockResolvedValue(neynarResponse); const getSSLHubRpcClientMock = require('@farcaster/hub-nodejs').getSSLHubRpcClient; const validateMock = getSSLHubRpcClientMock().validateMessage as jest.Mock; @@ -33,12 +34,7 @@ export function mockNeynarResponse(fid: number, addresses: string[]) { }); validateMock.mockResolvedValue(buildFarcasterResponse(fid)); - // Mock the response from Neynar - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve(neynarResponse), - }), - ) as jest.Mock; + return { validateMock, }; diff --git a/src/internal/neynar/exceptions/FetchError.ts b/src/internal/neynar/exceptions/FetchError.ts new file mode 100644 index 0000000000..3a3af7f9e4 --- /dev/null +++ b/src/internal/neynar/exceptions/FetchError.ts @@ -0,0 +1,6 @@ +export class FetchError extends Error { + constructor(message: string) { + super(message); + this.name = 'FetchError'; + } +} diff --git a/src/internal/neynar/neynar.integ.ts b/src/internal/neynar/neynar.integ.ts new file mode 100644 index 0000000000..adf0a11653 --- /dev/null +++ b/src/internal/neynar/neynar.integ.ts @@ -0,0 +1,14 @@ +import { NeynarClient } from './neynarClient'; + +describe('integration tests', () => { + const neynarClient = new NeynarClient(); + + it('bulk data lookup should find all users', async () => { + const fidsToLookup = [3, 194519]; // dwr and polak.eth fids + const response = await neynarClient.user.bulkUserLookup(fidsToLookup); + expect(response?.users.length).toEqual(2); + for (const user of response?.users!) { + expect(fidsToLookup).toContain(user.fid); + } + }); +}); diff --git a/src/internal/neynar/neynarClient.ts b/src/internal/neynar/neynarClient.ts new file mode 100644 index 0000000000..8a8e628846 --- /dev/null +++ b/src/internal/neynar/neynarClient.ts @@ -0,0 +1,14 @@ +import { NeynarUserFunction } from './user/neynarUserFunctions'; + +export class NeynarClient { + private apiKey; + + public user: NeynarUserFunction; + constructor(apiKey = 'NEYNAR_API_DOCS') { + this.apiKey = apiKey; + + // Import various function types at construction to have better readability and + // composability of our interfaces. + this.user = new NeynarUserFunction(apiKey); + } +} diff --git a/src/internal/neynar/user/neynarUserFunctions.test.ts b/src/internal/neynar/user/neynarUserFunctions.test.ts new file mode 100644 index 0000000000..02b98651f1 --- /dev/null +++ b/src/internal/neynar/user/neynarUserFunctions.test.ts @@ -0,0 +1,33 @@ +import { NeynarClient } from '../neynarClient'; +import { FetchError } from '../exceptions/FetchError'; + +describe('neynar user functions', () => { + let fetchMock = jest.fn(); + let status = 200; + + beforeEach(() => { + status = 200; + global.fetch = jest.fn(() => + Promise.resolve({ + status, + json: fetchMock, + }), + ) as jest.Mock; + }); + + it('should return fetch response correctly', async () => { + fetchMock.mockResolvedValue({ + users: [{ fid: 1 }], + }); + const client = new NeynarClient(); + const resp = await client.user.bulkUserLookup([1]); + expect(resp?.users[0]?.fid).toEqual(1); + }); + + it('fails on a non-200', async () => { + status = 401; + const client = new NeynarClient(); + const resp = client.user.bulkUserLookup([1]); + await expect(resp).rejects.toThrow(FetchError); + }); +}); diff --git a/src/internal/neynar/user/neynarUserFunctions.ts b/src/internal/neynar/user/neynarUserFunctions.ts new file mode 100644 index 0000000000..815c08a81f --- /dev/null +++ b/src/internal/neynar/user/neynarUserFunctions.ts @@ -0,0 +1,78 @@ +import { FetchError } from '../exceptions/FetchError'; + +export interface NeynarUserModel { + fid: number; + custody_address: string; + username: string; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + }; + }; + follower_count: number; + verifications: string[]; +} +export interface NeynarBulkUserLookupModel { + users: NeynarUserModel[]; +} + +export class NeynarUserFunction { + private apiKey; + constructor(apiKey = 'NEYNAR_API_DOCS') { + this.apiKey = apiKey; + } + async bulkUserLookup(farcasterIDs: number[]): Promise { + const options = { + method: 'GET', + url: `https://api.neynar.com/v2/farcaster/user/bulk?fids=${farcasterIDs.join(',')}`, + headers: { accept: 'application/json', api_key: this.apiKey }, + }; + const resp = await fetch(options.url, { headers: options.headers }); + if (resp.status !== 200) { + throw new FetchError(`non-200 status returned from neynar : ${resp.status}`); + } + const responseBody = await resp.json(); + return this.convertToNeynarResponseModel(responseBody); + } + + private convertToNeynarResponseModel(data: any): NeynarBulkUserLookupModel | undefined { + if (!data) { + return; + } + + const response: NeynarBulkUserLookupModel = { + users: [], + }; + + for (const user of data.users) { + const formattedUser = this.convertToNeynarUserModel(user); + if (formattedUser) { + response.users.push(formattedUser); + } + } + return response; + } + + private convertToNeynarUserModel(data: any): NeynarUserModel | undefined { + if (!data) { + return; + } + + return { + fid: data.fid ?? 0, + custody_address: data.custody_address ?? '', + username: data.username ?? '', + display_name: data.display_name ?? '', + pfp_url: data.pfp_url ?? '', + profile: { + bio: { + text: data.profile?.bio?.text ?? '', + }, + }, + follower_count: data.follower_count ?? 0, + verifications: Array.isArray(data.verifications) ? data.verifications : [], + }; + } +} From 2cf5ea39326042b455b3b8ecce770090378a9945 Mon Sep 17 00:00:00 2001 From: Rob Polak Date: Mon, 29 Jan 2024 13:25:32 -0600 Subject: [PATCH 2/3] PR Feedback --- src/core/getFrameAccountAddress.test.ts | 18 ++--- src/core/getFrameAccountAddress.ts | 5 +- src/core/getFrameValidatedMessage.test.ts | 18 ++--- src/internal/neynar/neynarClient.ts | 14 ---- .../neynar/user/neynarUserFunctions.ts | 78 ------------------- .../neynar/exceptions/FetchError.ts | 0 .../neynar/neynar.integ.ts | 7 +- .../neynar/user/neynarUserFunctions.test.ts | 10 +-- src/utils/neynar/user/neynarUserFunctions.ts | 73 +++++++++++++++++ 9 files changed, 93 insertions(+), 130 deletions(-) delete mode 100644 src/internal/neynar/neynarClient.ts delete mode 100644 src/internal/neynar/user/neynarUserFunctions.ts rename src/{internal => utils}/neynar/exceptions/FetchError.ts (100%) rename src/{internal => utils}/neynar/neynar.integ.ts (65%) rename src/{internal => utils}/neynar/user/neynarUserFunctions.test.ts (72%) create mode 100644 src/utils/neynar/user/neynarUserFunctions.ts diff --git a/src/core/getFrameAccountAddress.test.ts b/src/core/getFrameAccountAddress.test.ts index 79754d5f73..301759f546 100644 --- a/src/core/getFrameAccountAddress.test.ts +++ b/src/core/getFrameAccountAddress.test.ts @@ -1,18 +1,10 @@ import { getFrameAccountAddress } from './getFrameAccountAddress'; import { mockNeynarResponse } from './mock'; +import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; -const bulkUserLookupMock = jest.fn(); -jest.mock('../internal/neynar/neynarClient', () => { +jest.mock('../utils/neynar/user/neynarUserFunctions', () => { return { - NeynarClient: jest.fn().mockImplementation(() => { - return { - user: { - bulkUserLookup: bulkUserLookupMock, - // other user functions can be mocked here - }, - // other properties and methods of NeynarClient can be mocked here - }; - }), + neynarBulkUserLookup: jest.fn() }; }); @@ -38,7 +30,7 @@ describe('getFrameAccountAddress', () => { it('should return the first verification for valid input', async () => { const fid = 1234; const addresses = ['0xaddr1']; - mockNeynarResponse(fid, addresses, bulkUserLookupMock); + mockNeynarResponse(fid, addresses, neynarBulkUserLookup as jest.Mock); const response = await getFrameAccountAddress(fakeFrameData, fakeApiKey); expect(response).toEqual(addresses[0]); @@ -47,7 +39,7 @@ describe('getFrameAccountAddress', () => { it('when the call from farcaster fails we should return undefined', async () => { const fid = 1234; const addresses = ['0xaddr1']; - const { validateMock } = mockNeynarResponse(fid, addresses, bulkUserLookupMock); + const { validateMock } = mockNeynarResponse(fid, addresses, neynarBulkUserLookup as jest.Mock); validateMock.mockClear(); validateMock.mockResolvedValue({ isOk: () => { diff --git a/src/core/getFrameAccountAddress.ts b/src/core/getFrameAccountAddress.ts index f674ec7d74..5719a167a1 100644 --- a/src/core/getFrameAccountAddress.ts +++ b/src/core/getFrameAccountAddress.ts @@ -1,5 +1,5 @@ import { getFrameValidatedMessage } from './getFrameValidatedMessage'; -import { NeynarClient } from '../internal/neynar/neynarClient'; +import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; type FidResponse = { verifications: string[]; @@ -27,8 +27,7 @@ async function getFrameAccountAddress( // Get the Farcaster ID from the message const farcasterID = validatedMessage?.data?.fid ?? 0; // Get the user verifications from the Farcaster Indexer - const neynarClient = new NeynarClient(); - const bulkUserLookupResponse = await neynarClient.user.bulkUserLookup([farcasterID]); + const bulkUserLookupResponse = await neynarBulkUserLookup([farcasterID]); if (bulkUserLookupResponse?.users) { const userVerifications = bulkUserLookupResponse?.users[0] as FidResponse; if (userVerifications.verifications) { diff --git a/src/core/getFrameValidatedMessage.test.ts b/src/core/getFrameValidatedMessage.test.ts index 98b3d2f4c1..9df535bcc2 100644 --- a/src/core/getFrameValidatedMessage.test.ts +++ b/src/core/getFrameValidatedMessage.test.ts @@ -1,18 +1,10 @@ import { mockNeynarResponse } from './mock'; import { getFrameValidatedMessage } from './getFrameValidatedMessage'; +import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; -const bulkUserLookupMock = jest.fn(); -jest.mock('../internal/neynar/neynarClient', () => { +jest.mock('../utils/neynar/user/neynarUserFunctions', () => { return { - NeynarClient: jest.fn().mockImplementation(() => { - return { - user: { - bulkUserLookup: bulkUserLookupMock, - // other user functions can be mocked here - }, - // other properties and methods of NeynarClient can be mocked here - }; - }), + neynarBulkUserLookup: jest.fn() }; }); @@ -31,7 +23,7 @@ describe('getFrameValidatedMessage', () => { it('should return undefined if the message is invalid', async () => { const fid = 1234; const addresses = ['0xaddr1']; - const { validateMock } = mockNeynarResponse(fid, addresses, bulkUserLookupMock); + const { validateMock } = mockNeynarResponse(fid, addresses, neynarBulkUserLookup as jest.Mock); validateMock.mockClear(); validateMock.mockResolvedValue({ isOk: () => { @@ -47,7 +39,7 @@ describe('getFrameValidatedMessage', () => { it('should return the message if the message is valid', async () => { const fid = 1234; const addresses = ['0xaddr1']; - mockNeynarResponse(fid, addresses, bulkUserLookupMock); + mockNeynarResponse(fid, addresses, neynarBulkUserLookup as jest.Mock); const fakeFrameData = { trustedData: {}, }; diff --git a/src/internal/neynar/neynarClient.ts b/src/internal/neynar/neynarClient.ts deleted file mode 100644 index 8a8e628846..0000000000 --- a/src/internal/neynar/neynarClient.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NeynarUserFunction } from './user/neynarUserFunctions'; - -export class NeynarClient { - private apiKey; - - public user: NeynarUserFunction; - constructor(apiKey = 'NEYNAR_API_DOCS') { - this.apiKey = apiKey; - - // Import various function types at construction to have better readability and - // composability of our interfaces. - this.user = new NeynarUserFunction(apiKey); - } -} diff --git a/src/internal/neynar/user/neynarUserFunctions.ts b/src/internal/neynar/user/neynarUserFunctions.ts deleted file mode 100644 index 815c08a81f..0000000000 --- a/src/internal/neynar/user/neynarUserFunctions.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { FetchError } from '../exceptions/FetchError'; - -export interface NeynarUserModel { - fid: number; - custody_address: string; - username: string; - display_name: string; - pfp_url: string; - profile: { - bio: { - text: string; - }; - }; - follower_count: number; - verifications: string[]; -} -export interface NeynarBulkUserLookupModel { - users: NeynarUserModel[]; -} - -export class NeynarUserFunction { - private apiKey; - constructor(apiKey = 'NEYNAR_API_DOCS') { - this.apiKey = apiKey; - } - async bulkUserLookup(farcasterIDs: number[]): Promise { - const options = { - method: 'GET', - url: `https://api.neynar.com/v2/farcaster/user/bulk?fids=${farcasterIDs.join(',')}`, - headers: { accept: 'application/json', api_key: this.apiKey }, - }; - const resp = await fetch(options.url, { headers: options.headers }); - if (resp.status !== 200) { - throw new FetchError(`non-200 status returned from neynar : ${resp.status}`); - } - const responseBody = await resp.json(); - return this.convertToNeynarResponseModel(responseBody); - } - - private convertToNeynarResponseModel(data: any): NeynarBulkUserLookupModel | undefined { - if (!data) { - return; - } - - const response: NeynarBulkUserLookupModel = { - users: [], - }; - - for (const user of data.users) { - const formattedUser = this.convertToNeynarUserModel(user); - if (formattedUser) { - response.users.push(formattedUser); - } - } - return response; - } - - private convertToNeynarUserModel(data: any): NeynarUserModel | undefined { - if (!data) { - return; - } - - return { - fid: data.fid ?? 0, - custody_address: data.custody_address ?? '', - username: data.username ?? '', - display_name: data.display_name ?? '', - pfp_url: data.pfp_url ?? '', - profile: { - bio: { - text: data.profile?.bio?.text ?? '', - }, - }, - follower_count: data.follower_count ?? 0, - verifications: Array.isArray(data.verifications) ? data.verifications : [], - }; - } -} diff --git a/src/internal/neynar/exceptions/FetchError.ts b/src/utils/neynar/exceptions/FetchError.ts similarity index 100% rename from src/internal/neynar/exceptions/FetchError.ts rename to src/utils/neynar/exceptions/FetchError.ts diff --git a/src/internal/neynar/neynar.integ.ts b/src/utils/neynar/neynar.integ.ts similarity index 65% rename from src/internal/neynar/neynar.integ.ts rename to src/utils/neynar/neynar.integ.ts index adf0a11653..fb28f664f2 100644 --- a/src/internal/neynar/neynar.integ.ts +++ b/src/utils/neynar/neynar.integ.ts @@ -1,11 +1,10 @@ -import { NeynarClient } from './neynarClient'; -describe('integration tests', () => { - const neynarClient = new NeynarClient(); +import {neynarBulkUserLookup} from "./user/neynarUserFunctions"; +describe('integration tests', () => { it('bulk data lookup should find all users', async () => { const fidsToLookup = [3, 194519]; // dwr and polak.eth fids - const response = await neynarClient.user.bulkUserLookup(fidsToLookup); + const response = await neynarBulkUserLookup(fidsToLookup); expect(response?.users.length).toEqual(2); for (const user of response?.users!) { expect(fidsToLookup).toContain(user.fid); diff --git a/src/internal/neynar/user/neynarUserFunctions.test.ts b/src/utils/neynar/user/neynarUserFunctions.test.ts similarity index 72% rename from src/internal/neynar/user/neynarUserFunctions.test.ts rename to src/utils/neynar/user/neynarUserFunctions.test.ts index 02b98651f1..62ad17b05e 100644 --- a/src/internal/neynar/user/neynarUserFunctions.test.ts +++ b/src/utils/neynar/user/neynarUserFunctions.test.ts @@ -1,5 +1,6 @@ -import { NeynarClient } from '../neynarClient'; import { FetchError } from '../exceptions/FetchError'; +import {neynarBulkUserLookup} from "./neynarUserFunctions"; + describe('neynar user functions', () => { let fetchMock = jest.fn(); @@ -19,15 +20,14 @@ describe('neynar user functions', () => { fetchMock.mockResolvedValue({ users: [{ fid: 1 }], }); - const client = new NeynarClient(); - const resp = await client.user.bulkUserLookup([1]); + + const resp = await neynarBulkUserLookup([1]); expect(resp?.users[0]?.fid).toEqual(1); }); it('fails on a non-200', async () => { status = 401; - const client = new NeynarClient(); - const resp = client.user.bulkUserLookup([1]); + const resp = neynarBulkUserLookup([1]); await expect(resp).rejects.toThrow(FetchError); }); }); diff --git a/src/utils/neynar/user/neynarUserFunctions.ts b/src/utils/neynar/user/neynarUserFunctions.ts new file mode 100644 index 0000000000..c0a396e101 --- /dev/null +++ b/src/utils/neynar/user/neynarUserFunctions.ts @@ -0,0 +1,73 @@ +import { FetchError } from '../exceptions/FetchError'; + +export const NEYNAR_DEFAULT_API_KEY = "NEYNAR_API_DOCS"; +export interface NeynarUserModel { + fid: number; + custody_address: string; + username: string; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + }; + }; + follower_count: number; + verifications: string[]; +} +export interface NeynarBulkUserLookupModel { + users: NeynarUserModel[]; +} + +export async function neynarBulkUserLookup(farcasterIDs: number[], apiKey: string = NEYNAR_DEFAULT_API_KEY): Promise { + const options = { + method: 'GET', + url: `https://api.neynar.com/v2/farcaster/user/bulk?fids=${farcasterIDs.join(',')}`, + headers: { accept: 'application/json', api_key: apiKey }, + }; + const resp = await fetch(options.url, { headers: options.headers }); + if (resp.status !== 200) { + throw new FetchError(`non-200 status returned from neynar : ${resp.status}`); + } + const responseBody = await resp.json(); + return convertToNeynarResponseModel(responseBody); +} + +function convertToNeynarResponseModel(data: any): NeynarBulkUserLookupModel | undefined { + if (!data) { + return; + } + + const response: NeynarBulkUserLookupModel = { + users: [], + }; + + for (const user of data.users) { + const formattedUser = convertToNeynarUserModel(user); + if (formattedUser) { + response.users.push(formattedUser); + } + } + return response; +} + +function convertToNeynarUserModel(data: any): NeynarUserModel | undefined { + if (!data) { + return; + } + + return { + fid: data.fid ?? 0, + custody_address: data.custody_address ?? '', + username: data.username ?? '', + display_name: data.display_name ?? '', + pfp_url: data.pfp_url ?? '', + profile: { + bio: { + text: data.profile?.bio?.text ?? '', + }, + }, + follower_count: data.follower_count ?? 0, + verifications: Array.isArray(data.verifications) ? data.verifications : [], + }; +} \ No newline at end of file From a654fba956789ccb2ad380ae29c0ecdfc171332c Mon Sep 17 00:00:00 2001 From: Rob Polak Date: Mon, 29 Jan 2024 13:29:24 -0600 Subject: [PATCH 3/3] formatting --- src/core/getFrameAccountAddress.test.ts | 4 ++-- src/core/getFrameAccountAddress.ts | 2 +- src/core/getFrameValidatedMessage.test.ts | 4 ++-- src/utils/neynar/neynar.integ.ts | 3 +-- src/utils/neynar/user/neynarUserFunctions.test.ts | 3 +-- src/utils/neynar/user/neynarUserFunctions.ts | 9 ++++++--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/core/getFrameAccountAddress.test.ts b/src/core/getFrameAccountAddress.test.ts index 301759f546..df462ae406 100644 --- a/src/core/getFrameAccountAddress.test.ts +++ b/src/core/getFrameAccountAddress.test.ts @@ -1,10 +1,10 @@ import { getFrameAccountAddress } from './getFrameAccountAddress'; import { mockNeynarResponse } from './mock'; -import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; +import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; jest.mock('../utils/neynar/user/neynarUserFunctions', () => { return { - neynarBulkUserLookup: jest.fn() + neynarBulkUserLookup: jest.fn(), }; }); diff --git a/src/core/getFrameAccountAddress.ts b/src/core/getFrameAccountAddress.ts index 5719a167a1..e1fb6d602b 100644 --- a/src/core/getFrameAccountAddress.ts +++ b/src/core/getFrameAccountAddress.ts @@ -1,5 +1,5 @@ import { getFrameValidatedMessage } from './getFrameValidatedMessage'; -import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; +import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; type FidResponse = { verifications: string[]; diff --git a/src/core/getFrameValidatedMessage.test.ts b/src/core/getFrameValidatedMessage.test.ts index 9df535bcc2..158f83a8ad 100644 --- a/src/core/getFrameValidatedMessage.test.ts +++ b/src/core/getFrameValidatedMessage.test.ts @@ -1,10 +1,10 @@ import { mockNeynarResponse } from './mock'; import { getFrameValidatedMessage } from './getFrameValidatedMessage'; -import {neynarBulkUserLookup} from "../utils/neynar/user/neynarUserFunctions"; +import { neynarBulkUserLookup } from '../utils/neynar/user/neynarUserFunctions'; jest.mock('../utils/neynar/user/neynarUserFunctions', () => { return { - neynarBulkUserLookup: jest.fn() + neynarBulkUserLookup: jest.fn(), }; }); diff --git a/src/utils/neynar/neynar.integ.ts b/src/utils/neynar/neynar.integ.ts index fb28f664f2..17d6714399 100644 --- a/src/utils/neynar/neynar.integ.ts +++ b/src/utils/neynar/neynar.integ.ts @@ -1,5 +1,4 @@ - -import {neynarBulkUserLookup} from "./user/neynarUserFunctions"; +import { neynarBulkUserLookup } from './user/neynarUserFunctions'; describe('integration tests', () => { it('bulk data lookup should find all users', async () => { diff --git a/src/utils/neynar/user/neynarUserFunctions.test.ts b/src/utils/neynar/user/neynarUserFunctions.test.ts index 62ad17b05e..c48152d66d 100644 --- a/src/utils/neynar/user/neynarUserFunctions.test.ts +++ b/src/utils/neynar/user/neynarUserFunctions.test.ts @@ -1,6 +1,5 @@ import { FetchError } from '../exceptions/FetchError'; -import {neynarBulkUserLookup} from "./neynarUserFunctions"; - +import { neynarBulkUserLookup } from './neynarUserFunctions'; describe('neynar user functions', () => { let fetchMock = jest.fn(); diff --git a/src/utils/neynar/user/neynarUserFunctions.ts b/src/utils/neynar/user/neynarUserFunctions.ts index c0a396e101..a16b3b52e2 100644 --- a/src/utils/neynar/user/neynarUserFunctions.ts +++ b/src/utils/neynar/user/neynarUserFunctions.ts @@ -1,6 +1,6 @@ import { FetchError } from '../exceptions/FetchError'; -export const NEYNAR_DEFAULT_API_KEY = "NEYNAR_API_DOCS"; +export const NEYNAR_DEFAULT_API_KEY = 'NEYNAR_API_DOCS'; export interface NeynarUserModel { fid: number; custody_address: string; @@ -19,7 +19,10 @@ export interface NeynarBulkUserLookupModel { users: NeynarUserModel[]; } -export async function neynarBulkUserLookup(farcasterIDs: number[], apiKey: string = NEYNAR_DEFAULT_API_KEY): Promise { +export async function neynarBulkUserLookup( + farcasterIDs: number[], + apiKey: string = NEYNAR_DEFAULT_API_KEY, +): Promise { const options = { method: 'GET', url: `https://api.neynar.com/v2/farcaster/user/bulk?fids=${farcasterIDs.join(',')}`, @@ -70,4 +73,4 @@ function convertToNeynarUserModel(data: any): NeynarUserModel | undefined { follower_count: data.follower_count ?? 0, verifications: Array.isArray(data.verifications) ? data.verifications : [], }; -} \ No newline at end of file +}