import { resolveExplorerApiBase } from '@/libs/frontend-api-client/api-base' import type { Block } from './blocks' import type { Transaction } from './transactions' import type { AddressInfo, AddressTokenBalance, AddressTokenTransfer, TransactionSummary, } from './addresses' export function getExplorerApiBase() { return resolveExplorerApiBase() } export async function fetchBlockscoutJson(path: string): Promise { const response = await fetch(`${getExplorerApiBase()}${path}`) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } return response.json() as Promise } type HashLike = string | { hash?: string | null } | null | undefined type StringLike = string | number | null | undefined export function extractHash(value: HashLike): string { if (!value) return '' return typeof value === 'string' ? value : value.hash || '' } export function extractLabel(value: unknown): string { if (!value || typeof value !== 'object') return '' const candidate = value as { name?: string | null label?: string | null display_name?: string | null symbol?: string | null } return candidate.name || candidate.label || candidate.display_name || candidate.symbol || '' } function toNumber(value: unknown): number { if (typeof value === 'number') return value if (typeof value === 'string' && value.trim() !== '') return Number(value) return 0 } function toNullableNumber(value: unknown): number | undefined { if (value == null) return undefined const numeric = toNumber(value) return Number.isFinite(numeric) ? numeric : undefined } export interface BlockscoutAddressRef { hash?: string | null name?: string | null label?: string | null is_contract?: boolean is_verified?: boolean } export interface BlockscoutTokenRef { address?: string | null name?: string | null symbol?: string | null decimals?: StringLike type?: string | null total_supply?: string | null holders?: StringLike } export interface BlockscoutTokenTransfer { block_hash?: string block_number?: StringLike from?: BlockscoutAddressRef | null to?: BlockscoutAddressRef | null log_index?: StringLike method?: string | null timestamp?: string | null token?: BlockscoutTokenRef | null total?: { decimals?: StringLike value?: string | null } | null transaction_hash?: string type?: string | null } export interface BlockscoutDecodedInput { method_call?: string | null method_id?: string | null parameters?: Array<{ name?: string | null type?: string | null value?: unknown }> } export interface BlockscoutInternalTransaction { from?: BlockscoutAddressRef | null to?: BlockscoutAddressRef | null created_contract?: BlockscoutAddressRef | null success?: boolean | null error?: string | null result?: string | null timestamp?: string | null transaction_hash?: string | null type?: string | null value?: string | null } 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 fee?: { value?: string | null } | string | null method?: string | null revert_reason?: string | null transaction_tag?: string | null decoded_input?: BlockscoutDecodedInput | null token_transfers?: BlockscoutTokenTransfer[] | null actions?: unknown[] | null } 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, fee: typeof raw.fee === 'string' ? raw.fee : raw.fee?.value || undefined, method: raw.method || undefined, revert_reason: raw.revert_reason || undefined, transaction_tag: raw.transaction_tag || undefined, decoded_input: raw.decoded_input ? { method_call: raw.decoded_input.method_call || undefined, method_id: raw.decoded_input.method_id || undefined, parameters: Array.isArray(raw.decoded_input.parameters) ? raw.decoded_input.parameters.map((parameter) => ({ name: parameter.name || undefined, type: parameter.type || undefined, value: parameter.value, })) : [], } : undefined, token_transfers: Array.isArray(raw.token_transfers) ? raw.token_transfers.map((transfer) => ({ block_number: toNullableNumber(transfer.block_number), from_address: extractHash(transfer.from), from_label: extractLabel(transfer.from), to_address: extractHash(transfer.to), to_label: extractLabel(transfer.to), token_address: transfer.token?.address || '', token_name: transfer.token?.name || undefined, token_symbol: transfer.token?.symbol || undefined, token_decimals: toNullableNumber(transfer.token?.decimals) ?? toNullableNumber(transfer.total?.decimals) ?? 18, amount: transfer.total?.value || '0', type: transfer.type || undefined, timestamp: transfer.timestamp || undefined, })) : [], } } 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), } } interface BlockscoutAddress { hash: string coin_balance?: string | null is_contract?: boolean is_verified?: boolean name?: string | 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[] has_token_transfers?: boolean has_tokens?: boolean creation_transaction_hash?: string | null token?: BlockscoutTokenRef | null } export function normalizeAddressInfo( raw: BlockscoutAddress, counters: { transactions_count?: number | string token_balances_count?: number | string token_transfers_count?: number | string internal_transactions_count?: number | string logs_count?: number | string }, chainId: number, ): AddressInfo { 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 { address: raw.hash, chain_id: chainId, transaction_count: Number(counters.transactions_count || 0), token_count: Number(counters.token_balances_count || 0), token_transfer_count: Number(counters.token_transfers_count || 0), internal_transaction_count: Number(counters.internal_transactions_count || 0), logs_count: Number(counters.logs_count || 0), is_contract: !!raw.is_contract, is_verified: !!raw.is_verified, has_token_transfers: !!raw.has_token_transfers, has_tokens: !!raw.has_tokens, balance: raw.coin_balance || undefined, creation_transaction_hash: raw.creation_transaction_hash || undefined, label: raw.name || raw.token?.symbol || undefined, tags, token_contract: raw.token?.address ? { address: raw.token.address, symbol: raw.token.symbol || undefined, name: raw.token.name || undefined, decimals: toNullableNumber(raw.token.decimals), type: raw.token.type || undefined, total_supply: raw.token.total_supply || undefined, holders: toNullableNumber(raw.token.holders), } : undefined, } } export function normalizeAddressTokenBalance(raw: { token?: BlockscoutTokenRef | null value?: string | null }): AddressTokenBalance { return { token_address: raw.token?.address || '', token_name: raw.token?.name || undefined, token_symbol: raw.token?.symbol || undefined, token_type: raw.token?.type || undefined, token_decimals: toNullableNumber(raw.token?.decimals) ?? 18, value: raw.value || '0', holder_count: raw.token?.holders != null ? toNumber(raw.token.holders) : undefined, total_supply: raw.token?.total_supply || undefined, } } export function normalizeAddressTokenTransfer(raw: BlockscoutTokenTransfer): AddressTokenTransfer { return { transaction_hash: raw.transaction_hash || '', block_number: toNullableNumber(raw.block_number) ?? 0, timestamp: raw.timestamp || undefined, from_address: extractHash(raw.from), from_label: extractLabel(raw.from), to_address: extractHash(raw.to), to_label: extractLabel(raw.to), token_address: raw.token?.address || '', token_name: raw.token?.name || undefined, token_symbol: raw.token?.symbol || undefined, token_decimals: toNullableNumber(raw.token?.decimals) ?? toNullableNumber(raw.total?.decimals) ?? 18, value: raw.total?.value || '0', type: raw.type || undefined, } }