Merge pull request #4 from void-57/main

feat: Add comprehensive wallet features
This commit is contained in:
Aniruddha 2025-12-30 00:11:49 +05:30 committed by GitHub
commit 1a956912ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1228 additions and 178 deletions

View File

@ -895,6 +895,7 @@ main {
margin: 0 auto;
}
aside {
view-transition-name: search-history;
padding-bottom: 1.5rem;
@ -958,6 +959,101 @@ aside h4 {
padding-bottom: 0.5rem;
}
/* Transaction list styling matching BTC wallet */
.transaction {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.8rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.1);
}
.transaction:last-child {
border-bottom: none;
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
}
.transaction__icon .icon {
width: 1.5rem;
height: 1.5rem;
fill: rgba(var(--text-color), 0.7);
}
.transaction.in .icon {
fill: var(--green);
}
.transaction.out .icon.sent {
fill: var(--danger-color);
}
.transaction__time {
font-size: 0.85rem;
color: rgba(var(--text-color), 0.7);
}
.transaction__amount {
font-weight: 600;
font-size: 0.95rem;
}
.transaction.in .transaction__amount {
color: var(--green);
}
.transaction.out .transaction__amount {
color: var(--danger-color);
}
.transaction__receiver {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.9);
word-break: break-all;
}
.tx-participant {
color: var(--accent-color);
text-decoration: none;
font-family: monospace;
font-size: 0.85rem;
}
.tx-participant:hover {
text-decoration: underline;
}
.wrap-around {
word-break: break-all;
}
.transaction__id {
display: inline-flex;
align-items: center;
}
.transaction__id .icon {
width: 1rem;
height: 1rem;
fill: currentColor;
}
#transaction_list {
max-height: 500px;
overflow-y: auto;
/* Hide scrollbar for Chrome, Safari and Opera */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
#transaction_list::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
#error_section {
display: grid;
height: 100%;
@ -1179,3 +1275,17 @@ aside h4 {
animation: none !important;
}
}
.transaction__time {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
.transaction__amount {
font-size: 0.75rem;
margin-left: auto;
white-space: nowrap;
flex-shrink: 0;
}

View File

@ -1113,6 +1113,101 @@ aside {
}
}
/* Transaction list styling matching BTC wallet */
.transaction {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.8rem;
padding: 1rem;
border-bottom: solid thin rgba(var(--text-color), 0.1);
&:last-child {
border-bottom: none;
}
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
.icon {
width: 1.5rem;
height: 1.5rem;
fill: rgba(var(--text-color), 0.7);
}
}
.transaction.in .icon {
fill: var(--green);
}
.transaction.out .icon.sent {
fill: var(--danger-color);
}
.transaction__time {
font-size: 0.85rem;
color: rgba(var(--text-color), 0.7);
}
.transaction__amount {
font-weight: 600;
font-size: 0.95rem;
}
.transaction.in .transaction__amount {
color: var(--green);
}
.transaction.out .transaction__amount {
color: var(--danger-color);
}
.transaction__receiver {
font-size: 0.9rem;
color: rgba(var(--text-color), 0.9);
word-break: break-all;
}
.tx-participant {
color: var(--accent-color);
text-decoration: none;
font-family: monospace;
font-size: 0.85rem;
&:hover {
text-decoration: underline;
}
}
.wrap-around {
word-break: break-all;
}
.transaction__id {
display: inline-flex;
align-items: center;
.icon {
width: 1rem;
height: 1rem;
fill: currentColor;
}
}
#transaction_list {
max-height: 500px;
overflow-y: auto;
/* Hide scrollbar for Chrome, Safari and Opera */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
}
@media only screen and (max-width: 640px) {
.hide-on-small {
display: none;
@ -1172,7 +1267,9 @@ aside {
}
}
aside {
min-width: 18rem;
border-right: solid thin rgba(var(--text-color), 0.3);
overflow-y: auto;
@ -1239,3 +1336,36 @@ aside {
animation: none !important;
}
}
// Transaction display styles
.transaction__time {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
.transaction__amount {
font-size: 0.75rem;
margin-left: auto;
white-space: nowrap;
flex-shrink: 0;
}
.transaction {
grid-template-columns: auto 1fr;
gap: 0.5rem;
}
.transaction__icon {
display: flex;
align-items: flex-start;
padding-top: 0.2rem;
.icon {
width: 1rem;
height: 1rem;
}
}

View File

@ -11,6 +11,7 @@
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
rel="stylesheet">
</head>
<body class="hidden">
@ -170,7 +171,7 @@
<div id="transaction_result_popup__content" class="grid gap-2"></div>
</sm-popup>
<script>
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
/* FLO blockchain configuration - These constants must be defined before loading FLO scripts */
const floGlobals = {
blockchain: "FLO",
tokenURL: 'https://ranchimallflo.ranchimall.net/',
@ -195,7 +196,7 @@
const uiGlobals = {}
const { html, svg, render: renderElem } = uhtml;
uiGlobals.connectionErrorNotification = []
//Checks for internet connection status
// Check for internet connection status and show notification if offline
if (!navigator.onLine)
uiGlobals.connectionErrorNotification.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
window.addEventListener('offline', () => {
@ -207,12 +208,12 @@
})
notify('We are back online.', 'success')
})
// Use instead of document.getElementById
// Helper function to get element by ID (shorthand for document.getElementById)
function getRef(elementId) {
return document.getElementById(elementId)
}
let zIndex = 50
// function required for popups or modals to appear
// Opens a popup/modal and manages the popup stack for proper layering
function openPopup(popupId, pinned) {
if (popupStack.peek() === undefined) {
document.addEventListener('keydown', (e) => {
@ -243,7 +244,7 @@
switch (e.target.id) {
}
})
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
// Display toast notifications. Pass 'error' or 'success' as mode parameter
function notify(message, mode, options = {}) {
let icon
switch (mode) {
@ -258,7 +259,20 @@
if (mode === 'error') {
console.error(message)
}
return getRef("notification_drawer").push(message, { icon, ...options });
// Ensure notification drawer element exists and is fully initialized
const notificationDrawer = getRef("notification_drawer");
if (!notificationDrawer || typeof notificationDrawer.push !== 'function') {
console.warn('Notification drawer not ready, logging message:', message);
// Queue the notification to show later when drawer is ready
if (!window._pendingNotifications) {
window._pendingNotifications = [];
}
window._pendingNotifications.push({ message, mode, options, icon });
return null;
}
return notificationDrawer.push(message, { icon, ...options });
}
// displays a popup for asking permission. Use this instead of JS confirm
/**
@ -399,7 +413,7 @@
this.routingStart(this.state)
}
if (this.routes[page]) {
//Actual routing step
// Execute the route handler for the current page
await this.routes[page](this.state)
this.lastPage = page
} else {
@ -549,27 +563,27 @@
}
// put this once near the top with your other globals
// Initialize IndexedDB for storing contact addresses
let idbReady;
window.addEventListener('load', () => {
// 1) Initialize IndexedDB BEFORE any routing/reads
// Initialize the database before any routing or data reads
idbReady = compactIDB.initDB('floEthereum', { contacts: {} })
.then((res) => { console.log(res); })
.catch((err) => { console.error(err); });
// 2) After DB is ready, wire listeners and route once
// Set up event listeners and routing after database is ready
idbReady.then(() => {
const routeNow = () => router.routeTo(location.hash);
// Utility/UI listeners
// Set up UI event listeners for copy notifications and ripple effects
document.addEventListener('copy', () => notify('copied', 'success'));
document.addEventListener('pointerdown', (e) => {
const target = e.target.closest('button:not(:disabled), .interactive:not(:disabled)');
if (target) createRipple(e, target);
});
// Ethereum / MetaMask
// Handle Ethereum provider and MetaMask connection
if (window.ethereum) {
window.ethereum.on('chainChanged', (chainId) => {
window.currentChainId = chainId;
@ -594,7 +608,7 @@
routeNow();
});
// Account status hooks (kept exactly like your current code)
// Listen for MetaMask account changes
ethereum.on('accountsChanged', (accounts) => {
getRef('eth_balance_wrapper').classList.add('hidden');
setMetaMaskStatus(accounts.length > 0);
@ -606,7 +620,7 @@
setMetaMaskStatus(false);
});
} else {
// no ethereum provider—just route
// No MetaMask detected, proceed with normal routing
routeNow();
}
@ -615,6 +629,19 @@
});
});
// Process pending notifications after a delay to ensure custom elements are ready
setTimeout(() => {
if (window._pendingNotifications && window._pendingNotifications.length > 0) {
const notificationDrawer = getRef("notification_drawer");
if (notificationDrawer && typeof notificationDrawer.push === 'function') {
window._pendingNotifications.forEach(({ message, icon, options }) => {
notificationDrawer.push(message, { icon, ...options });
});
window._pendingNotifications = [];
}
}
}, 1000); // Give custom elements time to fully initialize
router.addRoute('404', () => {
renderElem(getRef('page_container'), html`
@ -638,8 +665,8 @@
</h2>
<sm-form oninvalid="handleInvalidSearch()">
<div id="input_wrapper">
<sm-input id="check_balance_input" class="password-field flex-1" placeholder="FLO/BTC private key or Eth address"
type="password" animate required>
<sm-input id="check_balance_input" class="password-field flex-1" placeholder="Address, private key, or tx hash"
type="password" animate>
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M21,10h-8.35C11.83,7.67,9.61,6,7,6c-3.31,0-6,2.69-6,6s2.69,6,6,6c2.61,0,4.83-1.67,5.65-4H13l2,2l2-2l2,2l4-4.04L21,10z M7,15c-1.65,0-3-1.35-3-3c0-1.65,1.35-3,3-3s3,1.35,3,3C10,13.65,8.65,15,7,15z"> </path> </g> </svg>
<label slot="right" class="interact">
<input type="checkbox" class="hidden" autocomplete="off" readonly
@ -649,8 +676,8 @@
</label>
</sm-input>
<div class="multi-state-button">
<button id="check_balance_button" class="button button--primary h-100 w-100" type="submit" onclick=${() => checkBalance()} disabled>
Check balance
<button id="check_balance_button" class="button button--primary h-100 w-100" type="submit" onclick=${() => checkBalance()}>
Search
</button>
</div>
</div>
@ -662,6 +689,12 @@
renderError('Please switch MetaMask to Ethereum Mainnet')
}
renderSearchedAddressList()
// Handle URL parameters after page is rendered
// Use setTimeout to ensure DOM is fully ready
setTimeout(() => {
handleUrlParams();
}, 100);
}
function renderError(title, description) {
if (!title)
@ -727,28 +760,95 @@
console.error(error)
})
}
// Track current page and address for transaction pagination
let currentPage = 1;
let currentAddress = '';
let currentFloAddress = '';
let allTransactions = [];
const TRANSACTIONS_PER_PAGE = 10;
function checkBalance(ethAddress, floAddress) {
if (!ethAddress) {
let keyToConvert = document.querySelector('#check_balance_input').value.trim()
// Check if input is empty
if (!keyToConvert) {
notify('Please enter an Ethereum address, private key, or transaction hash', 'error');
return;
}
// Check if it's a valid Ethereum address
if (ethOperator.isValidAddress(keyToConvert)) {
ethAddress = keyToConvert
} else {
}
// Check if it's a transaction hash (0x followed by 64 hex characters)
else if (/^0x[0-9a-fA-F]{64}$/.test(keyToConvert)) {
viewTransactionDetails(keyToConvert);
return;
}
// Otherwise, try to convert as private key
else {
try {
if (/^[0-9a-fA-F]{64}$/.test(keyToConvert)) {
keyToConvert = coinjs.privkey2wif(keyToConvert)
}
const ethPrivateKey = coinjs.wif2privkey(keyToConvert).privkey;
ethAddress = floEthereum.ethAddressFromPrivateKey(ethPrivateKey)
floAddress = floCrypto.getFloID(keyToConvert)
} catch (error) {
notify('Invalid input. Please enter a valid Ethereum address or private key.', 'error');
return;
}
}
}
if (!ethAddress) return
buttonLoader('check_balance_button', true)
Promise.all([
// Reset pagination when checking new address
currentPage = 1;
currentAddress = ethAddress;
currentFloAddress = floAddress;
loadTransactionsPage(ethAddress, floAddress, currentPage);
}
async function loadTransactionsPage(ethAddress, floAddress, page) {
buttonLoader('check_balance_button', true);
try {
const results = await Promise.allSettled([
ethOperator.getBalance(ethAddress),
ethOperator.getTokenBalance(ethAddress, 'usdc'),
ethOperator.getTokenBalance(ethAddress, 'usdt')
])
.then(([etherBalance, usdcBalance, usdtBalance]) => {
ethOperator.getTokenBalance(ethAddress, 'usdt'),
ethOperator.getTransactionHistory(ethAddress, {
page: page,
offset: TRANSACTIONS_PER_PAGE,
sort: 'desc'
})
]);
// Extract balance and transaction data, using defaults if any request failed
const etherBalance = results[0].status === 'fulfilled' ? results[0].value : '0';
const usdcBalance = results[1].status === 'fulfilled' ? results[1].value : '0';
const usdtBalance = results[2].status === 'fulfilled' ? results[2].value : '0';
const transactions = results[3].status === 'fulfilled' ? results[3].value : [];
// Store transactions for filtering
allTransactions = transactions;
// Log warnings if any API requests failed
if (results[0].status === 'rejected') {
console.warn('Failed to fetch ETH balance:', results[0].reason);
}
if (results[1].status === 'rejected') {
console.warn('Failed to fetch USDC balance:', results[1].reason);
}
if (results[2].status === 'rejected') {
console.warn('Failed to fetch USDT balance:', results[2].reason);
}
if (results[3].status === 'rejected') {
console.warn('Failed to fetch transaction history:', results[3].reason);
}
compactIDB.readData('contacts', floAddress || ethAddress).then(result => {
if (result) return
compactIDB.addData('contacts', {
@ -759,6 +859,29 @@
console.error(error)
})
})
renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, transactions, page);
} catch (error) {
notify(error.message || error, 'error');
} finally {
buttonLoader('check_balance_button', false);
}
}
function renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, transactions, page) {
// Update URL to reflect the current address being viewed
const url = new URL(window.location);
url.searchParams.set('address', ethAddress);
url.searchParams.delete('tx');
url.searchParams.delete('page');
// Update browser URL without reloading the page
window.history.pushState({}, '', url.pathname + url.search + url.hash);
// Determine if pagination buttons should be enabled
const hasNextPage = transactions.length >= TRANSACTIONS_PER_PAGE;
const hasPrevPage = page > 1;
renderElem(getRef('eth_balance_wrapper'), html`
<div class="grid">
<div class="label">ETH address</div>
@ -787,8 +910,42 @@
</li>
</ul>
</div>
`)
getRef('eth_balance_wrapper').classList.remove('hidden')
<div class="grid gap-1 margin-top-1">
<div class="flex align-center space-between">
<h4>Transactions</h4>
<sm-chips id="tx_filter_chips" onchange=${(e) => {
const selectedValue = e.detail?.value || e.target.value || 'all';
filterTransactions(selectedValue, transactions, ethAddress);
}}>
<sm-chip value="all" selected>All</sm-chip>
<sm-chip value="sent">Sent</sm-chip>
<sm-chip value="received">Received</sm-chip>
</sm-chips>
</div>
<ul id="transaction_list" class="grid gap-0-5">
${renderTransactionList(transactions, ethAddress, 'all')}
</ul>
<div class="flex align-center space-between gap-1 margin-top-1">
<button
class="button button--small"
onclick=${() => loadPreviousPage()}
?disabled=${!hasPrevPage}
>
Previous
</button>
<span class="color-0-7">Page ${page}</span>
<button
class="button button--small"
onclick=${() => loadNextPage()}
?disabled=${!hasNextPage}
>
Next
</button>
</div>
</div>
`);
getRef('eth_balance_wrapper').classList.remove('hidden');
getRef('eth_balance_wrapper').animate([
{
transform: 'translateY(-1rem)',
@ -802,13 +959,266 @@
easing: 'ease',
duration: 300,
fill: 'forwards'
})
}).catch((error) => {
notify(error, 'error')
}).finally(() => {
buttonLoader('check_balance_button', false)
})
});
}
function loadNextPage() {
currentPage++;
loadTransactionsPage(currentAddress, currentFloAddress, currentPage);
}
function loadPreviousPage() {
if (currentPage > 1) {
currentPage--;
loadTransactionsPage(currentAddress, currentFloAddress, currentPage);
}
}
function renderTransactionList(transactions, userAddress, filter = 'all') {
if (!transactions || transactions.length === 0) {
return html`<li class="text-center color-0-8"><p>No transactions found</p></li>`;
}
let filteredTxs = transactions;
if (filter === 'sent') {
filteredTxs = transactions.filter(tx => tx.isSent);
} else if (filter === 'received') {
filteredTxs = transactions.filter(tx => tx.isReceived);
}
if (filteredTxs.length === 0) {
return html`<li class="text-center color-0-8"><p>No ${filter} transactions found</p></li>`;
}
return filteredTxs.map(tx => {
const date = new Date(tx.timestamp * 1000);
const formattedDate = date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const isReceived = tx.isReceived;
const type = isReceived ? 'in' : 'out';
const amountClass = isReceived ? 'tx-received' : 'tx-sent';
const amountPrefix = isReceived ? '+' : '-';
const displayAddress = isReceived ? tx.from : tx.to;
const directionText = isReceived ? 'Received from' : 'Sent to';
// Arrow icons matching BTC wallet
const icon = isReceived
? svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`
: svg`<svg class="icon sent" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
const className = `transaction grid ${type}`;
return html`
<li class=${className}>
<div class="transaction__icon">${icon}</div>
<div class="grid gap-0-5">
<div class="flex gap-1">
<time class="transaction__time">${formattedDate}</time>
<div class="transaction__amount">${amountPrefix}${tx.value.toFixed(8)} ${tx.symbol}</div>
</div>
<div class="transaction__receiver">
${directionText} <a href="#" class="tx-participant wrap-around">${displayAddress}</a>
</div>
<div class="flex gap-0-5 flex-wrap align-center">
<button class="button button--small gap-0-3 align-center button--colored transaction__id" onclick=${() => viewTransactionDetails(tx.hash)}>
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
View details
</button>
</div>
</div>
</li>
`;
});
}
function filterTransactions(filter, transactions, userAddress) {
const txList = getRef('transaction_list');
if (txList) {
const renderedList = renderTransactionList(transactions, userAddress, filter);
renderElem(txList, html`${renderedList}`);
}
}
async function viewTransactionDetails(txHash, preserveAddress = false) {
try {
buttonLoader('check_balance_button', true);
// Update URL to show the transaction hash
const url = new URL(window.location);
url.searchParams.set('tx', txHash);
url.searchParams.delete('page');
// Remove address from URL unless viewing from transaction history
if (!preserveAddress) {
url.searchParams.delete('address');
}
// Update browser URL without reloading the page
window.history.pushState({}, '', url.pathname + url.search + url.hash);
const txDetails = await ethOperator.getTransactionDetails(txHash);
let formattedDate = 'Pending';
if (txDetails.timestamp) {
const date = new Date(txDetails.timestamp * 1000);
formattedDate = date.toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
const statusClass = txDetails.status === 'success' ? 'tx-success' : txDetails.status === 'pending' ? 'color-0-7' : 'tx-failed';
const statusText = txDetails.status === 'success' ? 'Success' : txDetails.status === 'pending' ? 'Pending' : 'Failed';
// Show transaction details
renderElem(getRef('eth_balance_wrapper'), html`
<div class="grid gap-1">
<div class="flex align-center space-between">
<h3>Transaction Details</h3>
<button class="button button--small" onclick=${() => {
const url = new URL(window.location);
url.searchParams.delete('tx');
window.history.pushState({}, '', url.pathname + url.search + url.hash);
// Go back to address view
handleUrlParams();
}}>
Back
</button>
</div>
<div class="grid gap-0-5">
<div class="label">Status</div>
<strong class=${statusClass}>${statusText}</strong>
</div>
<div class="grid gap-0-5">
<div class="label">Transaction Hash</div>
<sm-copy value=${txDetails.hash}></sm-copy>
</div>
<div class="grid gap-0-5">
<div class="label">From</div>
<sm-copy value=${txDetails.from}></sm-copy>
</div>
<div class="grid gap-0-5">
<div class="label">To</div>
<sm-copy value=${txDetails.to}></sm-copy>
</div>
${txDetails.tokenTransfer ? html`
<div class="grid gap-0-5">
<div class="label">Token Transfer</div>
<strong>${txDetails.tokenTransfer.value} ${txDetails.tokenTransfer.symbol}</strong>
</div>
` : html`
<div class="grid gap-0-5">
<div class="label">Value</div>
<strong>${txDetails.value} ETH</strong>
</div>
`}
<div class="grid gap-0-5">
<div class="label">Gas Fee</div>
<strong>${txDetails.gasFee ? txDetails.gasFee.toFixed(8) : 'N/A'} ETH</strong>
</div>
<div class="grid gap-0-5">
<div class="label">Block Number</div>
<strong>${txDetails.blockNumber || 'Pending'}</strong>
</div>
<div class="grid gap-0-5">
<div class="label">Confirmations</div>
<strong>${txDetails.confirmations}</strong>
</div>
<div class="grid gap-0-5">
<div class="label">Timestamp</div>
<strong>${formattedDate}</strong>
</div>
<a class="button button--primary" target="_blank" href=${'https://etherscan.io/tx/' + txHash}>
View on Etherscan
</a>
</div>
`);
getRef('eth_balance_wrapper').classList.remove('hidden');
} catch (error) {
console.error('Error in viewTransactionDetails:', error);
notify(error.message || 'Failed to fetch transaction details', 'error');
} finally {
buttonLoader('check_balance_button', false);
}
}
// Function to handle URL parameters and load appropriate data
function handleUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
// Check for transaction hash parameter
const txHash = urlParams.get('tx');
if (txHash && /^0x[0-9a-fA-F]{64}$/.test(txHash)) {
// Populate input with tx hash
const searchInput = document.querySelector('#check_balance_input');
if (searchInput) {
searchInput.value = txHash;
const nativeInput = searchInput.querySelector('input');
if (nativeInput) {
nativeInput.value = txHash;
}
}
// viewTransactionDetails handles its own loading state
viewTransactionDetails(txHash);
return;
}
// Check for address parameter
const address = urlParams.get('address');
if (address && ethOperator.isValidAddress(address)) {
// Populate the input field - target both custom element and native input
const searchInput = document.querySelector('#check_balance_input');
const nativeInput = searchInput?.querySelector('input');
if (nativeInput) {
nativeInput.value = address;
nativeInput.dispatchEvent(new Event('input', { bubbles: true }));
}
if (searchInput) {
searchInput.value = address;
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
// Load the address (checkBalance handles its own loading state)
checkBalance(address);
} else {
// No parameters, hide the balance wrapper
const balanceWrapper = getRef('eth_balance_wrapper');
if (balanceWrapper) {
balanceWrapper.classList.add('hidden');
}
}
}
// Handle browser back/forward navigation
window.addEventListener('popstate', () => {
// Check if we're on the balance page
if (window.location.hash.includes('balance') || window.location.hash === '#/' || window.location.hash === '') {
handleUrlParams();
}
});
function handleInvalidSearch() {
if (document.startViewTransition)
document.startViewTransition(() => {
@ -861,7 +1271,7 @@
<div class="grid gap-0-5">
<sm-input class="receiver-address" placeholder="Receiver's Ethereum address" data-eth-address animate required ></sm-input>
<div class="flex flex-direction-column gap-0-5">
<sm-input class="receiver-amount amount-shown flex-1" placeholder="Amount" type="number" step="0.000001" min="0.000001" error-text="Amount should be grater than 0.000001 ETHER" animate required>
<sm-input class="receiver-amount amount-shown flex-1" placeholder="Amount" type="number" step="0.000001" min="0.000001" error-text="Amount should be greater than 0.000001 ETHER" animate required>
<div class="asset-symbol flex" slot="icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> <g clip-path="url(#clip0_201_2)"> <path d="M12 0L19.6368 12.4368L12.1633 16.8L4.36325 12.4368L12 0Z"/> <path d="M12 24L4.36325 13.6099L11.8367 18L19.6368 13.6099L12 24Z"/> </g> <defs> <clipPath id="clip0_201_2"> <rect width="24" height="24" fill="white"/> </clipPath> </defs> </svg>
</div>
@ -932,7 +1342,7 @@
const asset = e.target.value
const amountInput = getRef('send_tx_form').querySelector('.receiver-amount')
amountInput.value = ''
amountInput.setAttribute('error-text', `Amount should be grater than 0.000001 ${asset.toUpperCase()}`)
amountInput.setAttribute('error-text', `Amount should be greater than 0.000001 ${asset.toUpperCase()}`)
document.querySelectorAll('.asset-symbol').forEach(elem => {
elem.innerHTML = assetIcons[asset]
})
@ -942,28 +1352,186 @@
const receiver = getRef('send_tx_form').querySelector('.receiver-address').value.trim();
const amount = getRef('send_tx_form').querySelector('.receiver-amount').value.trim();
const asset = getRef('asset_selector').value;
try {
const confirmation = await getConfirmation('Send transaction', {
message: `You are about to send ${amount} ${asset.toUpperCase()} to ${receiver}`,
confirmText: 'Send',
})
buttonLoader('send_tx_button', true)
if (!confirmation) return
let privateKey = getRef('private_key_input').value.trim()
// First, get basic confirmation
const initialConfirmation = await getConfirmation('Confirm transaction', {
message: `Calculating gas fees for sending ${amount} ${asset.toUpperCase()} to ${receiver}...`,
confirmText: 'Continue',
});
if (!initialConfirmation) return;
// Show loading state while calculating gas
buttonLoader('send_tx_button', true);
// Get private key
let privateKey = getRef('private_key_input').value.trim();
if (/^[0-9a-fA-F]{64}$/.test(privateKey)) {
privateKey = coinjs.privkey2wif(privateKey)
privateKey = coinjs.privkey2wif(privateKey);
}
privateKey = coinjs.wif2privkey(privateKey).privkey;
// Calculate gas fees
let gasEstimate, feeData, estimatedGasFee, maxGasFee, totalCostETH;
try {
// Get provider for gas estimation
const provider = ethOperator.getProvider(true);
if (!provider) throw new Error('Provider not available');
// Get fee data
feeData = await provider.getFeeData();
// Estimate gas limit
if (asset === 'ether') {
gasEstimate = await ethOperator.estimateGas({
privateKey,
receiver,
amount
});
} else {
// For token transfers, estimate is typically higher
gasEstimate = ethers.BigNumber.from('65000'); // Typical ERC20 transfer gas
}
// Calculate priority fee and max fee
const priorityFee = feeData.maxPriorityFeePerGas || ethers.utils.parseUnits("1.5", "gwei");
let maxFee = feeData.maxFeePerGas;
if (!maxFee || maxFee.lt(priorityFee)) {
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas || ethers.utils.parseUnits("1", "gwei");
maxFee = baseFee.mul(2).add(priorityFee);
}
const minMaxFee = priorityFee.mul(15).div(10);
if (maxFee.lt(minMaxFee)) {
maxFee = minMaxFee;
}
// Calculate estimated gas fee (using base fee + priority fee for estimation)
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas || ethers.utils.parseUnits("1", "gwei");
const estimatedGasPrice = baseFee.add(priorityFee);
estimatedGasFee = parseFloat(ethers.utils.formatEther(gasEstimate.mul(estimatedGasPrice)));
// Calculate max possible gas fee
maxGasFee = parseFloat(ethers.utils.formatEther(gasEstimate.mul(maxFee)));
// Calculate total cost in ETH
totalCostETH = asset === 'ether' ? (parseFloat(amount) + estimatedGasFee) : estimatedGasFee;
} catch (gasError) {
console.error('Gas estimation error:', gasError);
buttonLoader('send_tx_button', false);
notify('Failed to estimate gas fees. Please try again.', 'error');
return;
}
buttonLoader('send_tx_button', false);
// Show detailed confirmation with gas fees
const gasConfirmationPopup = html.node`
<sm-popup id="gas_confirmation_popup">
<header slot="header" class="popup__header">
<h4>Review Transaction</h4>
</header>
<div class="grid gap-1-5" style="padding: 1.5rem;">
<div class="grid gap-1">
<h5>Transaction Details</h5>
<div class="grid gap-0-5" style="padding: 1rem; border-radius: 0.5rem; background: rgba(var(--text-color), 0.06);">
<div class="flex space-between">
<span class="label">Amount:</span>
<strong class="amount-shown">${amount} ${asset.toUpperCase()}</strong>
</div>
<div class="flex space-between">
<span class="label">To:</span>
<span style="font-family: monospace; font-size: 0.9rem;">${receiver.slice(0, 10)}...${receiver.slice(-8)}</span>
</div>
</div>
</div>
<div class="grid gap-1">
<h5>Gas Fee Estimate</h5>
<div class="grid gap-0-5" style="padding: 1rem; border-radius: 0.5rem; background: rgba(var(--text-color), 0.06);">
<div class="flex space-between">
<span class="label">Estimated Gas:</span>
<strong class="amount-shown">${estimatedGasFee.toFixed(6)} ETH</strong>
</div>
<div class="flex space-between">
<span class="label">Max Gas Fee:</span>
<span class="amount-shown" style="color: rgba(var(--text-color), 0.7);">${maxGasFee.toFixed(6)} ETH</span>
</div>
<div class="flex space-between">
<span class="label">Gas Limit:</span>
<span>${gasEstimate.toString()}</span>
</div>
<hr style="border: none; border-top: 1px solid rgba(var(--text-color), 0.1); margin: 0.5rem 0;">
<div class="flex space-between">
<strong>Total Cost (Est.):</strong>
<strong class="amount-shown">${totalCostETH.toFixed(6)} ETH</strong>
</div>
</div>
<p style="font-size: 0.875rem; color: rgba(var(--text-color), 0.7);">
<svg class="icon" style="width: 16px; height: 16px; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
Gas fees vary based on network congestion. The actual fee may be lower than estimated.
</p>
</div>
<div class="flex gap-0-5">
<button class="button flex-1" onclick="closeGasConfirmation()">Cancel</button>
<button class="button button--primary flex-1" onclick="confirmAndSend()">Confirm & Send</button>
</div>
</div>
</sm-popup>
`;
document.body.appendChild(gasConfirmationPopup);
// Store transaction data for confirmation
window.pendingTxData = {
receiver,
amount,
asset,
privateKey
};
// Define close function
window.closeGasConfirmation = () => {
closePopup();
setTimeout(() => {
gasConfirmationPopup.remove();
delete window.pendingTxData;
delete window.closeGasConfirmation;
delete window.confirmAndSend;
}, 300);
};
// Define confirm and send function
window.confirmAndSend = async () => {
closePopup();
gasConfirmationPopup.remove();
const { receiver, amount, asset, privateKey } = window.pendingTxData;
delete window.pendingTxData;
delete window.closeGasConfirmation;
delete window.confirmAndSend;
buttonLoader('send_tx_button', true);
try {
switch (asset) {
case 'ether': {
const tx = await ethOperator.sendTransaction({
privateKey,
receiver,
amount,
})
showTransactionResult('pending', { txHash: tx.hash })
await tx.wait()
showTransactionResult('confirmed', { txHash: tx.hash })
});
showTransactionResult('pending', { txHash: tx.hash });
await tx.wait();
showTransactionResult('confirmed', { txHash: tx.hash });
break;
}
case 'usdc':
@ -973,29 +1541,40 @@
receiver,
amount,
token: asset
})
showTransactionResult('pending', { txHash: tx.hash })
await tx.wait()
showTransactionResult('confirmed', { txHash: tx.hash })
});
showTransactionResult('pending', { txHash: tx.hash });
await tx.wait();
showTransactionResult('confirmed', { txHash: tx.hash });
break;
}
}
getRef('send_tx_form').reset()
getRef('sender_balance_container').classList.add('hidden')
getRef('send_tx_form').reset();
getRef('sender_balance_container').classList.add('hidden');
} catch (e) {
console.error(e.message)
console.error(e.message);
const regex = /\(error=({.*?}),/;
const match = e.message.match(regex);
if (match && match[1]) {
const { code } = JSON.parse(match[1]);
if (code === -32000)
showTransactionResult('failed', { description: `Insufficient ${asset.toUpperCase()} balance` })
showTransactionResult('failed', { description: `Insufficient ${asset.toUpperCase()} balance` });
else {
showTransactionResult('failed', { description: e.message })
showTransactionResult('failed', { description: e.message });
}
} else {
showTransactionResult('failed', { description: e.message });
}
} finally {
buttonLoader('send_tx_button', false)
buttonLoader('send_tx_button', false);
}
};
openPopup('gas_confirmation_popup');
} catch (e) {
console.error(e);
notify(e.message || 'Transaction failed', 'error');
buttonLoader('send_tx_button', false);
}
}
//Show transaction phase

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //ethOperator v1.0.2
(function (EXPORTS) { // ethOperator v1.0.2
/* ETH Crypto and API Operator */
if (!window.ethers)
return console.error('ethers.js not found')
@ -240,61 +240,51 @@
usdc: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
usdt: "0xdac17f958d2ee523a2206206994597c13d831ec7"
}
function getProvider() {
// switches provider based on whether the user is using MetaMask or not
if (window.ethereum) {
/**
* Get Ethereum provider (MetaMask or public RPC)
* @param {boolean} readOnly - If true, use public RPC; if false, use MetaMask when available
* @returns {ethers.providers.Provider} Ethereum provider instance
*/
const getProvider = ethOperator.getProvider = (readOnly = false) => {
if (!readOnly && window.ethereum) {
return new ethers.providers.Web3Provider(window.ethereum);
} else {
return new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/6e12fee52bdd48208f0d82fb345bcb3c`)
}
}
function connectToMetaMask() {
return new Promise((resolve, reject) => {
// if (typeof window.ethereum === "undefined")
// return reject("MetaMask not installed");
return resolve(true)
ethereum
.request({ method: 'eth_requestAccounts' })
.then((accounts) => {
console.log('Connected to MetaMask')
return resolve(accounts)
})
.catch((err) => {
console.log(err)
return reject(err)
})
})
}
// connectToMetaMask();
// Note: MetaMask connection is handled in the UI layer, not here
const getBalance = ethOperator.getBalance = async (address) => {
try {
if (!address || !isValidAddress(address))
return new Error('Invalid address');
// Get the balance
const provider = getProvider();
// Use read-only provider (public RPC) for balance checks
const provider = getProvider(true);
const balanceWei = await provider.getBalance(address);
const balanceEth = parseFloat(ethers.utils.formatEther(balanceWei));
return balanceEth;
} catch (error) {
console.error('Error:', error.message);
return error;
console.error('Balance error:', error.message);
return 0;
}
}
const getTokenBalance = ethOperator.getTokenBalance = async (address, token, { contractAddress } = {}) => {
try {
// if (!window.ethereum.isConnected()) {
// await connectToMetaMask();
// }
if (!token)
return new Error("Token not specified");
if (!CONTRACT_ADDRESSES[token] && contractAddress)
return new Error('Contract address of token not available')
const usdcContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, getProvider());
let balance = await usdcContract.balanceOf(address);
balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // Assuming 6 decimals
// Use read-only provider (public RPC) for token balance checks
const provider = getProvider(true);
const tokenAddress = CONTRACT_ADDRESSES[token] || contractAddress;
const tokenContract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
let balance = await tokenContract.balanceOf(address);
balance = parseFloat(ethers.utils.formatUnits(balance, 6)); // USDC and USDT use 6 decimals
return balance;
} catch (e) {
console.error(e);
console.error('Token balance error:', e);
return 0;
}
}
@ -317,28 +307,269 @@
const provider = getProvider();
const signer = new ethers.Wallet(privateKey, provider);
const limit = await estimateGas({ privateKey, receiver, amount })
// Get current fee data from the network
const feeData = await provider.getFeeData();
// Calculate priority fee (tip to miners) - use 1.5 gwei or the network's suggested priority fee, whichever is higher
const priorityFee = feeData.maxPriorityFeePerGas || ethers.utils.parseUnits("1.5", "gwei");
// Calculate max fee per gas (base fee + priority fee)
// Use the network's suggested maxFeePerGas or calculate it manually
let maxFee = feeData.maxFeePerGas;
// If maxFeePerGas is not available or is less than priority fee, calculate it
if (!maxFee || maxFee.lt(priorityFee)) {
// Get the base fee from the latest block and add our priority fee
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas || ethers.utils.parseUnits("1", "gwei");
// maxFee = (baseFee * 2) + priorityFee to account for potential base fee increases
maxFee = baseFee.mul(2).add(priorityFee);
}
// Ensure maxFee is at least 1.5x the priority fee for safety
const minMaxFee = priorityFee.mul(15).div(10); // 1.5x priority fee
if (maxFee.lt(minMaxFee)) {
maxFee = minMaxFee;
}
// Creating and sending the transaction object
return signer.sendTransaction({
to: receiver,
value: ethers.utils.parseUnits(amount, "ether"),
gasLimit: limit,
nonce: signer.getTransactionCount(),
maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"),
nonce: await signer.getTransactionCount(),
maxPriorityFeePerGas: priorityFee,
maxFeePerGas: maxFee,
})
} catch (e) {
throw new Error(e)
}
}
/**
* Send ERC20 tokens (USDC or USDT)
* @param {object} params - Transaction parameters
* @param {string} params.token - Token symbol ('usdc' or 'usdt')
* @param {string} params.privateKey - Sender's private key
* @param {string} params.amount - Amount to send
* @param {string} params.receiver - Recipient's Ethereum address
* @param {string} params.contractAddress - Optional custom contract address
* @returns {Promise} Transaction promise
*/
const sendToken = ethOperator.sendToken = async ({ token, privateKey, amount, receiver, contractAddress }) => {
// Create a wallet using the private key
const wallet = new ethers.Wallet(privateKey, getProvider());
// Contract interface
const tokenContract = new ethers.Contract(CONTRACT_ADDRESSES[token] || contractAddress, ERC20ABI, wallet);
// Convert the amount to the smallest unit of USDC (wei)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6); // Assuming 6 decimals for USDC
// Call the transfer function on the USDC contract
// Convert amount to smallest unit (both USDC and USDT use 6 decimals)
const amountWei = ethers.utils.parseUnits(amount.toString(), 6);
return tokenContract.transfer(receiver, amountWei)
}
const ETHERSCAN_API_KEY = 'M3YBAHI21FVE7VS2FEKU6ZFGRA128WUVQK';
/**
* Get transaction history for an Ethereum address
* @param {string} address - Ethereum address
* @param {object} options - Optional parameters
* @returns {Promise<Array>} Array of transactions
*/
const getTransactionHistory = ethOperator.getTransactionHistory = async (address, options = {}) => {
try {
if (!address || !isValidAddress(address)) {
throw new Error('Invalid Ethereum address');
}
const {
startBlock = 0,
endBlock = 99999999,
page = 1,
offset = 100,
sort = 'desc'
} = options;
// Fetch normal transactions using V2 API
const normalTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=txlist&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
const normalTxResponse = await fetch(normalTxUrl);
const normalTxData = await normalTxResponse.json();
if (normalTxData.status !== '1') {
if (normalTxData.message === 'No transactions found') {
return [];
}
// Provide more detailed error messages
if (normalTxData.result && normalTxData.result.includes('Invalid API Key')) {
throw new Error('Invalid Etherscan API Key. Please check your API key.');
}
if (normalTxData.result && normalTxData.result.includes('Max rate limit reached')) {
throw new Error('Etherscan API rate limit reached. Please try again later.');
}
throw new Error(`Etherscan API Error: ${normalTxData.message || normalTxData.result || 'Failed to fetch transactions'}`);
}
// Fetch ERC20 token transfers using V2 API
const tokenTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=tokentx&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
const tokenTxResponse = await fetch(tokenTxUrl);
const tokenTxData = await tokenTxResponse.json();
const tokenTransfers = tokenTxData.status === '1' ? tokenTxData.result : [];
// Combine and sort transactions
const allTransactions = [...normalTxData.result, ...tokenTransfers];
// Sort by timestamp (descending)
allTransactions.sort((a, b) => parseInt(b.timeStamp) - parseInt(a.timeStamp));
// Parse and format transactions
return allTransactions.map(tx => {
const isTokenTransfer = tx.tokenSymbol !== undefined;
const isReceived = tx.to.toLowerCase() === address.toLowerCase();
let value, symbol, decimals;
if (isTokenTransfer) {
decimals = parseInt(tx.tokenDecimal) || 18;
value = parseFloat(ethers.utils.formatUnits(tx.value, decimals));
symbol = tx.tokenSymbol || 'TOKEN';
} else {
value = parseFloat(ethers.utils.formatEther(tx.value));
symbol = 'ETH';
}
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: value,
symbol: symbol,
timestamp: parseInt(tx.timeStamp),
blockNumber: parseInt(tx.blockNumber),
isReceived: isReceived,
isSent: !isReceived,
gasUsed: tx.gasUsed ? parseInt(tx.gasUsed) : 0,
gasPrice: tx.gasPrice ? parseFloat(ethers.utils.formatUnits(tx.gasPrice, 'gwei')) : 0,
isError: tx.isError === '1' || tx.txreceipt_status === '0',
contractAddress: tx.contractAddress || null,
tokenName: tx.tokenName || null,
confirmations: tx.confirmations ? parseInt(tx.confirmations) : 0,
nonce: tx.nonce ? parseInt(tx.nonce) : 0,
input: tx.input || '0x',
isTokenTransfer: isTokenTransfer
};
});
} catch (error) {
console.error('Error fetching transaction history:', error);
throw error;
}
};
/**
* Get detailed information about a specific transaction
* @param {string} txHash - Transaction hash
* @returns {Promise<Object>} Transaction details
*/
const getTransactionDetails = ethOperator.getTransactionDetails = async (txHash) => {
try {
if (!txHash || !/^0x([A-Fa-f0-9]{64})$/.test(txHash)) {
throw new Error('Invalid transaction hash');
}
// Use read-only provider for fetching transaction details
const provider = getProvider(true);
// Get transaction details
const tx = await provider.getTransaction(txHash);
if (!tx) {
throw new Error('Transaction not found');
}
// Get transaction receipt for status and gas used
const receipt = await provider.getTransactionReceipt(txHash);
// Get current block number for confirmations
const currentBlock = await provider.getBlockNumber();
// Get block details for timestamp
const block = await provider.getBlock(tx.blockNumber);
// Calculate gas fee
const gasUsed = receipt ? receipt.gasUsed : null;
const effectiveGasPrice = receipt ? receipt.effectiveGasPrice : tx.gasPrice;
const gasFee = gasUsed && effectiveGasPrice ?
parseFloat(ethers.utils.formatEther(gasUsed.mul(effectiveGasPrice))) : null;
// Check if it's a token transfer by examining logs
let tokenTransfer = null;
if (receipt && receipt.logs.length > 0) {
// Try to decode ERC20 Transfer event
const transferEventSignature = ethers.utils.id('Transfer(address,address,uint256)');
const transferLog = receipt.logs.find(log => log.topics[0] === transferEventSignature);
if (transferLog) {
try {
const tokenContract = new ethers.Contract(transferLog.address, ERC20ABI, provider);
const [symbol, decimals] = await Promise.all([
tokenContract.symbol().catch(() => 'TOKEN'),
tokenContract.decimals().catch(() => 18)
]);
const from = ethers.utils.getAddress('0x' + transferLog.topics[1].slice(26));
const to = ethers.utils.getAddress('0x' + transferLog.topics[2].slice(26));
const value = parseFloat(ethers.utils.formatUnits(transferLog.data, decimals));
tokenTransfer = {
from,
to,
value,
symbol,
contractAddress: transferLog.address
};
} catch (e) {
console.warn('Could not decode token transfer:', e);
}
}
}
return {
hash: tx.hash,
from: tx.from,
to: tx.to,
value: parseFloat(ethers.utils.formatEther(tx.value)),
symbol: 'ETH',
blockNumber: tx.blockNumber,
timestamp: block ? block.timestamp : null,
confirmations: currentBlock - tx.blockNumber,
gasLimit: tx.gasLimit.toString(),
gasUsed: gasUsed ? gasUsed.toString() : null,
gasPrice: parseFloat(ethers.utils.formatUnits(tx.gasPrice, 'gwei')),
gasFee: gasFee,
nonce: tx.nonce,
input: tx.data,
status: receipt ? (receipt.status === 1 ? 'success' : 'failed') : 'pending',
isError: receipt ? receipt.status !== 1 : false,
tokenTransfer: tokenTransfer,
logs: receipt ? receipt.logs : [],
type: tx.type
};
} catch (error) {
console.error('Error fetching transaction details:', error);
throw error;
}
};
/**
* Check if a string is a valid transaction hash
* @param {string} hash - Potential transaction hash
* @returns {boolean}
*/
const isValidTxHash = ethOperator.isValidTxHash = (hash) => {
return /^0x([A-Fa-f0-9]{64})$/.test(hash);
};
})('object' === typeof module ? module.exports : window.ethOperator = {});