diff --git a/src/lib/components/identity-badge/identity-badge.svelte b/src/lib/components/identity-badge/identity-badge.svelte index 4f0aa5c0f..3105cb376 100644 --- a/src/lib/components/identity-badge/identity-badge.svelte +++ b/src/lib/components/identity-badge/identity-badge.svelte @@ -23,6 +23,11 @@ avatarImgElem?: HTMLImageElement | undefined; isReverse?: boolean; tag?: string | undefined; + /** + * If provided, this URL will be used for the avatar instead of the one fetched from the ENS store. + * This is useful for contexts entirely rendered on the server (like share images) or to prevent flash of content on initial load. + */ + avatarSrc?: string | undefined; } let { @@ -40,6 +45,7 @@ avatarImgElem = $bindable(), isReverse = false, tag = undefined, + avatarSrc = undefined, }: Props = $props(); run(() => { @@ -108,7 +114,7 @@ {/key} diff --git a/src/lib/stores/ens/ens.ts b/src/lib/stores/ens/ens.ts index b9820bd05..e694b4bff 100644 --- a/src/lib/stores/ens/ens.ts +++ b/src/lib/stores/ens/ens.ts @@ -50,3 +50,45 @@ 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)], + ); + + const [records, avatarUrl] = await Promise.all([ + Promise.all(promises), + resolver?.getAvatar(), + ]); + + return { + ensName, + avatarUrl: avatarUrl ?? undefined, + records: Object.fromEntries(records) as Record, + }; + } + 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/(pages)/app/(app)/[accountId]/+page.svelte b/src/routes/(pages)/app/(app)/[accountId]/+page.svelte index 29068dce8..6b0fb7003 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/+page.svelte +++ b/src/routes/(pages)/app/(app)/[accountId]/+page.svelte @@ -35,9 +35,16 @@ data.profileData.account.address.toLowerCase() === $walletStore.address?.toLowerCase(); $: profileChainData = filterCurrentChainData(data.profileData?.chainData ?? []); + $: shareImage = + data.profileData?.account.address && + `/api/share-images/profile/${data.profileData.account.address}`; - + {#if data.error && data.type === 'is-repo-driver-account-id'}
@@ -72,13 +80,14 @@ size="gigantic" showAvatar={false} disableTooltip + avatarSrc={data.ensData?.avatarUrl} />