- 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.
333 lines
11 KiB
TypeScript
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,
|
|
}
|
|
}
|