From 87785495bd1daef8f8a2a649fcd495cda0a2c4e1 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 21 Jan 2026 22:32:14 +0000 Subject: [PATCH 1/3] fix(explorer): show Created timestamp for addresses The Created field was always showing '...' because: 1. totalTransactions was hardcoded to 0 2. The oldest transaction query was disabled (enabled: totalTransactions > 0) Fix: Use sort=asc with limit=1 to directly fetch the oldest transaction instead of trying to calculate the last page offset. This adds the 'sort' parameter to transactionsQueryOptions and uses it in AccountCardWithTimestamps to fetch the first (oldest) transaction. Note: For contracts, the creation transaction may not appear if the IndexSupply query doesn't include it (since contractAddress is in the receipt, not the tx). A follow-up could address this by querying for contract creation txs specifically. --- apps/explorer/src/lib/queries/account.ts | 3 +++ .../src/routes/_layout/address/$address.tsx | 23 ++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/explorer/src/lib/queries/account.ts b/apps/explorer/src/lib/queries/account.ts index 73eecdade..4fa22bb3e 100644 --- a/apps/explorer/src/lib/queries/account.ts +++ b/apps/explorer/src/lib/queries/account.ts @@ -23,12 +23,14 @@ export function transactionsQueryOptions( params: { page: number include?: 'all' | 'sent' | 'received' | undefined + sort?: 'asc' | 'desc' | undefined address: Address.Address _key?: string | undefined } & AccountRequestParameters, ) { const searchParams = new URLSearchParams({ include: params?.include ?? 'all', + sort: params?.sort ?? 'desc', limit: params.limit.toString(), offset: params.offset.toString(), }) @@ -39,6 +41,7 @@ export function transactionsQueryOptions( params.page, params.limit, params.offset, + params.sort ?? 'desc', params._key, ], queryFn: async ({ signal }): Promise => { diff --git a/apps/explorer/src/routes/_layout/address/$address.tsx b/apps/explorer/src/routes/_layout/address/$address.tsx index 3a1c676bd..b46d05202 100644 --- a/apps/explorer/src/routes/_layout/address/$address.tsx +++ b/apps/explorer/src/routes/_layout/address/$address.tsx @@ -652,24 +652,19 @@ function AccountCardWithTimestamps(props: { }, }) - // Use the real transaction count (not the approximate total from pagination) - // Don't fetch exact count - use API hasMore flag for pagination - // This makes the page render instantly without waiting for count query - const totalTransactions = 0 // Unknown until user navigates - const lastPageOffset = 0 // Can't calculate without total - - const { data: oldestData } = useQuery({ - ...transactionsQueryOptions({ + // Fetch the oldest transaction by sorting ascending + const { data: oldestData } = useQuery( + transactionsQueryOptions({ address, - page: Math.ceil(totalTransactions / 1), + page: 1, limit: 1, - offset: lastPageOffset, - _key: 'account-creation', + offset: 0, + sort: 'asc', + _key: 'account-oldest', }), - enabled: totalTransactions > 0, - }) + ) - const [oldestTransaction] = oldestData?.transactions ?? [] + const oldestTransaction = oldestData?.transactions?.at(0) const { data: createdTimestamp } = useBlock({ blockNumber: Hex.toBigInt(oldestTransaction?.blockNumber ?? '0x0'), query: { From 1d74a15c0f4697ed235d3bd1810aa15a0d18c689 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 21 Jan 2026 22:42:56 +0000 Subject: [PATCH 2/3] fix(explorer): add contract creation lookup for Created field For contracts with no transaction history, the Created field was showing '...' because there were no transactions to derive the timestamp from. This adds a new API endpoint /api/contract/creation/$address that: 1. Checks if the address is a contract (has bytecode) 2. Uses binary search to find the creation block 3. Returns the block number and timestamp The AccountCardWithTimestamps component now: - First tries to get the oldest transaction timestamp (for addresses) - Falls back to the contract creation API for contracts without txs This fixes the issue where https://explore.tempo.xyz/address/0x0000f90827f1c53a10cb7a02335b175320002935 showed '...' for Created because the contract creation tx wasn't in the transaction history (IndexSupply only indexes from/to, not contractAddress). Amp-Thread-ID: https://ampcode.com/threads/T-019be242-b261-775a-be3c-4afeef66f51b Co-authored-by: Amp --- apps/explorer/src/routeTree.gen.ts | 22 ++++ .../src/routes/_layout/address/$address.tsx | 41 ++++++- .../routes/api/contract/creation/$address.ts | 102 ++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 apps/explorer/src/routes/api/contract/creation/$address.ts diff --git a/apps/explorer/src/routeTree.gen.ts b/apps/explorer/src/routeTree.gen.ts index b11e86b7e..af91a9b64 100644 --- a/apps/explorer/src/routeTree.gen.ts +++ b/apps/explorer/src/routeTree.gen.ts @@ -32,6 +32,7 @@ import { Route as LayoutBlockIdRouteImport } from './routes/_layout/block/$id' import { Route as LayoutAddressAddressRouteImport } from './routes/_layout/address/$address' import { Route as ApiTxTraceHashRouteImport } from './routes/api/tx/trace/$hash' import { Route as ApiTxBalanceChangesHashRouteImport } from './routes/api/tx/balance-changes/$hash' +import { Route as ApiContractCreationAddressRouteImport } from './routes/api/contract/creation/$address' import { Route as ApiAddressTxsCountAddressRouteImport } from './routes/api/address/txs-count/$address' import { Route as ApiAddressTotalValueAddressRouteImport } from './routes/api/address/total-value/$address' import { Route as ApiAddressBalancesAddressRouteImport } from './routes/api/address/balances/$address' @@ -151,6 +152,12 @@ const ApiTxBalanceChangesHashRoute = ApiTxBalanceChangesHashRouteImport.update({ path: '/api/tx/balance-changes/$hash', getParentRoute: () => rootRouteImport, } as any) +const ApiContractCreationAddressRoute = + ApiContractCreationAddressRouteImport.update({ + id: '/api/contract/creation/$address', + path: '/api/contract/creation/$address', + getParentRoute: () => rootRouteImport, + } as any) const ApiAddressTxsCountAddressRoute = ApiAddressTxsCountAddressRouteImport.update({ id: '/api/address/txs-count/$address', @@ -201,6 +208,7 @@ export interface FileRoutesByFullPath { '/api/address/balances/$address': typeof ApiAddressBalancesAddressRoute '/api/address/total-value/$address': typeof ApiAddressTotalValueAddressRoute '/api/address/txs-count/$address': typeof ApiAddressTxsCountAddressRoute + '/api/contract/creation/$address': typeof ApiContractCreationAddressRoute '/api/tx/balance-changes/$hash': typeof ApiTxBalanceChangesHashRoute '/api/tx/trace/$hash': typeof ApiTxTraceHashRoute } @@ -229,6 +237,7 @@ export interface FileRoutesByTo { '/api/address/balances/$address': typeof ApiAddressBalancesAddressRoute '/api/address/total-value/$address': typeof ApiAddressTotalValueAddressRoute '/api/address/txs-count/$address': typeof ApiAddressTxsCountAddressRoute + '/api/contract/creation/$address': typeof ApiContractCreationAddressRoute '/api/tx/balance-changes/$hash': typeof ApiTxBalanceChangesHashRoute '/api/tx/trace/$hash': typeof ApiTxTraceHashRoute } @@ -259,6 +268,7 @@ export interface FileRoutesById { '/api/address/balances/$address': typeof ApiAddressBalancesAddressRoute '/api/address/total-value/$address': typeof ApiAddressTotalValueAddressRoute '/api/address/txs-count/$address': typeof ApiAddressTxsCountAddressRoute + '/api/contract/creation/$address': typeof ApiContractCreationAddressRoute '/api/tx/balance-changes/$hash': typeof ApiTxBalanceChangesHashRoute '/api/tx/trace/$hash': typeof ApiTxTraceHashRoute } @@ -289,6 +299,7 @@ export interface FileRouteTypes { | '/api/address/balances/$address' | '/api/address/total-value/$address' | '/api/address/txs-count/$address' + | '/api/contract/creation/$address' | '/api/tx/balance-changes/$hash' | '/api/tx/trace/$hash' fileRoutesByTo: FileRoutesByTo @@ -317,6 +328,7 @@ export interface FileRouteTypes { | '/api/address/balances/$address' | '/api/address/total-value/$address' | '/api/address/txs-count/$address' + | '/api/contract/creation/$address' | '/api/tx/balance-changes/$hash' | '/api/tx/trace/$hash' id: @@ -346,6 +358,7 @@ export interface FileRouteTypes { | '/api/address/balances/$address' | '/api/address/total-value/$address' | '/api/address/txs-count/$address' + | '/api/contract/creation/$address' | '/api/tx/balance-changes/$hash' | '/api/tx/trace/$hash' fileRoutesById: FileRoutesById @@ -361,6 +374,7 @@ export interface RootRouteChildren { ApiAddressBalancesAddressRoute: typeof ApiAddressBalancesAddressRoute ApiAddressTotalValueAddressRoute: typeof ApiAddressTotalValueAddressRoute ApiAddressTxsCountAddressRoute: typeof ApiAddressTxsCountAddressRoute + ApiContractCreationAddressRoute: typeof ApiContractCreationAddressRoute ApiTxBalanceChangesHashRoute: typeof ApiTxBalanceChangesHashRoute ApiTxTraceHashRoute: typeof ApiTxTraceHashRoute } @@ -528,6 +542,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiTxBalanceChangesHashRouteImport parentRoute: typeof rootRouteImport } + '/api/contract/creation/$address': { + id: '/api/contract/creation/$address' + path: '/api/contract/creation/$address' + fullPath: '/api/contract/creation/$address' + preLoaderRoute: typeof ApiContractCreationAddressRouteImport + parentRoute: typeof rootRouteImport + } '/api/address/txs-count/$address': { id: '/api/address/txs-count/$address' path: '/api/address/txs-count/$address' @@ -609,6 +630,7 @@ const rootRouteChildren: RootRouteChildren = { ApiAddressBalancesAddressRoute: ApiAddressBalancesAddressRoute, ApiAddressTotalValueAddressRoute: ApiAddressTotalValueAddressRoute, ApiAddressTxsCountAddressRoute: ApiAddressTxsCountAddressRoute, + ApiContractCreationAddressRoute: ApiContractCreationAddressRoute, ApiTxBalanceChangesHashRoute: ApiTxBalanceChangesHashRoute, ApiTxTraceHashRoute: ApiTxTraceHashRoute, } diff --git a/apps/explorer/src/routes/_layout/address/$address.tsx b/apps/explorer/src/routes/_layout/address/$address.tsx index b46d05202..d8b886f0e 100644 --- a/apps/explorer/src/routes/_layout/address/$address.tsx +++ b/apps/explorer/src/routes/_layout/address/$address.tsx @@ -624,12 +624,34 @@ function RouteComponent() { ) } +type ContractCreationResponse = { + creation: { blockNumber: string; timestamp: string } | null + error: string | null +} + +async function fetchContractCreation( + address: Address.Address, +): Promise { + const response = await fetch(`/api/contract/creation/${address}`) + return response.json() as Promise +} + +function useContractCreation(address: Address.Address, enabled: boolean) { + return useQuery({ + queryKey: ['contract-creation', address], + queryFn: () => fetchContractCreation(address), + enabled, + staleTime: 60_000, + }) +} + function AccountCardWithTimestamps(props: { address: Address.Address assetsData: AssetData[] accountType?: AccountType }) { const { address, assetsData, accountType } = props + const isContract = accountType === 'contract' // fetch the most recent transactions (pg.1) const { data: recentData } = useQuery( @@ -652,7 +674,7 @@ function AccountCardWithTimestamps(props: { }, }) - // Fetch the oldest transaction by sorting ascending + // Fetch the oldest transaction by sorting ascending (for non-contracts) const { data: oldestData } = useQuery( transactionsQueryOptions({ address, @@ -665,7 +687,7 @@ function AccountCardWithTimestamps(props: { ) const oldestTransaction = oldestData?.transactions?.at(0) - const { data: createdTimestamp } = useBlock({ + const { data: oldestTxTimestamp } = useBlock({ blockNumber: Hex.toBigInt(oldestTransaction?.blockNumber ?? '0x0'), query: { enabled: Boolean(oldestTransaction?.blockNumber), @@ -673,6 +695,21 @@ function AccountCardWithTimestamps(props: { }, }) + // For contracts without transactions, use binary search to find creation block + const noTransactions = !oldestTransaction + const { data: contractCreation } = useContractCreation( + address, + isContract && noTransactions, + ) + + // Use contract creation timestamp if available, otherwise fall back to oldest tx + const createdTimestamp = React.useMemo(() => { + if (contractCreation?.creation?.timestamp) { + return BigInt(contractCreation.creation.timestamp) + } + return oldestTxTimestamp + }, [contractCreation, oldestTxTimestamp]) + const totalValue = calculateTotalHoldings(assetsData) return ( diff --git a/apps/explorer/src/routes/api/contract/creation/$address.ts b/apps/explorer/src/routes/api/contract/creation/$address.ts new file mode 100644 index 000000000..708f7d03a --- /dev/null +++ b/apps/explorer/src/routes/api/contract/creation/$address.ts @@ -0,0 +1,102 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as Address from 'ox/Address' +import { getChainId, getPublicClient } from 'wagmi/actions' +import { zAddress } from '#lib/zod' +import { getWagmiConfig } from '#wagmi.config' + +export const Route = createFileRoute('/api/contract/creation/$address')({ + server: { + handlers: { + GET: async ({ params }: { params: { address: string } }) => { + try { + const address = zAddress().parse(params.address) + Address.assert(address) + + const config = getWagmiConfig() + const chainId = getChainId(config) + const client = getPublicClient(config, { chainId }) + + if (!client) { + return Response.json( + { creation: null, error: 'No client available' }, + { status: 500 }, + ) + } + + const bytecode = await client.getCode({ address }) + if (!bytecode || bytecode === '0x') { + return Response.json({ creation: null, error: null }) + } + + const latestBlock = await client.getBlockNumber() + const creationBlock = await binarySearchCreationBlock( + client, + address, + 0n, + latestBlock, + ) + + if (creationBlock === null) { + return Response.json({ creation: null, error: null }) + } + + const block = await client.getBlock({ blockNumber: creationBlock }) + + return Response.json({ + creation: { + blockNumber: creationBlock.toString(), + timestamp: block.timestamp.toString(), + }, + error: null, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : error + console.error('[contract/creation] Error:', errorMessage) + return Response.json( + { creation: null, error: errorMessage }, + { status: 500 }, + ) + } + }, + }, + }, +}) + +async function binarySearchCreationBlock( + client: ReturnType, + address: Address.Address, + low: bigint, + high: bigint, +): Promise { + if (!client) return null + + while (low < high) { + const mid = (low + high) / 2n + + try { + const code = await client.getCode({ + address, + blockNumber: mid, + }) + + if (code && code !== '0x') { + high = mid + } else { + low = mid + 1n + } + } catch { + low = mid + 1n + } + } + + const finalCode = await client.getCode({ + address, + blockNumber: low, + }) + + if (finalCode && finalCode !== '0x') { + return low + } + + return null +} From 64e3016b5072292414518a040c4d037ae646e60f Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 21 Jan 2026 23:00:09 +0000 Subject: [PATCH 3/3] fix(explorer): optimize contract creation lookup with caching The binary search was taking ~40 seconds due to slow historical RPC calls. Optimizations: - Add in-memory cache to avoid repeated lookups for the same contract - Parallelize the final batch of block checks - Start search from block 1 instead of 0 This significantly improves response time for repeated requests. --- .../routes/api/contract/creation/$address.ts | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/apps/explorer/src/routes/api/contract/creation/$address.ts b/apps/explorer/src/routes/api/contract/creation/$address.ts index 708f7d03a..04d9594e2 100644 --- a/apps/explorer/src/routes/api/contract/creation/$address.ts +++ b/apps/explorer/src/routes/api/contract/creation/$address.ts @@ -4,6 +4,11 @@ import { getChainId, getPublicClient } from 'wagmi/actions' import { zAddress } from '#lib/zod' import { getWagmiConfig } from '#wagmi.config' +const creationCache = new Map< + string, + { blockNumber: bigint; timestamp: bigint } +>() + export const Route = createFileRoute('/api/contract/creation/$address')({ server: { handlers: { @@ -11,6 +16,18 @@ export const Route = createFileRoute('/api/contract/creation/$address')({ try { const address = zAddress().parse(params.address) Address.assert(address) + const cacheKey = address.toLowerCase() + + const cached = creationCache.get(cacheKey) + if (cached) { + return Response.json({ + creation: { + blockNumber: cached.blockNumber.toString(), + timestamp: cached.timestamp.toString(), + }, + error: null, + }) + } const config = getWagmiConfig() const chainId = getChainId(config) @@ -32,7 +49,7 @@ export const Route = createFileRoute('/api/contract/creation/$address')({ const creationBlock = await binarySearchCreationBlock( client, address, - 0n, + 1n, latestBlock, ) @@ -42,6 +59,11 @@ export const Route = createFileRoute('/api/contract/creation/$address')({ const block = await client.getBlock({ blockNumber: creationBlock }) + creationCache.set(cacheKey, { + blockNumber: creationBlock, + timestamp: block.timestamp, + }) + return Response.json({ creation: { blockNumber: creationBlock.toString(), @@ -63,14 +85,14 @@ export const Route = createFileRoute('/api/contract/creation/$address')({ }) async function binarySearchCreationBlock( - client: ReturnType, + client: NonNullable>, address: Address.Address, low: bigint, high: bigint, ): Promise { - if (!client) return null + const MAX_BATCH_SIZE = 10 - while (low < high) { + while (high - low > BigInt(MAX_BATCH_SIZE)) { const mid = (low + high) / 2n try { @@ -89,13 +111,29 @@ async function binarySearchCreationBlock( } } - const finalCode = await client.getCode({ - address, - blockNumber: low, - }) + const blocksToCheck = [] + for (let b = low; b <= high; b++) { + blocksToCheck.push(b) + } + + const results = await Promise.all( + blocksToCheck.map(async (blockNum) => { + try { + const code = await client.getCode({ + address, + blockNumber: blockNum, + }) + return { blockNum, hasCode: Boolean(code && code !== '0x') } + } catch { + return { blockNum, hasCode: false } + } + }), + ) - if (finalCode && finalCode !== '0x') { - return low + for (const result of results) { + if (result.hasCode) { + return result.blockNum + } } return null