Skip to content
Open
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
3 changes: 3 additions & 0 deletions apps/explorer/src/lib/queries/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
Expand All @@ -39,6 +41,7 @@ export function transactionsQueryOptions(
params.page,
params.limit,
params.offset,
params.sort ?? 'desc',
params._key,
],
queryFn: async ({ signal }): Promise<TransactionsApiResponse> => {
Expand Down
22 changes: 22 additions & 0 deletions apps/explorer/src/routeTree.gen.ts

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

62 changes: 47 additions & 15 deletions apps/explorer/src/routes/_layout/address/$address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,34 @@ function RouteComponent() {
)
}

type ContractCreationResponse = {
creation: { blockNumber: string; timestamp: string } | null
error: string | null
}

async function fetchContractCreation(
address: Address.Address,
): Promise<ContractCreationResponse> {
const response = await fetch(`/api/contract/creation/${address}`)
return response.json() as Promise<ContractCreationResponse>
}

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(
Expand All @@ -652,32 +674,42 @@ 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 (for non-contracts)
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 { data: createdTimestamp } = useBlock({
const oldestTransaction = oldestData?.transactions?.at(0)
const { data: oldestTxTimestamp } = useBlock({
blockNumber: Hex.toBigInt(oldestTransaction?.blockNumber ?? '0x0'),
query: {
enabled: Boolean(oldestTransaction?.blockNumber),
select: (block) => block.timestamp,
},
})

// 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 (
Expand Down
140 changes: 140 additions & 0 deletions apps/explorer/src/routes/api/contract/creation/$address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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'

const creationCache = new Map<
string,
{ blockNumber: bigint; timestamp: bigint }
>()

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 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)
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,
1n,
latestBlock,
)

if (creationBlock === null) {
return Response.json({ creation: null, error: null })
}

const block = await client.getBlock({ blockNumber: creationBlock })

creationCache.set(cacheKey, {
blockNumber: creationBlock,
timestamp: block.timestamp,
})

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: NonNullable<ReturnType<typeof getPublicClient>>,
address: Address.Address,
low: bigint,
high: bigint,
): Promise<bigint | null> {
const MAX_BATCH_SIZE = 10

while (high - low > BigInt(MAX_BATCH_SIZE)) {
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 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 }
}
}),
)

for (const result of results) {
if (result.hasCode) {
return result.blockNum
}
}

return null
}
Loading