-
Notifications
You must be signed in to change notification settings - Fork 167
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
fix: handle neynar responses + add additional information #35
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should create a new instance of So far it seems we are doing:
So my personal feeling after reading this three times, I think we should avoid using Classes just because:
@robpolak would you be ok to rewrite this as Functional-oriented instead of Class-oriented? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. classes have been purged.. it does make testing much easier. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dope, reading the PR now |
||
const bulkUserLookupResponse = await neynarClient.user.bulkUserLookup([farcasterID]); | ||
if (bulkUserLookupResponse?.users) { | ||
const userVerifications = bulkUserLookupResponse?.users[0] as FidResponse; | ||
if (userVerifications.verifications) { | ||
return userVerifications.verifications[0]; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class FetchError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = 'FetchError'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. QQ: would we be better off with the official SDK? Implementing our own client on top of their HTTP api is more work than we might want to do over the long run. One con to using the SDK would be that the bundlesize is a bit high: https://bundlephobia.com/package/@neynar/[email protected] though because this would be running server-side it's not as big a deal cc @Zizzamia There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be open to it.. looks like most of the library is auto-generated.. I wanted to keep our imports lightweight, but it is a great question on future support / extensibility. |
||
private apiKey; | ||
constructor(apiKey = 'NEYNAR_API_DOCS') { | ||
this.apiKey = apiKey; | ||
} | ||
async bulkUserLookup(farcasterIDs: number[]): Promise<NeynarBulkUserLookupModel | undefined> { | ||
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 : [], | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, is
integ
the new way folks calle2e
?I am not agaist that, I am just curios as I never saw this prefix before.
cc @robpolak @cnasc