Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra authored Aug 30, 2024
2 parents 364af80 + 3e4713b commit d51348a
Show file tree
Hide file tree
Showing 12 changed files with 5,382 additions and 6,473 deletions.
212 changes: 140 additions & 72 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ import { useClusterPath } from '@utils/url';
import { MetadataPointer, TokenMetadata } from '@validators/accounts/token-extension';
import Link from 'next/link';
import { redirect, useSelectedLayoutSegment } from 'next/navigation';
import React, { PropsWithChildren, Suspense } from 'react';
import React, { PropsWithChildren, Suspense, useMemo } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { create } from 'superstruct';
import useSWRImmutable from 'swr/immutable';
import { Address } from 'web3js-experimental';

import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { useCompressedNft } from '@/app/providers/compressed-nft';
import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';
import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';
import { MintAccountInfo } from '@/app/validators/accounts/token';

const IDENTICON_WIDTH = 64;

Expand Down Expand Up @@ -272,76 +273,7 @@ function AccountHeader({
}

if (isToken && !isTokenInfoLoading) {
let token: { logoURI?: string; name?: string } = {};
let unverified = false;

const metadataExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'tokenMetadata'
);
const metadataPointerExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'metadataPointer'
);

if (metadataPointerExtension && metadataExtension) {
const tokenMetadata = create(metadataExtension.state, TokenMetadata);
const { metadataAddress } = create(metadataPointerExtension.state, MetadataPointer);

// Handles the basic case where MetadataPointer is reference the Token Metadata extension directly
// Does not handle the case where MetadataPointer is pointing at a separate account.
if (metadataAddress?.toString() === address) {
token.name = tokenMetadata.name;
}
}
// Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority
else if (!parsedData?.nftData?.metadata.data.uri && tokenInfo) {
token = tokenInfo;
} else if (parsedData?.nftData) {
token = {
logoURI: parsedData?.nftData?.json?.image,
name: parsedData?.nftData?.json?.name ?? parsedData?.nftData.metadata.data.name,
};
if (!tokenInfo?.verified) {
unverified = true;
}
} else if (tokenInfo) {
token = tokenInfo;
}

return (
<div className="row align-items-end">
{unverified && (
<div className="alert alert-warning alert-scam" role="alert">
Warning! Token names and logos are not unique. This token may have spoofed its name and logo to
look like another token. Verify the token&apos;s mint address to ensure it is correct.
</div>
)}
<div className="col-auto">
<div className="avatar avatar-lg header-avatar-top">
{token?.logoURI ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt="token logo"
className="avatar-img rounded-circle border border-4 border-body"
height={16}
src={token.logoURI}
width={16}
/>
) : (
<Identicon
address={address}
className="avatar-img rounded-circle border border-body identicon-wrapper"
style={{ width: IDENTICON_WIDTH }}
/>
)}
</div>
</div>

<div className="col mb-3 ms-n3 ms-md-n2">
<h6 className="header-pretitle">Token</h6>
<h2 className="header-title">{token?.name || 'Unknown Token'}</h2>
</div>
</div>
);
return <TokenMintHeader address={address} mintInfo={mintInfo} parsedData={parsedData} tokenInfo={tokenInfo} />;
}

const fallback = (
Expand All @@ -362,6 +294,142 @@ function AccountHeader({
return fallback;
}

function TokenMintHeader({
address,
tokenInfo,
mintInfo,
parsedData,
}: {
address: string;
tokenInfo?: FullTokenInfo;
mintInfo?: MintAccountInfo;
parsedData?: TokenProgramData;
}): JSX.Element {
const metadataExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'tokenMetadata'
);
const metadataPointerExtension = mintInfo?.extensions?.find(
({ extension }: { extension: string }) => extension === 'metadataPointer'
);

const defaultCard = useMemo(
() => (
<TokenMintHeaderCard
token={tokenInfo ? tokenInfo : { logoURI: undefined, name: undefined }}
address={address}
unverified={tokenInfo ? !tokenInfo.verified : false}
/>
),
[address, tokenInfo]
);

if (metadataPointerExtension && metadataExtension) {
return (
<>
<ErrorBoundary fallback={defaultCard}>
<Suspense fallback={defaultCard}>
<Token22MintHeader
address={address}
metadataExtension={metadataExtension as any}
metadataPointerExtension={metadataPointerExtension as any}
/>
</Suspense>
</ErrorBoundary>
</>
);
}
// Fall back to legacy token list when there is stub metadata (blank uri), updatable by default by the mint authority
else if (!parsedData?.nftData?.metadata.data.uri && tokenInfo) {
return defaultCard;
} else if (parsedData?.nftData) {
const token = {
logoURI: parsedData?.nftData?.json?.image,
name: parsedData?.nftData?.json?.name ?? parsedData?.nftData.metadata.data.name,
};
return <TokenMintHeaderCard token={token} address={address} unverified={!tokenInfo?.verified} />;
} else if (tokenInfo) {
return defaultCard;
}
return defaultCard;
}

function Token22MintHeader({
address,
metadataExtension,
metadataPointerExtension,
}: {
address: string;
metadataExtension: { extension: 'tokenMetadata'; state?: any };
metadataPointerExtension: { extension: 'metadataPointer'; state?: any };
}) {
const tokenMetadata = create(metadataExtension.state, TokenMetadata);
const { metadataAddress } = create(metadataPointerExtension.state, MetadataPointer);
const metadata = useMetadataJsonLink(tokenMetadata.uri, { suspense: true });

if (!metadata) {
throw new Error(`Could not load metadata from given URI: ${tokenMetadata.uri}`);
}

// Handles the basic case where MetadataPointer is referencing the Token Metadata extension directly
// Does not handle the case where MetadataPointer is pointing at a separate account.
if (metadataAddress?.toString() === address) {
return (
<TokenMintHeaderCard
address={address}
token={{ logoURI: metadata.image, name: metadata.name }}
unverified={false}
/>
);
}
throw new Error('Metadata loading for non-token 2022 programs is not yet supported');
}

function TokenMintHeaderCard({
address,
token,
unverified,
}: {
address: string;
token: { name?: string | undefined; logoURI?: string | undefined };
unverified: boolean;
}) {
return (
<div className="row align-items-end">
{unverified && (
<div className="alert alert-warning alert-scam" role="alert">
Warning! Token names and logos are not unique. This token may have spoofed its name and logo to look
like another token. Verify the token&apos;s mint address to ensure it is correct.
</div>
)}
<div className="col-auto">
<div className="avatar avatar-lg header-avatar-top">
{token?.logoURI ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt="token logo"
className="avatar-img rounded-circle border border-4 border-body"
height={16}
src={token.logoURI}
width={16}
/>
) : (
<Identicon
address={address}
className="avatar-img rounded-circle border border-body identicon-wrapper"
style={{ width: IDENTICON_WIDTH }}
/>
)}
</div>
</div>

<div className="col mb-3 ms-n3 ms-md-n2">
<h6 className="header-pretitle">Token</h6>
<h2 className="header-title">{token?.name || 'Unknown Token'}</h2>
</div>
</div>
);
}

