(function (EXPORTS) { "use strict"; const hederaAPI = EXPORTS; // API Configuration - Mainnet Only const NETWORK_CONFIG = { mirrorNode: 'https://mainnet-public.mirrornode.hedera.com', jsonRpcRelay: 'https://mainnet.hashio.io/api', chainId: 295, // Hedera Mainnet explorer: 'https://hashscan.io/mainnet' }; /** * Get network configuration */ function getNetworkConfig() { return NETWORK_CONFIG; } /** * Get account balance using Hedera Mirror Node API * @param {string} address - EVM address (0x...) or Account ID (0.0.xxxx) * @returns {Promise} - Balance information */ hederaAPI.getBalance = async function(address) { try { const config = getNetworkConfig(); // Clean address address = address.trim(); // Determine if it's an EVM address or Account ID let endpoint; if (address.startsWith('0x')) { // EVM address format endpoint = `${config.mirrorNode}/api/v1/accounts/${address}`; } else if (address.match(/^\d+\.\d+\.\d+$/)) { // Account ID format (0.0.xxxx) endpoint = `${config.mirrorNode}/api/v1/accounts/${address}`; } else { throw new Error('Invalid address format. Use EVM address (0x...) or Account ID (0.0.xxxx)'); } const response = await fetch(endpoint); if (!response.ok) { if (response.status === 404) { throw new Error('Account not found. Make sure the account exists on the network.'); } throw new Error(`API Error: ${response.status} ${response.statusText}`); } const data = await response.json(); // Convert balance from tinybars to HBAR (1 HBAR = 100,000,000 tinybars) const balanceInTinybars = parseInt(data.balance.balance); const balanceInHbar = balanceInTinybars / 100000000; return { address: address, accountId: data.account, evmAddress: data.evm_address, balance: balanceInHbar, balanceTinybars: balanceInTinybars, autoRenewPeriod: data.auto_renew_period, expiryTimestamp: data.expiry_timestamp, memo: data.memo, key: data.key }; } catch (error) { console.error('Error fetching balance:', error); throw error; } }; /** * Get transaction history using Hedera Mirror Node API * @param {string} address - EVM address or Account ID * @param {Object} options - Query options (limit, order, timestamp) * @returns {Promise} - Transaction history */ hederaAPI.getTransactionHistory = async function(address, options = {}) { try { const config = getNetworkConfig(); address = address.trim(); // Build query parameters for account endpoint const params = new URLSearchParams(); params.append('limit', options.limit || 25); params.append('order', options.order || 'desc'); if (options.timestamp) { params.append('timestamp', options.timestamp); } // Use the account endpoint which includes transactions let endpoint; if (address.startsWith('0x')) { endpoint = `${config.mirrorNode}/api/v1/accounts/${address}?${params}`; } else if (address.match(/^\d+\.\d+\.\d+$/)) { endpoint = `${config.mirrorNode}/api/v1/accounts/${address}?${params}`; } else { throw new Error('Invalid address format'); } const response = await fetch(endpoint); if (!response.ok) { if (response.status === 404) { throw new Error('Account not found'); } throw new Error(`API Error: ${response.status}`); } const data = await response.json(); // Check if transactions exist in response if (!data.transactions || data.transactions.length === 0) { return { transactions: [], links: data.links || {} }; } // Process transactions const transactions = data.transactions.map(tx => { // Determine transaction type and amount let type = 'unknown'; let amount = 0; let counterparty = null; if (tx.transfers && tx.transfers.length > 0) { // Find transfers involving our address const accountId = data.account; // Use the account ID from response const ourTransfers = tx.transfers.filter(t => t.account === address || t.account === accountId ); if (ourTransfers.length > 0) { const transfer = ourTransfers[0]; amount = Math.abs(transfer.amount) / 100000000; // Convert to HBAR if (transfer.amount > 0) { type = 'receive'; // Find sender const senderTransfer = tx.transfers.find(t => t.amount < 0); if (senderTransfer) counterparty = senderTransfer.account; } else { type = 'send'; // Find receiver const receiverTransfer = tx.transfers.find(t => t.amount > 0 && t.account !== address && t.account !== accountId); if (receiverTransfer) counterparty = receiverTransfer.account; } } } // Convert Base64 transaction hash to hex format let hexHash = tx.transaction_hash; if (hexHash && !hexHash.startsWith('0x')) { try { const binaryString = atob(hexHash); hexHash = '0x' + Array.from(binaryString) .map(char => char.charCodeAt(0).toString(16).padStart(2, '0')) .join(''); } catch (e) { console.warn('Could not convert hash to hex:', e); } } return { id: tx.transaction_id, hash: hexHash, // Add transaction hash in hex format consensusTimestamp: tx.consensus_timestamp, type: type, amount: amount, counterparty: counterparty, result: tx.result, name: tx.name, memo: tx.memo_base64 ? atob(tx.memo_base64) : '', charged_tx_fee: tx.charged_tx_fee / 100000000, // Convert to HBAR max_fee: tx.max_fee ? tx.max_fee / 100000000 : 0, valid_start_timestamp: tx.valid_start_timestamp, node: tx.node, scheduled: tx.scheduled, nonce: tx.nonce, transfers: tx.transfers }; }); return { transactions: transactions, links: data.links || {} }; } catch (error) { console.error('Error fetching transaction history:', error); throw error; } }; /** * Get transaction details by transaction ID * @param {string} transactionId - Transaction ID * @returns {Promise} - Transaction details */ hederaAPI.getTransactionById = async function(transactionId) { try { const config = getNetworkConfig(); const endpoint = `${config.mirrorNode}/api/v1/transactions/${transactionId}`; const response = await fetch(endpoint); if (!response.ok) { if (response.status === 404) { throw new Error('Transaction not found'); } throw new Error(`API Error: ${response.status}`); } const data = await response.json(); if (data.transactions && data.transactions.length > 0) { const tx = data.transactions[0]; // Convert Base64 transaction hash to hex format let hexHash = tx.transaction_hash; if (hexHash && !hexHash.startsWith('0x')) { try { // Decode Base64 to binary, then convert to hex const binaryString = atob(hexHash); hexHash = '0x' + Array.from(binaryString) .map(char => char.charCodeAt(0).toString(16).padStart(2, '0')) .join(''); } catch (e) { console.warn('Could not convert hash to hex:', e); } } // Fetch block number based on consensus timestamp let blockNumber = null; try { const blockEndpoint = `${config.mirrorNode}/api/v1/blocks?timestamp=gte:${tx.consensus_timestamp}&limit=1&order=asc`; console.log('Fetching block from:', blockEndpoint); const blockResponse = await fetch(blockEndpoint); console.log('Block response status:', blockResponse.status); if (blockResponse.ok) { const blockData = await blockResponse.json(); console.log('Block data:', blockData); if (blockData.blocks && blockData.blocks.length > 0) { blockNumber = blockData.blocks[0].number; console.log('Block number found:', blockNumber); } else { console.warn('No blocks found in response'); } } else { console.warn('Block fetch failed with status:', blockResponse.status); } } catch (e) { console.warn('Could not fetch block number:', e); } let memo=''; return { id: tx.transaction_id, hash: hexHash, // Transaction hash in hex format consensusTimestamp: tx.consensus_timestamp, result: tx.result, name: tx.name, memo: memo, charged_tx_fee: tx.charged_tx_fee / 100000000, max_fee: tx.max_fee ? tx.max_fee / 100000000 : 0, valid_start_timestamp: tx.valid_start_timestamp, node: tx.node, transfers: tx.transfers, block_number: blockNumber, raw: tx }; } throw new Error('Transaction not found'); } catch (error) { console.error('Error fetching transaction:', error); throw error; } }; /** * Send HBAR using JSON-RPC Relay (EVM-compatible) * @param {string} fromPrivateKey - Sender's private key (hex format) * @param {string} toAddress - Recipient's EVM address * @param {number} amount - Amount in HBAR * @param {string} memo - Optional memo * @returns {Promise} - Transaction result */ hederaAPI.sendHBAR = async function(fromPrivateKey, toAddress, amount, memo = '') { try { const config = getNetworkConfig(); // Validate inputs if (!fromPrivateKey || fromPrivateKey.length !== 64) { throw new Error('Invalid private key format. Expected 64-character hex string.'); } if (!toAddress || !toAddress.startsWith('0x')) { throw new Error('Invalid recipient address. Expected EVM address (0x...)'); } if (amount <= 0) { throw new Error('Amount must be greater than 0'); } // Use Web3.js to create and sign the transaction if (typeof Web3 === 'undefined') { throw new Error('Web3.js is required for sending transactions'); } const web3 = new Web3(config.jsonRpcRelay); // Add 0x prefix to private key if not present const privateKey = fromPrivateKey.startsWith('0x') ? fromPrivateKey : '0x' + fromPrivateKey; // Create account from private key const account = web3.eth.accounts.privateKeyToAccount(privateKey); const fromAddress = account.address; // Get current gas price const gasPrice = await web3.eth.getGasPrice(); // Get nonce const nonce = await web3.eth.getTransactionCount(fromAddress, 'pending'); // Convert HBAR to Wei (1 HBAR = 10^18 Wei in EVM context) const amountString = typeof amount === 'number' ? amount.toFixed(18) : amount.toString(); const amountInWei = web3.utils.toWei(amountString, 'ether'); // Prepare transaction object for gas estimation let gasLimit = 21000; // Default for existing accounts // Try to estimate gas (will be higher for new accounts) try { const estimatedGas = await web3.eth.estimateGas({ from: fromAddress, to: toAddress, value: amountInWei }); gasLimit = Math.floor(estimatedGas * 1.2); // Add 20% buffer console.log('Estimated gas:', estimatedGas, 'Using:', gasLimit); } catch (error) { // If estimation fails, use high limit for new account creation console.warn('Gas estimation failed, using high limit for potential new account:', error.message); gasLimit = 800000; // High limit for new account auto-creation } // Prepare transaction const tx = { from: fromAddress, to: toAddress, value: amountInWei, gas: gasLimit, gasPrice: gasPrice, nonce: nonce, chainId: config.chainId }; // Sign transaction const signedTx = await account.signTransaction(tx); // Send transaction const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); return { success: true, transactionHash: receipt.transactionHash, blockNumber: receipt.blockNumber, from: receipt.from, to: receipt.to, gasUsed: receipt.gasUsed, status: receipt.status, explorerUrl: `${config.explorer}/transaction/${receipt.transactionHash}` }; } catch (error) { console.error('Error sending HBAR:', error); // Parse error message let errorMessage = error.message; if (error.message.includes('insufficient funds')) { errorMessage = 'Insufficient balance to complete this transaction'; } else if (error.message.includes('nonce')) { errorMessage = 'Transaction nonce error. Please try again.'; } else if (error.message.includes('gas')) { errorMessage = 'Gas estimation failed. Please check the transaction details.'; } throw new Error(errorMessage); } }; /** * Validate address format * @param {string} address - Address to validate * @returns {Object} - Validation result */ hederaAPI.validateAddress = function(address) { address = address.trim(); // Check EVM address format if (address.startsWith('0x')) { const isValid = /^0x[a-fA-F0-9]{40}$/.test(address); return { valid: isValid, type: 'evm', address: address }; } // Check Account ID format (0.0.xxxx) if (address.match(/^\d+\.\d+\.\d+$/)) { return { valid: true, type: 'accountId', address: address }; } return { valid: false, type: 'unknown', address: address }; }; /** * Format timestamp to readable date * @param {string} timestamp - Consensus timestamp * @returns {string} - Formatted date */ hederaAPI.formatTimestamp = function(timestamp) { if (!timestamp) return 'N/A'; // Timestamp format: seconds.nanoseconds const [seconds, nanoseconds] = timestamp.split('.'); const date = new Date(parseInt(seconds) * 1000); return date.toLocaleString(); }; })(typeof module === "object" ? module.exports : (window.hederaAPI = {}));