Files
explorer-monorepo/frontend/src/services/api/blockscout.ts
defiQUG f46bd213ba refactor: rename SolaceScanScout to Solace and update related configurations
- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
2026-04-10 12:52:17 -07:00

333 lines
11 KiB
TypeScript

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<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
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,
}
}