Skip to content
Closed
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
83 changes: 46 additions & 37 deletions apps/explorer/src/routes/api/address/metadata/$address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,51 +32,45 @@ export const Route = createFileRoute('/api/address/metadata/$address')({
server: {
handlers: {
GET: async ({ params }) => {
const fallback: AddressMetadataResponse = {
address: params.address,
chainId: 0,
accountType: 'empty',
}

if (!hasIndexSupply()) return Response.json(fallback)
const { id: chainId } = getTempoChain()

try {
const address = zAddress().parse(params.address)
Address.assert(address)

const client = getBatchedClient()
const { id: chainId } = getTempoChain()
const isTip20 = isTip20Address(address)
const isVirtual = VirtualAddress.validate(address)

const bytecodePromise = getCode(client, { address }).catch(
const bytecode = await getCode(client, { address }).catch(
() => undefined,
)
const baseResponse: AddressMetadataResponse = {
address,
chainId,
accountType: getAccountType(bytecode),
}

if (!hasIndexSupply()) return Response.json(baseResponse)

let response: AddressMetadataResponse

if (isVirtual) {
const [bytecode, result] = await Promise.all([
bytecodePromise,
fetchVirtualAddressTransferAggregate(address, chainId).catch(
() => ({
count: 0,
oldestTimestamp: undefined,
latestTimestamp: undefined,
}),
),
])
response = {
const result = await fetchVirtualAddressTransferAggregate(
address,
chainId,
accountType: getAccountType(bytecode),
).catch(() => ({
count: 0,
oldestTimestamp: undefined,
latestTimestamp: undefined,
}))
response = {
...baseResponse,
txCount: result.count ?? 0,
lastActivityTimestamp: parseTimestamp(result.latestTimestamp),
createdTimestamp: parseTimestamp(result.oldestTimestamp),
}
} else if (isTip20) {
const [bytecode, result, holdersRows] = await Promise.all([
bytecodePromise,
const [result, holdersRows] = await Promise.all([
fetchTokenTransferAggregate(address, chainId).catch(() => ({
oldestTimestamp: undefined,
latestTimestamp: undefined,
Expand All @@ -86,29 +80,39 @@ export const Route = createFileRoute('/api/address/metadata/$address')({
),
])
response = {
address,
chainId,
accountType: getAccountType(bytecode),
...baseResponse,
holdersCount: holdersRows[0]?.count ?? 0,
lastActivityTimestamp: parseTimestamp(result.latestTimestamp),
createdTimestamp: parseTimestamp(result.oldestTimestamp),
}
} else {
const [bytecode, result] = await Promise.all([
bytecodePromise,
const aggregate = await Promise.allSettled([
fetchAddressTxAggregate(address, chainId),
])
const result = aggregate[0]
if (result.status === 'rejected') console.error(result.reason)
response = {
address,
chainId,
accountType: getAccountType(bytecode),
txCount: result.count ?? 0,
...baseResponse,
txCount:
result.status === 'fulfilled' ? result.value.count : undefined,
lastActivityTimestamp: parseTimestamp(
result.latestTxsBlockTimestamp,
result.status === 'fulfilled'
? result.value.latestTxsBlockTimestamp
: undefined,
),
createdTimestamp: parseTimestamp(result.oldestTxsBlockTimestamp),
createdTxHash: result.oldestTxHash,
createdBy: result.oldestTxFrom,
createdTimestamp: parseTimestamp(
result.status === 'fulfilled'
? result.value.oldestTxsBlockTimestamp
: undefined,
),
createdTxHash:
result.status === 'fulfilled'
? result.value.oldestTxHash
: undefined,
createdBy:
result.status === 'fulfilled'
? result.value.oldestTxFrom
: undefined,
}
}

Expand All @@ -120,6 +124,11 @@ export const Route = createFileRoute('/api/address/metadata/$address')({
} catch (error) {
console.error(error)
const errorMessage = error instanceof Error ? error.message : error
const fallback: AddressMetadataResponse = {
address: params.address,
chainId,
accountType: 'empty',
}
return Response.json(
{ ...fallback, error: String(errorMessage) },
{ status: 500 },
Expand Down
91 changes: 91 additions & 0 deletions apps/explorer/test/address-metadata.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
getCode: vi.fn(),
hasIndexSupply: vi.fn(),
getBatchedClient: vi.fn(),
getTempoChain: vi.fn(),
isTip20Address: vi.fn(),
validateVirtualAddress: vi.fn(),
fetchAddressTxAggregate: vi.fn(),
}))

vi.mock('@tanstack/react-router', () => ({
createFileRoute: () => (config: unknown) => ({
options: config,
}),
}))

vi.mock('viem/actions', () => ({
getCode: mocks.getCode,
}))

vi.mock('#lib/env', () => ({
hasIndexSupply: mocks.hasIndexSupply,
}))

vi.mock('#wagmi.config.ts', () => ({
getBatchedClient: mocks.getBatchedClient,
getTempoChain: mocks.getTempoChain,
}))

vi.mock('#lib/domain/tip20', () => ({
isTip20Address: mocks.isTip20Address,
}))

vi.mock('ox/tempo', () => ({
VirtualAddress: {
validate: mocks.validateVirtualAddress,
},
}))

vi.mock('#lib/server/tempo-queries', () => ({
fetchAddressTxAggregate: mocks.fetchAddressTxAggregate,
fetchTokenHoldersCountRows: vi.fn(),
fetchTokenTransferAggregate: vi.fn(),
fetchVirtualAddressTransferAggregate: vi.fn(),
}))

import { Route } from '../src/routes/api/address/metadata/$address'

describe('/api/address/metadata/$address', () => {
const address = '0x112fd4042E442C3C12C67AD23587b0afe36eB74E'
const handler = Route.options.server.handlers.GET

beforeEach(() => {
vi.clearAllMocks()
mocks.getTempoChain.mockReturnValue({ id: 31318 })
mocks.getBatchedClient.mockReturnValue({})
mocks.hasIndexSupply.mockReturnValue(true)
mocks.isTip20Address.mockReturnValue(false)
mocks.validateVirtualAddress.mockReturnValue(false)
})

it('uses the active chain id in fallback responses', async () => {
mocks.hasIndexSupply.mockReturnValue(false)
mocks.getCode.mockResolvedValue(undefined)

const response = await handler({ params: { address } })

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
address,
chainId: 31318,
accountType: 'empty',
})
})

it('keeps contract account type when tx aggregate fetch fails', async () => {
mocks.getCode.mockResolvedValue('0x60016000')
mocks.fetchAddressTxAggregate.mockRejectedValue(new Error('Status: 400'))

const response = await handler({ params: { address } })

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
address,
chainId: 31318,
accountType: 'contract',
})
})
})
Loading