Files
explorer-monorepo/frontend/src/services/api/transactions.ts
defiQUG 0972178cc5 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

231 lines
7.2 KiB
TypeScript

import { ApiResponse } from './client'
import { fetchBlockscoutJson, normalizeTransaction, type BlockscoutInternalTransaction } from './blockscout'
import { resolveExplorerApiBase } from '../../../libs/frontend-api-client/api-base'
export interface Transaction {
chain_id: number
hash: string
block_number: number
block_hash: string
transaction_index: number
from_address: string
to_address?: string
value: string
gas_price?: number
max_fee_per_gas?: number
max_priority_fee_per_gas?: number
gas_limit: number
gas_used?: number
status?: number
input_data?: string
contract_address?: string
created_at: string
fee?: string
method?: string
revert_reason?: string
transaction_tag?: string
decoded_input?: {
method_call?: string
method_id?: string
parameters: Array<{
name?: string
type?: string
value?: unknown
}>
}
token_transfers?: TransactionTokenTransfer[]
}
export interface TransactionTokenTransfer {
block_number?: number
from_address: string
from_label?: string
to_address: string
to_label?: string
token_address: string
token_name?: string
token_symbol?: string
token_decimals: number
amount: string
type?: string
timestamp?: string
}
export interface TransactionInternalCall {
from_address: string
from_label?: string
to_address?: string
to_label?: string
contract_address?: string
contract_label?: string
type?: string
value: string
success?: boolean
error?: string
result?: string
timestamp?: string
}
export interface TransactionLookupDiagnostic {
checked_hash: string
chain_id: number
explorer_indexed: boolean
rpc_transaction_found: boolean
rpc_receipt_found: boolean
latest_block_number?: number
rpc_url?: string
}
const CHAIN_138_PUBLIC_RPC_URL = 'https://rpc-http-pub.d-bis.org'
function resolvePublicRpcUrl(chainId: number): string | null {
if (chainId !== 138) {
return null
}
const envValue = (process.env.NEXT_PUBLIC_CHAIN_138_RPC_URL || '').trim()
return envValue || CHAIN_138_PUBLIC_RPC_URL
}
async function fetchJsonWithStatus<T>(input: RequestInfo | URL, init?: RequestInit): Promise<{ ok: boolean; status: number; data: T | null }> {
const response = await fetch(input, init)
let data: T | null = null
try {
data = (await response.json()) as T
} catch {
data = null
}
return { ok: response.ok, status: response.status, data }
}
async function fetchRpcResult<T>(rpcUrl: string, method: string, params: unknown[]): Promise<T | null> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 6000)
try {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params,
}),
signal: controller.signal,
})
if (!response.ok) {
return null
}
const payload = (await response.json()) as { result?: T | null }
return payload.result ?? null
} catch {
return null
} finally {
clearTimeout(timeout)
}
}
async function diagnoseMissingTransaction(chainId: number, hash: string): Promise<TransactionLookupDiagnostic> {
const diagnostic: TransactionLookupDiagnostic = {
checked_hash: hash,
chain_id: chainId,
explorer_indexed: false,
rpc_transaction_found: false,
rpc_receipt_found: false,
}
const explorerLookup = await fetchJsonWithStatus<unknown>(`${resolveExplorerApiBase()}/api/v2/transactions/${hash}`)
diagnostic.explorer_indexed = explorerLookup.ok
const rpcUrl = resolvePublicRpcUrl(chainId)
if (!rpcUrl) {
return diagnostic
}
diagnostic.rpc_url = rpcUrl
const [transactionResult, receiptResult, latestBlockHex] = await Promise.all([
fetchRpcResult<string | Record<string, unknown>>(rpcUrl, 'eth_getTransactionByHash', [hash]),
fetchRpcResult<string | Record<string, unknown>>(rpcUrl, 'eth_getTransactionReceipt', [hash]),
fetchRpcResult<string>(rpcUrl, 'eth_blockNumber', []),
])
diagnostic.rpc_transaction_found = transactionResult != null
diagnostic.rpc_receipt_found = receiptResult != null
if (typeof latestBlockHex === 'string' && latestBlockHex.startsWith('0x')) {
diagnostic.latest_block_number = parseInt(latestBlockHex, 16)
}
return diagnostic
}
function normalizeInternalTransactions(items: BlockscoutInternalTransaction[] | null | undefined): TransactionInternalCall[] {
if (!Array.isArray(items)) {
return []
}
return items.map((item) => ({
from_address: item.from?.hash || '',
from_label: item.from?.name || item.from?.label || undefined,
to_address: item.to?.hash || undefined,
to_label: item.to?.name || item.to?.label || undefined,
contract_address: item.created_contract?.hash || undefined,
contract_label: item.created_contract?.name || item.created_contract?.label || undefined,
type: item.type || undefined,
value: item.value || '0',
success: item.success ?? undefined,
error: item.error || undefined,
result: item.result || undefined,
timestamp: item.timestamp || undefined,
}))
}
export const transactionsApi = {
get: async (chainId: number, hash: string): Promise<ApiResponse<Transaction>> => {
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 }> => {
try {
const { data } = await transactionsApi.get(chainId, hash)
return { ok: true, data }
} catch {
return { ok: false, data: null }
}
},
diagnoseMissing: async (chainId: number, hash: string): Promise<TransactionLookupDiagnostic> => {
return diagnoseMissingTransaction(chainId, hash)
},
getInternalTransactionsSafe: async (hash: string): Promise<{ ok: boolean; data: TransactionInternalCall[] }> => {
try {
const raw = await fetchBlockscoutJson<{ items?: BlockscoutInternalTransaction[] } | BlockscoutInternalTransaction[]>(
`/api/v2/transactions/${hash}/internal-transactions`
)
const items = Array.isArray(raw) ? raw : raw.items
return { ok: true, data: normalizeInternalTransactions(items) }
} catch {
return { ok: false, data: [] }
}
},
list: async (chainId: number, page: number, pageSize: number): Promise<ApiResponse<Transaction[]>> => {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
})
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 { data } = await transactionsApi.list(chainId, page, pageSize)
return { ok: true, data }
} catch {
return { ok: false, data: [] }
}
},
}