blockchain-api
Version:
API utilities for interacting with the Exatechl2 blockchain
380 lines (328 loc) • 11.8 kB
text/typescript
/**
* Transaction Operations
*/
import { Transaction, RpcTxStatus } from '../../types';
import { callRpc } from '../../core/rpc';
import { callExplorerApi } from '../../core/explorerApi';
import { getBlockByNumber, getLatestBlockNumber } from '../blocks/blocks';
import {
formatWeiToEth,
parseTimestampToMillis,
parseHexToDecimal,
standardizeTimestamp,
formatRelativeTime
} from '../../utils/formatters';
import { logger } from '../../utils/logger';
export interface TransactionReceipt {
transactionHash: string;
transactionIndex: string;
blockHash: string;
blockNumber: string;
from: string;
to: string | null;
cumulativeGasUsed: string;
gasUsed: string;
contractAddress: string | null;
logs: unknown[];
logsBloom: string;
status: string;
effectiveGasPrice: string;
type: string;
}
/**
* @param {string} txHash
* @returns {Promise<Transaction>}
*/
export async function getTransactionByHash(txHash: string): Promise<Transaction> {
return callRpc<Transaction>('eth_getTransactionByHash', [txHash]);
}
/**
* @param {string} txHash
* @returns {Promise<TransactionReceipt>}
*/
export async function getTransactionReceipt(txHash: string): Promise<TransactionReceipt> {
return callRpc<TransactionReceipt>('eth_getTransactionReceipt', [txHash]);
}
/**
* @param {number} count
* @returns {Promise<Transaction[]>}
*/
export async function getLatestTransactions(count = 10): Promise<Transaction[]> {
try {
const latestBlockHex = await getLatestBlockNumber();
const latestBlock = parseHexToDecimal(latestBlockHex);
const transactions: Transaction[] = [];
const blocksToFetch = Math.min(count, 10);
for (let i = 0; i < blocksToFetch; i++) {
const blockNumber = latestBlock - i;
const blockData = await getBlockByNumber(blockNumber, true);
if (blockData && blockData.transactions) {
const blockTimestamp = blockData.timestamp;
const txsWithTimestamp = (blockData.transactions as Transaction[]).map(tx => ({
...tx,
timestamp: blockTimestamp,
blockHash: blockData.hash,
}));
transactions.push(...txsWithTimestamp);
if (transactions.length >= count) {
break;
}
}
}
return transactions.slice(0, count);
} catch (error) {
logger.error('Error getting latest transactions:', error);
throw error;
}
}
/**
* @returns {Promise<string>}
*/
export async function getGasPrice(): Promise<string> {
return callRpc('eth_gasPrice');
}
/**
* @param {Transaction[]} transactions
* @returns {Transaction[]}
*/
export function formatTransactionsForDisplay(transactions: Transaction[]): Transaction[] {
return transactions.map(tx => {
let txFee = '0';
const gasUsed = tx.gasUsed || '0x0';
const gasPrice = tx.gasPrice || tx.effectiveGasPrice || '0x0';
const gas = tx.gas || '0x0';
if (tx.gasUsed && tx.effectiveGasPrice) {
const gasUsedNum = parseHexToDecimal(tx.gasUsed);
const gasPriceNum = parseHexToDecimal(tx.effectiveGasPrice);
const feeValue = gasUsedNum * gasPriceNum;
txFee = formatWeiToEth(feeValue, 8).replace(' tEXT', '');
} else if (tx.gas && tx.gasPrice) {
const gasNum = parseHexToDecimal(tx.gas);
const gasPriceNum = parseHexToDecimal(tx.gasPrice);
const feeValue = gasNum * gasPriceNum;
txFee = formatWeiToEth(feeValue, 8).replace(' tEXT', '');
}
let status: 'success' | 'pending' | 'failed' = 'pending';
if (tx.blockNumber) {
const txStatus = tx.status as RpcTxStatus;
status = txStatus === '0x1' ? 'success' :
txStatus === '0x0' ? 'failed' : 'success';
}
const capitalizedStatus = status.charAt(0).toUpperCase() + status.slice(1);
let valueFormatted;
try {
if (tx.value) {
valueFormatted = formatWeiToEth(tx.value, 6);
} else {
valueFormatted = '0 tEXT';
}
} catch (e) {
logger.error('Error parsing transaction value:', e);
valueFormatted = '0 tEXT';
}
const timestamp = parseTimestampToMillis(tx.timestamp || Date.now());
const age = formatRelativeTime(timestamp);
let blockNumberFormatted;
let blockNumberValue;
try {
if (tx.blockNumber) {
blockNumberFormatted = tx.blockNumber.toString();
blockNumberValue = typeof tx.blockNumber === 'string' ?
(tx.blockNumber.startsWith('0x') ? parseInt(tx.blockNumber, 16) : parseInt(tx.blockNumber)) :
tx.blockNumber;
} else {
blockNumberFormatted = 'Pending';
blockNumberValue = null;
}
} catch (e) {
logger.error('Error parsing block number:', e);
blockNumberFormatted = 'Unknown';
blockNumberValue = null;
}
let methodName = 'Transfer';
let methodSignature = '';
let methodDisplayName = '';
if (tx.method_name) {
methodName = tx.method_name;
if (methodName.includes('(')) {
methodDisplayName = methodName.split('(')[0];
} else {
methodDisplayName = methodName;
}
} else if (tx.method_signature) {
methodName = tx.method_signature;
methodSignature = tx.method_signature;
if (methodName.includes('(')) {
methodDisplayName = methodName.split('(')[0];
} else {
methodDisplayName = methodName;
}
} else {
if (tx.input && tx.input !== '0x') {
methodName = 'Contract Call';
methodDisplayName = 'Contract Call';
if (tx.input.length >= 10) {
methodSignature = tx.input.substring(0, 10);
}
if (!tx.to) {
methodName = 'Contract Creation';
methodDisplayName = 'Contract Creation';
}
} else {
methodDisplayName = 'Transfer';
}
}
if (methodDisplayName.toLowerCase() === 'transfer') {
methodDisplayName = 'Transfer';
} else if (methodDisplayName.toLowerCase() === 'approve') {
methodDisplayName = 'Approve';
} else if (methodDisplayName.toLowerCase() === 'mint') {
methodDisplayName = 'Mint';
} else if (methodDisplayName.toLowerCase() === 'withdraw') {
methodDisplayName = 'Withdraw';
} else if (methodDisplayName.toLowerCase() === 'deposit') {
methodDisplayName = 'Deposit';
} else if (methodDisplayName.toLowerCase() === 'swap') {
methodDisplayName = 'Swap';
}
const nonceDecimal = parseHexToDecimal(tx.nonce);
return {
hash: tx.hash,
from: tx.from,
to: tx.to || 'Contract Creation',
value: valueFormatted,
status,
statusCapitalized: capitalizedStatus,
timestamp,
age,
method: methodName,
methodDisplayName,
methodSignature: methodSignature,
txFee: txFee === '0' ? '0 tEXT' : `${txFee} tEXT`,
txnFee: txFee === '0' ? '0 tEXT' : `${txFee} tEXT`,
blockNumber: blockNumberFormatted,
block: blockNumberValue,
gas,
gasPrice,
gasUsed,
nonce: tx.nonce || '0x0',
input: tx.input || '0x',
gasUsedDecimal: parseHexToDecimal(gasUsed),
gasPriceGwei: parseHexToDecimal(gasPrice) / 1e9 > 0 ? (parseHexToDecimal(gasPrice) / 1e9).toFixed(2) : '0',
gasDecimal: parseHexToDecimal(gas),
nonceDecimal
};
});
}
export interface PaginatedTransactions {
transactions: Transaction[];
totalCount: number;
totalPages: number;
page: number;
perPage: number;
}
/**
* @param {number} page
* @param {number} perPage
* @param {boolean} debug
* @returns {Promise<PaginatedTransactions>}
*/
export async function getLatestTransactionsFromExplorer(
page = 1,
perPage = 10,
debug = false
): Promise<PaginatedTransactions> {
try {
interface ExplorerTransactionsResponse {
data?: any[];
transactions?: any[];
meta?: {
total?: number;
count?: number;
total_pages?: number;
};
totalCount?: number;
total?: number;
count?: number;
status?: string;
}
if (debug) logger.debug(`Fetching transactions: page=${page}, perPage=${perPage}`);
const response = await callExplorerApi<ExplorerTransactionsResponse>('transactions', {
limit: perPage,
offset: (page - 1) * perPage
});
if (debug) {
logger.debug('API Response Structure:', JSON.stringify(response).substring(0, 200) + '...');
logger.debug('Response received, processing transactions...');
}
let txData: any[] = [];
if (Array.isArray(response)) {
txData = response;
} else if (response.data && Array.isArray(response.data)) {
txData = response.data;
} else if (response.transactions && Array.isArray(response.transactions)) {
txData = response.transactions;
} else {
logger.error('Unexpected API response format:', response);
throw new Error('Unexpected API response format. Check logs for details.');
}
if (debug) logger.debug(`Processing ${txData.length} transactions...`);
const transactions = txData.map((txData: any) => {
const transaction: any = {
hash: txData.hash,
blockNumber: txData.block_number
? (txData.block_number.startsWith?.('0x') ? txData.block_number : `0x${parseInt(txData.block_number).toString(16)}`)
: undefined,
blockHash: txData.block_hash,
from: txData.from_address || txData.from,
to: txData.to_address || txData.to,
value: txData.value
? (txData.value.startsWith?.('0x') ? txData.value : `0x${BigInt(txData.value).toString(16)}`)
: '0x0',
gas: txData.gas
? (txData.gas.startsWith?.('0x') ? txData.gas : `0x${parseInt(txData.gas).toString(16)}`)
: undefined,
gasPrice: txData.gas_price
? (txData.gas_price.startsWith?.('0x') ? txData.gas_price : `0x${BigInt(txData.gas_price).toString(16)}`)
: undefined,
gasUsed: txData.gas_used
? (txData.gas_used.startsWith?.('0x') ? txData.gas_used : `0x${parseInt(txData.gas_used).toString(16)}`)
: undefined,
nonce: txData.nonce
? (txData.nonce.startsWith?.('0x') ? txData.nonce : `0x${parseInt(txData.nonce).toString(16)}`)
: undefined,
transactionIndex: txData.transaction_index
? (txData.transaction_index.startsWith?.('0x') ? txData.transaction_index : `0x${parseInt(txData.transaction_index).toString(16)}`)
: undefined,
input: txData.input || txData.input_data || txData.data || '0x',
status: txData.status,
timestamp: standardizeTimestamp(txData.timestamp, true) || Date.now(),
method_signature: txData.method_signature,
method_name: txData.method_name
};
return transaction;
});
const formattedTransactions = formatTransactionsForDisplay(transactions);
const totalCount =
(response.meta && response.meta.total) ||
response.total ||
response.totalCount ||
response.count ||
(response.meta && response.meta.count) ||
transactions.length;
const totalPages =
(response.meta && response.meta.total_pages) ||
Math.ceil(totalCount / perPage);
if (debug) logger.debug(`Returning ${transactions.length} transactions, totalCount: ${totalCount}, totalPages: ${totalPages}`);
return {
transactions: formattedTransactions,
totalCount,
totalPages,
page,
perPage
};
} catch (error: any) {
logger.error('Error fetching transactions from Explorer DB:', error);
throw new Error(`Failed to get transactions from Explorer DB: ${error.message}`);
}
}