Fix explorer routing, links, and frontend API loading
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { apiClient, ApiResponse } from './client'
|
||||
import { ApiResponse } from './client'
|
||||
import { fetchBlockscoutJson, normalizeTransactionSummary } from './blockscout'
|
||||
|
||||
export interface AddressInfo {
|
||||
address: string
|
||||
@@ -28,11 +29,53 @@ export interface TransactionSummary {
|
||||
|
||||
export const addressesApi = {
|
||||
get: async (chainId: number, address: string): Promise<ApiResponse<AddressInfo>> => {
|
||||
return apiClient.get<AddressInfo>(`/api/v1/addresses/${chainId}/${address}`)
|
||||
const [raw, counters] = await Promise.all([
|
||||
fetchBlockscoutJson<{
|
||||
hash: string
|
||||
is_contract: boolean
|
||||
name?: string | null
|
||||
token?: { symbol?: string | null } | null
|
||||
public_tags?: Array<{ label?: string; display_name?: string; name?: string } | string>
|
||||
private_tags?: Array<{ label?: string; display_name?: string; name?: string } | string>
|
||||
watchlist_names?: string[]
|
||||
}>(`/api/v2/addresses/${address}`),
|
||||
fetchBlockscoutJson<{
|
||||
transactions_count?: number
|
||||
token_balances_count?: number
|
||||
}>(`/api/v2/addresses/${address}/tabs-counters`),
|
||||
])
|
||||
|
||||
const tags = [
|
||||
...(raw.public_tags || []),
|
||||
...(raw.private_tags || []),
|
||||
...(raw.watchlist_names || []),
|
||||
]
|
||||
.map((tag) => {
|
||||
if (typeof tag === 'string') return tag
|
||||
return tag.display_name || tag.label || tag.name || ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return {
|
||||
data: {
|
||||
address: raw.hash,
|
||||
chain_id: chainId,
|
||||
transaction_count: Number(counters.transactions_count || 0),
|
||||
token_count: Number(counters.token_balances_count || 0),
|
||||
is_contract: !!raw.is_contract,
|
||||
label: raw.name || raw.token?.symbol || undefined,
|
||||
tags,
|
||||
},
|
||||
}
|
||||
},
|
||||
/** Use when you need to check response.ok before setting state (avoids treating 4xx/5xx body as data). */
|
||||
getSafe: async (chainId: number, address: string): Promise<{ ok: boolean; data: AddressInfo | null }> => {
|
||||
return apiClient.getSafe<AddressInfo>(`/api/v1/addresses/${chainId}/${address}`)
|
||||
try {
|
||||
const { data } = await addressesApi.get(chainId, address)
|
||||
return { ok: true, data }
|
||||
} catch {
|
||||
return { ok: false, data: null }
|
||||
}
|
||||
},
|
||||
getTransactionsSafe: async (
|
||||
chainId: number,
|
||||
@@ -42,16 +85,11 @@ export const addressesApi = {
|
||||
): Promise<{ ok: boolean; data: TransactionSummary[] }> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
chain_id: chainId.toString(),
|
||||
from_address: address,
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
})
|
||||
const raw = (await apiClient.get(`/api/v1/transactions?${params.toString()}`)) as unknown as {
|
||||
data?: TransactionSummary[]
|
||||
items?: TransactionSummary[]
|
||||
}
|
||||
const data = Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.items) ? raw.items : []
|
||||
const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/addresses/${address}/transactions?${params.toString()}`)
|
||||
const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeTransactionSummary(item as never)) : []
|
||||
return { ok: true, data }
|
||||
} catch {
|
||||
return { ok: false, data: [] }
|
||||
@@ -65,16 +103,11 @@ export const addressesApi = {
|
||||
pageSize = 20
|
||||
): Promise<ApiResponse<TransactionSummary[]>> => {
|
||||
const params = new URLSearchParams({
|
||||
chain_id: chainId.toString(),
|
||||
from_address: address,
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
})
|
||||
const raw = (await apiClient.get(`/api/v1/transactions?${params.toString()}`)) as unknown as {
|
||||
data?: TransactionSummary[]
|
||||
items?: TransactionSummary[]
|
||||
}
|
||||
const data = Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.items) ? raw.items : []
|
||||
const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/addresses/${address}/transactions?${params.toString()}`)
|
||||
const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeTransactionSummary(item as never)) : []
|
||||
return { data }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient, ApiResponse } from './client'
|
||||
import { ApiResponse } from './client'
|
||||
import { fetchBlockscoutJson, normalizeBlock } from './blockscout'
|
||||
|
||||
export interface Block {
|
||||
chain_id: number
|
||||
@@ -22,12 +23,6 @@ export interface BlockListParams {
|
||||
order?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
/** Normalize list response: backend may return { data: T[] } or { items: T[] }. */
|
||||
function normalizeListResponse<T>(raw: { data?: T[]; items?: T[] }): ApiResponse<T[]> {
|
||||
const data = Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.items) ? raw.items : []
|
||||
return { data }
|
||||
}
|
||||
|
||||
export const blocksApi = {
|
||||
list: async (params: BlockListParams): Promise<ApiResponse<Block[]>> => {
|
||||
const queryParams = new URLSearchParams()
|
||||
@@ -40,16 +35,18 @@ export const blocksApi = {
|
||||
if (params.sort) queryParams.append('sort', params.sort)
|
||||
if (params.order) queryParams.append('order', params.order)
|
||||
|
||||
const raw = (await apiClient.get(`/api/v1/blocks?${queryParams.toString()}`)) as unknown as { data?: Block[]; items?: Block[] }
|
||||
return normalizeListResponse(raw)
|
||||
const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/blocks?${queryParams.toString()}`)
|
||||
const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeBlock(item as never, params.chain_id)) : []
|
||||
return { data }
|
||||
},
|
||||
|
||||
getByNumber: async (chainId: number, number: number): Promise<ApiResponse<Block>> => {
|
||||
return apiClient.get<Block>(`/api/v1/blocks/${chainId}/${number}`)
|
||||
const raw = await fetchBlockscoutJson<unknown>(`/api/v2/blocks/${number}`)
|
||||
return { data: normalizeBlock(raw as never, chainId) }
|
||||
},
|
||||
|
||||
getByHash: async (chainId: number, hash: string): Promise<ApiResponse<Block>> => {
|
||||
return apiClient.get<Block>(`/api/v1/blocks/${chainId}/hash/${hash}`)
|
||||
const raw = await fetchBlockscoutJson<unknown>(`/api/v2/blocks/${hash}`)
|
||||
return { data: normalizeBlock(raw as never, chainId) }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
109
frontend/src/services/api/blockscout.ts
Normal file
109
frontend/src/services/api/blockscout.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { Block } from './blocks'
|
||||
import type { Transaction } from './transactions'
|
||||
import type { TransactionSummary } from './addresses'
|
||||
|
||||
export function getExplorerApiBase() {
|
||||
return (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export async function fetchBlockscoutJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${getExplorerApiBase()}${path}`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
type HashLike = string | { hash?: string | null } | null | undefined
|
||||
|
||||
export function extractHash(value: HashLike): string {
|
||||
if (!value) return ''
|
||||
return typeof value === 'string' ? value : value.hash || ''
|
||||
}
|
||||
|
||||
function toNumber(value: unknown): number {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value === 'string' && value.trim() !== '') return Number(value)
|
||||
return 0
|
||||
}
|
||||
|
||||
interface BlockscoutBlock {
|
||||
hash: string
|
||||
height: number | string
|
||||
timestamp: string
|
||||
miner: HashLike
|
||||
transaction_count: number | string
|
||||
gas_used: number | string
|
||||
gas_limit: number | string
|
||||
}
|
||||
|
||||
interface BlockscoutTransaction {
|
||||
hash: string
|
||||
block_number: number | string
|
||||
from: HashLike
|
||||
to: HashLike
|
||||
value: string
|
||||
status?: string | null
|
||||
result?: string | null
|
||||
gas_price?: number | string | null
|
||||
gas_limit: number | string
|
||||
gas_used?: number | string | null
|
||||
max_fee_per_gas?: number | string | null
|
||||
max_priority_fee_per_gas?: number | string | null
|
||||
raw_input?: string | null
|
||||
timestamp: string
|
||||
created_contract?: HashLike
|
||||
}
|
||||
|
||||
function normalizeStatus(raw: BlockscoutTransaction): number {
|
||||
const value = (raw.status || raw.result || '').toString().toLowerCase()
|
||||
if (value === 'success' || value === 'ok' || value === '1') return 1
|
||||
if (value === 'error' || value === 'failed' || value === '0') return 0
|
||||
return 0
|
||||
}
|
||||
|
||||
export function normalizeBlock(raw: BlockscoutBlock, chainId: number): Block {
|
||||
return {
|
||||
chain_id: chainId,
|
||||
number: toNumber(raw.height),
|
||||
hash: raw.hash,
|
||||
timestamp: raw.timestamp,
|
||||
miner: extractHash(raw.miner),
|
||||
transaction_count: toNumber(raw.transaction_count),
|
||||
gas_used: toNumber(raw.gas_used),
|
||||
gas_limit: toNumber(raw.gas_limit),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeTransaction(raw: BlockscoutTransaction, chainId: number): Transaction {
|
||||
return {
|
||||
chain_id: chainId,
|
||||
hash: raw.hash,
|
||||
block_number: toNumber(raw.block_number),
|
||||
block_hash: '',
|
||||
transaction_index: 0,
|
||||
from_address: extractHash(raw.from),
|
||||
to_address: extractHash(raw.to) || undefined,
|
||||
value: raw.value || '0',
|
||||
gas_price: raw.gas_price != null ? toNumber(raw.gas_price) : undefined,
|
||||
max_fee_per_gas: raw.max_fee_per_gas != null ? toNumber(raw.max_fee_per_gas) : undefined,
|
||||
max_priority_fee_per_gas: raw.max_priority_fee_per_gas != null ? toNumber(raw.max_priority_fee_per_gas) : undefined,
|
||||
gas_limit: toNumber(raw.gas_limit),
|
||||
gas_used: raw.gas_used != null ? toNumber(raw.gas_used) : undefined,
|
||||
status: normalizeStatus(raw),
|
||||
input_data: raw.raw_input || undefined,
|
||||
contract_address: extractHash(raw.created_contract) || undefined,
|
||||
created_at: raw.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeTransactionSummary(raw: BlockscoutTransaction): TransactionSummary {
|
||||
return {
|
||||
hash: raw.hash,
|
||||
block_number: toNumber(raw.block_number),
|
||||
from_address: extractHash(raw.from),
|
||||
to_address: extractHash(raw.to) || undefined,
|
||||
value: raw.value || '0',
|
||||
status: normalizeStatus(raw),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient, ApiResponse } from './client'
|
||||
import { fetchBlockscoutJson, normalizeTransaction } from './blockscout'
|
||||
|
||||
export interface Transaction {
|
||||
chain_id: number
|
||||
@@ -22,33 +23,31 @@ export interface Transaction {
|
||||
|
||||
export const transactionsApi = {
|
||||
get: async (chainId: number, hash: string): Promise<ApiResponse<Transaction>> => {
|
||||
return apiClient.get<Transaction>(`/api/v1/transactions/${chainId}/${hash}`)
|
||||
const raw = await fetchBlockscoutJson<unknown>(`/api/v2/transactions/${hash}`)
|
||||
return { data: normalizeTransaction(raw as never, chainId) }
|
||||
},
|
||||
/** Use when you need to check response.ok before setting state (avoids treating 4xx/5xx body as data). */
|
||||
getSafe: async (chainId: number, hash: string): Promise<{ ok: boolean; data: Transaction | null }> => {
|
||||
return apiClient.getSafe<Transaction>(`/api/v1/transactions/${chainId}/${hash}`)
|
||||
try {
|
||||
const { data } = await transactionsApi.get(chainId, hash)
|
||||
return { ok: true, data }
|
||||
} catch {
|
||||
return { ok: false, data: null }
|
||||
}
|
||||
},
|
||||
list: async (chainId: number, page: number, pageSize: number): Promise<ApiResponse<Transaction[]>> => {
|
||||
const params = new URLSearchParams({
|
||||
chain_id: chainId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
})
|
||||
const raw = (await apiClient.get(`/api/v1/transactions?${params.toString()}`)) as unknown as { data?: Transaction[]; items?: Transaction[] }
|
||||
const data = Array.isArray(raw?.data) ? raw.data : Array.isArray(raw?.items) ? raw.items : []
|
||||
const raw = await fetchBlockscoutJson<{ items?: unknown[] }>(`/api/v2/transactions?${params.toString()}`)
|
||||
const data = Array.isArray(raw?.items) ? raw.items.map((item) => normalizeTransaction(item as never, chainId)) : []
|
||||
return { data }
|
||||
},
|
||||
/** Use when you need to check ok before setting state (avoids treating error body as list). */
|
||||
listSafe: async (chainId: number, page: number, pageSize: number): Promise<{ ok: boolean; data: Transaction[] }> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
chain_id: chainId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
})
|
||||
const raw = await apiClient.getSafe<Transaction[]>(`/api/v1/transactions?${params.toString()}`)
|
||||
if (!raw.ok) return { ok: false, data: [] }
|
||||
const data = Array.isArray(raw.data) ? raw.data : []
|
||||
const { data } = await transactionsApi.list(chainId, page, pageSize)
|
||||
return { ok: true, data }
|
||||
} catch {
|
||||
return { ok: false, data: [] }
|
||||
|
||||
Reference in New Issue
Block a user