Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
9 changes: 5 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
},
"overrides": {
"@noble/hashes": "^1.8.0",
"@noble/curves": "^1.8.0"
"@noble/curves": "^1.8.0",
"axios": "^1.15.0"
},
"dependencies": {
"@erc7730/sdk": "^0.1.3",
"@openscan/network-connectors": "1.6.0",
"@openscan/network-connectors": "1.7",
"@rainbow-me/rainbowkit": "^2.2.8",
"@react-native-async-storage/async-storage": "^1.24.0",
"@tanstack/react-query": "^5.90.21",
Expand Down
35 changes: 35 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ import {
LazyRpcs,
LazySearch,
LazySettings,
LazySolanaAccount,
LazySolanaNetwork,
LazySolanaSlot,
LazySolanaSlots,
LazySolanaToken,
LazySolanaTx,
LazySolanaTxs,
LazySolanaValidators,
LazySupporters,
LazyTokenDetails,
LazyTx,
Expand Down Expand Up @@ -151,6 +159,33 @@ function AppContent() {
<Route path="tbtc/address/:address" element={<LazyBitcoinAddress />} />
<Route path="tbtc/mempool" element={<LazyBitcoinMempool />} />
<Route path="tbtc/mempool/:filter" element={<LazyBitcoinTx />} />
{/* Solana Mainnet routes (must come before :networkId catch-all) */}
<Route path="sol" element={<LazySolanaNetwork />} />
<Route path="sol/slots" element={<LazySolanaSlots />} />
<Route path="sol/slot/:filter" element={<LazySolanaSlot />} />
<Route path="sol/txs" element={<LazySolanaTxs />} />
<Route path="sol/tx/:filter" element={<LazySolanaTx />} />
<Route path="sol/account/:address" element={<LazySolanaAccount />} />
<Route path="sol/token/:mint" element={<LazySolanaToken />} />
<Route path="sol/validators" element={<LazySolanaValidators />} />
{/* Solana Devnet routes */}
<Route path="sol-devnet" element={<LazySolanaNetwork />} />
<Route path="sol-devnet/slots" element={<LazySolanaSlots />} />
<Route path="sol-devnet/slot/:filter" element={<LazySolanaSlot />} />
<Route path="sol-devnet/txs" element={<LazySolanaTxs />} />
<Route path="sol-devnet/tx/:filter" element={<LazySolanaTx />} />
<Route path="sol-devnet/account/:address" element={<LazySolanaAccount />} />
<Route path="sol-devnet/token/:mint" element={<LazySolanaToken />} />
<Route path="sol-devnet/validators" element={<LazySolanaValidators />} />
{/* Solana Testnet routes */}
<Route path="sol-testnet" element={<LazySolanaNetwork />} />
<Route path="sol-testnet/slots" element={<LazySolanaSlots />} />
<Route path="sol-testnet/slot/:filter" element={<LazySolanaSlot />} />
<Route path="sol-testnet/txs" element={<LazySolanaTxs />} />
<Route path="sol-testnet/tx/:filter" element={<LazySolanaTx />} />
<Route path="sol-testnet/account/:address" element={<LazySolanaAccount />} />
<Route path="sol-testnet/token/:mint" element={<LazySolanaToken />} />
<Route path="sol-testnet/validators" element={<LazySolanaValidators />} />
{/* EVM network routes — validated */}
<Route path=":networkId" element={<ValidateNetwork />}>
<Route index element={<LazyChain />} />
Expand Down
27 changes: 27 additions & 0 deletions src/components/LazyComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ const BitcoinTransactionsPage = lazy(() => import("./pages/bitcoin/BitcoinTransa
const BitcoinAddressPage = lazy(() => import("./pages/bitcoin/BitcoinAddressPage"));
const BitcoinMempoolPage = lazy(() => import("./pages/bitcoin/BitcoinMempoolPage"));

// Lazy load page components - Solana
const SolanaNetwork = lazy(() => import("./pages/solana"));
const SolanaSlotsPage = lazy(() => import("./pages/solana/SolanaSlotsPage"));
const SolanaSlotPage = lazy(() => import("./pages/solana/SolanaSlotPage"));
const SolanaTransactionsPage = lazy(() => import("./pages/solana/SolanaTransactionsPage"));
const SolanaTransactionPage = lazy(() => import("./pages/solana/SolanaTransactionPage"));
const SolanaAccountPage = lazy(() => import("./pages/solana/SolanaAccountPage"));
const SolanaTokenPage = lazy(() => import("./pages/solana/SolanaTokenPage"));
const SolanaValidatorsPage = lazy(() => import("./pages/solana/SolanaValidatorsPage"));

// Lazy load page components - EVM
const Chain = lazy(() => import("./pages/evm/network"));
const Blocks = lazy(() => import("./pages/evm/blocks"));
Expand Down Expand Up @@ -54,6 +64,14 @@ export const LazyBitcoinTx = withSuspense(BitcoinTransactionPage);
export const LazyBitcoinTxs = withSuspense(BitcoinTransactionsPage);
export const LazyBitcoinAddress = withSuspense(BitcoinAddressPage);
export const LazyBitcoinMempool = withSuspense(BitcoinMempoolPage);
export const LazySolanaNetwork = withSuspense(SolanaNetwork);
export const LazySolanaSlots = withSuspense(SolanaSlotsPage);
export const LazySolanaSlot = withSuspense(SolanaSlotPage);
export const LazySolanaTxs = withSuspense(SolanaTransactionsPage);
export const LazySolanaTx = withSuspense(SolanaTransactionPage);
export const LazySolanaAccount = withSuspense(SolanaAccountPage);
export const LazySolanaToken = withSuspense(SolanaTokenPage);
export const LazySolanaValidators = withSuspense(SolanaValidatorsPage);
export const LazyBlocks = withSuspense(Blocks);
export const LazyBlock = withSuspense(Block);
export const LazyTxs = withSuspense(Txs);
Expand Down Expand Up @@ -91,6 +109,15 @@ export function preloadAllRoutes() {
import("./pages/bitcoin/BitcoinTransactionsPage");
import("./pages/bitcoin/BitcoinAddressPage");
import("./pages/bitcoin/BitcoinMempoolPage");
// Solana pages
import("./pages/solana");
import("./pages/solana/SolanaSlotsPage");
import("./pages/solana/SolanaSlotPage");
import("./pages/solana/SolanaTransactionsPage");
import("./pages/solana/SolanaTransactionPage");
import("./pages/solana/SolanaAccountPage");
import("./pages/solana/SolanaTokenPage");
import("./pages/solana/SolanaValidatorsPage");
// EVM pages
import("./pages/evm/network");
import("./pages/evm/blocks");
Expand Down
9 changes: 9 additions & 0 deletions src/components/navbar/NetworkBlockIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps)
setGasPrice(null); // Bitcoin doesn't have gas
setIsLoading(false);
}
} else if (network.type === "solana" && dataService?.isSolana()) {
// Fetch Solana current slot
const adapter = dataService.getSolanaAdapter();
const slot = await adapter.getLatestSlot();
if (isMounted) {
setBlockNumber(slot);
setGasPrice(null); // Solana doesn't have gas in the EVM sense
setIsLoading(false);
}
} else if (network.type === "evm") {
// Fetch EVM block number
const urls = getRPCUrls(networkRpcKey, rpcUrls);
Expand Down
190 changes: 190 additions & 0 deletions src/components/pages/solana/SolanaAccountDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import type { SolanaAccount, SolanaSignatureInfo } from "../../../types";
import { formatSlotNumber, formatSol, shortenSolanaAddress } from "../../../utils/solanaUtils";
import CopyButton from "../../common/CopyButton";

interface SolanaAccountDisplayProps {
account: SolanaAccount;
signatures: SolanaSignatureInfo[];
networkId: string;
}

const SolanaAccountDisplay: React.FC<SolanaAccountDisplayProps> = React.memo(
({ account, signatures, networkId }) => {
const { t } = useTranslation("solana");

const accountTypeLabel = account.executable ? t("account.program") : t("account.wallet");

return (
<div className="page-with-analysis">
<div className="block-display-card">
<div className="block-display-header">
<span className="block-label">{t("account.title")}</span>
<span className="block-status-badge block-status-finalized">{accountTypeLabel}</span>
</div>

{/* Address — full width on top */}
<div className="tx-details">
<div className="tx-row">
<span className="tx-label">{t("account.address")}:</span>
<span
className="tx-value tx-mono"
style={{ display: "inline-flex", alignItems: "center" }}
>
{account.address}
<CopyButton value={account.address} />
</span>
</div>
</div>

{/* Two-column layout: account details | token holdings */}
<div className="btc-tx-details-grid">
{/* Left column — account details */}
<div className="btc-tx-details-column">
<div className="tx-row">
<span className="tx-label">{t("account.balance")}:</span>
<span className="tx-value tx-value-highlight">{formatSol(account.lamports)}</span>
</div>

<div className="tx-row">
<span className="tx-label">{t("account.owner")}:</span>
<span className="tx-value tx-mono">
<Link
to={`/${networkId}/account/${account.owner}`}
className="link-accent tx-mono"
title={account.owner}
>
{shortenSolanaAddress(account.owner, 10, 10)}
</Link>
</span>
</div>

<div className="tx-row">
<span className="tx-label">{t("account.executable")}:</span>
<span className="tx-value">
{account.executable ? t("account.yes") : t("account.no")}
</span>
</div>

<div className="tx-row">
<span className="tx-label">{t("account.dataSize")}:</span>
<span className="tx-value">{account.space.toLocaleString()} bytes</span>
</div>

<div className="tx-row">
<span className="tx-label">{t("account.rentEpoch")}:</span>
<span className="tx-value">{account.rentEpoch}</span>
</div>
</div>

{/* Right column — token holdings */}
<div className="btc-tx-details-column">
<div className="block-display-section">
<h3 className="block-display-section-title">
{t("account.tokenHoldings")}
{account.tokenAccounts && account.tokenAccounts.length > 0
? ` (${account.tokenAccounts.length})`
: ""}
</h3>
{account.tokenAccounts && account.tokenAccounts.length > 0 ? (
<div className="table-wrapper">
<table className="dash-table">
<thead>
<tr>
<th>{t("token.mint")}</th>
<th>{t("token.amount")}</th>
</tr>
</thead>
<tbody>
{account.tokenAccounts.map((holding) => (
<tr key={holding.tokenAccount}>
<td className="table-cell-mono">
<Link
to={`/${networkId}/token/${holding.mint}`}
className="table-cell-address"
title={holding.mint}
>
{shortenSolanaAddress(holding.mint, 8, 8)}
</Link>
</td>
<td className="table-cell-value">{holding.amount.uiAmountString}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="card-content">
<p className="text-muted margin-0">{t("account.noTokens")}</p>
</div>
)}
</div>
</div>
</div>

{/* Recent Transactions */}
<div className="block-display-section">
<h3 className="block-display-section-title">
{t("account.recentTransactions")}{" "}
{signatures.length > 0 ? `(${signatures.length})` : ""}
</h3>
{signatures.length > 0 ? (
<div className="table-wrapper">
<table className="dash-table">
<thead>
<tr>
<th>{t("transaction.signature")}</th>
<th>{t("transaction.status")}</th>
<th>{t("transaction.slot")}</th>
</tr>
</thead>
<tbody>
{signatures.map((sig) => (
<tr key={sig.signature}>
<td className="table-cell-mono">
<Link
to={`/${networkId}/tx/${sig.signature}`}
className="table-cell-address"
title={sig.signature}
>
{shortenSolanaAddress(sig.signature, 12, 12)}
</Link>
</td>
<td>
{sig.err ? (
<span className="table-status-badge table-status-failed">
✗ {t("transactions.failed")}
</span>
) : (
<span className="table-status-badge table-status-success">
✓ {t("transactions.success")}
</span>
)}
</td>
<td>
<Link to={`/${networkId}/slot/${sig.slot}`} className="table-cell-number">
{formatSlotNumber(sig.slot)}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="card-content">
<p className="text-muted margin-0">{t("account.noTransactions")}</p>
</div>
)}
</div>
</div>
</div>
);
},
);

SolanaAccountDisplay.displayName = "SolanaAccountDisplay";

export default SolanaAccountDisplay;
Loading
Loading