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
26 changes: 3 additions & 23 deletions cmd/rpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
"path"
Expand Down Expand Up @@ -425,29 +424,10 @@ func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.C

// injectConfig() injects the config.json into the HTML file
func injectConfig(html string, config lib.Config, r *http.Request) string {
forwardedHost := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Host"), ",")[0])
if forwardedHost == "" {
forwardedHost = r.Host
}

scheme := "http"
if r.TLS != nil {
scheme = "https"
}
if forwardedProto := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Proto"), ",")[0]); forwardedProto != "" {
scheme = forwardedProto
}

hostname := forwardedHost
if host, _, err := net.SplitHostPort(forwardedHost); err == nil {
hostname = host
}

injectedConfig, err := json.Marshal(map[string]any{
"rpcURL": config.RPCUrl,
"adminRPCURL": config.AdminRPCUrl,
"explorerBaseURL": fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(hostname, config.ExplorerPort)),
"chainId": config.ChainId,
"rpcURL": config.RPCUrl,
"adminRPCURL": config.AdminRPCUrl,
"chainId": config.ChainId,
})
if err != nil {
injectedConfig = []byte("{}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export const ExplorerSidebar = (): React.JSX.Element => {
type="text"
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search blocks, transactions, addresses..."
placeholder="Search blocks, transactions, addresses, orders..."
className="h-full min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-zinc-500 outline-none"
/>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ExplorerTopBar = (): React.JSX.Element => {
type="text"
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search blocks, transactions, addresses..."
placeholder="Search blocks, transactions, addresses, orders..."
className="h-full min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-zinc-500 outline-none"
/>
</label>
Expand Down
3 changes: 2 additions & 1 deletion cmd/rpc/web/explorer/src/components/search/SearchFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const SearchFilters: React.FC<SearchFiltersProps> = ({ filters, onFilterChange }
{ value: 'blocks', label: 'Blocks' },
{ value: 'transactions', label: 'Transactions' },
{ value: 'addresses', label: 'Addresses' },
{ value: 'validators', label: 'Validators' }
{ value: 'validators', label: 'Validators' },
{ value: 'orders', label: 'Orders' }
]

const dateOptions = [
Expand Down
77 changes: 71 additions & 6 deletions cmd/rpc/web/explorer/src/components/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {

// Calculate actual counts from filtered results (using same logic as getFilteredResults)
const getActualCounts = () => {
if (!results) return { all: 0, blocks: 0, transactions: 0, addresses: 0, validators: 0 }
if (!results) return { all: 0, blocks: 0, transactions: 0, addresses: 0, validators: 0, orders: 0 }

// Use the same filtering logic as getFilteredResults to get accurate counts
const uniqueBlocksMap = new Map()
const uniqueTxMap = new Map()
const uniqueAddressesMap = new Map()
const uniqueValidatorsMap = new Map()
const uniqueOrdersMap = new Map()

// Process blocks with same validation as getFilteredResults
results.blocks?.forEach((block: any) => {
Expand Down Expand Up @@ -98,12 +99,23 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
}
})

// Process orders
results.orders?.forEach((order: any) => {
if (order && order.data) {
const oid = order.id || order.data.id || order.data.Id
if (oid && !uniqueOrdersMap.has(oid)) {
uniqueOrdersMap.set(oid, true)
}
}
})

return {
all: uniqueBlocksMap.size + uniqueTxMap.size + uniqueAddressesMap.size + uniqueValidatorsMap.size,
all: uniqueBlocksMap.size + uniqueTxMap.size + uniqueAddressesMap.size + uniqueValidatorsMap.size + uniqueOrdersMap.size,
blocks: uniqueBlocksMap.size,
transactions: uniqueTxMap.size,
addresses: uniqueAddressesMap.size,
validators: uniqueValidatorsMap.size
validators: uniqueValidatorsMap.size,
orders: uniqueOrdersMap.size
}
}

Expand All @@ -114,7 +126,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
{ id: 'blocks', label: 'Blocks', count: actualCounts.blocks },
{ id: 'transactions', label: 'Transactions', count: actualCounts.transactions },
{ id: 'addresses', label: 'Addresses', count: actualCounts.addresses },
{ id: 'validators', label: 'Validators', count: actualCounts.validators }
{ id: 'validators', label: 'Validators', count: actualCounts.validators },
{ id: 'orders', label: 'Orders', count: actualCounts.orders }
]

const parseTimestampToDate = (timestamp: unknown): Date | null => {
Expand Down Expand Up @@ -393,7 +406,40 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
{ label: 'Auto-Compound:', value: `${(item.compound ?? false) ? 'Yes' : 'No'}` },
{ label: 'Net Address:', value: `${item.netAddress ? item.netAddress : 'tcp://delegation'}` }
] as FieldConfig[]
}
},
order: (() => {
const oid = item.id || item.Id || ''
const committee = item.committee ?? item.Chain ?? ''
const buyerSendAddr = item.buyerSendAddress || item.BuyerSendAddress || ''
const orderStatus = buyerSendAddr ? 'Locked' : 'Active'
const amountForSale = item.amountForSale ?? item.AmountForSale ?? 0
const requestedAmount = item.requestedAmount ?? item.RequestedAmount ?? 0
const sellerSendAddr = item.sellersSendAddress || item.SellersSendAddress || 'N/A'
const sellerReceiveAddr = item.sellerReceiveAddress || item.SellerReceiveAddress || 'N/A'

return {
icon: 'fa-right-left',
iconColor: 'text-yellow-500',
bgColor: 'bg-yellow-700/30',
badgeClass: GREEN_BADGE_CLASS,
badgeText: orderStatus,
title: `Order ${oid.length > 16 ? truncateHash(oid, 8) : oid}`,
borderColor: 'border-gray-400/10',
hoverColor: 'hover:border-gray-400/20',
linkTo: `/order/${committee}/${oid}`,
copyValue: oid,
copyLabel: 'Copy Order ID',
fields: [
{ label: 'Order ID:', value: truncateHash(oid, 10) },
{ label: 'Committee:', value: String(committee) },
{ label: 'Status:', value: orderStatus },
{ label: 'Amount For Sale:', value: `${toCNPY(Number(amountForSale)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} CNPY` },
{ label: 'Requested:', value: `${toCNPY(Number(requestedAmount)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} CNPY` },
{ label: 'Seller Send:', value: truncateHash(sellerSendAddr, 6) },
{ label: 'Seller Receive:', value: truncateHash(sellerReceiveAddr, 6) },
] as FieldConfig[]
}
})()
}

