diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 13ad65f..90956b9 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -1784,61 +1784,86 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
f"Invalid swap type for: {coin_from.name} -> {coin_to.name}"
)
- def notify(self, event_type, event_data, cursor=None) -> None:
- show_event = event_type not in self._disabled_notification_types
- if event_type == NT.OFFER_RECEIVED:
- offer_id: bytes = bytes.fromhex(event_data["offer_id"])
- self.log.debug(f"Received new offer {self.log.id(offer_id)}")
- if self.ws_server and show_event:
- event_data["event"] = "new_offer"
- self.ws_server.send_message_to_all(json.dumps(event_data))
- elif event_type == NT.BID_RECEIVED:
- offer_id: bytes = bytes.fromhex(event_data["offer_id"])
- offer_type: str = event_data["type"]
- bid_id: bytes = bytes.fromhex(event_data["bid_id"])
- self.log.info(
- f"Received valid bid {self.log.id(bid_id)} for {offer_type} offer {self.log.id(offer_id)}"
- )
- if self.ws_server and show_event:
- event_data["event"] = "new_bid"
- self.ws_server.send_message_to_all(json.dumps(event_data))
- elif event_type == NT.BID_ACCEPTED:
- bid_id: bytes = bytes.fromhex(event_data["bid_id"])
- self.log.info(f"Received valid bid accept for {self.log.id(bid_id)}")
- if self.ws_server and show_event:
- event_data["event"] = "bid_accepted"
- self.ws_server.send_message_to_all(json.dumps(event_data))
- else:
- self.log.warning(f"Unknown notification {event_type}")
-
+ def _process_notification_safe(self, event_type, event_data) -> None:
try:
+ show_event = event_type not in self._disabled_notification_types
+ if event_type == NT.OFFER_RECEIVED:
+ offer_id: bytes = bytes.fromhex(event_data["offer_id"])
+ self.log.debug(f"Received new offer {self.log.id(offer_id)}")
+ if self.ws_server and show_event:
+ event_data["event"] = "new_offer"
+ self.ws_server.send_message_to_all(json.dumps(event_data))
+ elif event_type == NT.BID_RECEIVED:
+ offer_id: bytes = bytes.fromhex(event_data["offer_id"])
+ offer_type: str = event_data["type"]
+ bid_id: bytes = bytes.fromhex(event_data["bid_id"])
+ self.log.info(
+ f"Received valid bid {self.log.id(bid_id)} for {offer_type} offer {self.log.id(offer_id)}"
+ )
+ if self.ws_server and show_event:
+ event_data["event"] = "new_bid"
+ self.ws_server.send_message_to_all(json.dumps(event_data))
+ elif event_type == NT.BID_ACCEPTED:
+ bid_id: bytes = bytes.fromhex(event_data["bid_id"])
+ self.log.info(f"Received valid bid accept for {self.log.id(bid_id)}")
+ if self.ws_server and show_event:
+ event_data["event"] = "bid_accepted"
+ self.ws_server.send_message_to_all(json.dumps(event_data))
+ elif event_type == NT.SWAP_COMPLETED:
+ bid_id: bytes = bytes.fromhex(event_data["bid_id"])
+ self.log.info(f"Swap completed for bid {self.log.id(bid_id)}")
+ event_data["event"] = "swap_completed"
+
+ if self.ws_server and show_event:
+ self.ws_server.send_message_to_all(json.dumps(event_data))
+ else:
+ self.log.warning(f"Unknown notification {event_type}")
+
now: int = self.getTime()
- use_cursor = self.openDB(cursor)
- self.add(
- Notification(
- active_ind=1,
- created_at=now,
- event_type=int(event_type),
- event_data=bytes(json.dumps(event_data), "UTF-8"),
- ),
- use_cursor,
- )
+ use_cursor = self.openDB(None)
+ try:
+ self.add(
+ Notification(
+ active_ind=1,
+ created_at=now,
+ event_type=int(event_type),
+ event_data=bytes(json.dumps(event_data), "UTF-8"),
+ ),
+ use_cursor,
+ )
- use_cursor.execute(
- "DELETE FROM notifications WHERE record_id NOT IN (SELECT record_id FROM notifications WHERE active_ind=1 ORDER BY created_at ASC LIMIT ?)",
- (self._keep_notifications,),
- )
+ use_cursor.execute(
+ "DELETE FROM notifications WHERE record_id NOT IN (SELECT record_id FROM notifications WHERE active_ind=1 ORDER BY created_at ASC LIMIT ?)",
+ (self._keep_notifications,),
+ )
- if show_event:
- self._notifications_cache[now] = (event_type, event_data)
- while len(self._notifications_cache) > self._show_notifications:
- # dicts preserve insertion order in Python 3.7+
- self._notifications_cache.pop(next(iter(self._notifications_cache)))
+ if show_event:
+ self._notifications_cache[now] = (event_type, event_data)
+ while len(self._notifications_cache) > self._show_notifications:
+ # dicts preserve insertion order in Python 3.7+
+ self._notifications_cache.pop(next(iter(self._notifications_cache)))
- finally:
- if cursor is None:
+ finally:
self.closeDB(use_cursor)
+ except Exception as ex:
+ self.log.error(
+ f"Notification processing failed for event_type {event_type}: {ex}"
+ )
+
+ def notify(self, event_type, event_data, cursor=None) -> None:
+ """Submit notification for processing in isolated thread."""
+ try:
+ self.thread_pool.submit(
+ self._process_notification_safe, event_type, event_data
+ )
+ except Exception as ex:
+ self.log.error(f"Failed to submit notification to thread pool: {ex}")
+ try:
+ self._process_notification_safe(event_type, event_data)
+ except Exception as ex2:
+ self.log.error(f"Notification fallback also failed: {ex2}")
+
def buildNotificationsCache(self, cursor):
self._notifications_cache.clear()
q = cursor.execute(
@@ -2093,6 +2118,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"Rate mismatch.",
)
+ def validateMessageNets(self, message_nets: str) -> None:
+ try:
+ self.expandMessageNets(message_nets)
+ except Exception as e:
+ raise ValueError(f"Invalid message networks: {e}")
+
def ensureWalletCanSend(
self, ci, swap_type, ensure_balance: int, estimated_fee: int, for_offer=True
) -> None:
@@ -6112,6 +6143,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.setState(BidStates.SWAP_COMPLETED)
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB()
+ self.notify(NT.SWAP_COMPLETED, {"bid_id": bid_id.hex()})
elif state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns:
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
@@ -6381,6 +6413,18 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.setState(BidStates.SWAP_COMPLETED)
self.saveBid(bid_id, bid)
+ try:
+ self.notify(
+ NT.SWAP_COMPLETED,
+ {
+ "bid_id": bid_id.hex(),
+ },
+ )
+ except Exception as ex:
+ self.log.warning(
+ f"Failed to send swap completion notification: {ex}"
+ )
+
return True # Mark bid for archiving
if save_bid:
@@ -6673,6 +6717,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if not was_received:
bid.setState(BidStates.SWAP_COMPLETED)
+ try:
+ self.notify(
+ NT.SWAP_COMPLETED,
+ {
+ "bid_id": bid_id.hex(),
+ },
+ )
+ except Exception as ex:
+ self.log.warning(
+ f"Failed to send swap completion notification: {ex}"
+ )
else:
# Could already be processed if spend was detected in the mempool
self.log.warning(
diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py
index 20b2f90..43bddef 100644
--- a/basicswap/basicswap_util.py
+++ b/basicswap/basicswap_util.py
@@ -243,6 +243,7 @@ class NotificationTypes(IntEnum):
OFFER_RECEIVED = auto()
BID_RECEIVED = auto()
BID_ACCEPTED = auto()
+ SWAP_COMPLETED = auto()
class ConnectionRequestTypes(IntEnum):
diff --git a/basicswap/static/js/modules/notification-manager.js b/basicswap/static/js/modules/notification-manager.js
index f22378d..97d6d0c 100644
--- a/basicswap/static/js/modules/notification-manager.js
+++ b/basicswap/static/js/modules/notification-manager.js
@@ -7,6 +7,7 @@ const NotificationManager = (function() {
showBidAccepted: true,
showBalanceChanges: true,
showOutgoingTransactions: true,
+ showSwapCompleted: true,
notificationDuration: 20000
};
@@ -34,8 +35,166 @@ const NotificationManager = (function() {
}
let config = loadConfig();
+ let notificationHistory = [];
+ const MAX_HISTORY_ITEMS = 10;
- function ensureToastContainer() {
+ function loadNotificationHistory() {
+ try {
+ const saved = localStorage.getItem('notification_history');
+ if (saved) {
+ notificationHistory = JSON.parse(saved);
+ }
+ } catch (e) {
+ console.error('Error loading notification history:', e);
+ notificationHistory = [];
+ }
+ }
+
+ function saveNotificationHistory() {
+ try {
+ localStorage.setItem('notification_history', JSON.stringify(notificationHistory));
+ } catch (e) {
+ console.error('Error saving notification history:', e);
+ }
+ }
+
+ function addToHistory(title, type, options) {
+ const historyItem = {
+ id: Date.now(),
+ title: title,
+ type: type,
+ subtitle: options.subtitle || '',
+ coinSymbol: options.coinSymbol || '',
+ coinFrom: options.coinFrom || null,
+ coinTo: options.coinTo || null,
+ timestamp: new Date().toLocaleString(),
+ timestampMs: Date.now()
+ };
+
+ notificationHistory.unshift(historyItem);
+
+ if (notificationHistory.length > MAX_HISTORY_ITEMS) {
+ notificationHistory = notificationHistory.slice(0, MAX_HISTORY_ITEMS);
+ }
+
+ saveNotificationHistory();
+ updateHistoryDropdown();
+ }
+
+ function updateHistoryDropdown() {
+ const dropdown = document.getElementById('notification-history-dropdown');
+ const mobileDropdown = document.getElementById('notification-history-dropdown-mobile');
+
+ const emptyMessage = '
No notifications yet
';
+ const emptyMessageMobile = 'No notifications yet
';
+
+ if (notificationHistory.length === 0) {
+ if (dropdown) dropdown.innerHTML = emptyMessage;
+ if (mobileDropdown) mobileDropdown.innerHTML = emptyMessageMobile;
+ return;
+ }
+
+ const clearAllButton = `
+
+
+
+ `;
+
+ let historyHTML = '';
+ let mobileHistoryHTML = '';
+
+ notificationHistory.forEach(item => {
+ let coinIconHtml = '';
+ if (item.coinSymbol) {
+ const coinIcon = getCoinIcon(item.coinSymbol);
+ coinIconHtml = `
`;
+ }
+
+ const typeIcon = getToastIcon(item.type);
+ const typeColor = getToastColor(item.type, item);
+ const typeIconHtml = `${typeIcon}
`;
+
+ let enhancedTitle = item.title;
+ if ((item.type === 'new_offer' || item.type === 'new_bid') && item.coinFrom && item.coinTo) {
+ const coinFromIcon = getCoinIcon(getCoinDisplayName(item.coinFrom));
+ const coinToIcon = getCoinIcon(getCoinDisplayName(item.coinTo));
+ const coinFromName = getCoinDisplayName(item.coinFrom);
+ const coinToName = getCoinDisplayName(item.coinTo);
+
+ enhancedTitle = item.title
+ .replace(new RegExp(`(\\d+\\.\\d+)\\s+${coinFromName}`, 'g'), `
$1 ${coinFromName}`)
+ .replace(new RegExp(`(\\d+\\.\\d+)\\s+${coinToName}`, 'g'), `
$1 ${coinToName}`);
+ }
+
+ const clickAction = getNotificationClickAction(item);
+ const itemHTML = `
+
+
+ ${typeIconHtml}
+ ${coinIconHtml}
+
+
${enhancedTitle}
+ ${item.subtitle ? `
${item.subtitle}
` : ''}
+
${item.timestamp}
+
+
+
+ `;
+
+ historyHTML += itemHTML;
+
+ const mobileItemHTML = `
+
+
+ ${typeIconHtml}
+ ${coinIconHtml}
+
+
${enhancedTitle}
+ ${item.subtitle ? `
${item.subtitle}
` : ''}
+
${item.timestamp}
+
+
+
+ `;
+
+ mobileHistoryHTML += mobileItemHTML;
+ });
+
+ historyHTML += clearAllButton;
+ mobileHistoryHTML += clearAllButton;
+
+ if (dropdown) dropdown.innerHTML = historyHTML;
+ if (mobileDropdown) mobileDropdown.innerHTML = mobileHistoryHTML;
+ }
+
+ function getNotificationClickAction(item) {
+ if (item.type === 'balance_change' && item.coinSymbol) {
+ return `window.location.href='/wallet/${item.coinSymbol.toLowerCase()}'`;
+ }
+
+ if (item.type === 'new_offer') {
+ return `window.location.href='/offers'`;
+ }
+
+ if (item.type === 'new_bid' || item.type === 'bid_accepted') {
+ return `window.location.href='/bids'`;
+ }
+
+ if (item.title.includes('offer') || item.title.includes('Offer')) {
+ return `window.location.href='/offers'`;
+ }
+
+ if (item.title.includes('bid') || item.title.includes('Bid') || item.title.includes('swap') || item.title.includes('Swap')) {
+ return `window.location.href='/bids'`;
+ }
+
+ return null;
+}
+
+function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
const floating_div = document.createElement('div');
@@ -66,6 +225,9 @@ const NotificationManager = (function() {
'bid_accepted': ``,
+ 'swap_completed': ``,
'balance_change': ``,
@@ -81,6 +243,7 @@ const NotificationManager = (function() {
'new_offer': 'bg-blue-500',
'new_bid': 'bg-green-500',
'bid_accepted': 'bg-purple-500',
+ 'swap_completed': 'bg-green-600',
'balance_change': 'bg-yellow-500',
'success': 'bg-blue-500'
};
@@ -96,20 +259,67 @@ const NotificationManager = (function() {
return colors[type] || colors['success'];
}
+// Todo: Remove later and use global.
+ function getCoinDisplayName(coinId) {
+ const coinMap = {
+ 1: 'PART',
+ 2: 'BTC',
+ 3: 'LTC',
+ 4: 'DCR',
+ 5: 'NMC',
+ 6: 'XMR',
+ 7: 'PART (Blind)',
+ 8: 'PART (Anon)',
+ 9: 'WOW',
+ 11: 'PIVX',
+ 12: 'DASH',
+ 13: 'FIRO',
+ 14: 'NAV',
+ 15: 'LTC (MWEB)',
+ 17: 'BCH',
+ 18: 'DOGE'
+ };
+ return coinMap[coinId] || `Coin ${coinId}`;
+ }
+
+// Todo: Remove later.
+ function formatCoinAmount(amount, coinId) {
+ const divisors = {
+ 1: 100000000, // PART - 8 decimals
+ 2: 100000000, // BTC - 8 decimals
+ 3: 100000000, // LTC - 8 decimals
+ 4: 100000000, // DCR - 8 decimals
+ 5: 100000000, // NMC - 8 decimals
+ 6: 1000000000000, // XMR - 12 decimals
+ 7: 100000000, // PART (Blind) - 8 decimals
+ 8: 100000000, // PART (Anon) - 8 decimals
+ 9: 100000000000, // WOW - 11 decimals
+ 11: 100000000, // PIVX - 8 decimals
+ 12: 100000000, // DASH - 8 decimals
+ 13: 100000000, // FIRO - 8 decimals
+ 14: 100000000, // NAV - 8 decimals
+ 15: 100000000, // LTC (MWEB) - 8 decimals
+ 17: 100000000, // BCH - 8 decimals
+ 18: 100000000 // DOGE - 8 decimals
+ };
+
+ const divisor = divisors[coinId] || 100000000;
+ const displayAmount = amount / divisor;
+
+ return displayAmount.toFixed(8).replace(/\.?0+$/, '');
+ }
+
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
-
+ loadNotificationHistory();
+ updateHistoryDropdown();
this.initializeBalanceTracking();
if (window.CleanupManager) {
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
-
- if (this.balanceTimeouts) {
- Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
- }
- console.log('NotificationManager disposed');
+ mgr.dispose();
});
}
@@ -121,6 +331,16 @@ const NotificationManager = (function() {
return this;
},
+ getConfig: function() {
+ return { ...config };
+ },
+
+ clearAllNotifications: function() {
+ notificationHistory = [];
+ localStorage.removeItem('notification_history');
+ updateHistoryDropdown();
+ },
+
getSettings: function() {
return { ...config };
},
@@ -170,24 +390,45 @@ const NotificationManager = (function() {
setTimeout(() => {
this.createToast(
- 'New network offer',
+ '
1.00000000 BTC →
15.50000000 XMR',
'new_offer',
- { offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view offer' }
+ {
+ offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
+ subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR',
+ coinFrom: 2,
+ coinTo: 6
+ }
);
}, 3000);
setTimeout(() => {
this.createToast(
- 'New bid received',
+ '
0.50000000 BTC →
7.75000000 XMR',
'new_bid',
- { bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view bid' }
+ {
+ bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
+ subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR',
+ coinFrom: 2,
+ coinTo: 6
+ }
);
}, 3500);
+
+ setTimeout(() => {
+ this.createToast(
+ 'Swap completed successfully',
+ 'swap_completed',
+ {
+ bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
+ subtitle: 'Click to view details'
+ }
+ );
+ }, 4000);
+
},
-
-
initializeBalanceTracking: function() {
+ this.checkAndResetStaleBalanceTracking();
fetch('/json/walletbalances')
.then(response => response.json())
@@ -201,14 +442,17 @@ const NotificationManager = (function() {
const storageKey = `prev_balance_${coinKey}`;
const pendingStorageKey = `prev_pending_${coinKey}`;
-
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, balance.toString());
+ localStorage.setItem(`${storageKey}_timestamp`, Date.now().toString());
}
if (!localStorage.getItem(pendingStorageKey)) {
localStorage.setItem(pendingStorageKey, pending.toString());
+ localStorage.setItem(`${pendingStorageKey}_timestamp`, Date.now().toString());
}
});
+
+ localStorage.setItem('last_balance_fetch', Date.now().toString());
}
})
.catch(error => {
@@ -216,14 +460,50 @@ const NotificationManager = (function() {
});
},
+ checkAndResetStaleBalanceTracking: function() {
+ const lastFetch = localStorage.getItem('last_balance_fetch');
+ const now = Date.now();
+ const staleThreshold = 10 * 60 * 1000;
+
+ if (!lastFetch || (now - parseInt(lastFetch)) > staleThreshold) {
+ console.log('Resetting stale balance tracking to prevent false notifications');
+ this.resetBalanceTracking();
+ }
+ },
+
+ resetBalanceTracking: function() {
+ const keys = Object.keys(localStorage);
+ keys.forEach(key => {
+ if (key.startsWith('prev_balance_') || key.startsWith('prev_pending_') || key.startsWith('last_notification_') || key.startsWith('balance_change_')) {
+ localStorage.removeItem(key);
+ }
+ });
+ },
+
+ getNotificationHistory: function() {
+ return notificationHistory;
+ },
+
+ clearNotificationHistory: function() {
+ notificationHistory = [];
+ localStorage.removeItem('notification_history');
+ updateHistoryDropdown();
+ },
+
+ updateHistoryDropdown: function() {
+ updateHistoryDropdown();
+ },
+
createToast: function(title, type = 'success', options = {}) {
+ const plainTitle = title.replace(/<[^>]*>/g, '');
+ addToHistory(plainTitle, type, options);
+
const messages = ensureToastContainer();
const message = document.createElement('li');
- const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const toastId = `toast-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
const iconColor = getToastColor(type, options);
const icon = getToastIcon(type);
-
let coinIconHtml = '';
if (options.coinSymbol) {
const coinIcon = getCoinIcon(options.coinSymbol);
@@ -278,7 +558,6 @@ const NotificationManager = (function() {
`;
messages.appendChild(message);
-
setTimeout(() => {
if (message.parentNode) {
message.classList.add('toast-slide-out');
@@ -286,6 +565,7 @@ const NotificationManager = (function() {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
+
}, 300);
}
}, config.notificationDuration);
@@ -298,16 +578,42 @@ const NotificationManager = (function() {
switch (data.event) {
case 'new_offer':
- toastTitle = `New network offer`;
+ if (data.coin_from && data.coin_to && data.amount_from && data.amount_to) {
+ const coinFromName = getCoinDisplayName(data.coin_from);
+ const coinToName = getCoinDisplayName(data.coin_to);
+ const amountFrom = formatCoinAmount(data.amount_from, data.coin_from);
+ const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
+ const coinFromIcon = getCoinIcon(coinFromName);
+ const coinToIcon = getCoinIcon(coinToName);
+ toastTitle = `
${amountFrom} ${coinFromName} →
${amountTo} ${coinToName}`;
+ toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
+ toastOptions.coinFrom = data.coin_from;
+ toastOptions.coinTo = data.coin_to;
+ } else {
+ toastTitle = `New network offer`;
+ toastOptions.subtitle = 'Click to view offer';
+ }
toastType = 'new_offer';
toastOptions.offerId = data.offer_id;
- toastOptions.subtitle = 'Click to view offer';
shouldShowToast = config.showNewOffers;
break;
case 'new_bid':
- toastTitle = `New bid received`;
+ if (data.coin_from && data.coin_to && data.bid_amount && data.bid_amount_to) {
+ const coinFromName = getCoinDisplayName(data.coin_from);
+ const coinToName = getCoinDisplayName(data.coin_to);
+ const bidAmountFrom = formatCoinAmount(data.bid_amount, data.coin_from);
+ const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
+ const coinFromIcon = getCoinIcon(coinFromName);
+ const coinToIcon = getCoinIcon(coinToName);
+ toastTitle = `
${bidAmountFrom} ${coinFromName} →
${bidAmountTo} ${coinToName}`;
+ toastOptions.subtitle = `New bid • Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
+ toastOptions.coinFrom = data.coin_from;
+ toastOptions.coinTo = data.coin_to;
+ } else {
+ toastTitle = `New bid received`;
+ toastOptions.subtitle = 'Click to view bid';
+ }
toastOptions.bidId = data.bid_id;
- toastOptions.subtitle = 'Click to view bid';
toastType = 'new_bid';
shouldShowToast = config.showNewBids;
break;
@@ -318,6 +624,15 @@ const NotificationManager = (function() {
toastType = 'bid_accepted';
shouldShowToast = config.showBidAccepted;
break;
+
+ case 'swap_completed':
+ toastTitle = `Swap completed successfully`;
+ toastOptions.bidId = data.bid_id;
+ toastOptions.subtitle = 'Click to view details';
+ toastType = 'swap_completed';
+ shouldShowToast = config.showSwapCompleted;
+ break;
+
case 'coin_balance_updated':
if (data.coin && config.showBalanceChanges) {
this.handleBalanceUpdate(data);
@@ -350,7 +665,6 @@ const NotificationManager = (function() {
},
fetchAndShowBalanceChange: function(coinSymbol) {
-
fetch('/json/walletbalances')
.then(response => response.json())
.then(balanceData => {
@@ -371,6 +685,8 @@ const NotificationManager = (function() {
coinsToCheck.forEach(coinData => {
this.checkSingleCoinBalance(coinData, coinSymbol);
});
+
+ localStorage.setItem('last_balance_fetch', Date.now().toString());
}
})
.catch(error => {
@@ -385,15 +701,48 @@ const NotificationManager = (function() {
const coinKey = coinData.name.replace(/\s+/g, '_');
const storageKey = `prev_balance_${coinKey}`;
const pendingStorageKey = `prev_pending_${coinKey}`;
+ const lastNotificationKey = `last_notification_${coinKey}`;
+
const prevBalance = parseFloat(localStorage.getItem(storageKey)) || 0;
const prevPending = parseFloat(localStorage.getItem(pendingStorageKey)) || 0;
-
+ const lastNotificationTime = parseInt(localStorage.getItem(lastNotificationKey)) || 0;
const balanceIncrease = currentBalance - prevBalance;
const pendingIncrease = currentPending - prevPending;
const pendingDecrease = prevPending - currentPending;
+
+ const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
+ const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
+
+ if (totalChange > maxReasonableChange && totalChange > 1.0) {
+ console.log(`Detected potentially stale balance data for ${coinData.name}, resetting tracking`);
+ localStorage.setItem(storageKey, currentBalance.toString());
+ localStorage.setItem(pendingStorageKey, currentPending.toString());
+ return;
+ }
+
+ const now = Date.now();
+ const minTimeBetweenNotifications = 30000;
+ const balanceChangeKey = `balance_change_${coinKey}`;
+ const lastBalanceChange = localStorage.getItem(balanceChangeKey);
+
+ const currentChangeSignature = `${currentBalance}_${currentPending}`;
+
+ if (lastBalanceChange === currentChangeSignature) {
+ localStorage.setItem(storageKey, currentBalance.toString());
+ localStorage.setItem(pendingStorageKey, currentPending.toString());
+ return;
+ }
+
+ if (now - lastNotificationTime < minTimeBetweenNotifications) {
+ localStorage.setItem(storageKey, currentBalance.toString());
+ localStorage.setItem(pendingStorageKey, currentPending.toString());
+ localStorage.setItem(balanceChangeKey, currentChangeSignature);
+ return;
+ }
+
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
@@ -408,7 +757,9 @@ const NotificationManager = (function() {
variantInfo = ` (${coinData.name.replace('Litecoin ', '')})`;
}
- if (balanceIncrease > 0.00000001) {
+ let notificationShown = false;
+
+ if (balanceIncrease > 0.00000001 && config.showBalanceChanges) {
const displayAmount = balanceIncrease.toFixed(8).replace(/\.?0+$/, '');
const subtitle = isPendingToConfirmed ? 'Funds confirmed' : 'Incoming funds confirmed';
this.createToast(
@@ -419,6 +770,7 @@ const NotificationManager = (function() {
subtitle: subtitle
}
);
+ notificationShown = true;
}
if (balanceIncrease < -0.00000001 && config.showOutgoingTransactions) {
@@ -431,6 +783,7 @@ const NotificationManager = (function() {
subtitle: 'Funds sent'
}
);
+ notificationShown = true;
}
if (pendingIncrease > 0.00000001) {
@@ -443,6 +796,7 @@ const NotificationManager = (function() {
subtitle: 'Incoming funds pending'
}
);
+ notificationShown = true;
}
if (pendingIncrease < -0.00000001 && config.showOutgoingTransactions && !isPendingToConfirmed) {
@@ -455,9 +809,9 @@ const NotificationManager = (function() {
subtitle: 'Funds sending'
}
);
+ notificationShown = true;
}
-
if (pendingDecrease > 0.00000001 && !isPendingToConfirmed) {
const displayAmount = pendingDecrease.toFixed(8).replace(/\.?0+$/, '');
this.createToast(
@@ -468,11 +822,16 @@ const NotificationManager = (function() {
subtitle: 'Pending funds confirmed'
}
);
+ notificationShown = true;
}
-
localStorage.setItem(storageKey, currentBalance.toString());
localStorage.setItem(pendingStorageKey, currentPending.toString());
+ localStorage.setItem(balanceChangeKey, currentChangeSignature);
+
+ if (notificationShown) {
+ localStorage.setItem(lastNotificationKey, now.toString());
+ }
},
@@ -480,6 +839,20 @@ const NotificationManager = (function() {
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;
+ },
+
+ manualResetBalanceTracking: function() {
+ this.resetBalanceTracking();
+ this.initializeBalanceTracking();
+ },
+
+ dispose: function() {
+ if (this.balanceTimeouts) {
+ Object.values(this.balanceTimeouts).forEach(timeout => {
+ clearTimeout(timeout);
+ });
+ this.balanceTimeouts = {};
+ }
}
};
@@ -488,7 +861,9 @@ const NotificationManager = (function() {
while (element.nodeName !== "BUTTON") {
element = element.parentNode;
}
- element.parentNode.parentNode.removeChild(element.parentNode);
+ const toastElement = element.parentNode;
+
+ toastElement.parentNode.removeChild(toastElement);
};
return publicAPI;
@@ -496,13 +871,21 @@ const NotificationManager = (function() {
window.NotificationManager = NotificationManager;
-document.addEventListener('DOMContentLoaded', function() {
+window.resetBalanceTracking = function() {
+ if (window.NotificationManager && window.NotificationManager.manualResetBalanceTracking) {
+ window.NotificationManager.manualResetBalanceTracking();
+ }
+};
+window.testNotification = function() {
+ if (window.NotificationManager) {
+ window.NotificationManager.createToast('Test Notification', 'success', { subtitle: 'This is a test notification' });
+ }
+};
+
+document.addEventListener('DOMContentLoaded', function() {
if (!window.notificationManagerInitialized) {
window.NotificationManager.initialize(window.notificationConfig || {});
window.notificationManagerInitialized = true;
}
});
-
-
-console.log('NotificationManager initialized');
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html
index a3cbf77..2124c54 100644
--- a/basicswap/templates/header.html
+++ b/basicswap/templates/header.html
@@ -45,6 +45,32 @@
};
})();
+ function toggleNotificationDropdown(event) {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ const dropdown = document.getElementById('notification-history-dropdown');
+ const mobileDropdown = document.getElementById('notification-history-dropdown-mobile');
+
+ if (window.innerWidth >= 768) {
+ if (dropdown) {
+ dropdown.classList.toggle('hidden');
+ }
+ } else {
+ if (mobileDropdown) {
+ mobileDropdown.classList.toggle('hidden');
+ }
+ }
+ }
+
+ function clearAllNotifications() {
+ if (window.NotificationManager && typeof window.NotificationManager.clearAllNotifications === 'function') {
+ window.NotificationManager.clearAllNotifications();
+ }
+ }
+
(function() {
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
@@ -159,11 +185,10 @@
-
+
{{ new_offer_svg | safe }}
- Place new Offer
+ Place New Offer
@@ -173,7 +198,7 @@
{{ love_svg | safe }}
- Donate
+ Donations
@@ -398,7 +423,18 @@
{% endif %}
-
+
+
+
+
+ No notifications yet
+
+
+
+