From ba119eafa4e731b6318110fbe86219cdc48ef84e Mon Sep 17 00:00:00 2001 From: codingsh Date: Sun, 31 Mar 2024 15:42:00 -0300 Subject: [PATCH 1/9] feat(farcaster): enable offchain resolver to farcaster names #194 --- src/addressResolvers/farcaster.ts | 112 ++++++++++++++++++ .../addressResolvers/farcaster.test.ts | 12 ++ 2 files changed, 124 insertions(+) create mode 100644 src/addressResolvers/farcaster.ts create mode 100644 test/integration/addressResolvers/farcaster.test.ts diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts new file mode 100644 index 00000000..eb6c82ba --- /dev/null +++ b/src/addressResolvers/farcaster.ts @@ -0,0 +1,112 @@ +import { capture } from '@snapshot-labs/snapshot-sentry'; +import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils'; + +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/search'; +const API_KEY = 'NEYNAR_API_DOCS'; // add api key on .env + +interface User { + username: string; + verified_addresses: { + eth_addresses: string[]; + sol_addresses: string[]; + }; + pfp_url: string; +} + +interface UserDetails { + [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 } }); + if (!response.ok) { + throw new Error(`Falha ao buscar dados da API. Status: ${response.status}`); + } + return response.json(); +} + +export async function lookupAddresses(addresses) { + const results = {}; + const addressesQuery = addresses.join(','); + + try { + const url = `${NEYNAR_API_URL}?addresses=${addressesQuery}`; + const userDetails = await fetchData(url, { + method: 'GET' + }); + + 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; + } 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}?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 + }; + } + } + } 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."; + } + } + + return results; +} diff --git a/test/integration/addressResolvers/farcaster.test.ts b/test/integration/addressResolvers/farcaster.test.ts new file mode 100644 index 00000000..075c804c --- /dev/null +++ b/test/integration/addressResolvers/farcaster.test.ts @@ -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'] +}); From c852b1de7dd71a7bb4552fcbff7609ecb43eaa0c Mon Sep 17 00:00:00 2001 From: codingsh Date: Sun, 31 Mar 2024 15:48:38 -0300 Subject: [PATCH 2/9] chore(farcaster): fix neynar api url users --- src/addressResolvers/farcaster.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index eb6c82ba..4b286c37 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -3,7 +3,7 @@ import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress 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/search'; +const NEYNAR_API_URL = 'https://api.neynar.com/v2/farcaster/user/'; const API_KEY = 'NEYNAR_API_DOCS'; // add api key on .env interface User { @@ -32,7 +32,7 @@ export async function lookupAddresses(addresses) { const addressesQuery = addresses.join(','); try { - const url = `${NEYNAR_API_URL}?addresses=${addressesQuery}`; + const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; const userDetails = await fetchData(url, { method: 'GET' }); @@ -68,9 +68,12 @@ async function fetchUserDetailsByUsername(username) { 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}?q=${username}&viewer_fid=${fid}`, { - method: 'GET', - }); + 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 { From f04f71cac414032879948a0519c9139ee1fefc14 Mon Sep 17 00:00:00 2001 From: codingsh Date: Sun, 31 Mar 2024 15:50:43 -0300 Subject: [PATCH 3/9] Update farcaster.ts --- src/addressResolvers/farcaster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index 4b286c37..7317bd11 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -22,7 +22,7 @@ interface UserDetails { async function fetchData(url, options = {}) { const response = await fetch(url, { ...options, headers: { accept: 'application/json', api_key: API_KEY, ...options.headers } }); if (!response.ok) { - throw new Error(`Falha ao buscar dados da API. Status: ${response.status}`); + throw new Error(`Failed to fetch data from the API. Status: ${response.status}`); } return response.json(); } From 215654f56b4cfabcc90a4c138804c84076b7eba1 Mon Sep 17 00:00:00 2001 From: codingsh Date: Wed, 10 Apr 2024 20:05:26 +0000 Subject: [PATCH 4/9] chore(fetch): fixed farcaster script --- package.json | 1 + src/addressResolvers/farcaster.ts | 162 ++++++++++++++---------------- 2 files changed, 78 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index dc8c204d..28b72c7a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "eslint": "^6.7.2", "express": "^4.17.1", "jsdom": "^19.0.0", + "node-fetch": "^3.3.2", "nodemon": "^2.0.7", "redis": "^4.6.10", "sharp": "^0.30.1", diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index 7317bd11..3e331f3a 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -4,9 +4,10 @@ import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress 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 +const API_KEY = process.env.NEYNAR_API_KEY ?? ''; -interface User { + +interface UserDetails { username: string; verified_addresses: { eth_addresses: string[]; @@ -15,101 +16,92 @@ interface User { pfp_url: string; } -interface UserDetails { - [address: string]: User[] | string; +interface ApiResponse { + [address: string]: UserDetails[]; } -async function fetchData(url, options = {}) { - const response = await fetch(url, { ...options, headers: { accept: 'application/json', api_key: API_KEY, ...options.headers } }); - if (!response.ok) { - throw new Error(`Failed to fetch data from the API. Status: ${response.status}`); - } - return response.json(); +interface UserResult { + username?: string; + eth_addresses?: string[]; + sol_addresses?: string[]; + pfp_url?: string; } -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' - }); +async function fetchData(url: string, method: string = 'GET'): Promise { + 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; +} - 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."; - } - }); +export async function lookupAddresses( + addresses: string[] +): Promise<{ [key: string]: UserResult | string }> { + const results: { [key: string]: UserResult | string } = {}; + try { + const addressesQuery = addresses.join(','); + const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; + const userDetails = await fetchData(url); - return results; - } catch (error) { - console.error(`Error fetching address details:`, error); - throw new Error(`Error fetching address details.`); + 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); + throw new Error(`Error fetching address details.`); + } + return results; } -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 - }; - } - } - } catch (error) { - console.error(`Error fetching user details ${username}:`, error); - throw new FetchError(`Error fetching user details ${username}.`); +async function fetchUserDetailsByUsername(username: string): Promise { + 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 + }; + } } - return null; + } 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(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."; - } +export async function resolveNames( + 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); + if (userDetails) { + results[handle] = userDetails; + } else { + results[handle] = 'User not found or error searching for details.'; } - - return results; + } + return results; } From 5027da0af69387d2342fd22acfa93c987bd773b4 Mon Sep 17 00:00:00 2001 From: codingsh Date: Wed, 10 Apr 2024 20:59:49 +0000 Subject: [PATCH 5/9] chore(farcaster): fix function to running test --- package.json | 3 +- src/addressResolvers/farcaster.ts | 46 ++++++++++++++----------------- src/addressResolvers/index.ts | 9 +++++- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 28b72c7a..fb552661 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "redis": "^4.6.10", "sharp": "^0.30.1", "ts-node": "^10.8.1", - "typescript": "^4.7.3" + "typescript": "^4.7.3", + "viem": "^2.9.15" }, "devDependencies": { "@types/express": "^4.17.11", diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index 3e331f3a..01ca070d 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -1,12 +1,10 @@ -import { capture } from '@snapshot-labs/snapshot-sentry'; -import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils'; +import { getAddress } from 'viem'; 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: { @@ -27,8 +25,7 @@ interface UserResult { pfp_url?: string; } - -async function fetchData(url: string, method: string = 'GET'): Promise { +async function fetchData(url: string, method = 'GET'): Promise { const headers = { Accept: 'application/json', api_key: API_KEY @@ -40,23 +37,17 @@ async function fetchData(url: string, method: string = 'GET'): Promise { return response.json() as Promise; } -export async function lookupAddresses( - addresses: string[] -): Promise<{ [key: string]: UserResult | string }> { - const results: { [key: string]: UserResult | string } = {}; +export async function lookupAddresses(addresses: string[]): Promise<{ [key: string]: string }> { + const results: { [key: string]: string } = {}; try { const addressesQuery = addresses.join(','); const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; const userDetails = await fetchData(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 }; + if (Array.isArray(data) && data.length > 0 && data[0].username) { + const checksumAddress = getAddress(address); + results[checksumAddress] = data[0].username + '.fcast.id'; } else { results[address] = 'No user found for this address.'; } @@ -68,6 +59,7 @@ export async function lookupAddresses( return results; } + async function fetchUserDetailsByUsername(username: string): Promise { try { const transferData = await fetchData<{ transfers: any[] }>(`${FNAMES_API_URL}${username}`); @@ -76,9 +68,12 @@ async function fetchUserDetailsByUsername(username: string): Promise 0) { - const { username, verified_addresses, pfp_url } = userDetails.result.users[0]; + const { username, verified_addresses } = userDetails.result.users[0]; + const eth_addresses_checksummed = verified_addresses.eth_addresses.map(address => + getAddress(address) + ); return { - eth_addresses: verified_addresses.eth_addresses, + eth_addresses: eth_addresses_checksummed, username }; } @@ -90,18 +85,17 @@ async function fetchUserDetailsByUsername(username: string): Promise { - const results: { [handle: string]: UserResult | string } = {}; +export async function resolveNames(handles: string[]): Promise<{ [handle: string]: string | undefined }> { + const results: { [handle: string]: string | undefined } = {}; for (const handle of handles) { const normalizedHandle = handle.replace('.fcast.id', ''); const userDetails = await fetchUserDetailsByUsername(normalizedHandle); - if (userDetails) { - results[handle] = userDetails; - } else { - results[handle] = 'User not found or error searching for details.'; + + if (userDetails && userDetails.eth_addresses && userDetails.eth_addresses.length > 0) { + results[handle] = userDetails.eth_addresses[0]; } } return results; } + + diff --git a/src/addressResolvers/index.ts b/src/addressResolvers/index.ts index e4f7c80c..f713f7c2 100644 --- a/src/addressResolvers/index.ts +++ b/src/addressResolvers/index.ts @@ -2,6 +2,7 @@ import * as ensResolver from './ens'; import * as lensResolver from './lens'; import * as unstoppableDomainResolver from './unstoppableDomains'; import * as starknetResolver from './starknet'; +import * as farcasterResolver from './farcaster'; import cache from './cache'; import { Address, @@ -13,7 +14,13 @@ import { } from './utils'; import { timeAddressResolverResponse as timeResponse } from '../helpers/metrics'; -const RESOLVERS = [ensResolver, unstoppableDomainResolver, lensResolver, starknetResolver]; +const RESOLVERS = [ + ensResolver, + unstoppableDomainResolver, + lensResolver, + starknetResolver, + farcasterResolver +]; const MAX_LOOKUP_ADDRESSES = 50; const MAX_RESOLVE_NAMES = 5; From de688be178d69218a756c4b3e4528d4542656481 Mon Sep 17 00:00:00 2001 From: codingsh Date: Wed, 10 Apr 2024 22:34:10 +0000 Subject: [PATCH 6/9] chore(farcaster): update types --- src/addressResolvers/farcaster.ts | 60 ++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index 01ca070d..3ee8f9a6 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -1,3 +1,5 @@ +import { capture } from '@snapshot-labs/snapshot-sentry'; +import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils'; import { getAddress } from 'viem'; export const NAME = 'Farcaster'; @@ -6,9 +8,9 @@ const NEYNAR_API_URL = 'https://api.neynar.com/v2/farcaster/user/'; const API_KEY = process.env.NEYNAR_API_KEY ?? ''; interface UserDetails { - username: string; + username: Handle; verified_addresses: { - eth_addresses: string[]; + eth_addresses: Address[]; sol_addresses: string[]; }; pfp_url: string; @@ -19,26 +21,31 @@ interface ApiResponse { } interface UserResult { - username?: string; - eth_addresses?: string[]; + username?: Handle; + eth_addresses?: Address[]; sol_addresses?: string[]; pfp_url?: string; } -async function fetchData(url: string, method = 'GET'): Promise { +export async function fetchData(url: string): Promise { const headers = { Accept: 'application/json', api_key: API_KEY }; - const response = await fetch(url, { method, headers }); + const response = await fetch(url, { headers }); if (!response.ok) { - throw new Error(`Failed to fetch data from the API. Status: ${response.status}`); + const error = new FetchError(`Failed to fetch data from the API. Status: ${response.status}`); + if (!isSilencedError(error)) { + capture(error, { tags: { issue: 'api_fetch_failure' } }); + } + throw error; } return response.json() as Promise; } -export async function lookupAddresses(addresses: string[]): Promise<{ [key: string]: string }> { - const results: { [key: string]: string } = {}; + +export async function lookupAddresses(addresses: Address[]): Promise<{ [key: Handle]: Address }> { + const results: { [key: Handle]: Address } = {}; try { const addressesQuery = addresses.join(','); const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; @@ -53,14 +60,16 @@ export async function lookupAddresses(addresses: string[]): Promise<{ [key: stri } } } catch (error) { - console.error(`Error fetching address details:`, error); - throw new Error(`Error fetching address details.`); + if (!isSilencedError(error)) { + capture(error, { input: { addresses }, tags: { issue: 'lookup_addresses_failure' } }); + } + throw new FetchError('Error fetching address details.'); } return results; } -async function fetchUserDetailsByUsername(username: string): Promise { +export async function fetchUserDetailsByUsername(username: Handle): Promise { try { const transferData = await fetchData<{ transfers: any[] }>(`${FNAMES_API_URL}${username}`); if (transferData.transfers.length > 0) { @@ -68,34 +77,43 @@ async function fetchUserDetailsByUsername(username: string): Promise 0) { - const { username, verified_addresses } = userDetails.result.users[0]; - const eth_addresses_checksummed = verified_addresses.eth_addresses.map(address => - getAddress(address) + const user = userDetails.result.users[0]; + const eth_addresses_checksummed = user.verified_addresses.eth_addresses.filter( + isEvmAddress ); + return { + username: user.username, eth_addresses: eth_addresses_checksummed, - username + sol_addresses: user.verified_addresses.sol_addresses, + pfp_url: user.pfp_url }; } } } catch (error) { - console.error(`Error fetching user details for ${username}:`, error); - throw new Error(`Error fetching user details for ${username}.`); + if (!isSilencedError(error)) { + capture(error, { input: { username }, tags: { issue: 'fetch_user_details_failure' } }); + } + throw new FetchError(`Error fetching user details for ${username}.`); } return null; } -export async function resolveNames(handles: string[]): Promise<{ [handle: string]: string | undefined }> { - const results: { [handle: string]: string | undefined } = {}; + +export async function resolveNames(handles: Handle[]): Promise> { + const results: Record = {}; for (const handle of handles) { const normalizedHandle = handle.replace('.fcast.id', ''); const userDetails = await fetchUserDetailsByUsername(normalizedHandle); if (userDetails && userDetails.eth_addresses && userDetails.eth_addresses.length > 0) { - results[handle] = userDetails.eth_addresses[0]; + const checksumAddress = getAddress(userDetails.eth_addresses[0]); + results[handle] = checksumAddress; } + } return results; } + From e0a88b8b9f8d4424cf95d1829cf25a452881e914 Mon Sep 17 00:00:00 2001 From: codingsh Date: Thu, 11 Apr 2024 14:39:34 -0300 Subject: [PATCH 7/9] chore(farcaster): add path alias and DRY functions --- .env.example | 1 + src/addressResolvers/farcaster.ts | 104 +++++++++++++++++++----------- tsconfig.json | 8 ++- 3 files changed, 76 insertions(+), 37 deletions(-) diff --git a/.env.example b/.env.example index 1c640c35..3a2e1291 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,4 @@ IPFS_GATEWAY=cloudflare-ipfs.com REDIS_URL= SENTRY_DSN= SENTRY_TRACE_SAMPLE_RATE= +NEYNAR_API_KEY= diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index 3ee8f9a6..ac719388 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -46,59 +46,91 @@ export async function fetchData(url: string): Promise { export async function lookupAddresses(addresses: Address[]): Promise<{ [key: Handle]: Address }> { const results: { [key: Handle]: Address } = {}; + try { - const addressesQuery = addresses.join(','); - const url = `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; + const url = buildLookupUrl(addresses); const userDetails = await fetchData(url); - - for (const [address, data] of Object.entries(userDetails)) { - if (Array.isArray(data) && data.length > 0 && data[0].username) { - const checksumAddress = getAddress(address); - results[checksumAddress] = data[0].username + '.fcast.id'; - } else { - results[address] = 'No user found for this address.'; - } - } + processUserDetails(userDetails, results); } catch (error) { - if (!isSilencedError(error)) { - capture(error, { input: { addresses }, tags: { issue: 'lookup_addresses_failure' } }); - } - throw new FetchError('Error fetching address details.'); + handleLookupError(error, addresses); } + return results; } +export function buildLookupUrl(addresses: Address[]): string { + const addressesQuery = addresses.join(','); + return `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; +} + +export function processUserDetails(userDetails: ApiResponse, results: { [key: Handle]: Address }): void { + for (const [address, data] of Object.entries(userDetails)) { + if (isValidUserData(data)) { + const checksumAddress = getAddress(address); + results[checksumAddress] = `${data[0].username}.fcast.id`; + } else { + results[address] = 'No user found for this address.'; + } + } +} + +export function isValidUserData(data: any): boolean { + return Array.isArray(data) && data.length > 0 && data[0].username; +} + +export function handleLookupError(error: any, addresses: Address[]): void { + if (!isSilencedError(error)) { + capture(error, { input: { addresses }, tags: { issue: 'lookup_addresses_failure' } }); + } + throw new FetchError('Error fetching address details.'); +} + + export async function fetchUserDetailsByUsername(username: Handle): Promise { 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 user = userDetails.result.users[0]; - const eth_addresses_checksummed = user.verified_addresses.eth_addresses.filter( - isEvmAddress - ); - - return { - username: user.username, - eth_addresses: eth_addresses_checksummed, - sol_addresses: user.verified_addresses.sol_addresses, - pfp_url: user.pfp_url - }; - } + const userDetails = await getUserDetails(username); + if (userDetails) { + return formatUserDetails(userDetails); } } catch (error) { - if (!isSilencedError(error)) { - capture(error, { input: { username }, tags: { issue: 'fetch_user_details_failure' } }); + handleUserDetailsError(error, username); + } + return null; +} + +export async function getUserDetails(username: Handle): Promise<{ users: UserDetails[] } | null> { + 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) { + return userDetails.result; } - throw new FetchError(`Error fetching user details for ${username}.`); } return null; } +export function formatUserDetails(userDetails: { users: UserDetails[] }): UserResult { + const user = userDetails.users[0]; + const eth_addresses_checksummed = user.verified_addresses.eth_addresses.filter(isEvmAddress); + return { + username: user.username, + eth_addresses: eth_addresses_checksummed, + sol_addresses: user.verified_addresses.sol_addresses, + pfp_url: user.pfp_url + }; +} + +export function handleUserDetailsError(error: any, username: Handle): void { + if (!isSilencedError(error)) { + capture(error, { input: { username }, tags: { issue: 'fetch_user_details_failure' } }); + } + throw new FetchError(`Error fetching user details for ${username}.`); +} + + export async function resolveNames(handles: Handle[]): Promise> { const results: Record = {}; diff --git a/tsconfig.json b/tsconfig.json index 63c891f5..1499487f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,12 @@ "skipLibCheck": true, "sourceMap": true, "inlineSources": true, - "sourceRoot": "/" + "sourceRoot": "/", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@/resolvers": ["./src/resolvers/*"], + "@/address": ["./src/addressResolvers/*"] + }, } } From fddb55f85e1856435e969aac6a58f41781c17ab6 Mon Sep 17 00:00:00 2001 From: codingsh Date: Mon, 15 Apr 2024 01:38:08 +0000 Subject: [PATCH 8/9] chore(farcaster): fix code review --- package.json | 4 +- src/addressResolvers/farcaster.ts | 145 ++++++++++++++---------------- tsconfig.json | 7 +- yarn.lock | 47 ++++++++++ 4 files changed, 119 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index fb552661..95b1cc55 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@snapshot-labs/snapshot-metrics": "^1.4.1", "@snapshot-labs/snapshot-sentry": "^1.5.5", "@snapshot-labs/snapshot.js": "^0.11.16", + "@types/node-fetch": "^2.6.11", "@unstoppabledomains/resolution": "^9.2.2", "axios": "^0.25.0", "canvas": "^2.9.0", @@ -42,8 +43,7 @@ "redis": "^4.6.10", "sharp": "^0.30.1", "ts-node": "^10.8.1", - "typescript": "^4.7.3", - "viem": "^2.9.15" + "typescript": "^4.7.3" }, "devDependencies": { "@types/express": "^4.17.11", diff --git a/src/addressResolvers/farcaster.ts b/src/addressResolvers/farcaster.ts index ac719388..be9e2223 100644 --- a/src/addressResolvers/farcaster.ts +++ b/src/addressResolvers/farcaster.ts @@ -1,6 +1,7 @@ import { capture } from '@snapshot-labs/snapshot-sentry'; -import { graphQlCall, Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils'; -import { getAddress } from 'viem'; +import { Address, Handle, FetchError, isSilencedError, isEvmAddress } from './utils'; +import { getAddress } from '@ethersproject/address'; +import fetch from 'node-fetch'; export const NAME = 'Farcaster'; const FNAMES_API_URL = 'https://fnames.farcaster.xyz/transfers?name='; @@ -27,125 +28,117 @@ interface UserResult { pfp_url?: string; } -export async function fetchData(url: string): Promise { +async function fetchData(url: string): Promise { const headers = { Accept: 'application/json', api_key: API_KEY }; const response = await fetch(url, { headers }); if (!response.ok) { - const error = new FetchError(`Failed to fetch data from the API. Status: ${response.status}`); - if (!isSilencedError(error)) { - capture(error, { tags: { issue: 'api_fetch_failure' } }); + const e = new FetchError(`Failed to fetch data from the API. Status: ${response.status}`); + if (!isSilencedError(e)) { + capture(e, { tags: { issue: 'api_fetch_failure' } }); } - throw error; + throw e; } return response.json() as Promise; } - -export async function lookupAddresses(addresses: Address[]): Promise<{ [key: Handle]: Address }> { - const results: { [key: Handle]: Address } = {}; - - try { - const url = buildLookupUrl(addresses); - const userDetails = await fetchData(url); - processUserDetails(userDetails, results); - } catch (error) { - handleLookupError(error, addresses); - } - - return results; +function isValidUserData(data: any): boolean { + return Array.isArray(data) && data.length > 0 && data[0].username; } -export function buildLookupUrl(addresses: Address[]): string { - const addressesQuery = addresses.join(','); - return `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; +function formatUserDetails(userDetails: { users: UserDetails[] }): UserResult { + const user = userDetails.users[0]; + return { + username: user.username, + eth_addresses: user.verified_addresses.eth_addresses.filter(isEvmAddress), + sol_addresses: user.verified_addresses.sol_addresses, + pfp_url: user.pfp_url + }; } -export function processUserDetails(userDetails: ApiResponse, results: { [key: Handle]: Address }): void { - for (const [address, data] of Object.entries(userDetails)) { - if (isValidUserData(data)) { - const checksumAddress = getAddress(address); - results[checksumAddress] = `${data[0].username}.fcast.id`; - } else { - results[address] = 'No user found for this address.'; +async function getUserDetails(username: Handle): Promise<{ users: UserDetails[] } | null> { + 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) { + return userDetails.result; } } + return null; } -export function isValidUserData(data: any): boolean { - return Array.isArray(data) && data.length > 0 && data[0].username; -} - -export function handleLookupError(error: any, addresses: Address[]): void { - if (!isSilencedError(error)) { - capture(error, { input: { addresses }, tags: { issue: 'lookup_addresses_failure' } }); +function handleUserDetailsError(e: any, username: Handle): void { + if (!isSilencedError(e)) { + capture(e, { input: { username }, tags: { issue: 'fetch_user_details_failure' } }); } - throw new FetchError('Error fetching address details.'); + throw new FetchError(`Error fetching user details for ${username}.`); } - - -export async function fetchUserDetailsByUsername(username: Handle): Promise { +async function fetchUserDetailsByUsername(username: Handle): Promise { try { const userDetails = await getUserDetails(username); if (userDetails) { return formatUserDetails(userDetails); } - } catch (error) { - handleUserDetailsError(error, username); + } catch (e) { + handleUserDetailsError(e, username); } return null; } -export async function getUserDetails(username: Handle): Promise<{ users: UserDetails[] } | null> { - 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) { - return userDetails.result; - } +function buildLookupUrl(addresses: Address[]): string { + const filteredAddresses = addresses.filter(isEvmAddress); + if (filteredAddresses.length === 0) { + throw new FetchError('No valid Ethereum addresses provided.'); } - return null; + const addressesQuery = filteredAddresses.join(','); + return `${NEYNAR_API_URL}bulk-by-address?addresses=${addressesQuery}`; } -export function formatUserDetails(userDetails: { users: UserDetails[] }): UserResult { - const user = userDetails.users[0]; - const eth_addresses_checksummed = user.verified_addresses.eth_addresses.filter(isEvmAddress); - return { - username: user.username, - eth_addresses: eth_addresses_checksummed, - sol_addresses: user.verified_addresses.sol_addresses, - pfp_url: user.pfp_url - }; +function processUserDetails(userDetails: ApiResponse, results: { [key: Handle]: Address }): void { + for (const [address, data] of Object.entries(userDetails)) { + if (isValidUserData(data)) { + const checksumAddress = getAddress(address); + results[checksumAddress] = `${data[0].username}.fcast.id`; + } + } } -export function handleUserDetailsError(error: any, username: Handle): void { - if (!isSilencedError(error)) { - capture(error, { input: { username }, tags: { issue: 'fetch_user_details_failure' } }); +function handleLookupError(e: any, addresses: Address[]): void { + if (!isSilencedError(e)) { + capture(e, { input: { addresses }, tags: { issue: 'lookup_addresses_failure' } }); } - throw new FetchError(`Error fetching user details for ${username}.`); + throw new FetchError('No user found for this address.'); } - - export async function resolveNames(handles: Handle[]): Promise> { const results: Record = {}; for (const handle of handles) { const normalizedHandle = handle.replace('.fcast.id', ''); - const userDetails = await fetchUserDetailsByUsername(normalizedHandle); - - if (userDetails && userDetails.eth_addresses && userDetails.eth_addresses.length > 0) { - const checksumAddress = getAddress(userDetails.eth_addresses[0]); - results[handle] = checksumAddress; + try { + const userDetails = await fetchUserDetailsByUsername(normalizedHandle); + if (userDetails && userDetails.eth_addresses) { + results[handle] = getAddress(userDetails.eth_addresses[0]); + } + } catch (e) { + console.error(`Error resolving name for handle ${handle}:`, e); } - } return results; } - - +export async function lookupAddresses(addresses: Address[]): Promise<{ [key: Handle]: Address }> { + const results: { [key: Handle]: Address } = {}; + try { + const url = buildLookupUrl(addresses); + const userDetails = await fetchData(url); + processUserDetails(userDetails, results); + } catch (e) { + handleLookupError(e, addresses); + } + return results; +} diff --git a/tsconfig.json b/tsconfig.json index 1499487f..0735377f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,11 +13,6 @@ "sourceMap": true, "inlineSources": true, "sourceRoot": "/", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@/resolvers": ["./src/resolvers/*"], - "@/address": ["./src/addressResolvers/*"] - }, + "baseUrl": "." } } diff --git a/yarn.lock b/yarn.lock index cebebb7a..190bace4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2409,6 +2409,14 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/node-fetch@^2.6.11": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*", "@types/node@^14.14.21": version "14.17.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.17.tgz#4ec7b71bbcb01a4e55455b60b18b1b6a783fe31d" @@ -3502,6 +3510,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-urls@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" @@ -4098,6 +4111,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -4183,6 +4204,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5729,6 +5757,11 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@^2.6.12, node-fetch@^2.6.5, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -5736,6 +5769,15 @@ node-fetch@^2.6.12, node-fetch@^2.6.5, node-fetch@^2.7.0: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -7220,6 +7262,11 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From b24f960a673f9fd7945ec1f802966a3ba779443a Mon Sep 17 00:00:00 2001 From: codingsh Date: Tue, 16 Apr 2024 04:22:37 -0300 Subject: [PATCH 9/9] chore(farcaster): fix tscofing and package.json --- package.json | 4 ++-- tsconfig.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 58ef09df..55fe3243 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@snapshot-labs/snapshot-metrics": "^1.4.1", "@snapshot-labs/snapshot-sentry": "^1.5.5", "@snapshot-labs/snapshot.js": "^0.11.17", - "@types/node-fetch": "^2.6.11", "@unstoppabledomains/resolution": "^9.2.2", "axios": "^0.25.0", "canvas": "^2.9.0", @@ -38,7 +37,7 @@ "eslint": "^6.7.2", "express": "^4.17.1", "jsdom": "^19.0.0", - "node-fetch": "^3.3.2", + "node-fetch": "^2.7.0", "nodemon": "^2.0.7", "redis": "^4.6.10", "sharp": "^0.30.1", @@ -49,6 +48,7 @@ "@types/express": "^4.17.11", "@types/jest": "^28.1.0", "@types/node": "^14.14.21", + "@types/node-fetch": "^2.6.11", "@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/parser": "^2.33.0", "eslint-plugin-prettier": "^3.1.3", diff --git a/tsconfig.json b/tsconfig.json index 0735377f..63c891f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ "skipLibCheck": true, "sourceMap": true, "inlineSources": true, - "sourceRoot": "/", - "baseUrl": "." + "sourceRoot": "/" } }