Merge pull request #4 from void-57/main
feat: Add comprehensive wallet features
This commit is contained in:
commit
1a956912ab
110
css/main.css
110
css/main.css
@ -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;
|
||||
}
|
||||
|
||||
130
css/main.scss
130
css/main.scss
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
691
index.html
691
index.html
@ -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
|
||||
|
||||
@ -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 = {});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user