Skip to content
Merged
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
12 changes: 10 additions & 2 deletions app/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useHotkeys } from '@mantine/hooks';
import { useCluster } from '@providers/cluster';
import { VersionedMessage } from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { Cluster, clusterSlug } from '@utils/cluster';
import { SearchElement } from '@utils/token-search';
import bs58 from 'bs58';
import { useRouter, useSearchParams } from 'next/navigation';
Expand All @@ -18,6 +18,7 @@ import { FetchedDomainInfo } from '../api/domain-info/[domain]/route';
import { FeatureInfoType } from '../utils/feature-gate/types';
import { LOADER_IDS, LoaderName, PROGRAM_INFO_BY_ID, SPECIAL_IDS, SYSVAR_IDS } from '../utils/programs';
import { searchTokens } from '../utils/token-search';
import { pickClusterParams } from '../utils/url';
import { useDebouncedAsync } from '../utils/use-debounce-async';
import { MIN_MESSAGE_LENGTH } from './inspector/RawInputCard';

Expand Down Expand Up @@ -48,7 +49,14 @@ export function SearchBar() {
if (meta.action === 'select-option') {
// Always use the pathname directly if it contains query params
if (pathname.includes('?')) {
router.push(pathname);
const [path, currentSearchParamsString] = pathname.split('?');
// break pathname to preserve existing params and allow to keep same cluster
const nextPath = pickClusterParams(
path,
new URLSearchParams(currentSearchParamsString),
new URLSearchParams(`cluster=${clusterSlug(cluster)}`)
);
router.push(nextPath);
} else {
// Only preserve existing query params for paths without their own params
const nextQueryString = searchParams?.toString();
Expand Down
173 changes: 171 additions & 2 deletions app/components/account/UnknownAccountCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
'use client';

import { Address } from '@components/common/Address';
import { SolBalance } from '@components/common/SolBalance';
import { TableCardBody } from '@components/common/TableCardBody';
import { Account } from '@providers/accounts';
import { useCluster } from '@providers/cluster';
import { address as createAddress, createSolanaRpc } from '@solana/kit';
import { Cluster, clusterName, clusterSlug, clusterUrl } from '@utils/cluster';
import { addressLabel } from '@utils/tx';
import React from 'react';
import { useClusterPath } from '@utils/url';
import { useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';

export function UnknownAccountCard({ account }: { account: Account }) {
const { cluster } = useCluster();
Expand Down Expand Up @@ -32,7 +38,11 @@ export function UnknownAccountCard({ account }: { account: Account }) {
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-end">
{account.lamports === 0 ? 'Account does not exist' : <SolBalance lamports={account.lamports} />}
{account.lamports === 0 ? (
<AccountNofFound account={account} />
) : (
<SolBalance lamports={account.lamports} />
)}
</td>
</tr>

Expand All @@ -58,3 +68,162 @@ export function UnknownAccountCard({ account }: { account: Account }) {
</div>
);
}

type SearchStatus = 'idle' | 'searching' | 'found' | 'not-found';

function useClusterAccountSearch(address: string, currentCluster: Cluster, _enableCustomClsuter?: boolean) {
const searchParams = useSearchParams();
const [status, setStatus] = useState<SearchStatus>('idle');
const [searchingCluster, setSearchingCluster] = useState<Cluster | null>(null);
const [foundCluster, setFoundCluster] = useState<Cluster | null>(null);
const searchIdRef = useRef(0);

const sleep = () => new Promise(res => setTimeout(res, 700));

// Extract the customUrl value to avoid re-running effect on searchParams object change
const customUrl = searchParams?.get('customUrl');

useEffect(() => {
// Increment search ID to track this specific search
const currentSearchId = ++searchIdRef.current;

// NOTE: This ref-based approach prevents duplicate requests within a single component instance,
// but if multiple instances of UnknownAccountCard are rendered (e.g., as both a Suspense fallback
// and returned from CompressedNftCard), each instance will still make its own RPC requests.
// Consider moving the search logic to a parent component or using React Context to share state
// between instances if duplicate requests become a performance issue.

const clusters = [Cluster.MainnetBeta, Cluster.Devnet, Cluster.Testnet].filter(c => c !== currentCluster);

const hasCustomUrlEnabled = Boolean(customUrl);

// add custom url if parameter is present
if (hasCustomUrlEnabled) {
clusters.push(Cluster.Custom);
}

const searchClusters = async () => {
// Check if this search is still the current one
if (searchIdRef.current !== currentSearchId) return;

setStatus('searching');

// search cluster one by one to not make extra requests
for (const cluster of clusters) {
// Check if this search has been superseded
if (searchIdRef.current !== currentSearchId) return;

setSearchingCluster(cluster);

try {
let url = clusterUrl(cluster, '');

// adjust url for a custom cluster as `clusterUrl` does not return one
if (customUrl && cluster === Cluster.Custom) {
url = customUrl;
}

const rpc = createSolanaRpc(url);
const accountInfo = await rpc.getAccountInfo(createAddress(address), { encoding: 'base64' }).send();

// Check again before updating state
if (searchIdRef.current !== currentSearchId) return;

if (accountInfo.value !== null) {
setFoundCluster(cluster);
setStatus('found');
return;
} else {
// not only prevent span but allow component to react properly without making the structure complex
await sleep();
}
} catch (error) {
// Check if this search is still active before continuing
if (searchIdRef.current !== currentSearchId) return;
// Continue to next cluster
}
}

// Final check before updating not-found status
if (searchIdRef.current === currentSearchId) {
setStatus('not-found');
setSearchingCluster(null);
}
};

searchClusters();

// Cleanup function to mark this search as cancelled
return () => {
if (searchIdRef.current === currentSearchId) {
searchIdRef.current += 1;
}
};
}, [address, currentCluster, customUrl]);

return { foundCluster, searchingCluster, status };
}

const LABELS = {
'not-found': 'Account does not exist',
};

function AccountNofFound({ account, labels = LABELS }: { account: Account; labels?: typeof LABELS }) {
const { cluster } = useCluster();
const address = account.pubkey.toBase58();
const { status, searchingCluster, foundCluster } = useClusterAccountSearch(address, cluster);

if (status === 'searching' && searchingCluster !== null) {
return (
<span>
<SearchingAddressIndicator searchingCluster={searchingCluster} />
<span className="align-middle">{labels['not-found']}</span>
</span>
);
}

const isAddressFoundOnAnotherClsuter = status === 'found' && foundCluster !== null;

return isAddressFoundOnAnotherClsuter ? (
<span>
<AdjacentAddressLink address={address} foundCluster={foundCluster} />
<span className="align-middle">{labels['not-found']}</span>
</span>
) : (
<span>{labels['not-found']}</span>
);
}

function AdjacentAddressLink({ address, foundCluster }: { address: string; foundCluster: Cluster }) {
const moniker = clusterSlug(foundCluster);
const foundClusterPath = useClusterPath({
additionalParams: new URLSearchParams(`cluster=${moniker}`),
pathname: `/address/${address}`,
});

return (
<a href={foundClusterPath} className="text-info align-middle" style={{ marginRight: '5px' }}>
Found on {clusterName(foundCluster)}
</a>
);
}

function SearchingAddressIndicator({ searchingCluster }: { searchingCluster: Cluster }) {
const spinnerCls = 'spinner-grow spinner-grow-sm';

return (
<>
<span
style={{
height: '10px',
marginRight: '5px',
width: '10px',
}}
className={`${spinnerCls} align-middle d-inline-block`}
/>
<span className="text-muted align-middle" style={{ marginRight: '10px', verticalAlign: 'middle' }}>
checking {clusterName(searchingCluster).toLowerCase()}
</span>
</>
);
}
Loading