2026-04-07 23:22:12 -07:00
|
|
|
import { resolveExplorerApiBase } from '@/libs/frontend-api-client/api-base'
|
2026-03-28 00:21:18 -07:00
|
|
|
import type { Block } from './blocks'
|
|
|
|
|
import type { Transaction } from './transactions'
|
2026-04-10 12:52:17 -07:00
|
|
|
import type {
|
|
|
|
|
AddressInfo,
|
|
|
|
|
AddressTokenBalance,
|
|
|
|
|
AddressTokenTransfer,
|
|
|
|
|
TransactionSummary,
|
|
|
|
|
} from './addresses'
|
2026-03-28 00:21:18 -07:00
|
|
|
|
|
|
|
|
export function getExplorerApiBase() {
|
2026-04-07 23:22:12 -07:00
|
|
|
return resolveExplorerApiBase()
|
2026-03-28 00:21:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-10 12:52:17 -07:00
|
|
|
type StringLike = string | number | null | undefined
|
2026-03-28 00:21:18 -07:00
|
|
|
|
|
|
|
|
export function extractHash(value: HashLike): string {
|
|
|
|
|
if (!value) return ''
|
|
|
|
|
return typeof value === 'string' ? value : value.hash || ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
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 || ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:21:18 -07:00
|
|
|
function toNumber(value: unknown): number {
|
|
|
|
|
if (typeof value === 'number') return value
|
|
|
|
|
if (typeof value === 'string' && value.trim() !== '') return Number(value)
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 12:52:17 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:21:18 -07:00
|
|
|
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
|
2026-04-10 12:52:17 -07:00
|
|
|
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
|
2026-03-28 00:21:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-10 12:52:17 -07:00
|
|
|
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,
|
|
|
|
|
}))
|
|
|
|
|
: [],
|
2026-03-28 00:21:18 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 12:52:17 -07:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|