const config = configs[type as keyof typeof configs]
Expand Down Expand Up @@ -442,6 +488,10 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
linkTo = `/account/${item.address}`
} else if (type === 'validator' && field.label === 'Address:') {
linkTo = `/validator/${item.address}`
} else if (type === 'order' && field.label === 'Order ID:') {
const oid = item.id || item.Id || ''
const committee = item.committee ?? item.Chain ?? ''
linkTo = `/order/${committee}/${oid}`
}

return (
Expand Down Expand Up @@ -492,6 +542,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
const uniqueTxMap = new Map()
const uniqueAddressesMap = new Map()
const uniqueValidatorsMap = new Map()
const uniqueOrdersMap = new Map()

// Process blocks and remove duplicates - filter out invalid blocks
results.blocks?.forEach((block: any) => {
Expand Down Expand Up @@ -537,11 +588,22 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
}
})

// Process orders
results.orders?.forEach((order: any) => {
if (order && order.data) {
const oid = order.id || order.data.id || order.data.Id
if (oid && !uniqueOrdersMap.has(oid)) {
uniqueOrdersMap.set(oid, { ...order.data, resultType: 'order' })
}
}
})

// Get unique arrays from Maps
const uniqueBlocks = Array.from(uniqueBlocksMap.values())
const uniqueTransactions = Array.from(uniqueTxMap.values())
const uniqueAddresses = Array.from(uniqueAddressesMap.values())
const uniqueValidators = Array.from(uniqueValidatorsMap.values())
const uniqueOrders = Array.from(uniqueOrdersMap.values())