function DetailsSections({
children,
pubkey,
Expand Down
6 changes: 3 additions & 3 deletions app/api/domain-info/[domain]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Connection } from "@solana/web3.js"
import { NextResponse } from "next/server"

import { MAINNET_BETA_URL } from "@/app/utils/cluster"
import { getDomainInfo } from "@/app/utils/domain-info"
import { getANSDomainInfo,getDomainInfo } from "@/app/utils/domain-info"

type Params = {
params: {
Expand All @@ -20,12 +20,12 @@ export async function GET(
// This is an API route so won't affect client bundle
// We only fetch domains on mainnet
const connection = new Connection(MAINNET_BETA_URL);
const domainInfo = await getDomainInfo(domain, connection);
const domainInfo = await (domain.substring(domain.length - 4) === '.sol' ? getDomainInfo(domain, connection) : getANSDomainInfo(domain, connection));

return NextResponse.json(domainInfo, {
headers: {
// 24 hours
"Cache-Control": "max-age=86400"
"Cache-Control": "max-age=86400",
}
});
}
2 changes: 1 addition & 1 deletion app/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface SearchOptions {
}

const hasDomainSyntax = (value: string) => {
return value.length > 4 && value.substring(value.length - 4) === '.sol';
return value.length > 3 && value.split('.').length === 2;
};

export function SearchBar() {
Expand Down
22 changes: 18 additions & 4 deletions app/components/account/DomainsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ import React from 'react';

import { DomainInfo } from '@/app/utils/domain-info';

import { useUserANSDomains } from '../../utils/ans-domains';

export function DomainsCard({ address }: { address: string }) {
const [domains, domainsLoading] = useUserDomains(address);
const [domainsANS, domainsANSLoading] = useUserANSDomains(address);

if (domainsLoading && (!domains || domains.length === 0)) {
if (
(domainsLoading && (!domains || domains.length === 0)) ||
(domainsANSLoading && (!domainsANS || domainsANS.length === 0))
) {
return <LoadingCard message="Loading domains" />;
} else if (!domains) {
} else if (!domains || !domainsANS) {
return <ErrorCard text="Failed to fetch domains" />;
}

if (domains.length === 0) {
if (domains.length === 0 && domainsANS.length === 0) {
return <ErrorCard text="No domain name found" />;
}

let allDomains = domains;

if (domainsANS) {
allDomains = [...allDomains, ...domainsANS];
}

allDomains.sort((a, b) => a.name.localeCompare(b.name));

return (
<div className="card">
<div className="card-header align-items-center">
Expand All @@ -35,7 +49,7 @@ export function DomainsCard({ address }: { address: string }) {
</tr>
</thead>
<tbody className="list">
{domains.map(domain => (
{allDomains.map(domain => (
<RenderDomainRow key={domain.address.toBase58()} domainInfo={domain} />
))}
</tbody>
Expand Down
12 changes: 10 additions & 2 deletions app/providers/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { create } from 'superstruct';
import { HistoryProvider } from './history';
import { RewardsProvider } from './rewards';
import { TokensProvider } from './tokens';
import { getStakeActivation } from './utils/stake';
export { useAccountHistory } from './history';

const Metadata = programs.metadata.Metadata;
Expand Down Expand Up @@ -338,10 +339,17 @@ async function handleParsedAccountData(
case 'stake': {
const parsed = create(info, StakeAccount);
const isDelegated = parsed.type === 'delegated';
const activation = isDelegated ? await connection.getStakeActivation(accountKey) : undefined;

// TODO(ngundotra): replace with web3.js fix when live
const activation = isDelegated ? await getStakeActivation(connection, accountKey) : undefined;
return {
activation,
activation: activation
? {
active: Number(activation.active),
inactive: Number(activation.inactive),
state: activation.status as any,
}
: undefined,
parsed,
program: accountData.program,
};
Expand Down
Loading

0 comments on commit d51348a

Please sign in to comment.