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

feat(farcaster): enable offchain resolver to farcaster names #194 #203

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
115 changes: 115 additions & 0 deletions src/addressResolvers/farcaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils';
developerfred marked this conversation as resolved.
Show resolved Hide resolved

developerfred marked this conversation as resolved.
Show resolved Hide 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 = 'NEYNAR_API_DOCS'; // add api key on .env
developerfred marked this conversation as resolved.
Show resolved Hide resolved

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

interface UserDetails {
developerfred marked this conversation as resolved.
Show resolved Hide resolved
[address: string]: User[] | string;
}

async function fetchData(url, options = {}) {
const response = await fetch(url, { ...options, headers: { accept: 'application/json', api_key: API_KEY, ...options.headers } });
developerfred marked this conversation as resolved.
Show resolved Hide resolved
developerfred marked this conversation as resolved.
Show resolved Hide resolved
if (!response.ok) {
throw new Error(`Failed to fetch data from the API. Status: ${response.status}`);
}
return response.json();
}

export async function lookupAddresses(addresses) {
const results = {};
const addressesQuery = addresses.join(',');

try {
const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`;
const userDetails = await fetchData(url, {
method: 'GET'
developerfred marked this conversation as resolved.
Show resolved Hide resolved
});

Object.entries(userDetails).forEach(([address, data]) => {
if (Array.isArray(data) && data.length > 0) {
const user = data[0];
if ('username' in user && 'verified_addresses' in user && 'pfp_url' in user) {
results[address] = {
username: user.username,
eth_addresses: user.verified_addresses.eth_addresses ?? [],
sol_addresses: user.verified_addresses.sol_addresses ?? [],
pfp_url: user.pfp_url
};
} else {
console.warn(`Incomplete user data for address: ${address}`);
results[address] = "Incomplete user data.";
}
} else {
results[address] = "No user found for this address.";
}
});

return results;
developerfred marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
console.error(`Error fetching address details:`, error);
throw new Error(`Error fetching address details.`);
}
}

async function fetchUserDetailsByUsername(username) {
try {
const transferData = await fetchData(`${FNAMES_API_URL}${username}`);
if (transferData.transfers.length > 0) {
const fid = 197049; // using fid arbitrary to use neymar search api
const userDetails = await fetchData(
`${NEYNAR_API_URL}search?q=${username}&viewer_fid=${fid}`,
{
method: 'GET'
}
);
if (userDetails.result && userDetails.result.users.length > 0) {
const user = userDetails.result.users[0];
return {
username: user.username,
verified_addresses: {
eth_addresses: user.verified_addresses.eth_addresses,
sol_addresses: user.verified_addresses.sol_addresses
},
pfp: user.pfp.url
developerfred marked this conversation as resolved.
Show resolved Hide resolved
};
}
}
} catch (error) {
console.error(`Error fetching user details ${username}:`, error);
throw new FetchError(`Error fetching user details ${username}.`);
}
return null;
}


export async function resolveNames(handles) {
const results = {};

for (const handle of handles) {
const normalizedHandle = handle.includes('.fcast.id') ? handle.split('.fcast.id')[0] : handle;
const userDetails = await fetchUserDetailsByUsername(normalizedHandle);
if (userDetails) {
results[handle] = {
eth_addresses: userDetails.verified_addresses.eth_addresses,
sol_addresses: userDetails.verified_addresses.sol_addresses,
pfp_url: userDetails.pfp
};
} else {
results[handle] = "User not found or error searching for details.";
developerfred marked this conversation as resolved.
Show resolved Hide resolved
}
}

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']
});
developerfred marked this conversation as resolved.
Show resolved Hide resolved