// Determine which results to show based on activeTab
let filteredResults = []
Expand All @@ -551,7 +613,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
...uniqueBlocks,
...uniqueTransactions,
...uniqueAddresses,
...uniqueValidators
...uniqueValidators,
...uniqueOrders
]
} else if (activeTab === 'blocks') {
filteredResults = uniqueBlocks
Expand All @@ -561,6 +624,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({ results, filters }) => {
filteredResults = uniqueAddresses
} else if (activeTab === 'validators') {
filteredResults = uniqueValidators
} else if (activeTab === 'orders') {
filteredResults = uniqueOrders
}

// Apply filters if provided
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { Copy } from 'lucide-react'
import { useTxByHash, useBlockByHeight } from '../../hooks/useApi'
import { useTxByHash, useBlockByHeight, useLatestBlock } from '../../hooks/useApi'
import toast from 'react-hot-toast'
import { format, formatDistanceToNow, parseISO, isValid } from 'date-fns'

Expand Down Expand Up @@ -69,6 +69,17 @@ const TransactionDetailPage: React.FC = () => {
// Use the real hook to get transaction data
const { data: transactionData, isLoading, error } = useTxByHash(transactionHash || '')

// Get the latest confirmed block height to detect pending transactions.
// If the tx height is ahead of the current chain height, it hasn't been
// included in a block yet — it's a pending (mempool) transaction.
const { data: latestBlockData } = useLatestBlock()
const latestHeight = Number(
latestBlockData?.results?.[0]?.blockHeader?.height
?? latestBlockData?.results?.[0]?.height
?? latestBlockData?.height
?? 0
)

// Get block data to find all transactions in the same block
const txBlockHeight = transactionData?.result?.height || transactionData?.height || 0
const { data: blockData } = useBlockByHeight(txBlockHeight)
Expand All @@ -78,10 +89,13 @@ const TransactionDetailPage: React.FC = () => {
const transactionFeeMicro = transaction?.transaction?.fee || transaction?.fee || 0
const txType = transaction?.transaction?.type || transaction?.messageType || transaction?.type || 'send'

const txHeight = transaction?.height || transaction?.blockHeight || transaction?.block || 0
const rawStatus = String(transaction?.status || '').toLowerCase()
const isPending = txHeight === 0 || rawStatus === 'pending' || (latestHeight > 0 && txHeight > latestHeight)

// Helper function to normalize hash for comparison
const normalizeHash = (hash: string): string => {
if (!hash) return ''
// Remove '0x' prefix if present and convert to lowercase
return hash.replace(/^0x/i, '').toLowerCase()
}

Expand Down Expand Up @@ -243,10 +257,7 @@ const TransactionDetailPage: React.FC = () => {
}

// Extract data from the API response (using transaction already extracted above)
const blockHeight = transaction?.height || transaction?.blockHeight || transaction?.block || 0
const rawStatus = transaction?.status || ''
const isPending = blockHeight === 0 || rawStatus.toLowerCase() === 'pending'
const status = isPending ? 'pending' : (rawStatus || 'success')
const blockHeight = txHeight
const timestamp = transaction?.transaction?.time || transaction?.timestamp || transaction?.time || new Date().toISOString()
const fee = formatFee(transactionFeeMicro)

Expand Down Expand Up @@ -377,12 +388,20 @@ const TransactionDetailPage: React.FC = () => {

<div className="flex flex-col border-b border-gray-400/30 pb-4 gap-2">
<span className="text-gray-400 text-sm">Block</span>
<span className="text-primary">{blockHeight.toLocaleString()}</span>
{isPending ? (
<span className="text-sm font-medium text-yellow-400">Mempool</span>
) : (
<span className="text-primary">{blockHeight.toLocaleString()}</span>
)}
</div>

<div className="flex flex-col border-b border-gray-400/30 pb-4 gap-2">
<span className="text-gray-400 text-sm">Timestamp</span>
<span className="text-white text-sm">{formatTimestamp(timestamp)}</span>
{isPending ? (
<span className="text-sm font-medium text-yellow-400">Pending</span>
) : (
<span className="text-white text-sm">{formatTimestamp(timestamp)}</span>
)}
</div>

<div className="flex flex-col border-b border-gray-400/30 pb-4 gap-2">
Expand Down Expand Up @@ -535,7 +554,7 @@ const TransactionDetailPage: React.FC = () => {
<span className="text-gray-400 text-sm">Transaction Type</span>
<TransactionTypeBadge type={txType} />
</div>
{position !== null && (
{position !== null && !isPending && (
<div className="flex justify-between items-center">
<span className="text-gray-400 text-sm">Position in Block</span>
<span className="text-white text-sm">{position}</span>
Expand Down
44 changes: 39 additions & 5 deletions cmd/rpc/web/explorer/src/hooks/useSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import {
BlockByHash,
TxByHash,
Validator,
Account
Account,
AllOrders,
} from '../lib/api'
import { toCNPY } from '../lib/utils'

interface SearchResult {
type: 'block' | 'transaction' | 'address' | 'validator'
type: 'block' | 'transaction' | 'address' | 'validator' | 'order'
id: string
title: string
subtitle?: string
data: any
data: Record<string, unknown>
}

interface SearchResults {
Expand All @@ -24,6 +25,7 @@ interface SearchResults {
transactions: SearchResult[]
addresses: SearchResult[]
validators: SearchResult[]
orders: SearchResult[]
}

const formatAccountBalanceSubtitle = (amount: number | string | undefined) =>
Expand Down Expand Up @@ -61,7 +63,8 @@ export const useSearch = (searchTerm: string) => {
blocks: [],
transactions: [],
addresses: [],
validators: []
validators: [],
orders: []
}

// DIRECT SEARCH FOR BLOCKS, TRANSACTIONS, ACCOUNTS, AND VALIDATORS
Expand Down Expand Up @@ -351,14 +354,45 @@ export const useSearch = (searchTerm: string) => {
)
}

// 4. Search orders by ID across all paginated order-book results
searchPromises.push(
AllOrders()
.then((ordersList: Record<string, unknown>[]) => {
const termLower = term.toLowerCase()
const matchingOrders = ordersList.filter((order: Record<string, unknown>) => {
const id = String(order.id || order.Id || '')
return id.toLowerCase().includes(termLower)
})

matchingOrders.forEach((order: Record<string, unknown>) => {
const id = String(order.id || order.Id || '')
const committee = order.committee ?? order.Chain ?? ''
const buyerSendAddress = order.buyerSendAddress || order.BuyerSendAddress || ''
const status = buyerSendAddress ? 'Locked' : 'Active'

if (!searchResults.orders.some(o => o.id === id)) {
searchResults.orders.push({
type: 'order' as const,
id,
title: `Order ${id.length > 16 ? id.slice(0, 8) + '...' + id.slice(-8) : id}`,
subtitle: `Committee ${committee} · ${status}`,
data: order
})
}
})
})
.catch(err => console.log('Order search error:', err))
)

// Wait for all promises to complete
await Promise.all(searchPromises)

// Calculate total
const total = searchResults.blocks.length +
searchResults.transactions.length +
searchResults.addresses.length +
searchResults.validators.length
searchResults.validators.length +
searchResults.orders.length

setResults({
...searchResults,
Expand Down
Loading
Loading