From f3a4ba496ba3349b098062ca76501b3a17bca630 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Dec 2025 14:40:05 +0100 Subject: [PATCH 1/6] First pass profile share image --- src/lib/stores/ens/ens.ts | 39 ++++++++ .../utils/resolve-account-id-to-address.ts | 71 +++++++++++++ .../app/(app)/[accountId]/+page.server.ts | 99 +++++-------------- .../(pages)/[type]/[id]/+page.server.ts | 84 ++++++++++++++++ .../(pages)/[type]/[id]/+page.svelte | 4 + .../share-images/(pages)/[type]/[id]/types.ts | 1 + 6 files changed, 226 insertions(+), 72 deletions(-) create mode 100644 src/lib/utils/sdk/utils/resolve-account-id-to-address.ts diff --git a/src/lib/stores/ens/ens.ts b/src/lib/stores/ens/ens.ts index b9820bd05..5707c0568 100644 --- a/src/lib/stores/ens/ens.ts +++ b/src/lib/stores/ens/ens.ts @@ -50,3 +50,42 @@ export async function safeReverseLookup( return undefined; } } + +export async function resolveEnsProfile( + address: string, + currentNetworkProvider: AbstractProvider, + mainnetProvider: AbstractProvider | null, + chainId: number, +) { + if (!mainnetProvider) { + return null; + } + + try { + const ensName = await safeReverseLookup( + currentNetworkProvider, + mainnetProvider, + chainId, + address, + ); + + if (ensName) { + const resolver = await mainnetProvider.getResolver(ensName); + + const promises = ['description', 'url', 'com.twitter', 'com.github'].map( + async (recordName) => [recordName, await resolver?.getText(recordName)], + ); + + return { + ensName, + records: Object.fromEntries(await Promise.all(promises)) as Record< + string, + string | undefined + >, + }; + } + return null; + } catch { + return null; + } +} diff --git a/src/lib/utils/sdk/utils/resolve-account-id-to-address.ts b/src/lib/utils/sdk/utils/resolve-account-id-to-address.ts new file mode 100644 index 000000000..abc03cffb --- /dev/null +++ b/src/lib/utils/sdk/utils/resolve-account-id-to-address.ts @@ -0,0 +1,71 @@ +import { isAddress, type JsonRpcProvider } from 'ethers'; +import extractAddressFromAccountId from './extract-address-from-accountId'; +import { extractDriverNameFromAccountId } from './extract-driver-from-accountId'; +import { safeReverseLookup } from '$lib/stores/ens/ens'; + +export type ResolutionResult = + | { type: 'success'; address: string; resolvedEnsName?: string | null } + | { type: 'ens-not-resolved' } + | { + type: 'driver-account'; + driver: 'nft' | 'repo' | 'immutableSplits' | 'repoSubAccountDriver'; + accountId: string; + } + | { type: 'not-found' }; + +export async function resolveAccountIdToAddress( + universalAccountId: string, + currentNetworkProvider: JsonRpcProvider, + mainnetProvider: JsonRpcProvider | null, + chainId: number, +): Promise { + let address: string; + + if (isAddress(universalAccountId)) { + address = universalAccountId; + } else if ((universalAccountId as string).endsWith('.eth')) { + if (!mainnetProvider) { + return { type: 'ens-not-resolved' }; + } + + const lookupRes = await safeReverseLookup( + currentNetworkProvider, + mainnetProvider, + chainId, + universalAccountId, + ); + + if (!lookupRes) { + return { type: 'ens-not-resolved' }; + } + + address = lookupRes; + // We already resolved the name, so we can return it. + // However, the original code looked up the ENS name of the address *separately* sometimes. + // For .eth input, the input IS the ENS name (mostly). + // Let's pass it back as resolvedEnsName so the caller knows. + return { type: 'success', address, resolvedEnsName: universalAccountId }; + } else if (/^\d+$/.test(universalAccountId)) { + const driver = extractDriverNameFromAccountId(universalAccountId); + + switch (driver) { + case 'address': { + address = extractAddressFromAccountId(universalAccountId); + break; + } + case 'nft': + case 'repo': + case 'immutableSplits': + case 'repoSubAccountDriver': { + return { type: 'driver-account', driver, accountId: universalAccountId }; + } + default: { + return { type: 'not-found' }; + } + } + } else { + return { type: 'not-found' }; + } + + return { type: 'success', address }; +} diff --git a/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts b/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts index d742abb9f..164259717 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts @@ -10,10 +10,8 @@ import type { ProfilePageQuery, ProfilePageQueryVariables } from './__generated_ import { getVotingRounds } from '$lib/utils/multiplayer'; import { mapSplitsFromMultiplayerResults } from '$lib/components/splits/utils'; import { SUPPORTERS_SECTION_SUPPORT_ITEM_FRAGMENT } from '$lib/components/supporters-section/supporters.section.svelte'; -import { isAddress } from 'ethers'; -import extractAddressFromAccountId from '$lib/utils/sdk/utils/extract-address-from-accountId'; -import { extractDriverNameFromAccountId } from '$lib/utils/sdk/utils/extract-driver-from-accountId'; -import { getMainnetProvider, safeReverseLookup } from '$lib/stores/ens/ens'; +import { resolveAccountIdToAddress } from '$lib/utils/sdk/utils/resolve-account-id-to-address'; +import { getMainnetProvider, resolveEnsProfile } from '$lib/stores/ens/ens'; import { JsonRpcProvider } from 'ethers'; import { LINKED_IDENTITIES_CARD_FRAGMENT } from './components/linked-identities-card.svelte'; @@ -60,91 +58,48 @@ const PROFILE_PAGE_QUERY = gql` } `; -async function resolveEnsFields(address: string) { - if (!mainnetProvider) { - return null; - } - - try { - const ensName = await safeReverseLookup( - currentNetworkProvider, - mainnetProvider, - network.chainId, - address, - ); - - if (ensName) { - const resolver = await mainnetProvider.getResolver(ensName); - - const promises = ['description', 'url', 'com.twitter', 'com.github'].map( - async (recordName) => [recordName, await resolver?.getText(recordName)], - ); - - return { - ensName, - records: Object.fromEntries(await Promise.all(promises)), - }; - } - } catch { - return null; - } -} - export const load = async ({ params, fetch }) => { // Account ID here may be either a Drips Account ID, ENS name or an Ethereum address const { accountId: universalAccountId } = params; - let address: string; - - if (isAddress(universalAccountId)) { - address = universalAccountId; - } else if ((universalAccountId as string).endsWith('.eth')) { - if (!mainnetProvider) { - return { error: true, type: 'ens-not-resolved' as const }; - } - - const lookupRes = await safeReverseLookup( - currentNetworkProvider, - mainnetProvider, - network.chainId, - universalAccountId, - ); - - if (!lookupRes) { - return { error: true, type: 'ens-not-resolved' as const }; - } + const resolution = await resolveAccountIdToAddress( + universalAccountId, + currentNetworkProvider, + mainnetProvider, + network.chainId, + ); - address = lookupRes; - } else if (/^\d+$/.test(universalAccountId)) { - const driver = extractDriverNameFromAccountId(universalAccountId); + let address: string; - switch (driver) { - case 'address': { - address = extractAddressFromAccountId(universalAccountId); - break; + switch (resolution.type) { + case 'success': + address = resolution.address; + break; + case 'driver-account': + if (resolution.driver === 'nft') { + return redirect(301, `/app/drip-lists/${resolution.accountId}`); } - case 'nft': { - return redirect(301, `/app/drip-lists/${universalAccountId}`); - } - case 'repo': { + + if (resolution.driver === 'repo') { return { error: true, type: 'is-repo-driver-account-id' as const }; } - default: { - error(404, 'Not Found'); - } - } - } else { - error(404, 'Not Found'); + + return error(404, 'Not Found'); + case 'ens-not-resolved': + return { error: true, type: 'ens-not-resolved' as const }; + case 'not-found': + default: + error(404, 'Not Found'); } const [votingRounds, userRes, ensData] = await Promise.all([ - await getVotingRounds({ publisherAddress: address }, fetch), + getVotingRounds({ publisherAddress: address }, fetch), query( PROFILE_PAGE_QUERY, { address, chains: [network.gqlName] }, fetch, ), - resolveEnsFields(address), + resolveEnsProfile(address, currentNetworkProvider, mainnetProvider, network.chainId), ]); const votingRoundsWithResults = votingRounds.filter((v) => v.result); diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index fa868c5bc..43aff0d60 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -17,6 +17,13 @@ import { fetchEcosystem } from '../../../../../(pages)/app/(app)/ecosystems/[eco import getOrcidDisplayName from '$lib/utils/orcids/display-name.js'; import { getRound } from '$lib/utils/rpgf/rpgf.js'; import { getWaveProgram } from '$lib/utils/wave/wavePrograms.js'; +import { resolveAccountIdToAddress } from '$lib/utils/sdk/utils/resolve-account-id-to-address'; +import formatAddress from '$lib/utils/format-address'; +import { JsonRpcProvider } from 'ethers'; +import { getMainnetProvider, resolveEnsProfile } from '$lib/stores/ens/ens'; + +const currentNetworkProvider = new JsonRpcProvider(network.rpcUrl); +const mainnetProvider = network.enableEns ? getMainnetProvider() : null; function isShareImageType(value: string): value is ShareImageType { return Object.values(ShareImageType).includes(value as ShareImageType); @@ -230,6 +237,82 @@ async function loadRpgfRoundData(f: typeof fetch, id: string) { }; } +async function loadProfileData(f: typeof fetch, universalAccountId: string) { + const resolution = await resolveAccountIdToAddress( + universalAccountId, + currentNetworkProvider, + mainnetProvider, + network.chainId, + ); + + let address: string; + + switch (resolution.type) { + case 'success': + address = resolution.address; + break; + case 'driver-account': + if (resolution.driver === 'nft' || resolution.driver === 'repo') { + throw error(404, 'Use the Drip List share image for this account type'); + } + throw error(404, 'Not Found'); + case 'ens-not-resolved': + throw error(404, 'ENS not resolvable'); + case 'not-found': + default: + throw error(404, 'Not Found'); + } + + const profileQuery = gql` + query Profile($address: String!, $chains: [SupportedChain!]) { + userByAddress(address: $address, chains: $chains) { + account { + driver + address + accountId + } + chainData { + chain + support { + __typename + } + } + } + } + `; + + const [userRes, ensProfile] = await Promise.all([ + query(profileQuery, { address, chains: [network.gqlName] }, fetch), + resolveEnsProfile(address, currentNetworkProvider, mainnetProvider, network.chainId), + ]); + + const { userByAddress } = userRes; + + if (!userByAddress) { + return null; + } + + const chainData = filterCurrentChainData(userByAddress.chainData); + + return { + bgColor: '#5555FF', + type: '', + headline: ensProfile?.ensName ?? formatAddress(address), + avatarSrc: null, + stats: [ + { + icon: 'Ethereum', + label: formatAddress(address), + }, + { + icon: 'Heart', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + label: `${(chainData as any)?.support?.length ?? 0} Supporters`, + }, + ], + }; +} + const LOAD_FNS = { [ShareImageType.WAVE_PROGRAM]: loadWaveProgramData, [ShareImageType.PROJECT]: loadProjectData, @@ -237,6 +320,7 @@ const LOAD_FNS = { [ShareImageType.ECOSYSTEM]: loadEcosystemData, [ShareImageType.ORCID]: loadOrcidData, [ShareImageType.RPGF_ROUND]: loadRpgfRoundData, + [ShareImageType.PROFILE]: loadProfileData, } as const; export const load = async ({ params }) => { diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 0446f6c6c..34884061a 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -1,5 +1,7 @@ - + {#if data.error && data.type === 'is-repo-driver-account-id'} Date: Mon, 22 Dec 2025 16:15:07 +0100 Subject: [PATCH 5/6] Ensure that profile share imge with 1 supporter is not pluralized --- .../api/share-images/(pages)/[type]/[id]/+page.server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 627978f33..339ef8fa0 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -110,7 +110,7 @@ async function loadProjectData(f: typeof fetch, projectUrl: string) { ? [ { icon: 'DripList', - label: `${chainData.splits.dependencies.length} dependencie${chainData.splits.dependencies.length === 1 ? '' : 's'}`, + label: `${chainData.splits.dependencies.length} dependenc${chainData.splits.dependencies.length === 1 ? 'y' : 'ies'}`, }, ] : [], @@ -308,7 +308,7 @@ async function loadProfileData(f: typeof fetch, universalAccountId: string) { { icon: 'Heart', // eslint-disable-next-line @typescript-eslint/no-explicit-any - label: `${(chainData as any)?.support?.length ?? 0} Supporters`, + label: `${(chainData as any)?.support?.length ?? 0} Supporter${(chainData as any)?.support?.length === 1 ? '' : 's'}`, }, ], }; From 5be7577e1db20997fefb0d5265bec168b320919a Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Dec 2025 16:43:34 +0100 Subject: [PATCH 6/6] Corrent loadProfileData error messages --- .../api/share-images/(pages)/[type]/[id]/+page.server.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 339ef8fa0..6667139f4 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -253,9 +253,14 @@ async function loadProfileData(f: typeof fetch, universalAccountId: string) { address = resolution.address; break; case 'driver-account': - if (resolution.driver === 'nft' || resolution.driver === 'repo') { - throw error(404, 'Use the Drip List share image for this account type'); + if (resolution.driver === 'nft') { + throw error(404, 'Universal account ID is Drip List'); } + + if (resolution.driver === 'repo') { + throw error(404, 'Universal account ID is Project'); + } + throw error(404, 'Not Found'); case 'ens-not-resolved': throw error(404, 'ENS not resolvable');