{/* Quick-match mode */}
{state.mode === 'quick-match' && (
<>
@@ -246,6 +295,249 @@ export const SearchDropdown = forwardRef
)}
+ {/* Preview mode */}
+ {state.mode === 'preview' && (
+ <>
+ {/* Preview loading */}
+ {state.previewLoading && (
+
+ {[1, 2].map((i) => (
+
+ ))}
+
+ )}
+
+ {/* Preview error */}
+ {!state.previewLoading && state.error && (
+
+ Preview unavailable
+
+ )}
+
+ {/* Preview tx results */}
+ {!state.previewLoading && !state.error && state.previewType === 'tx' && (() => {
+ const data = state.previewData as TxPreviewResponse | null;
+ if (!data || (!data.cadence && !data.evm)) {
+ return (
+
+ Transaction not found
+
+ );
+ }
+ return (
+ <>
+ {data.cadence && (() => {
+ const idx = globalIdx++;
+ return (
+ <>
+
+
+ >
+ );
+ })()}
+
+ {data.evm && (() => {
+ const idx = globalIdx++;
+ const sectionLabel = data.link
+ ? 'EVM Transaction (linked)'
+ : 'EVM Transaction';
+ return (
+ <>
+
+
+ >
+ );
+ })()}
+ >
+ );
+ })()}
+
+ {/* Preview address results */}
+ {!state.previewLoading && !state.error && state.previewType === 'address' && (() => {
+ const data = state.previewData as AddressPreviewResponse | null;
+ if (!data || (!data.cadence && !data.evm)) {
+ return (
+
+ Address not found
+
+ );
+ }
+
+ const searchedFlow = state.quickMatches[0]?.type === 'flow-account';
+ const hasCOALink = !!(data.link || data.coa_link);
+
+ // Render a Cadence address card
+ const renderCadence = (isPrimary: boolean) => {
+ if (!data.cadence) return null;
+ const idx = globalIdx++;
+ const cadenceAddr = data.cadence.address.startsWith('0x') ? data.cadence.address : `0x${data.cadence.address}`;
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ // Render an EVM address card
+ const renderEVM = (isPrimary: boolean) => {
+ if (!data.evm) return null;
+ const idx = globalIdx++;
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ return searchedFlow ? (
+ <>
+ {renderCadence(true)}
+ {renderEVM(false)}
+ >
+ ) : (
+ <>
+ {renderEVM(true)}
+ {renderCadence(false)}
+ >
+ );
+ })()}
+ >
+ )}
+
{/* Fuzzy mode — loading */}
{state.mode === 'fuzzy' && state.isLoading && (
@@ -272,7 +564,8 @@ export const SearchDropdown = forwardRef
0) && (
No results found
@@ -282,12 +575,13 @@ export const SearchDropdown = forwardRef 0 ||
- state.fuzzyResults.tokens.length > 0 ||
- state.fuzzyResults.nft_collections.length > 0) && (
+ ((state.fuzzyResults &&
+ (state.fuzzyResults.contracts.length > 0 ||
+ state.fuzzyResults.tokens.length > 0 ||
+ state.fuzzyResults.nft_collections.length > 0)) ||
+ (state.evmResults && state.evmResults.length > 0)) && (
<>
- {state.fuzzyResults.contracts.length > 0 && (
+ {state.fuzzyResults && state.fuzzyResults.contracts.length > 0 && (
<>
{state.fuzzyResults.contracts.map((c: SearchContractResult) => {
@@ -314,7 +608,7 @@ export const SearchDropdown = forwardRef
)}
- {state.fuzzyResults.tokens.length > 0 && (
+ {state.fuzzyResults && state.fuzzyResults.tokens.length > 0 && (
<>
{state.fuzzyResults.tokens.map((t: SearchTokenResult) => {
@@ -340,7 +634,7 @@ export const SearchDropdown = forwardRef
)}
- {state.fuzzyResults.nft_collections.length > 0 && (
+ {state.fuzzyResults && state.fuzzyResults.nft_collections.length > 0 && (
<>
{state.fuzzyResults.nft_collections.map(
@@ -374,6 +668,38 @@ export const SearchDropdown = forwardRef
)}
+
+ {/* EVM Results */}
+ {state.evmResults && state.evmResults.length > 0 && (
+ <>
+
+ {state.evmResults.map((item: BSSearchItem, i: number) => {
+ const idx = globalIdx++;
+ const route = evmItemRoute(item);
+ const displayLabel = item.name || item.address || '?';
+ const sublabel = item.symbol
+ ? item.symbol
+ : item.address
+ ? `${item.address.slice(0, 8)}...${item.address.slice(-6)}`
+ : undefined;
+ return (
+ }
+ label={
+
+ }
+ sublabel={sublabel}
+ badge="EVM"
+ badgeClass="bg-blue-500/10 text-blue-400"
+ onClick={() => goTo(route)}
+ />
+ );
+ })}
+ >
+ )}
>
)}
diff --git a/frontend/app/components/evm/COAAccountPage.tsx b/frontend/app/components/evm/COAAccountPage.tsx
new file mode 100644
index 000000000..bb9f8c6e5
--- /dev/null
+++ b/frontend/app/components/evm/COAAccountPage.tsx
@@ -0,0 +1,271 @@
+import { useState, useEffect } from 'react';
+import { Link } from '@tanstack/react-router';
+import { ArrowLeft, Activity, ArrowRightLeft, Coins, Wallet, ExternalLink, FileText, Image as ImageIcon } from 'lucide-react';
+import Avatar from 'boring-avatars';
+import { colorsFromAddress, avatarVariant } from '@/components/AddressLink';
+import { CopyButton } from '@/components/animate-ui/components/buttons/copy';
+import { PageHeader } from '@/components/ui/PageHeader';
+import { GlassCard } from '@flowindex/flow-ui';
+import { getEVMAddress } from '@/api/evm';
+import { formatWei } from '@/lib/evmUtils';
+import { EVMTransactionList } from './EVMTransactionList';
+import { EVMInternalTxList } from './EVMInternalTxList';
+import { EVMTokenTransfers } from './EVMTokenTransfers';
+import { EVMTokenHoldings } from './EVMTokenHoldings';
+import { AccountActivityTab } from '@/components/account/AccountActivityTab';
+import { AccountTokensTab } from '@/components/account/AccountTokensTab';
+import { AccountNFTsTab } from '@/components/account/AccountNFTsTab';
+import { AccountContractsTab } from '@/components/account/AccountContractsTab';
+import { ensureHeyApiConfigured } from '@/api/heyapi';
+import { getFlowV1AccountByAddress } from '@/api/gen/find';
+import type { BSAddress } from '@/types/blockscout';
+
+interface COAAccountPageProps {
+ evmAddress: string;
+ flowAddress: string;
+}
+
+type ViewMode = 'cadence' | 'evm';
+type CadenceTab = 'activity' | 'tokens' | 'nfts' | 'contracts';
+type EVMTab = 'transactions' | 'internal' | 'transfers' | 'holdings';
+
+export function COAAccountPage({ evmAddress, flowAddress }: COAAccountPageProps) {
+ const [viewMode, setViewMode] = useState('cadence');
+ const [cadenceTab, setCadenceTab] = useState('activity');
+ const [evmTab, setEvmTab] = useState('transactions');
+
+ // EVM address info (for balance)
+ const [addressInfo, setAddressInfo] = useState(null);
+ const [evmLoading, setEvmLoading] = useState(true);
+
+ // Cadence account info (for contracts list)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [cadenceAccount, setCadenceAccount] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ setEvmLoading(true);
+
+ getEVMAddress(evmAddress)
+ .then((res) => {
+ if (!cancelled) setAddressInfo(res);
+ })
+ .catch(() => {})
+ .finally(() => {
+ if (!cancelled) setEvmLoading(false);
+ });
+
+ return () => { cancelled = true; };
+ }, [evmAddress]);
+
+ // Fetch Cadence account data for contracts
+ useEffect(() => {
+ let cancelled = false;
+ const load = async () => {
+ try {
+ await ensureHeyApiConfigured();
+ const res = await getFlowV1AccountByAddress({ path: { address: flowAddress } });
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const payload: any = (res.data as any)?.data?.[0] ?? null;
+ if (!cancelled && payload) {
+ setCadenceAccount({
+ contracts: payload.contracts || [],
+ });
+ }
+ } catch {
+ // Non-critical — contracts tab will just show empty
+ }
+ };
+ load();
+ return () => { cancelled = true; };
+ }, [flowAddress]);
+
+ const balance = addressInfo?.coin_balance ? formatWei(addressInfo.coin_balance) : '0';
+
+ const cadenceTabs: { id: CadenceTab; label: string; icon: typeof Activity }[] = [
+ { id: 'activity', label: 'Activity', icon: Activity },
+ { id: 'tokens', label: 'Tokens', icon: Coins },
+ { id: 'nfts', label: 'NFTs', icon: ImageIcon },
+ { id: 'contracts', label: 'Contracts', icon: FileText },
+ ];
+
+ const evmTabs: { id: EVMTab; label: string; icon: typeof Activity }[] = [
+ { id: 'transactions', label: 'Transactions', icon: Activity },
+ { id: 'internal', label: 'Internal Txs', icon: ArrowRightLeft },
+ { id: 'transfers', label: 'Token Transfers', icon: Coins },
+ { id: 'holdings', label: 'Token Holdings', icon: Wallet },
+ ];
+
+ const activeTabs = viewMode === 'cadence' ? cadenceTabs : evmTabs;
+ const activeTabId = viewMode === 'cadence' ? cadenceTab : evmTab;
+ const setActiveTabId = viewMode === 'cadence'
+ ? (id: string) => setCadenceTab(id as CadenceTab)
+ : (id: string) => setEvmTab(id as EVMTab);
+
+ return (
+
+
+
+
+
+
+
+ COA Account
+
+ COA
+
+
+ }
+ subtitle={
+
+ {/* Flow address */}
+
+ Flow
+
+ {flowAddress}
+
+
+
+ {/* EVM address */}
+
+ EVM
+ {evmAddress}
+
+
+
+ }
+ >
+
+
EVM Balance
+ {evmLoading ? (
+
+ ) : (
+
+ {balance} FLOW
+
+ )}
+
+
+
+ {/* View Mode Switcher + Tabs */}
+
+ {/* View Mode Buttons */}
+
+
+
+
+
+ {/* Mobile Tab Selector */}
+
+
+
+
+ {/* Desktop Tab Bar */}
+
+
+ {activeTabs.map(({ id, label, icon: Icon }) => {
+ const isActive = activeTabId === id;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Tab Content */}
+
+ {/* Cadence tabs */}
+ {viewMode === 'cadence' && cadenceTab === 'activity' && (
+
+ )}
+ {viewMode === 'cadence' && cadenceTab === 'tokens' && (
+
+ )}
+ {viewMode === 'cadence' && cadenceTab === 'nfts' && (
+
+ )}
+ {viewMode === 'cadence' && cadenceTab === 'contracts' && (
+
+ )}
+
+ {/* EVM tabs */}
+ {viewMode === 'evm' && evmTab === 'transactions' && (
+
+ )}
+ {viewMode === 'evm' && evmTab === 'internal' && (
+
+ )}
+ {viewMode === 'evm' && evmTab === 'transfers' && (
+
+ )}
+ {viewMode === 'evm' && evmTab === 'holdings' && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/evm/EVMAccountPage.tsx b/frontend/app/components/evm/EVMAccountPage.tsx
new file mode 100644
index 000000000..64b95ec11
--- /dev/null
+++ b/frontend/app/components/evm/EVMAccountPage.tsx
@@ -0,0 +1,254 @@
+import { useState, useEffect } from 'react';
+import { Link, useNavigate } from '@tanstack/react-router';
+import { ArrowLeft, Activity, ArrowRightLeft, Coins, Wallet, ExternalLink, FileCode2, ImageIcon } from 'lucide-react';
+import Avatar from 'boring-avatars';
+import { colorsFromAddress, avatarVariant } from '@/components/AddressLink';
+import { CopyButton } from '@/components/animate-ui/components/buttons/copy';
+import { PageHeader } from '@/components/ui/PageHeader';
+import { GlassCard } from '@flowindex/flow-ui';
+import { getEVMAddress } from '@/api/evm';
+import { formatWei } from '@/lib/evmUtils';
+import { EVMTransactionList } from './EVMTransactionList';
+import { EVMInternalTxList } from './EVMInternalTxList';
+import { EVMTokenTransfers } from './EVMTokenTransfers';
+import { EVMTokenHoldings } from './EVMTokenHoldings';
+import { AccountTokensTab } from '@/components/account/AccountTokensTab';
+import { EVMNFTsTab } from './EVMNFTsTab';
+import type { BSAddress } from '@/types/blockscout';
+
+interface EVMAccountPageProps {
+ address: string;
+ flowAddress?: string;
+ isCOA: boolean;
+ initialTab?: string;
+}
+
+type EVMTab = 'transactions' | 'internal' | 'transfers' | 'tokens' | 'nfts';
+
+const VALID_TABS: EVMTab[] = ['transactions', 'internal', 'transfers', 'tokens', 'nfts'];
+
+export function EVMAccountPage({ address, flowAddress, isCOA, initialTab }: EVMAccountPageProps) {
+ const navigate = useNavigate();
+ const [addressInfo, setAddressInfo] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState(
+ VALID_TABS.includes(initialTab as EVMTab) ? (initialTab as EVMTab) : 'transactions'
+ );
+
+ const handleTabChange = (tab: EVMTab) => {
+ setActiveTab(tab);
+ navigate({ search: (prev: Record) => ({ ...prev, tab }) } as any);
+ };
+
+ useEffect(() => {
+ let cancelled = false;
+ setLoading(true);
+
+ getEVMAddress(address)
+ .then((res) => {
+ if (!cancelled) setAddressInfo(res);
+ })
+ .catch((err) => {
+ console.warn('[EVMAccountPage] Failed to load address info:', err?.message);
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false);
+ });
+
+ return () => { cancelled = true; };
+ }, [address]);
+
+ const balance = addressInfo?.coin_balance ? formatWei(addressInfo.coin_balance) : '0';
+ const txCount = addressInfo?.transactions_count ?? 0;
+
+ const tabs: { id: EVMTab; label: string; icon: typeof Activity }[] = [
+ { id: 'transactions', label: 'Transactions', icon: Activity },
+ { id: 'internal', label: 'Internal Txs', icon: ArrowRightLeft },
+ { id: 'transfers', label: 'Token Transfers', icon: Coins },
+ { id: 'tokens', label: 'Tokens', icon: Wallet },
+ { id: 'nfts', label: 'NFTs', icon: ImageIcon },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ {addressInfo?.name || 'EVM Account'}
+ {isCOA && (
+
+ COA
+
+ )}
+ {addressInfo?.is_contract && (
+
+
+ Contract
+
+ )}
+ {addressInfo?.is_verified && (
+
+ Verified
+
+ )}
+
+ }
+ subtitle={
+
+
+ {address}
+
+
+ {isCOA && flowAddress && (
+
+ Linked Flow Account:
+
+ {flowAddress}
+
+
+
+ )}
+
+ }
+ >
+
+
Balance
+ {loading ? (
+
+ ) : (
+
+ {balance} FLOW
+
+ )}
+
+
+
+ {/* Stats Cards */}
+
+
+
+ Transactions
+ {loading ? (
+
+ ) : (
+ {txCount.toLocaleString()}
+ )}
+
+
+
+
+
+
+ Token Transfers
+ {loading ? (
+
+ ) : (
+ {(addressInfo?.token_transfers_count ?? 0).toLocaleString()}
+ )}
+
+
+
+
+
+
+ Balance
+ {loading ? (
+
+ ) : (
+ {balance} FLOW
+ )}
+
+
+
+
+
+
+ Type
+
+ {addressInfo?.is_contract ? 'Contract' : isCOA ? 'COA' : 'EOA'}
+
+
+
+
+ {/* Tabs */}
+
+ {/* Mobile Tab Selector */}
+
+
+
+
+ {/* Desktop Tab Bar */}
+
+
+ {tabs.map(({ id, label, icon: Icon }) => {
+ const isActive = activeTab === id;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Tab Content */}
+
+ {activeTab === 'transactions' &&
}
+ {activeTab === 'internal' &&
}
+ {activeTab === 'transfers' &&
}
+ {activeTab === 'tokens' && (
+ isCOA && flowAddress ? (
+
+ ) : (
+
+ )
+ )}
+ {activeTab === 'nfts' &&
}
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/evm/EVMInternalTxList.tsx b/frontend/app/components/evm/EVMInternalTxList.tsx
new file mode 100644
index 000000000..f0ba8e46a
--- /dev/null
+++ b/frontend/app/components/evm/EVMInternalTxList.tsx
@@ -0,0 +1,178 @@
+import { useState, useEffect, useCallback } from 'react';
+import { getEVMAddressInternalTxs, getEVMTransactionInternalTxs } from '@/api/evm';
+import { formatWei, formatGas, internalTxTypeLabel } from '@/lib/evmUtils';
+import { AddressLink } from '@/components/AddressLink';
+import { LoadMorePagination } from '@/components/LoadMorePagination';
+import type { BSInternalTransaction, BSPageParams } from '@/types/blockscout';
+
+interface EVMInternalTxListProps {
+ address?: string;
+ txHash?: string;
+}
+
+export function EVMInternalTxList({ address, txHash }: EVMInternalTxListProps) {
+ const [items, setItems] = useState