service.ts
Overview
The [service.ts](/projects/291/69103) file implements the **EVM Service** class, a core component of the Unified API Layer specialized for Ethereum and EVM-compatible blockchains. It encapsulates the business logic needed to interact with blockchain data sources, node RPC endpoints, and external APIs, providing a unified interface for:
Fetching account details (balances, tokens, nonce).
Retrieving transaction history with internal transactions.
Getting detailed transaction information.
Estimating gas usage and fetching gas fee data.
Sending raw transactions to the blockchain.
Retrieving and processing internal transactions via node trace methods or explorer APIs.
Fetching token metadata for NFTs (ERC721, ERC1155).
Handling block data retrieval.
Proxying arbitrary JSON-RPC requests.
The service integrates multiple dependencies, including the Blockbook indexer, Ethereum node RPC endpoints, a gas oracle for fee estimation, and auxiliary utilities for error handling, request retries, and data formatting.
This file is essential for providing a consistent and enriched API for EVM chains, abstracting away the complexities of underlying blockchain data models, RPC protocols, and token standards.
Classes and Interfaces
ServiceArgs (Interface)
Arguments required to instantiate the `Service` class.
Property | Type | Description |
|---|---|---|
`blockbook` | `Blockbook` | Blockbook client instance for blockchain data querying. |
`gasOracle` | `GasOracle` | Gas oracle instance for gas fee estimation. |
`explorerApiUrl` | `URL` | Base URL for the external blockchain explorer API. |
`logger` | `Logger` | Logger instance for structured logging. |
`client` | `PublicClient` | Ethereum client for RPC interactions. |
`rpcUrl` | `string` | JSON-RPC endpoint URL of the blockchain node. |
`string` | Optional API key for RPC requests. |
Service (Class)
Implements the core API business logic for EVM-compatible blockchains. This class fulfills the `API` interface and most of the [BaseAPI](/projects/291/69264) interface except `getInfo`.
Properties
Name | Type | Description |
|---|---|---|
`blockbook` | `Blockbook` | Blockbook client for blockchain data. |
`gasOracle` | `GasOracle` | Gas oracle for gas fee estimation. |
`explorerApiUrl` | `URL` | External explorer API base URL. |
`logger` | `Logger` | Structured logger scoped with namespace. |
`client` | `PublicClient` | Ethereum client for RPC interactions. |
`rpcUrl` | `string` | RPC endpoint URL for sending requests. |
`string` | Optional API key for RPC authentication. |
Constructor
constructor(args: ServiceArgs)
Initializes dependencies.
Starts the gas oracle to begin updating gas fees.
Public Methods
getAccount(pubkey: string): Promise<Account>
Fetches account details for the given public key/address.
Parameters:
pubkey: Ethereum address to fetch account data for.
Returns: A promise resolving to an
Accountcontaining:balance: Total ether balance.unconfirmedBalance: Unconfirmed balance.nonce: Transaction nonce.pubkey: The address.tokens: Array ofTokenBalanceobjects including ERC20, ERC721, ERC1155 tokens.
Behavior:
Queries Blockbook for address info with token balances.
Normalizes tokens from multiple standards into a unified token balance list.
Example:
const account = await service.getAccount('0xabc123...');
console.log(account.balance);
console.log(account.tokens); // ERC20, ERC721, ERC1155 tokens
getTxHistory(pubkey: string, cursor?: string, pageSize = 10, from?: number, to?: number): Promise<TxHistory>
Retrieves paginated transaction history including internal transactions for the specified address.
Parameters:
pubkey: Address whose transaction history is requested.cursor: Optional base64-encoded pagination cursor.pageSize: Number of transactions per page (default 10).from: Optional start block height filter.to: Optional end block height filter.
Returns: A promise resolving to
TxHistorycontaining:pubkey: The address queried.cursor: Cursor for next page (if any).txs: Array of transactions (Tx), each potentially including internal transactions.
Behavior:
Validates page size.
Decodes or initializes cursor.
Fetches transactions from Blockbook and internal transactions from explorer API.
Merges and orders both sets carefully to avoid duplicates.
Handles paginated fetching with cursor updates.
Example:
const txHistory = await service.getTxHistory('0xabc123', undefined, 20);
console.log(txHistory.txs.length);
console.log(txHistory.cursor); // Use this for next page
getTransaction(txid: string): Promise<Tx>
Fetches detailed transaction data by transaction hash, including internal transactions when available.
Parameters:
txid: Transaction hash.
Returns: A promise resolving to a
Txobject.Behavior:
Fetches transaction from Blockbook.
Enhances with internal transactions via external explorer API.
Example:
const tx = await service.getTransaction('0xdeadbeef...');
console.log(tx.internalTxs); // Internal transactions if any
estimateGas(body: EstimateGasBody): Promise<GasEstimate>
Estimates the gas limit required for a transaction.
Parameters:
body: Object containing:from: Sender address.to: Recipient address.value: Value to send.data: Optional transaction data.
Returns: A promise resolving to
{ gasLimit: string }.Behavior:
Uses the Ethereum client (
viem) to estimate gas.Converts inputs into appropriate hex and BigNumber formats.
Example:
const gasEstimate = await service.estimateGas({
from: '0xabc',
to: '0xdef',
value: '1000000000000000000', // 1 ETH in wei
data: '0x',
});
console.log(gasEstimate.gasLimit);
getGasFees(): Promise<GasFees>
Fetches current gas fee estimates at different speed tiers.
Returns: A promise resolving to
GasFeescontaining:baseFeePerGas: Current base fee.slow: Fee estimate for slow transactions.average: Fee estimate for average speed.fast: Fee estimate for fast confirmation speed.
Behavior:
Queries the gas oracle for base fee and percentile estimates.
Example:
const fees = await service.getGasFees();
console.log(fees.fast.maxFeePerGas);
sendTx(body: SendTxBody): Promise<string>
Broadcasts a raw signed transaction to the blockchain.
Parameters:
body: Object containing:hex: Raw transaction hex string.
Returns: A promise resolving to the transaction hash string.
Behavior:
Constructs JSON-RPC request
eth_sendRawTransaction.Sends request to RPC endpoint, optionally with API key.
Throws on error.
Example:
const txid = await service.sendTx({ hex: '0xf86c...' });
console.log(txid); // Transaction hash
handleBlock(hash: string): Promise<Array<BlockbookTx>>
Fetches all transactions within a block by block hash.
Parameters:
hash: Block hash.
Returns: A promise resolving to an array of Blockbook transactions.
Behavior:
Uses Blockbook to fetch all pages of transactions within the block.
getTokenMetadata(address: string, id: string, type: TokenType): Promise<TokenMetadata>
Retrieves metadata for a token (NFT) by contract address, token ID, and token type (ERC721 or ERC1155).
Parameters:
address: Contract address.id: Token identifier.type: Token standard type ('erc721'or'erc1155').
Returns: A promise resolving to
TokenMetadata, including:name: Token name.description: Token description.media: Object withurland optionaltype(e.g., image or video).
Behavior:
Calls appropriate contract method (
tokenURIoruri) viaviemclient.Resolves URI, handling IPFS/IPNS schemes via gateway.
Fetches metadata JSON from URI, supporting base64-encoded JSON.
Attempts to detect media type via HTTP HEAD request.
Falls back to empty metadata on error.
Example:
const metadata = await service.getTokenMetadata('0xabc...', '123', 'erc721');
console.log(metadata.media.url);
doRpcRequest(req: RPCRequest | RPCRequest[]): Promise<RPCResponse | RPCResponse[]>
Performs a generic JSON-RPC request or batch request to the configured RPC endpoint.
Parameters:
req: Single or array ofRPCRequestobjects.
Returns: Promise resolving to corresponding
RPCResponseor array of responses.Behavior:
Posts to RPC endpoint with optional API key.
Throws on error.
Internal (Private) Methods and Implementation Details
Transaction Handling and Internal Transaction Extraction
handleTransaction(tx: BlockbookTx): Tx
Converts Blockbook transaction data into unifiedTxformat, extracting token transfers and normalizing ERC token types.handleTransactionWithInternal(tx: BlockbookTx): Promise<Tx>
Converts a transaction and fetches internal transactions from explorer API for historical transactions.handleTransactionWithInternalTrace(tx: BlockbookTx, internalTxMethod?: InternalTxFetchMethod): Promise<Tx>
Similar but uses node trace RPC calls (debug_traceTransactionortrace_transaction) for internal tx extraction on recently confirmed transactions.Internal transaction fetch methods:
fetchInternalTxsByTxDebug(txid: string, retryCount?: number)fetchInternalTxsByTxTrace(txid: string, retryCount?: number)fetchInternalTxsByTxid(txid: string)fetchInternalTxsByBlockDebug(blockHash: string, retryCount?: number)fetchInternalTxsByBlockTrace(blockHash: string, retryCount?: number)fetchInternalTxsByAddress(address: string, page: number, pageSize: number, from?: number, to?: number)
These methods implement retry with exponential backoff on failures and parse call stack traces or explorer API responses to extract internal transaction calls.
Call Stack Processing
processCallStackDebug(calls?: DebugCallStack[], txs?: InternalTx[]): InternalTx[]
Recursively processes debug call stack traces to extract internal transactions with value transfers.processCallStackTrace(callStack: TraceCall[]): Record<string, InternalTx[] | undefined>
Processes trace call arrays into a map of transaction hashes to internal transaction arrays.
Pagination Helpers
getTxs(address: string, pageSize: number, cursor: Cursor, from?: number, to?: number): Promise<{hasMore: boolean, txs: Map<string, Tx>}>
Queries Blockbook for paginated transactions, applies filtering to avoid duplicates per cursor.getInternalTxs(address: string, pageSize: number, cursor: Cursor, from?: number, to?: number)
Fetches internal transactions via explorer API with filtering and pagination.
Important Implementation Details and Algorithms
Cursor-Based Pagination:
The service uses a cursor object encoding page numbers, last seen transaction IDs, and block heights, serialized as base64 JSON strings. This cursor enables correct pagination across multiple data sources (Blockbook and external explorer internal tx API), merging them into a unified timeline without duplication.Internal Transaction Merging:
Transactions from Blockbook and internal transactions from explorer APIs or node trace methods are merged carefully by block height and txid to avoid duplicates and ensure a complete transaction history including calls internal to contract executions.Robust Internal Transaction Retrieval:
The service attempts multiple methods to retrieve internal transactions:Using
debug_traceTransactionortrace_transactionRPC calls for recent transactions.Querying an external explorer API for historical internal transactions (fallback for pruned full nodes).
Each method includes retry logic with exponential backoff.
Token Metadata Resolution:
The service supports NFTs following ERC721 and ERC1155 token standards. It dynamically callstokenURIorurion the token contract to fetch metadata URIs, supports IPFS/IPNS URI schemes by rewriting to HTTP gateway URLs, and handles base64-encoded JSON metadata. It also attempts to detect media type (image or video) via HTTP HEAD requests.Gas Fee Estimation Integration:
The service integrates aGasOracleinstance that continuously tracks recent block gas prices and provides percentile-based fee estimates (slow, average, fast). The service exposes this data viagetGasFees().RPC Request Proxying:
Supports forwarding arbitrary JSON-RPC requests or batch requests to the underlying Ethereum node, which is useful for clients needing full RPC capabilities beyond the predefined API.
Interaction with Other System Components
Blockbook Indexer:
The service queries Blockbook extensively for blockchain data such as account balances, token balances, transactions, and blocks.Ethereum Node RPC Endpoint:
Used for sending raw transactions, tracing internal calls (debug_traceTransaction,trace_transaction), and proxying arbitrary RPC calls.Gas Oracle:
Provides gas fee price estimations used in fee-related API responses.External Explorer API:
Used as a fallback source for internal transaction data when node tracing is not available or for historical data.Logger:
For structured logging, error reporting, and warnings.Utilities:
Includes error handling, retry logic for HTTP requests, cursor validation, and address formatting.
Usage Examples
Fetching Account Info
const service = new Service({ blockbook, gasOracle, explorerApiUrl, logger, client, rpcUrl, rpcApiKey });
const account = await service.getAccount('0xYourAddress');
console.log(account.balance);
console.log(account.tokens);
Retrieving Transaction History with Pagination
const firstPage = await service.getTxHistory('0xYourAddress', undefined, 20);
console.log(firstPage.txs);
if (firstPage.cursor) {
const secondPage = await service.getTxHistory('0xYourAddress', firstPage.cursor, 20);
console.log(secondPage.txs);
}
Sending a Raw Transaction
const txid = await service.sendTx({ hex: '0xf86c...' });
console.log(`Transaction sent with hash: ${txid}`);
Estimating Gas Limit for a Transaction
const estimate = await service.estimateGas({
from: '0xYourAddress',
to: '0xRecipientAddress',
value: '1000000000000000000', // 1 ETH in wei
data: '0x',
});
console.log(`Estimated gas limit: ${estimate.gasLimit}`);
Getting Gas Fee Estimates
const fees = await service.getGasFees();
console.log(`Fast gas fee (maxFeePerGas): ${fees.fast.maxFeePerGas}`);
Fetching Token Metadata for an ERC721 Token
const metadata = await service.getTokenMetadata('0xNFTContractAddress', '12345', 'erc721');
console.log(metadata.name, metadata.media.url);
Mermaid Class Diagram
classDiagram
class Service {
-blockbook: Blockbook
-gasOracle: GasOracle
-explorerApiUrl: URL
-logger: Logger
-client: PublicClient
-rpcUrl: string
-rpcApiKey?: string
+constructor(args: ServiceArgs)
+getAccount(pubkey: string): Promise<Account>
+getTxHistory(pubkey: string, cursor?: string, pageSize?: number, from?: number, to?: number): Promise<TxHistory>
+getTransaction(txid: string): Promise<Tx>
+estimateGas(body: EstimateGasBody): Promise<GasEstimate>
+getGasFees(): Promise<GasFees>
+sendTx(body: SendTxBody): Promise<string>
+handleBlock(hash: string): Promise<Array<BlockbookTx>>
+getTokenMetadata(address: string, id: string, type: TokenType): Promise<TokenMetadata>
+doRpcRequest(req: RPCRequest | Array<RPCRequest>): Promise<RPCResponse | Array<RPCResponse>>
-handleTransaction(tx: BlockbookTx): Tx
-handleTransactionWithInternal(tx: BlockbookTx): Promise<Tx>
-handleTransactionWithInternalTrace(tx: BlockbookTx, internalTxMethod?: InternalTxFetchMethod): Promise<Tx>
-fetchInternalTxsByTxTrace(txid: string, retryCount?: number): Promise<Array<InternalTx> | undefined>
-fetchInternalTxsByTxDebug(txid: string, retryCount?: number): Promise<Array<InternalTx> | undefined>
-fetchInternalTxsByTxid(txid: string): Promise<Array<InternalTx> | undefined>
-fetchInternalTxsByBlockDebug(blockHash: string, retryCount?: number): Promise<Record<string, Array<InternalTx> | undefined>>
-fetchInternalTxsByBlockTrace(blockHash: string, retryCount?: number): Promise<Record<string, Array<InternalTx> | undefined>>
-fetchInternalTxsByAddress(address: string, page: number, pageSize: number, from?: number, to?: number): Promise<Array<ExplorerInternal