Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"eslint": "^6.7.2",
"express": "^4.17.1",
"jsdom": "^19.0.0",
"node-fetch": "^3.3.2",
Comment thread
developerfred marked this conversation as resolved.
Outdated
"nodemon": "^2.0.7",
"redis": "^4.6.10",
"sharp": "^0.30.1",
Expand Down
107 changes: 107 additions & 0 deletions src/addressResolvers/farcaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils';
Comment thread
developerfred marked this conversation as resolved.
Outdated

Comment thread
developerfred marked this conversation as resolved.
export const NAME = 'Farcaster';
const FNAMES_API_URL = 'https://fnames.farcaster.xyz/transfers?name=';
const NEYNAR_API_URL = 'https://api.neynar.com/v2/farcaster/user/';
const API_KEY = process.env.NEYNAR_API_KEY ?? '';


interface UserDetails {
username: string;
verified_addresses: {
eth_addresses: string[];
sol_addresses: string[];
};
pfp_url: string;
}

interface ApiResponse {
[address: string]: UserDetails[];
}

interface UserResult {
username?: string;
eth_addresses?: string[];
sol_addresses?: string[];
pfp_url?: string;
}


async function fetchData<T>(url: string, method: string = 'GET'): Promise<T> {
Comment thread
developerfred marked this conversation as resolved.
Outdated
const headers = {
Accept: 'application/json',
api_key: API_KEY
};
const response = await fetch(url, { method, headers });
if (!response.ok) {
throw new Error(`Failed to fetch data from the API. Status: ${response.status}`);
}
return response.json() as Promise<T>;
}

export async function lookupAddresses(
Comment thread
developerfred marked this conversation as resolved.
Outdated
addresses: string[]
): Promise<{ [key: string]: UserResult | string }> {
const results: { [key: string]: UserResult | string } = {};
try {
const addressesQuery = addresses.join(',');
Comment thread
developerfred marked this conversation as resolved.
Outdated
const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`;
const userDetails = await fetchData<ApiResponse>(url);

for (const [address, data] of Object.entries(userDetails)) {
if (Array.isArray(data) && data.length > 0) {
const {
username,
verified_addresses: { eth_addresses = [], sol_addresses = [] },
pfp_url
} = data[0];
results[address] = { username, eth_addresses, sol_addresses, pfp_url };
} else {
results[address] = 'No user found for this address.';
}
}
} catch (error) {
console.error(`Error fetching address details:`, error);
Comment thread
developerfred marked this conversation as resolved.
Outdated
throw new Error(`Error fetching address details.`);
}
return results;
}

async function fetchUserDetailsByUsername(username: string): Promise<UserResult | null> {
try {
const transferData = await fetchData<{ transfers: any[] }>(`${FNAMES_API_URL}${username}`);
if (transferData.transfers.length > 0) {
const userDetails = await fetchData<{ result: { users: UserDetails[] } }>(
`${NEYNAR_API_URL}search?q=${username}&viewer_fid=197049`
);
if (userDetails.result && userDetails.result.users.length > 0) {
const { username, verified_addresses, pfp_url } = userDetails.result.users[0];
return {
eth_addresses: verified_addresses.eth_addresses,
username
};
}
}
} catch (error) {
console.error(`Error fetching user details for ${username}:`, error);
throw new Error(`Error fetching user details for ${username}.`);
}
return null;
}

export async function resolveNames(
Comment thread
developerfred marked this conversation as resolved.
Outdated
handles: string[]
): Promise<{ [handle: string]: UserResult | string }> {
const results: { [handle: string]: UserResult | string } = {};
for (const handle of handles) {
const normalizedHandle = handle.replace('.fcast.id', '');
const userDetails = await fetchUserDetailsByUsername(normalizedHandle);
Comment thread
developerfred marked this conversation as resolved.
Outdated
if (userDetails) {
results[handle] = userDetails;
} else {
results[handle] = 'User not found or error searching for details.';
}
}
return results;
}
12 changes: 12 additions & 0 deletions test/integration/addressResolvers/farcaster.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { resolveNames, lookupAddresses } from '../../../src/addressResolvers/farcaster';
import testAddressResolver from './helper';

testAddressResolver({
name: 'Farcaster',
lookupAddresses,
resolveNames,
validAddress: '0xd1a8Dd23e356B9fAE27dF5DeF9ea025A602EC81e',
validDomain: 'codingsh.fcast.id',
blankAddress: '0x0000000000000000000000000000000000000000',
invalidDomains: ['domain.crypto', 'domain.eth', 'domain.com']
});
Comment thread
developerfred marked this conversation as resolved.