- 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.
231 lines
7.2 KiB
TypeScript
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: [] }
|
|
}
|
|
},
|
|
}
|