Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion src/lib/components/identity-badge/identity-badge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,6 +45,7 @@
avatarImgElem = $bindable(),
isReverse = false,
tag = undefined,
avatarSrc = undefined,
}: Props = $props();

run(() => {
Expand Down Expand Up @@ -108,7 +114,7 @@
<Avatar
size={currentSize}
bind:imgElem={avatarImgElem}
src={ens?.avatarUrl}
src={avatarSrc ?? ens?.avatarUrl}
placeholderSrc={blockyUrl}
/>
{/key}
Expand Down
42 changes: 42 additions & 0 deletions src/lib/stores/ens/ens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined>,
};
}
return null;
} catch {
return null;
}
}
71 changes: 71 additions & 0 deletions src/lib/utils/sdk/utils/resolve-account-id-to-address.ts
Original file line number Diff line number Diff line change
@@ -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<ResolutionResult> {
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 };
}
99 changes: 27 additions & 72 deletions src/routes/(pages)/app/(app)/[accountId]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<ProfilePageQuery, ProfilePageQueryVariables>(
PROFILE_PAGE_QUERY,
{ address, chains: [network.gqlName] },
fetch,
),
resolveEnsFields(address),
resolveEnsProfile(address, currentNetworkProvider, mainnetProvider, network.chainId),
]);

const votingRoundsWithResults = votingRounds.filter((v) => v.result);
Expand Down
13 changes: 11 additions & 2 deletions src/routes/(pages)/app/(app)/[accountId]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
</script>

<HeadMeta title={data.ensData?.ensName ?? data.profileData?.account.address ?? undefined} />
<HeadMeta
title={data.ensData?.ensName ?? data.profileData?.account.address ?? undefined}
image={shareImage}
twitterImage={shareImage}
/>

{#if data.error && data.type === 'is-repo-driver-account-id'}
<LargeEmptyState
Expand All @@ -62,6 +69,7 @@
size="gigantic"
showIdentity={false}
disableTooltip
avatarSrc={data.ensData?.avatarUrl}
/>
<div class="flex items-center sm:py-4">
<div class="flex flex-col gap-4">
Expand All @@ -72,13 +80,14 @@
size="gigantic"
showAvatar={false}
disableTooltip
avatarSrc={data.ensData?.avatarUrl}
/>
</h1>
<ul class="social-links">
<div in:fade>
<SocialLink network="ethereum" value={data.profileData.account.address} />
</div>
{#each Object.entries(socialLinkValues ?? {}) as [network, value]}
{#each Object.entries(socialLinkValues ?? {}) as [network, value] (value)}
{#if value}<li in:fade>
<SocialLink network={isNetwork(network) ? network : unreachable()} {value} />
</li>{/if}
Expand Down
Loading