feat: Add HBAR Transactions page with search, history, and detail views, including UI and API integration.
This commit is contained in:
parent
36d69d6181
commit
a3bdd17133
449
hederaBlockchainAPI.js
Normal file
449
hederaBlockchainAPI.js
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
(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<Object>} - 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<Object>} - 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<Object>} - 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Object>} - 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 amountInWei = web3.utils.toWei(amount.toString(), '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 = {}));
|
||||||
1399
index.html
1399
index.html
File diff suppressed because it is too large
Load Diff
81
style.css
81
style.css
@ -615,6 +615,7 @@ footer strong {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.75rem 0;
|
padding: 0.75rem 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-detail-row:last-child {
|
.tx-detail-row:last-child {
|
||||||
@ -635,14 +636,24 @@ footer strong {
|
|||||||
.detail-value code,
|
.detail-value code,
|
||||||
code.detail-value {
|
code.detail-value {
|
||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.5rem;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.95rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: normal;
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transaction hash specific styling */
|
||||||
|
.tx-detail-row code.detail-value,
|
||||||
|
.tx-detail-row .detail-value code {
|
||||||
|
color: var(--primary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value-wrapper {
|
.detail-value-wrapper {
|
||||||
@ -1420,6 +1431,67 @@ code.detail-value {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Error Modal Styles */
|
||||||
|
.modal-header.error {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 1.5rem 1rem;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--error);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
animation: scaleIn 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details-text {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal Footer */
|
/* Modal Footer */
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -2776,6 +2848,7 @@ body:has(.header) .container {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0.75rem 0;
|
padding: 0.75rem 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-detail-row:last-child {
|
.tx-detail-row:last-child {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user