From d08e09061fbda601ba78312f89bd13a1bb5a9c6b Mon Sep 17 00:00:00 2001 From: Gerlof van Ek Date: Sun, 8 Jun 2025 17:43:01 +0200 Subject: [PATCH] AMM (#310) * AMM * LINT + Fixes * Remove unused global variables. * BLACK * BLACK * AMM - Various Fixes/Features/Bug Fixes. * FLAKE * FLAKE * BLACK * Small fix * Fix * Auto-start option AMM + Various fixes/bugs/styling. * Updated createoffers.py * BLACK * AMM Styling * Update bid_xmr template confirm model. * Fixed bug with Create Default Configuration + Added confirm modal. * Fix: Better redirect. * Fixed adjust_rates_based_on_market + Removed debug / extra logging + Various fixes. * GUI v3.2.2 * Fix sub-header your-offers count when created offers by AMM. * Fix math. * Added USD prices + Add offers/bids checkbox enabled always checked. * Donation page. * Updated header.html + Typo. * Update on createoffer.py + BLACK * AMM: html updates. * AMM: Add all, minrate, and static options. * AMM: Amount step default 0.001 * Fix global settings. * Update createoffers.py * Fixed bug with autostart when save global settings + Various layout fixes. * Fixed bug with autostart with add/edit + Added new option Orderbook (Auto-Accept) * Fixed debug + New feature attempt bids first. * Fix: Orderbook (Auto-Accept) * Added bidding strategy: Only bid on auto-accept offers (best rates from auto-accept only) * Fix: with_extra_info * Small fix automation_strat_id * Various fixes. * Final fixes --- basicswap/basicswap.py | 92 +- basicswap/bin/run.py | 14 + basicswap/http_server.py | 87 +- basicswap/js_server.py | 67 +- basicswap/static/js/amm_counter.js | 255 ++ basicswap/static/js/amm_tables.js | 2367 +++++++++++++++++ basicswap/static/js/bids_sentreceived.js | 55 +- .../static/js/modules/summary-manager.js | 108 +- basicswap/static/js/offers.js | 59 +- basicswap/templates/amm.html | 2019 ++++++++++++++ basicswap/templates/bid_xmr.html | 90 +- basicswap/templates/donation.html | 334 +++ basicswap/templates/footer.html | 3 +- basicswap/templates/header.html | 265 +- basicswap/templates/inc_messages.html | 6 +- basicswap/templates/style.html | 54 +- basicswap/templates/unlock.html | 10 +- basicswap/ui/page_amm.py | 1405 ++++++++++ scripts/createoffers.py | 1438 ++++++++-- 19 files changed, 8374 insertions(+), 354 deletions(-) create mode 100644 basicswap/static/js/amm_counter.js create mode 100644 basicswap/static/js/amm_tables.js create mode 100644 basicswap/templates/amm.html create mode 100644 basicswap/templates/donation.html create mode 100644 basicswap/ui/page_amm.py diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index a1fe8b0..e565da2 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -529,6 +529,20 @@ class BasicSwap(BaseApp): def finalise(self): self.log.info("Finalising") + try: + from basicswap.ui.page_amm import stop_amm_process, get_amm_status + + amm_status = get_amm_status() + if amm_status == "running": + self.log.info("Stopping AMM process...") + success, msg = stop_amm_process(self) + if success: + self.log.info(f"AMM shutdown: {msg}") + else: + self.log.warning(f"AMM shutdown warning: {msg}") + except Exception as e: + self.log.error(f"Error stopping AMM during shutdown: {e}") + self.delay_event.set() self.chainstate_delay_event.set() @@ -1151,6 +1165,57 @@ class BasicSwap(BaseApp): nm += 1 self.log.info(f"Scanned {nm} unread messages.") + autostart_setting = self.settings.get("amm_autostart", False) + self.log.info(f"Checking AMM autostart setting: {autostart_setting}") + + if autostart_setting: + self.log.info("AMM autostart is enabled, starting AMM process...") + try: + from basicswap.ui.page_amm import ( + start_amm_process, + start_amm_process_force, + check_existing_amm_processes, + ) + + self.log.info("Waiting 2 seconds for BasicSwap to fully initialize...") + time.sleep(2) + + amm_host = self.settings.get("htmlhost", "127.0.0.1") + amm_port = self.settings.get("htmlport", 12700) + amm_debug = False + + self.log.info( + f"Starting AMM with host={amm_host}, port={amm_port}, debug={amm_debug}" + ) + + existing_pids = check_existing_amm_processes() + if existing_pids: + self.log.warning( + f"Found existing AMM processes: {existing_pids}. Using force start to clean up..." + ) + success, msg = start_amm_process_force( + self, amm_host, amm_port, debug=amm_debug + ) + if success: + self.log.info(f"AMM autostart force successful: {msg}") + else: + self.log.warning(f"AMM autostart force failed: {msg}") + else: + success, msg = start_amm_process( + self, amm_host, amm_port, debug=amm_debug + ) + if success: + self.log.info(f"AMM autostart successful: {msg}") + else: + self.log.warning(f"AMM autostart failed: {msg}") + except Exception as e: + self.log.error(f"AMM autostart error: {str(e)}") + import traceback + + self.log.error(traceback.format_exc()) + else: + self.log.info("AMM autostart is disabled") + def stopDaemon(self, coin) -> None: if coin in (Coins.XMR, Coins.DCR, Coins.WOW): return @@ -2343,6 +2408,10 @@ class BasicSwap(BaseApp): finally: self.closeDB(cursor) self.log.info(f"Sent OFFER {self.log.id(offer_id)}") + + if self.ws_server: + self.ws_server.send_message_to_all('{"event": "offer_created"}') + return offer_id def revokeOffer(self, offer_id, security_token=None) -> None: @@ -11247,7 +11316,15 @@ class BasicSwap(BaseApp): return "bitcoin-cash" if coin_id == Coins.FIRO: return "zcoin" - return chainparams[coin_id]["name"] + + # Handle coin variants that use base coin chainparams + use_coinid = coin_id + if coin_id == Coins.PART_ANON or coin_id == Coins.PART_BLIND: + use_coinid = Coins.PART + elif coin_id == Coins.LTC_MWEB: + use_coinid = Coins.LTC + + return chainparams[use_coinid]["name"] def lookupFiatRates( self, @@ -11432,17 +11509,18 @@ class BasicSwap(BaseApp): js = self.lookupFiatRates([int(coin_from), int(coin_to)]) rate = float(js[int(coin_from)]) / float(js[int(coin_to)]) js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1) + + js[name_from] = {"usd": js[int(coin_from)]} + js.pop(int(coin_from)) + js[name_to] = {"usd": js[int(coin_to)]} + js.pop(int(coin_to)) + rv["coingecko"] = js except Exception as e: rv["coingecko_error"] = str(e) if self.debug: self.log.error(traceback.format_exc()) - js[name_from] = {"usd": js[int(coin_from)]} - js.pop(int(coin_from)) - js[name_to] = {"usd": js[int(coin_to)]} - js.pop(int(coin_to)) - if output_array: def format_float(f): @@ -11451,7 +11529,7 @@ class BasicSwap(BaseApp): rv_array = [] if "coingecko_error" in rv: rv_array.append(("coingecko.com", "error", rv["coingecko_error"])) - if "coingecko" in rv: + elif "coingecko" in rv: js = rv["coingecko"] rv_array.append( ( diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py index 2587601..2835ff0 100755 --- a/basicswap/bin/run.py +++ b/basicswap/bin/run.py @@ -48,6 +48,20 @@ def signal_handler(sig, frame): sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8") ) if swap_client is not None and not swap_client.chainstate_delay_event.is_set(): + try: + from basicswap.ui.page_amm import stop_amm_process, get_amm_status + + amm_status = get_amm_status() + if amm_status == "running": + logger.info("Signal handler stopping AMM process...") + success, msg = stop_amm_process(swap_client) + if success: + logger.info(f"AMM signal shutdown: {msg}") + else: + logger.warning(f"AMM signal shutdown warning: {msg}") + except Exception as e: + logger.error(f"Error stopping AMM in signal handler: {e}") + swap_client.stopRunning() diff --git a/basicswap/http_server.py b/basicswap/http_server.py index e4bda48..77bbe28 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -25,7 +25,6 @@ from . import __version__ from .util import ( dumpj, toBool, - LockedCoinError, format_timestamp, ) from .chainparams import ( @@ -54,6 +53,7 @@ from .ui.page_automation import ( page_automation_strategy_new, ) +from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api from .ui.page_bids import page_bids, page_bid from .ui.page_offers import page_offers, page_offer, page_newoffer from .ui.page_tor import page_tor, get_tor_established_state @@ -227,6 +227,18 @@ class HttpHandler(BaseHTTPRequestHandler): swap_client.log.error(f"Error getting Tor state: {str(e)}") swap_client.log.error(traceback.format_exc()) + from .ui.page_amm import get_amm_status, get_amm_active_count + + try: + args_dict["current_status"] = get_amm_status() + args_dict["amm_active_count"] = get_amm_active_count(swap_client) + except Exception as e: + args_dict["current_status"] = "stopped" + args_dict["amm_active_count"] = 0 + if swap_client.debug: + swap_client.log.error(f"Error getting AMM state: {str(e)}") + swap_client.log.error(traceback.format_exc()) + if swap_client._show_notifications: args_dict["notifications"] = swap_client.getNotifications() @@ -626,10 +638,37 @@ class HttpHandler(BaseHTTPRequestHandler): clear_cookie_header = self._clear_session_cookie() extra_headers.append(clear_cookie_header) + try: + from basicswap.ui.page_amm import stop_amm_process, get_amm_status + + amm_status = get_amm_status() + if amm_status == "running": + swap_client.log.info("Web shutdown stopping AMM process...") + success, msg = stop_amm_process(swap_client) + if success: + swap_client.log.info(f"AMM web shutdown: {msg}") + else: + swap_client.log.warning(f"AMM web shutdown warning: {msg}") + except Exception as e: + swap_client.log.error(f"Error stopping AMM in web shutdown: {e}") + swap_client.stopRunning() return self.page_info("Shutting down", extra_headers=extra_headers) + def page_donation(self, url_split, post_string): + swap_client = self.server.swap_client + swap_client.checkSystemStatus() + summary = swap_client.getSummary() + + template = env.get_template("donation.html") + return self.render_template( + template, + { + "summary": summary, + }, + ) + def page_index(self, url_split): swap_client = self.server.swap_client swap_client.checkSystemStatus() @@ -660,6 +699,9 @@ class HttpHandler(BaseHTTPRequestHandler): self.end_headers() def handle_http(self, status_code, path, post_string="", is_json=False): + from basicswap.util import LockedCoinError + from basicswap.ui.util import getCoinName + swap_client = self.server.swap_client parsed = parse.urlparse(self.path) url_split = parsed.path.split("/") @@ -727,7 +769,11 @@ class HttpHandler(BaseHTTPRequestHandler): func = js_url_to_function(url_split) return func(self, url_split, post_string, is_json) except Exception as ex: - if swap_client.debug is True: + if isinstance(ex, LockedCoinError): + clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked" + swap_client.log.warning(clean_msg) + return js_error(self, clean_msg) + elif swap_client.debug is True: swap_client.log.error(traceback.format_exc()) return js_error(self, str(ex)) @@ -821,6 +867,8 @@ class HttpHandler(BaseHTTPRequestHandler): return page_bids(self, url_split, post_string, available=True) if page == "watched": return self.page_watched(url_split, post_string) + if page == "donation": + return self.page_donation(url_split, post_string) if page == "smsgaddresses": return page_smsgaddresses(self, url_split, post_string) if page == "identity": @@ -833,6 +881,41 @@ class HttpHandler(BaseHTTPRequestHandler): return page_automation_strategy(self, url_split, post_string) if page == "newautomationstrategy": return page_automation_strategy_new(self, url_split, post_string) + if page == "amm": + if len(url_split) > 2 and url_split[2] == "status": + query_params = {} + if parsed.query: + query_params = { + k: v[0] for k, v in parse.parse_qs(parsed.query).items() + } + status_data = amm_status_api( + swap_client, self.path, query_params + ) + self.putHeaders(200, "application/json") + return json.dumps(status_data).encode("utf-8") + elif len(url_split) > 2 and url_split[2] == "autostart": + query_params = {} + if parsed.query: + query_params = { + k: v[0] for k, v in parse.parse_qs(parsed.query).items() + } + autostart_data = amm_autostart_api( + swap_client, post_string, query_params + ) + self.putHeaders(200, "application/json") + return json.dumps(autostart_data).encode("utf-8") + elif len(url_split) > 2 and url_split[2] == "debug": + query_params = {} + if parsed.query: + query_params = { + k: v[0] for k, v in parse.parse_qs(parsed.query).items() + } + debug_data = amm_debug_api( + swap_client, post_string, query_params + ) + self.putHeaders(200, "application/json") + return json.dumps(debug_data).encode("utf-8") + return page_amm(self, url_split, post_string) if page == "shutdown": return self.page_shutdown(url_split, post_string) if page == "changepassword": diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 5e8c2ce..a1288c3 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -42,6 +42,7 @@ from .ui.util import ( ) from .ui.page_offers import postNewOffer from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey +from .db import Concepts def getFormData(post_string: str, is_json: bool): @@ -183,7 +184,19 @@ def js_wallets(self, url_split, post_string, is_json): def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: swap_client = self.server.swap_client - swap_client.checkSystemStatus() + + try: + swap_client.checkSystemStatus() + except Exception as e: + from basicswap.util import LockedCoinError + from basicswap.ui.util import getCoinName + + if isinstance(e, LockedCoinError): + error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet." + return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8") + else: + return bytes(json.dumps({"error": str(e)}), "UTF-8") + offer_id = None if len(url_split) > 3: if url_split[3] == "new": @@ -206,6 +219,12 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: if offer_id: filters["offer_id"] = offer_id + parsed_url = urllib.parse.urlparse(self.path) + query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {} + + if "with_extra_info" in query_params: + with_extra_info = toBool(query_params["with_extra_info"][0]) + if post_string != "": post_data = getFormData(post_string, is_json) filters["coin_from"] = setCoinFilter(post_data, "coin_from") @@ -273,6 +292,24 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: offer_data["feerate_from"] = o.from_feerate offer_data["feerate_to"] = o.to_feerate + offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0) + + if o.was_sent: + try: + strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id) + if strategy: + offer_data["local_automation_strat_id"] = strategy[0] + swap_client.log.debug( + f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}" + ) + else: + offer_data["local_automation_strat_id"] = 0 + except Exception as e: + swap_client.log.debug( + f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}" + ) + offer_data["local_automation_strat_id"] = 0 + rv.append(offer_data) return bytes(json.dumps(rv), "UTF-8") @@ -514,7 +551,19 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes: def js_sentbids(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client - swap_client.checkSystemStatus() + + try: + swap_client.checkSystemStatus() + except Exception as e: + from basicswap.util import LockedCoinError + from basicswap.ui.util import getCoinName + + if isinstance(e, LockedCoinError): + error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet." + return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8") + else: + return bytes(json.dumps({"error": str(e)}), "UTF-8") + post_data = getFormData(post_string, is_json) offer_id, filters = parseBidFilters(post_data) @@ -631,7 +680,19 @@ def js_rate(self, url_split, post_string, is_json) -> bytes: def js_index(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client - swap_client.checkSystemStatus() + + try: + swap_client.checkSystemStatus() + except Exception as e: + from basicswap.util import LockedCoinError + from basicswap.ui.util import getCoinName + + if isinstance(e, LockedCoinError): + error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet." + return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8") + else: + return bytes(json.dumps({"error": str(e)}), "UTF-8") + return bytes(json.dumps(swap_client.getSummary()), "UTF-8") diff --git a/basicswap/static/js/amm_counter.js b/basicswap/static/js/amm_counter.js new file mode 100644 index 0000000..ec665c6 --- /dev/null +++ b/basicswap/static/js/amm_counter.js @@ -0,0 +1,255 @@ +const AmmCounterManager = (function() { + const config = { + refreshInterval: 10000, + ammStatusEndpoint: '/amm/status', + retryDelay: 5000, + maxRetries: 3, + debug: false + }; + + let refreshTimer = null; + let fetchRetryCount = 0; + let lastAmmStatus = null; + + function isDebugEnabled() { + return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug; + } + + function debugLog(message, data) { + // if (isDebugEnabled()) { + // if (data) { + // console.log(`[AmmCounter] ${message}`, data); + // } else { + // console.log(`[AmmCounter] ${message}`); + // } + // } + } + + function updateAmmCounter(count, status) { + const ammCounter = document.getElementById('amm-counter'); + const ammCounterMobile = document.getElementById('amm-counter-mobile'); + + debugLog(`Updating AMM counter: count=${count}, status=${status}`); + + if (ammCounter) { + ammCounter.textContent = count; + ammCounter.classList.remove('bg-blue-500', 'bg-gray-400'); + ammCounter.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400'); + } + + if (ammCounterMobile) { + ammCounterMobile.textContent = count; + ammCounterMobile.classList.remove('bg-blue-500', 'bg-gray-400'); + ammCounterMobile.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400'); + } + + updateAmmTooltips(count, status); + } + + function updateAmmTooltips(count, status) { + debugLog(`updateAmmTooltips called with count=${count}, status=${status}`); + + const subheaderTooltip = document.getElementById('tooltip-amm-subheader'); + debugLog('Looking for tooltip-amm-subheader element:', subheaderTooltip); + + if (subheaderTooltip) { + const statusText = status === 'running' ? 'Active' : 'Inactive'; + + const newContent = ` +

Status: ${statusText}

+

Currently active offers/bids: ${count}

+ `; + + const statusParagraph = subheaderTooltip.querySelector('p:first-child'); + const countParagraph = subheaderTooltip.querySelector('p:last-child'); + + if (statusParagraph && countParagraph) { + statusParagraph.innerHTML = `Status: ${statusText}`; + countParagraph.innerHTML = `Currently active offers/bids: ${count}`; + debugLog(`Updated AMM subheader tooltip paragraphs: status=${statusText}, count=${count}`); + } else { + subheaderTooltip.innerHTML = newContent; + debugLog(`Replaced AMM subheader tooltip content: status=${statusText}, count=${count}`); + } + + refreshTooltipInstances('tooltip-amm-subheader', newContent); + } else { + debugLog('AMM subheader tooltip element not found - checking all tooltip elements'); + const allTooltips = document.querySelectorAll('[id*="tooltip"]'); + debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id)); + } + } + + function refreshTooltipInstances(tooltipId, newContent) { + const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`); + + triggers.forEach(trigger => { + if (trigger._tippy) { + trigger._tippy.setContent(newContent); + debugLog(`Updated Tippy instance content for ${tooltipId}`); + } else { + if (window.TooltipManager && typeof window.TooltipManager.create === 'function') { + window.TooltipManager.create(trigger, newContent, { + placement: trigger.getAttribute('data-tooltip-placement') || 'top' + }); + debugLog(`Created new Tippy instance for ${tooltipId}`); + } + } + }); + + if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') { + window.TooltipManager.refreshTooltip(tooltipId, newContent); + debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`); + } + + if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') { + setTimeout(() => { + window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`); + debugLog(`Re-initialized tooltips for ${tooltipId}`); + }, 50); + } + } + + function fetchAmmStatus() { + debugLog('Fetching AMM status...'); + + let url = config.ammStatusEndpoint; + if (isDebugEnabled()) { + url += '?debug=true'; + } + + return fetch(url, { + headers: { + 'Accept': 'application/json', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + lastAmmStatus = data; + debugLog('AMM status data received:', data); + updateAmmCounter(data.amm_active_count, data.status); + fetchRetryCount = 0; + return data; + }) + .catch(error => { + if (isDebugEnabled()) { + console.error('[AmmCounter] AMM status fetch error:', error); + } + + if (fetchRetryCount < config.maxRetries) { + fetchRetryCount++; + debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`); + + return new Promise(resolve => { + setTimeout(() => { + resolve(fetchAmmStatus()); + }, config.retryDelay); + }); + } else { + fetchRetryCount = 0; + throw error; + } + }); + } + + function startRefreshTimer() { + stopRefreshTimer(); + + debugLog('Starting AMM status refresh timer'); + + fetchAmmStatus() + .then(() => {}) + .catch(() => {}); + + refreshTimer = setInterval(() => { + fetchAmmStatus() + .then(() => {}) + .catch(() => {}); + }, config.refreshInterval); + } + + function stopRefreshTimer() { + if (refreshTimer) { + debugLog('Stopping AMM status refresh timer'); + clearInterval(refreshTimer); + refreshTimer = null; + } + } + + function setupWebSocketHandler() { + if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') { + debugLog('Setting up WebSocket handler for AMM status updates'); + window.WebSocketManager.addMessageHandler('message', (data) => { + if (data && data.event) { + debugLog('WebSocket event received, refreshing AMM status'); + fetchAmmStatus() + .then(() => {}) + .catch(() => {}); + } + }); + } + } + + function setupDebugListener() { + const debugCheckbox = document.getElementById('amm_debug'); + if (debugCheckbox) { + debugLog('Found AMM debug checkbox, setting up listener'); + + localStorage.setItem('amm_debug_enabled', debugCheckbox.checked ? 'true' : 'false'); + + debugCheckbox.addEventListener('change', function() { + localStorage.setItem('amm_debug_enabled', this.checked ? 'true' : 'false'); + debugLog(`Debug mode ${this.checked ? 'enabled' : 'disabled'}`); + }); + } + } + + const publicAPI = { + initialize: function(options = {}) { + Object.assign(config, options); + + setupWebSocketHandler(); + setupDebugListener(); + startRefreshTimer(); + + debugLog('AMM Counter Manager initialized'); + + if (window.CleanupManager) { + window.CleanupManager.registerResource('ammCounterManager', this, (mgr) => mgr.dispose()); + } + + return this; + }, + + fetchAmmStatus: fetchAmmStatus, + + updateCounter: updateAmmCounter, + + updateTooltips: updateAmmTooltips, + + startRefreshTimer: startRefreshTimer, + + stopRefreshTimer: stopRefreshTimer, + + dispose: function() { + debugLog('Disposing AMM Counter Manager'); + stopRefreshTimer(); + } + }; + + return publicAPI; +})(); + +document.addEventListener('DOMContentLoaded', function() { + if (!window.ammCounterManagerInitialized) { + window.AmmCounterManager = AmmCounterManager.initialize(); + window.ammCounterManagerInitialized = true; + } +}); diff --git a/basicswap/static/js/amm_tables.js b/basicswap/static/js/amm_tables.js new file mode 100644 index 0000000..bac643f --- /dev/null +++ b/basicswap/static/js/amm_tables.js @@ -0,0 +1,2367 @@ +const AmmTablesManager = (function() { + const config = { + refreshInterval: 30000, + debug: false + }; + + let refreshTimer = null; + let stateData = null; + let coinData = {}; + + const offersTab = document.getElementById('offers-tab'); + const bidsTab = document.getElementById('bids-tab'); + const offersContent = document.getElementById('offers-content'); + const bidsContent = document.getElementById('bids-content'); + const offersCount = document.getElementById('offers-count'); + const bidsCount = document.getElementById('bids-count'); + const offersBody = document.getElementById('amm-offers-body'); + const bidsBody = document.getElementById('amm-bids-body'); + const refreshButton = document.getElementById('refreshAmmTables'); + + function isDebugEnabled() { + return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug; + } + + function debugLog(message, data) { + // if (isDebugEnabled()) { + // if (data) { + // console.log(`[AmmTables] ${message}`, data); + // } else { + // console.log(`[AmmTables] ${message}`); + // } + // } + } + + function initializeTabs() { + if (offersTab && bidsTab && offersContent && bidsContent) { + offersTab.addEventListener('click', function() { + offersContent.classList.remove('hidden'); + offersContent.classList.add('block'); + bidsContent.classList.add('hidden'); + bidsContent.classList.remove('block'); + + offersTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white'); + bidsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white'); + }); + + bidsTab.addEventListener('click', function() { + offersContent.classList.add('hidden'); + offersContent.classList.remove('block'); + bidsContent.classList.remove('hidden'); + bidsContent.classList.add('block'); + + bidsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white'); + offersTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white'); + }); + + offersTab.click(); + } + } + + function getImageFilename(coinSymbol) { + if (!coinSymbol) return 'Unknown.png'; + + const coinNameToSymbol = { + 'bitcoin': 'BTC', + 'monero': 'XMR', + 'particl': 'PART', + 'particl anon': 'PART_ANON', + 'particl blind': 'PART_BLIND', + 'litecoin': 'LTC', + 'bitcoincash': 'BCH', + 'bitcoin cash': 'BCH', + 'firo': 'FIRO', + 'zcoin': 'FIRO', + 'pivx': 'PIVX', + 'dash': 'DASH', + 'ethereum': 'ETH', + 'dogecoin': 'DOGE', + 'decred': 'DCR', + 'namecoin': 'NMC', + 'zano': 'ZANO', + 'wownero': 'WOW' + }; + + let normalizedInput = coinSymbol.toLowerCase(); + + if (coinNameToSymbol[normalizedInput]) { + normalizedInput = coinNameToSymbol[normalizedInput]; + } + + const normalizedSymbol = normalizedInput.toUpperCase(); + + if (normalizedSymbol === 'FIRO' || normalizedSymbol === 'ZCOIN') return 'Firo.png'; + if (normalizedSymbol === 'BCH' || normalizedSymbol === 'BITCOINCASH') return 'Bitcoin-Cash.png'; + if (normalizedSymbol === 'PART_ANON' || normalizedSymbol === 'PARTICL_ANON') return 'Particl.png'; + if (normalizedSymbol === 'PART_BLIND' || normalizedSymbol === 'PARTICL_BLIND') return 'Particl.png'; + + if (window.CoinManager && window.CoinManager.getCoinBySymbol) { + const coin = window.CoinManager.getCoinBySymbol(normalizedSymbol); + if (coin && coin.image) return coin.image; + } + + const coinImages = { + 'BTC': 'Bitcoin.png', + 'XMR': 'Monero.png', + 'PART': 'Particl.png', + 'LTC': 'Litecoin.png', + 'FIRO': 'Firo.png', + 'PIVX': 'PIVX.png', + 'DASH': 'Dash.png', + 'ETH': 'Ethereum.png', + 'DOGE': 'Dogecoin.png', + 'DCR': 'Decred.png', + 'NMC': 'Namecoin.png', + 'ZANO': 'Zano.png', + 'WOW': 'Wownero.png' + }; + + const result = coinImages[normalizedSymbol] || 'Unknown.png'; + debugLog(`Coin symbol: ${coinSymbol}, normalized: ${normalizedSymbol}, image: ${result}`); + return result; + } + + function getCoinDisplayName(coinId) { + if (config.debug) { + console.log('[AMM Tables] getCoinDisplayName called with:', coinId, typeof coinId); + } + + if (typeof coinId === 'string') { + const lowerCoinId = coinId.toLowerCase(); + + if (lowerCoinId === 'part_anon' || + lowerCoinId === 'particl_anon' || + lowerCoinId === 'particl anon') { + if (config.debug) { + console.log('[AMM Tables] Matched Particl Anon variant:', coinId); + } + return 'Particl Anon'; + } + + if (lowerCoinId === 'part_blind' || + lowerCoinId === 'particl_blind' || + lowerCoinId === 'particl blind') { + if (config.debug) { + console.log('[AMM Tables] Matched Particl Blind variant:', coinId); + } + return 'Particl Blind'; + } + + if (lowerCoinId === 'ltc_mweb' || + lowerCoinId === 'litecoin_mweb' || + lowerCoinId === 'litecoin mweb') { + if (config.debug) { + console.log('[AMM Tables] Matched Litecoin MWEB variant:', coinId); + } + return 'Litecoin MWEB'; + } + } + + if (window.CoinManager && window.CoinManager.getDisplayName) { + const displayName = window.CoinManager.getDisplayName(coinId); + if (displayName) { + if (config.debug) { + console.log('[AMM Tables] CoinManager returned:', displayName); + } + return displayName; + } + } + + if (config.debug) { + console.log('[AMM Tables] Returning coin name as-is:', coinId); + } + return coinId; + } + + function createSwapColumn(coinFrom, coinTo) { + const fromImage = getImageFilename(coinFrom); + const toImage = getImageFilename(coinTo); + const fromDisplayName = getCoinDisplayName(coinFrom); + const toDisplayName = getCoinDisplayName(coinTo); + + return ` + +
+ + ${fromDisplayName} + + + + ${toDisplayName} + +
+ + `; + } + + function createActiveCount(templateName, activeItems) { + const count = activeItems && activeItems[templateName] ? activeItems[templateName].length : 0; + + return ` + + + ${count} + + + `; + } + + function renderOffersTable(stateData) { + if (!offersBody) return; + + debugLog('Rendering offers table with data:', stateData); + + let offers = []; + if (stateData && stateData.config) { + if (Array.isArray(stateData.config.offers)) { + offers = stateData.config.offers; + } else if (typeof stateData.config.offers === 'object') { + offers = [stateData.config.offers]; + } + } + + const activeOffers = stateData && stateData.state && stateData.state.offers ? stateData.state.offers : {}; + + if (offers.length === 0) { + offersBody.innerHTML = ` + + +
+ + + +

No offers configured

+

Edit the AMM configuration to add offers

+
+ + + `; + offersCount.textContent = '(0)'; + return; + } + + offersCount.textContent = `(${offers.length})`; + + let tableHtml = ''; + + offers.forEach(offer => { + const name = offer.name || 'Unnamed Offer'; + const coinFrom = offer.coin_from || ''; + const coinTo = offer.coin_to || ''; + const amount = parseFloat(offer.amount || 0); + const minrate = parseFloat(offer.minrate || 0); + const enabled = offer.enabled !== undefined ? offer.enabled : false; + const amountVariable = offer.amount_variable !== undefined ? offer.amount_variable : false; + const minCoinFromAmt = parseFloat(offer.min_coin_from_amt || 0); + const offerValidSeconds = parseInt(offer.offer_valid_seconds || 3600); + const rateTweakPercent = parseFloat(offer.ratetweakpercent || 0); + const adjustRatesValue = offer.adjust_rates_based_on_market || 'false'; + const adjustRates = adjustRatesValue !== 'false'; + const amountStep = offer.amount_step || 'N/A'; + + const amountToReceive = amount * minrate; + + const activeOffersCount = activeOffers[name] && Array.isArray(activeOffers[name]) ? + activeOffers[name].length : 0; + + tableHtml += ` + + +
${name}
+ + ${createSwapColumn(coinFrom, coinTo)} + +
${coinFrom == "Bitcoin" ? amount.toFixed(8) : amount.toFixed(4)}
+
${getCoinDisplayName(coinFrom)}
+
+ Min bal: ${coinFrom == "Bitcoin" ? minCoinFromAmt.toFixed(8) : minCoinFromAmt.toFixed(4)} +
+ + +
${minrate.toFixed(8)}
+
+ Tweak: ${rateTweakPercent > 0 ? '+' : ''}${rateTweakPercent}% +
+
+ Receive: ~${(amountToReceive * (rateTweakPercent / 100 + 1)).toFixed(4)} ${getCoinDisplayName(coinTo)} + ${(() => { + const usdValue = calculateUSDPrice(amountToReceive, coinTo); + return usdValue ? `
${formatUSDPrice(usdValue)}` : '
USD: N/A'; + })()} +
+ + +
+
+ + ${amountVariable ? 'Variable' : 'Fixed'} + + + + + + ${formatDuration(offerValidSeconds)} + +
+
+ + + + Rates: ${adjustRatesValue === 'static' ? 'Static' + : adjustRatesValue === 'only' ? 'Market' + : adjustRatesValue === 'minrate' ? 'Market (fallback)' + : adjustRatesValue === 'false' ? 'CoinGecko' + : adjustRatesValue === 'all' ? 'Auto (all)' + : adjustRates ? 'Auto (any)' + : 'Off'} +
+
+ + + + Step: ${amountStep} +
+
+ + +
+ + ${enabled ? 'Enabled' : 'Disabled'} + + +
+ + +
+ + +
+ + + `; + }); + + offersBody.innerHTML = tableHtml; + } + + function renderBidsTable(stateData) { + if (!bidsBody) return; + + debugLog('Rendering bids table with data:', stateData); + + let bids = []; + if (stateData && stateData.config) { + if (Array.isArray(stateData.config.bids)) { + bids = stateData.config.bids; + } else if (typeof stateData.config.bids === 'object') { + bids = [stateData.config.bids]; + } + } + + const activeBids = stateData && stateData.state && stateData.state.bids ? stateData.state.bids : {}; + + if (bids.length === 0) { + bidsBody.innerHTML = ` + + +
+ + + +

No bids configured

+

Edit the AMM configuration to add bids

+
+ + + `; + bidsCount.textContent = '(0)'; + return; + } + + bidsCount.textContent = `(${bids.length})`; + + let tableHtml = ''; + + bids.forEach(bid => { + const name = bid.name || 'Unnamed Bid'; + const coinFrom = bid.coin_from || ''; + const coinTo = bid.coin_to || ''; + const amount = parseFloat(bid.amount || 0); + const maxRate = parseFloat(bid.max_rate || 0); + const enabled = bid.enabled !== undefined ? bid.enabled : false; + const amountVariable = bid.amount_variable !== undefined ? bid.amount_variable : false; + const minCoinToBalance = parseFloat(bid.min_coin_to_balance || 0); + const maxConcurrent = parseInt(bid.max_concurrent || 1); + const amountToSend = amount * maxRate; + const activeBidsCount = activeBids[name] && Array.isArray(activeBids[name]) ? + activeBids[name].length : 0; + + tableHtml += ` + + +
${name}
+ + ${createSwapColumn(coinTo, coinFrom)} + +
${amount.toFixed(8)}
+
${getCoinDisplayName(coinFrom)}
+
+ Min ${getCoinDisplayName(coinTo)} Balance: ${minCoinToBalance.toFixed(8)} +
+ + +
${maxRate.toFixed(8)}
+
+ Send: ~${amountToSend.toFixed(8)} ${getCoinDisplayName(coinTo)} + ${(() => { + const usdValue = calculateUSDPrice(amountToSend, coinTo); + return usdValue ? `
${formatUSDPrice(usdValue)}` : '
USD: N/A'; + })()} +
+ + +
+
+ + ${amountVariable ? 'Variable' : 'Fixed'} + + + Max: ${maxConcurrent} + +
+
+ + +
+ + ${enabled ? 'Enabled' : 'Disabled'} + + +
+ + +
+ + +
+ + + `; + }); + + bidsBody.innerHTML = tableHtml; + } + + function formatDuration(seconds) { + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`; + return `${Math.floor(seconds / 86400)}d`; + } + + function getPriceKey(coin) { + if (!coin) return null; + + const lowerCoin = coin.toLowerCase(); + + const coinToTicker = { + 'particl': 'PART', + 'particl anon': 'PART', + 'particl blind': 'PART', + 'part': 'PART', + 'part anon': 'PART', + 'part blind': 'PART', + 'bitcoin': 'BTC', + 'btc': 'BTC', + 'monero': 'XMR', + 'xmr': 'XMR', + 'litecoin': 'LTC', + 'ltc': 'LTC', + 'wownero': 'WOW', + 'wow': 'WOW', + 'dash': 'DASH', + 'pivx': 'PIVX', + 'firo': 'FIRO', + 'xzc': 'FIRO', + 'zcoin': 'FIRO', + 'BTC': 'BTC', + 'LTC': 'LTC', + 'XMR': 'XMR', + 'PART': 'PART', + 'WOW': 'WOW', + 'FIRO': 'FIRO', + 'DASH': 'DASH', + 'PIVX': 'PIVX' + }; + + if (coinToTicker[lowerCoin]) { + return coinToTicker[lowerCoin]; + } + + if (coinToTicker[coin.toUpperCase()]) { + return coinToTicker[coin.toUpperCase()]; + } + + for (const [key, value] of Object.entries(coinToTicker)) { + if (lowerCoin.includes(key.toLowerCase())) { + return value; + } + } + + if (lowerCoin.includes('particl') || lowerCoin.includes('part')) { + return 'PART'; + } + + return coin.toUpperCase(); + } + + function calculateUSDPrice(amount, coinName) { + if (!window.latestPrices || !coinName || !amount) { + return null; + } + + const ticker = getPriceKey(coinName); + let coinPrice = null; + + if (typeof window.latestPrices[ticker] === 'number') { + coinPrice = window.latestPrices[ticker]; + } + + else if (typeof window.latestPrices[coinName] === 'number') { + coinPrice = window.latestPrices[coinName]; + } + + else if (typeof window.latestPrices[coinName.toUpperCase()] === 'number') { + coinPrice = window.latestPrices[coinName.toUpperCase()]; + } + + + if (!coinPrice || isNaN(coinPrice)) { + return null; + } + + return amount * coinPrice; + } + + function formatUSDPrice(usdValue) { + if (!usdValue || isNaN(usdValue)) return ''; + return `($${usdValue.toFixed(2)} USD)`; + } + + async function fetchLatestPrices() { + try { + const coins = 'BTC,LTC,XMR,PART,WOW,FIRO,DASH,PIVX'; + + const response = await fetch('/json/coinprices', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `coins=${encodeURIComponent(coins)}¤cy_to=USD&source=coingecko.com&match_input_key=true` + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + + if (data && data.rates) { + return data.rates; + } + return data; + } catch (error) { + console.error('Error fetching prices:', error); + return null; + } + } + + async function initializePrices() { + + if (window.priceManager && typeof window.priceManager.getLatestPrices === 'function') { + const prices = window.priceManager.getLatestPrices(); + if (prices && Object.keys(prices).length > 0) { + window.latestPrices = prices; + setTimeout(() => { + updateTables(); + }, 100); + return; + } + } + + const prices = await fetchLatestPrices(); + if (prices) { + window.latestPrices = prices; + setTimeout(() => { + updateTables(); + }, 100); + } + } + + function getInitialData() { + if (window.ammTablesConfig) { + const stateData = window.ammTablesConfig.stateData || {}; + let configData = window.ammTablesConfig.configData || {}; + + if (!configData || Object.keys(configData).length === 0) { + try { + if (window.ammTablesConfig.configContent) { + if (typeof window.ammTablesConfig.configContent === 'string') { + configData = JSON.parse(window.ammTablesConfig.configContent); + } else if (typeof window.ammTablesConfig.configContent === 'object') { + configData = window.ammTablesConfig.configContent; + } + } + } catch (error) { + debugLog('Error parsing config content:', error); + } + } + + debugLog('Initial state data:', stateData); + debugLog('Initial config data:', configData); + + return { + state: stateData, + config: configData + }; + } + return null; + } + + function parseStateData() { + const stateContent = document.querySelector('.font-mono.bg-gray-50.overflow-y-auto'); + if (!stateContent) return null; + + try { + const stateText = stateContent.textContent.trim(); + if (!stateText) return null; + + const parsedState = JSON.parse(stateText); + return { state: parsedState }; + } catch (error) { + debugLog('Error parsing state data:', error); + return null; + } + } + + function parseConfigData() { + const configTextarea = document.querySelector('textarea[name="config_content"]'); + if (!configTextarea) return null; + + try { + const configText = configTextarea.value.trim(); + if (!configText) return null; + + const parsedConfig = JSON.parse(configText); + return { config: parsedConfig }; + } catch (error) { + debugLog('Error parsing config data:', error); + return null; + } + } + + function getCombinedData() { + const initialData = getInitialData(); + if (initialData) { + return initialData; + } + + const stateData = parseStateData(); + const configData = parseConfigData(); + + return { + ...stateData, + ...configData + }; + } + function updateTables() { + const data = getCombinedData(); + if (!data) { + debugLog('No data available for tables'); + return; + } + + stateData = data; + debugLog('Updated state data:', stateData); + + renderOffersTable(stateData); + renderBidsTable(stateData); + } + + function startRefreshTimer() { + if (refreshTimer) { + clearInterval(refreshTimer); + } + + refreshTimer = setInterval(function() { + updateTables(); + }, config.refreshInterval); + + return refreshTimer; + } + + function stopRefreshTimer() { + if (refreshTimer) { + clearInterval(refreshTimer); + refreshTimer = null; + } + } + + function setupConfigFormListener() { + const configForm = document.querySelector('form[method="post"]'); + if (configForm) { + configForm.addEventListener('submit', function() { + localStorage.setItem('amm_update_tables', 'true'); + }); + + if (localStorage.getItem('amm_update_tables') === 'true') { + localStorage.removeItem('amm_update_tables'); + setTimeout(updateTables, 500); + } + } + } + + function setupButtonHandlers() { + const addOfferButton = document.getElementById('add-new-offer-btn'); + if (addOfferButton) { + addOfferButton.addEventListener('click', function() { + openAddModal('offer'); + }); + } + + const addBidButton = document.getElementById('add-new-bid-btn'); + if (addBidButton) { + addBidButton.addEventListener('click', function() { + openAddModal('bid'); + }); + } + + const addCancelButton = document.getElementById('add-amm-cancel'); + if (addCancelButton) { + addCancelButton.addEventListener('click', closeAddModal); + } + + const addSaveButton = document.getElementById('add-amm-save'); + if (addSaveButton) { + addSaveButton.addEventListener('click', saveNewItem); + } + + const editCancelButton = document.getElementById('edit-amm-cancel'); + if (editCancelButton) { + editCancelButton.addEventListener('click', closeEditModal); + } + + const editSaveButton = document.getElementById('edit-amm-save'); + if (editSaveButton) { + editSaveButton.addEventListener('click', saveEditedItem); + } + + document.addEventListener('click', function(e) { + if (e.target && (e.target.classList.contains('delete-amm-item') || e.target.closest('.delete-amm-item'))) { + const button = e.target.classList.contains('delete-amm-item') ? e.target : e.target.closest('.delete-amm-item'); + const type = button.getAttribute('data-type'); + const id = button.getAttribute('data-id'); + const name = button.getAttribute('data-name'); + + if (!id && !name) { + if (window.showErrorModal) { + window.showErrorModal('Error', 'Could not identify the item to delete.'); + } else { + alert('Error: Could not identify the item to delete.'); + } + return; + } + + if (window.showConfirmModal) { + window.showConfirmModal( + 'Confirm Deletion', + `Are you sure you want to delete this ${type}?\n\nName: ${name || 'Unnamed'}\n\nThis action cannot be undone.`, + function() { + deleteAmmItem(type, id, name); + } + ); + } else { + if (confirm(`Are you sure you want to delete this ${type}?`)) { + deleteAmmItem(type, id, name); + } + } + } + + if (e.target && (e.target.classList.contains('edit-amm-item') || e.target.closest('.edit-amm-item'))) { + const button = e.target.classList.contains('edit-amm-item') ? e.target : e.target.closest('.edit-amm-item'); + const type = button.getAttribute('data-type'); + const id = button.getAttribute('data-id'); + const name = button.getAttribute('data-name'); + + if (!id && !name) { + alert('Error: Could not identify the item to edit.'); + return; + } + + openEditModal(type, id, name); + } + }); + + const addModal = document.getElementById('add-amm-modal'); + if (addModal) { + addModal.addEventListener('click', function(e) { + if (e.target === addModal) { + closeAddModal(); + } + }); + } + + const editModal = document.getElementById('edit-amm-modal'); + if (editModal) { + editModal.addEventListener('click', function(e) { + if (e.target === editModal) { + closeEditModal(); + } + }); + } + } + + function openAddModal(type) { + debugLog(`Opening add modal for ${type}`); + + const coinFromCheck = document.getElementById('add-amm-coin-from'); + const coinToCheck = document.getElementById('add-amm-coin-to'); + + if (!coinFromCheck || !coinToCheck || coinFromCheck.options.length < 2 || coinToCheck.options.length < 2) { + if (window.showErrorModal) { + window.showErrorModal('Configuration Error', 'At least 2 different coins must be configured in BasicSwap to create AMM offers/bids. Please configure additional coins first.'); + } else { + alert('At least 2 different coins must be configured in BasicSwap to create AMM offers/bids. Please configure additional coins first.'); + } + return; + } + + const modalTitle = document.getElementById('add-modal-title'); + if (modalTitle) { + modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`; + } + + document.getElementById('add-amm-type').value = type; + + document.getElementById('add-amm-name').value = 'Unnamed Offer'; + document.getElementById('add-amm-enabled').checked = true; + + const coinFromSelect = document.getElementById('add-amm-coin-from'); + const coinToSelect = document.getElementById('add-amm-coin-to'); + + if (coinFromSelect && coinFromSelect.options.length > 0) { + coinFromSelect.selectedIndex = 0; + coinFromSelect.dispatchEvent(new Event('change', { bubbles: true })); + } + + if (coinToSelect && coinToSelect.options.length > 1) { + coinToSelect.selectedIndex = 1; + coinToSelect.dispatchEvent(new Event('change', { bubbles: true })); + } else if (coinToSelect && coinToSelect.options.length > 0) { + coinToSelect.selectedIndex = 0; + coinToSelect.dispatchEvent(new Event('change', { bubbles: true })); + } + + document.getElementById('add-amm-amount').value = ''; + + const adjustRatesSelect = document.getElementById('add-offer-adjust-rates'); + if (adjustRatesSelect) { + adjustRatesSelect.value = 'false'; + } + + if (type === 'offer') { + const offerFields = document.getElementById('add-offer-fields'); + if (offerFields) { + offerFields.classList.remove('hidden'); + } + + const bidFields = document.getElementById('add-bid-fields'); + if (bidFields) { + bidFields.classList.add('hidden'); + } + + document.getElementById('add-amm-rate-label').textContent = 'Minimum Rate'; + document.getElementById('add-amm-rate').value = '0.0001'; + document.getElementById('add-offer-ratetweakpercent').value = '0'; + document.getElementById('add-offer-min-coin-from-amt').value = ''; + document.getElementById('add-offer-valid-seconds').value = '3600'; + document.getElementById('add-offer-address').value = 'auto'; + document.getElementById('add-offer-min-swap-amount').value = '0.001'; + document.getElementById('add-offer-amount-step').value = '0.001'; + + const coinFrom = document.getElementById('add-amm-coin-from'); + const coinTo = document.getElementById('add-amm-coin-to'); + const swapType = document.getElementById('add-offer-swap-type'); + + if (coinFrom && coinTo && swapType) { + updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType); + } + } else if (type === 'bid') { + const offerFields = document.getElementById('add-offer-fields'); + if (offerFields) { + offerFields.classList.add('hidden'); + } + + const bidFields = document.getElementById('add-bid-fields'); + if (bidFields) { + bidFields.classList.remove('hidden'); + + document.getElementById('add-amm-rate-label').textContent = 'Max Rate'; + document.getElementById('add-amm-rate').value = '10000.0'; + document.getElementById('add-bid-min-coin-to-balance').value = '1.0'; + document.getElementById('add-bid-max-concurrent').value = '1'; + document.getElementById('add-bid-address').value = 'auto'; + document.getElementById('add-bid-min-swap-amount').value = '0.001'; + } + } + + if (coinFromSelect && coinToSelect) { + const handleCoinChange = function() { + const fromValue = coinFromSelect.value; + const toValue = coinToSelect.value; + + if (fromValue && toValue && fromValue === toValue) { + for (let i = 0; i < coinToSelect.options.length; i++) { + if (coinToSelect.options[i].value !== fromValue) { + coinToSelect.selectedIndex = i; + break; + } + } + } + }; + + coinFromSelect.addEventListener('change', handleCoinChange); + coinToSelect.addEventListener('change', handleCoinChange); + } + + if (type === 'offer') { + setupBiddingControls('add'); + } + + const modal = document.getElementById('add-amm-modal'); + if (modal) { + modal.classList.remove('hidden'); + } + } + + function closeAddModal() { + const modal = document.getElementById('add-amm-modal'); + if (modal) { + modal.classList.add('hidden'); + closeAllDropdowns(); + } + } + + function saveNewItem() { + const type = document.getElementById('add-amm-type').value; + + debugLog(`Saving new ${type}`); + + const configTextarea = document.querySelector('textarea[name="config_content"]'); + if (!configTextarea) { + alert('Error: Could not find the configuration textarea.'); + return; + } + + try { + const configText = configTextarea.value.trim(); + if (!configText) { + alert('Error: Configuration is empty.'); + return; + } + + const config = JSON.parse(configText); + + const uniqueId = `${type}_${Date.now()}`; + + const name = document.getElementById('add-amm-name').value.trim(); + + if (!name || name === '') { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Name is required and cannot be empty.'); + } else { + alert('Name is required and cannot be empty.'); + } + return; + } + + const coinFrom = document.getElementById('add-amm-coin-from').value; + const coinTo = document.getElementById('add-amm-coin-to').value; + + if (!coinFrom || !coinTo) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Please select both Coin From and Coin To.'); + } else { + alert('Please select both Coin From and Coin To.'); + } + return; + } + + if (coinFrom === coinTo) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Coin From and Coin To must be different.'); + } else { + alert('Coin From and Coin To must be different.'); + } + return; + } + + const newItem = { + id: uniqueId, + name: name, + enabled: document.getElementById('add-amm-enabled').checked, + coin_from: document.getElementById('add-amm-coin-from').value, + coin_to: document.getElementById('add-amm-coin-to').value, + amount: parseFloat(document.getElementById('add-amm-amount').value), + amount_variable: true + }; + + if (type === 'offer') { + newItem.minrate = parseFloat(document.getElementById('add-amm-rate').value); + newItem.ratetweakpercent = parseFloat(document.getElementById('add-offer-ratetweakpercent').value || '0'); + newItem.adjust_rates_based_on_market = document.getElementById('add-offer-adjust-rates').value; + newItem.swap_type = document.getElementById('add-offer-swap-type').value || 'adaptor_sig'; + const automationStrategyElement = document.getElementById('add-offer-automation-strategy'); + newItem.automation_strategy = automationStrategyElement ? automationStrategyElement.value : 'accept_all'; + + const minCoinFromAmt = document.getElementById('add-offer-min-coin-from-amt').value; + if (minCoinFromAmt) { + newItem.min_coin_from_amt = parseFloat(minCoinFromAmt); + } + + const validSeconds = document.getElementById('add-offer-valid-seconds').value; + if (validSeconds) { + const seconds = parseInt(validSeconds); + if (seconds < 600) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer valid seconds must be at least 600 (10 minutes)'); + } else { + alert('Offer valid seconds must be at least 600 (10 minutes)'); + } + return; + } + newItem.offer_valid_seconds = seconds; + } + + const address = document.getElementById('add-offer-address').value; + if (address) { + newItem.address = address; + } + + const minSwapAmount = document.getElementById('add-offer-min-swap-amount').value; + if (minSwapAmount) { + newItem.min_swap_amount = parseFloat(minSwapAmount); + } + + const amountStep = document.getElementById('add-offer-amount-step').value; + const offerAmount = parseFloat(document.getElementById('add-amm-amount').value); + + if (!amountStep || amountStep.trim() === '') { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.'); + } else { + alert('Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.'); + } + return; + } + + if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) { + const parsedValue = parseFloat(amountStep); + if (parsedValue <= 0) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.'); + } else { + alert('Offer Size Increment must be greater than zero.'); + } + return; + } + if (parsedValue < 0.001) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.'); + } else { + alert('Offer Size Increment must be at least 0.001.'); + } + return; + } + if (parsedValue > offerAmount) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`); + } else { + alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`); + } + return; + } + newItem.amount_step = parsedValue.toString(); + console.log(`Offer Size Increment set to: ${newItem.amount_step}`); + } else { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Invalid Offer Size Increment value. Please enter a valid decimal number.'); + } else { + alert('Invalid Offer Size Increment value. Please enter a valid decimal number.'); + } + return; + } + + const attemptBidsFirst = document.getElementById('add-offer-attempt-bids-first'); + if (attemptBidsFirst && attemptBidsFirst.checked) { + newItem.attempt_bids_first = true; + + const bidStrategy = document.getElementById('add-offer-bid-strategy').value; + if (bidStrategy) { + newItem.bid_strategy = bidStrategy; + } + + const maxBidPercentage = document.getElementById('add-offer-max-bid-percentage').value; + if (maxBidPercentage) { + newItem.max_bid_percentage = parseInt(maxBidPercentage); + } + + const bidRateTolerance = document.getElementById('add-offer-bid-rate-tolerance').value; + if (bidRateTolerance) { + newItem.bid_rate_tolerance = parseFloat(bidRateTolerance); + } + + const minRemainingOffer = document.getElementById('add-offer-min-remaining-offer').value; + if (minRemainingOffer) { + newItem.min_remaining_offer = parseFloat(minRemainingOffer); + } + } + } else if (type === 'bid') { + newItem.max_rate = parseFloat(document.getElementById('add-amm-rate').value); + newItem.offers_to_bid_on = document.getElementById('add-bid-offers-to-bid-on').value || 'all'; + + const minCoinToBalance = document.getElementById('add-bid-min-coin-to-balance').value; + if (minCoinToBalance) { + newItem.min_coin_to_balance = parseFloat(minCoinToBalance); + } + + const maxConcurrent = document.getElementById('add-bid-max-concurrent').value; + if (maxConcurrent) { + newItem.max_concurrent = parseInt(maxConcurrent); + } + + const address = document.getElementById('add-bid-address').value; + if (address) { + newItem.address = address; + } + + const minSwapAmount = document.getElementById('add-bid-min-swap-amount').value; + if (minSwapAmount) { + newItem.min_swap_amount = parseFloat(minSwapAmount); + } + } + + if (type === 'offer') { + if (!Array.isArray(config.offers)) { + config.offers = []; + } + config.offers.push(newItem); + } else if (type === 'bid') { + if (!Array.isArray(config.bids)) { + config.bids = []; + } + config.bids.push(newItem); + } else { + if (window.showErrorModal) { + window.showErrorModal('Error', `Invalid type ${type}`); + } else { + alert(`Error: Invalid type ${type}`); + } + return; + } + + const wasReadonly = configTextarea.hasAttribute('readonly'); + if (wasReadonly) { + configTextarea.removeAttribute('readonly'); + } + + configTextarea.value = JSON.stringify(config, null, 4); + + closeAddModal(); + + const saveButton = document.getElementById('save_config_btn'); + if (saveButton && !saveButton.disabled) { + saveButton.click(); + + setTimeout(() => { + if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) { + window.AmmCounterManager.fetchAmmStatus(); + } + if (window.SummaryManager && window.SummaryManager.fetchSummaryData) { + window.SummaryManager.fetchSummaryData(); + } + }, 1000); + } else { + const form = configTextarea.closest('form'); + if (form) { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = 'save_config'; + hiddenInput.value = 'true'; + form.appendChild(hiddenInput); + form.submit(); + } else { + if (window.showErrorModal) { + window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.'); + } else { + alert('Error: Could not save the configuration.'); + } + } + } + + if (wasReadonly) { + configTextarea.setAttribute('readonly', ''); + } + } catch (error) { + if (window.showErrorModal) { + window.showErrorModal('Configuration Error', `Error processing the configuration: ${error.message}`); + } else { + alert(`Error processing the configuration: ${error.message}`); + } + debugLog('Error saving new item:', error); + } + } + + function openEditModal(type, id, name) { + debugLog(`Opening edit modal for ${type} with id: ${id}, name: ${name}`); + + const configTextarea = document.querySelector('textarea[name="config_content"]'); + if (!configTextarea) { + alert('Error: Could not find the configuration textarea.'); + return; + } + + try { + const configText = configTextarea.value.trim(); + if (!configText) { + alert('Error: Configuration is empty.'); + return; + } + + const config = JSON.parse(configText); + + let item = null; + + if (type === 'offer' && Array.isArray(config.offers)) { + item = config.offers.find(offer => + (id && offer.id === id) || (!id && offer.name === name) + ); + } else if (type === 'bid' && Array.isArray(config.bids)) { + item = config.bids.find(bid => + (id && bid.id === id) || (!id && bid.name === name) + ); + } + + if (!item) { + alert(`Could not find the ${type} to edit.`); + return; + } + + const modalTitle = document.getElementById('edit-modal-title'); + if (modalTitle) { + modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`; + } + + document.getElementById('edit-amm-type').value = type; + document.getElementById('edit-amm-id').value = id || ''; + document.getElementById('edit-amm-original-name').value = name; + + document.getElementById('edit-amm-name').value = item.name || ''; + document.getElementById('edit-amm-enabled').checked = item.enabled || false; + + const coinFromSelect = document.getElementById('edit-amm-coin-from'); + const coinToSelect = document.getElementById('edit-amm-coin-to'); + + coinFromSelect.value = item.coin_from || ''; + coinToSelect.value = item.coin_to || ''; + + coinFromSelect.dispatchEvent(new Event('change', { bubbles: true })); + coinToSelect.dispatchEvent(new Event('change', { bubbles: true })); + + document.getElementById('edit-amm-amount').value = item.amount || ''; + + if (type === 'offer') { + const offerFields = document.getElementById('edit-offer-fields'); + if (offerFields) { + offerFields.classList.remove('hidden'); + } + + const bidFields = document.getElementById('edit-bid-fields'); + if (bidFields) { + bidFields.classList.add('hidden'); + } + + document.getElementById('edit-amm-rate').value = item.minrate || ''; + document.getElementById('edit-offer-ratetweakpercent').value = item.ratetweakpercent || '0'; + document.getElementById('edit-offer-min-coin-from-amt').value = item.min_coin_from_amt || ''; + document.getElementById('edit-offer-valid-seconds').value = item.offer_valid_seconds || '3600'; + document.getElementById('edit-offer-address').value = item.address || 'auto'; + document.getElementById('edit-offer-adjust-rates').value = item.adjust_rates_based_on_market || 'false'; + document.getElementById('edit-offer-swap-type').value = item.swap_type || 'adaptor_sig'; + document.getElementById('edit-offer-min-swap-amount').value = item.min_swap_amount || '0.001'; + document.getElementById('edit-offer-amount-step').value = item.amount_step || '0.001'; + const editAutomationStrategyElement = document.getElementById('edit-offer-automation-strategy'); + if (editAutomationStrategyElement) { + editAutomationStrategyElement.value = item.automation_strategy || 'accept_all'; + } + + const coinFrom = document.getElementById('edit-amm-coin-from'); + const coinTo = document.getElementById('edit-amm-coin-to'); + const swapType = document.getElementById('edit-offer-swap-type'); + + if (coinFrom && coinTo && swapType) { + updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType); + } + } else if (type === 'bid') { + const offerFields = document.getElementById('edit-offer-fields'); + if (offerFields) { + offerFields.classList.add('hidden'); + } + + const bidFields = document.getElementById('edit-bid-fields'); + if (bidFields) { + bidFields.classList.remove('hidden'); + + document.getElementById('edit-amm-rate-label').textContent = 'Max Rate'; + + document.getElementById('edit-amm-rate').value = item.max_rate || ''; + document.getElementById('edit-bid-min-coin-to-balance').value = item.min_coin_to_balance || ''; + document.getElementById('edit-bid-max-concurrent').value = item.max_concurrent || '1'; + document.getElementById('edit-bid-address').value = item.address || 'auto'; + document.getElementById('edit-bid-min-swap-amount').value = item.min_swap_amount || ''; + document.getElementById('edit-bid-offers-to-bid-on').value = item.offers_to_bid_on || 'all'; + } + } + + const editCoinFromSelect = coinFromSelect; + const editCoinToSelect = coinToSelect; + + if (editCoinFromSelect && editCoinToSelect) { + const handleEditCoinChange = function() { + const fromValue = editCoinFromSelect.value; + const toValue = editCoinToSelect.value; + + if (fromValue && toValue && fromValue === toValue) { + for (let i = 0; i < editCoinToSelect.options.length; i++) { + if (editCoinToSelect.options[i].value !== fromValue) { + editCoinToSelect.selectedIndex = i; + break; + } + } + } + }; + + editCoinFromSelect.removeEventListener('change', handleEditCoinChange); + editCoinToSelect.removeEventListener('change', handleEditCoinChange); + + editCoinFromSelect.addEventListener('change', handleEditCoinChange); + editCoinToSelect.addEventListener('change', handleEditCoinChange); + } + + if (type === 'offer') { + setupBiddingControls('edit'); + populateBiddingControls('edit', item); + } + + const modal = document.getElementById('edit-amm-modal'); + if (modal) { + modal.classList.remove('hidden'); + } + } catch (error) { + alert(`Error processing the configuration: ${error.message}`); + debugLog('Error opening edit modal:', error); + } + } + + function closeAllDropdowns() { + const dropdowns = document.querySelectorAll('.absolute.z-50'); + dropdowns.forEach(dropdown => { + dropdown.classList.add('hidden'); + }); + } + + + function closeEditModal() { + const modal = document.getElementById('edit-amm-modal'); + if (modal) { + modal.classList.add('hidden'); + closeAllDropdowns(); + } + } + + function saveEditedItem() { + const type = document.getElementById('edit-amm-type').value; + const id = document.getElementById('edit-amm-id').value; + const originalName = document.getElementById('edit-amm-original-name').value; + + debugLog(`Saving edited ${type} with id: ${id}, original name: ${originalName}`); + + const configTextarea = document.querySelector('textarea[name="config_content"]'); + if (!configTextarea) { + alert('Error: Could not find the configuration textarea.'); + return; + } + + try { + const configText = configTextarea.value.trim(); + if (!configText) { + alert('Error: Configuration is empty.'); + return; + } + + const config = JSON.parse(configText); + + const name = document.getElementById('edit-amm-name').value.trim(); + + if (!name || name === '') { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Name is required and cannot be empty.'); + } else { + alert('Name is required and cannot be empty.'); + } + return; + } + + const coinFrom = document.getElementById('edit-amm-coin-from').value; + const coinTo = document.getElementById('edit-amm-coin-to').value; + + if (!coinFrom || !coinTo) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Please select both Coin From and Coin To.'); + } else { + alert('Please select both Coin From and Coin To.'); + } + return; + } + + if (coinFrom === coinTo) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Coin From and Coin To must be different.'); + } else { + alert('Coin From and Coin To must be different.'); + } + return; + } + + const updatedItem = { + name: name, + enabled: document.getElementById('edit-amm-enabled').checked, + coin_from: document.getElementById('edit-amm-coin-from').value, + coin_to: document.getElementById('edit-amm-coin-to').value, + amount: parseFloat(document.getElementById('edit-amm-amount').value), + amount_variable: true + }; + + if (id) { + updatedItem.id = id; + } + + if (type === 'offer') { + updatedItem.minrate = parseFloat(document.getElementById('edit-amm-rate').value); + updatedItem.ratetweakpercent = parseFloat(document.getElementById('edit-offer-ratetweakpercent').value || '0'); + updatedItem.adjust_rates_based_on_market = document.getElementById('edit-offer-adjust-rates').value; + updatedItem.swap_type = document.getElementById('edit-offer-swap-type').value || 'adaptor_sig'; + const editAutomationStrategyElement = document.getElementById('edit-offer-automation-strategy'); + updatedItem.automation_strategy = editAutomationStrategyElement ? editAutomationStrategyElement.value : 'accept_all'; + + const minCoinFromAmt = document.getElementById('edit-offer-min-coin-from-amt').value; + if (minCoinFromAmt) { + updatedItem.min_coin_from_amt = parseFloat(minCoinFromAmt); + } + + const validSeconds = document.getElementById('edit-offer-valid-seconds').value; + if (validSeconds) { + const seconds = parseInt(validSeconds); + if (seconds < 600) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer valid seconds must be at least 600 (10 minutes)'); + } else { + alert('Offer valid seconds must be at least 600 (10 minutes)'); + } + return; + } + updatedItem.offer_valid_seconds = seconds; + } + + const address = document.getElementById('edit-offer-address').value; + if (address) { + updatedItem.address = address; + } + + const minSwapAmount = document.getElementById('edit-offer-min-swap-amount').value; + if (minSwapAmount) { + updatedItem.min_swap_amount = parseFloat(minSwapAmount); + } + + const amountStep = document.getElementById('edit-offer-amount-step').value; + const offerAmount = parseFloat(document.getElementById('edit-amm-amount').value); + + if (!amountStep || amountStep.trim() === '') { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.'); + } else { + alert('Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.'); + } + return; + } + + if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) { + const parsedValue = parseFloat(amountStep); + if (parsedValue <= 0) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.'); + } else { + alert('Offer Size Increment must be greater than zero.'); + } + return; + } + if (parsedValue < 0.001) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.'); + } else { + alert('Offer Size Increment must be at least 0.001.'); + } + return; + } + if (parsedValue > offerAmount) { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`); + } else { + alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`); + } + return; + } + updatedItem.amount_step = parsedValue.toString(); + console.log(`Offer Size Increment set to: ${updatedItem.amount_step}`); + } else { + if (window.showErrorModal) { + window.showErrorModal('Validation Error', 'Invalid Offer Size Increment value. Please enter a valid decimal number.'); + } else { + alert('Invalid Offer Size Increment value. Please enter a valid decimal number.'); + } + return; + } + + const attemptBidsFirst = document.getElementById('edit-offer-attempt-bids-first'); + if (attemptBidsFirst && attemptBidsFirst.checked) { + updatedItem.attempt_bids_first = true; + + const bidStrategy = document.getElementById('edit-offer-bid-strategy').value; + if (bidStrategy) { + updatedItem.bid_strategy = bidStrategy; + } + + const maxBidPercentage = document.getElementById('edit-offer-max-bid-percentage').value; + if (maxBidPercentage) { + updatedItem.max_bid_percentage = parseInt(maxBidPercentage); + } + + const bidRateTolerance = document.getElementById('edit-offer-bid-rate-tolerance').value; + if (bidRateTolerance) { + updatedItem.bid_rate_tolerance = parseFloat(bidRateTolerance); + } + + const minRemainingOffer = document.getElementById('edit-offer-min-remaining-offer').value; + if (minRemainingOffer) { + updatedItem.min_remaining_offer = parseFloat(minRemainingOffer); + } + } else { + updatedItem.attempt_bids_first = false; + } + } else if (type === 'bid') { + updatedItem.max_rate = parseFloat(document.getElementById('edit-amm-rate').value); + updatedItem.offers_to_bid_on = document.getElementById('edit-bid-offers-to-bid-on').value || 'all'; + + const minCoinToBalance = document.getElementById('edit-bid-min-coin-to-balance').value; + if (minCoinToBalance) { + updatedItem.min_coin_to_balance = parseFloat(minCoinToBalance); + } + + const maxConcurrent = document.getElementById('edit-bid-max-concurrent').value; + if (maxConcurrent) { + updatedItem.max_concurrent = parseInt(maxConcurrent); + } + + const address = document.getElementById('edit-bid-address').value; + if (address) { + updatedItem.address = address; + } + + const minSwapAmount = document.getElementById('edit-bid-min-swap-amount').value; + if (minSwapAmount) { + updatedItem.min_swap_amount = parseFloat(minSwapAmount); + } + } + + if (type === 'offer' && Array.isArray(config.offers)) { + const index = config.offers.findIndex(item => + (id && item.id === id) || (!id && item.name === originalName) + ); + + if (index !== -1) { + config.offers[index] = updatedItem; + debugLog(`Updated offer at index ${index}`); + } else { + alert(`Could not find the offer to update.`); + return; + } + } else if (type === 'bid' && Array.isArray(config.bids)) { + const index = config.bids.findIndex(item => + (id && item.id === id) || (!id && item.name === originalName) + ); + + if (index !== -1) { + config.bids[index] = updatedItem; + debugLog(`Updated bid at index ${index}`); + } else { + alert(`Could not find the bid to update.`); + return; + } + } else { + alert(`Error: Invalid type or no ${type}s found in config.`); + return; + } + + const wasReadonly = configTextarea.hasAttribute('readonly'); + if (wasReadonly) { + configTextarea.removeAttribute('readonly'); + } + + configTextarea.value = JSON.stringify(config, null, 4); + + closeEditModal(); + + const saveButton = document.getElementById('save_config_btn'); + if (saveButton && !saveButton.disabled) { + saveButton.click(); + + setTimeout(() => { + if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) { + window.AmmCounterManager.fetchAmmStatus(); + } + if (window.SummaryManager && window.SummaryManager.fetchSummaryData) { + window.SummaryManager.fetchSummaryData(); + } + }, 1000); + } else { + const form = configTextarea.closest('form'); + if (form) { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = 'save_config'; + hiddenInput.value = 'true'; + form.appendChild(hiddenInput); + form.submit(); + } else { + if (window.showErrorModal) { + window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.'); + } else { + alert('Error: Could not save the configuration.'); + } + } + } + + if (wasReadonly) { + configTextarea.setAttribute('readonly', ''); + } + } catch (error) { + alert(`Error processing the configuration: ${error.message}`); + debugLog('Error saving edited item:', error); + } + } + + function deleteAmmItem(type, id, name) { + debugLog(`Deleting ${type} with id: ${id}, name: ${name}`); + + const configTextarea = document.querySelector('textarea[name="config_content"]'); + if (!configTextarea) { + alert('Error: Could not find the configuration textarea.'); + return; + } + + try { + const configText = configTextarea.value.trim(); + if (!configText) { + alert('Error: Configuration is empty.'); + return; + } + + const config = JSON.parse(configText); + + if (type === 'offer' && Array.isArray(config.offers)) { + const index = config.offers.findIndex(item => + (id && item.id === id) || (!id && item.name === name) + ); + + if (index !== -1) { + config.offers.splice(index, 1); + debugLog(`Removed offer at index ${index}`); + } else { + alert(`Could not find the offer to delete.`); + return; + } + } else if (type === 'bid' && Array.isArray(config.bids)) { + const index = config.bids.findIndex(item => + (id && item.id === id) || (!id && item.name === name) + ); + + if (index !== -1) { + config.bids.splice(index, 1); + debugLog(`Removed bid at index ${index}`); + } else { + alert(`Could not find the bid to delete.`); + return; + } + } else { + alert(`Error: Invalid type or no ${type}s found in config.`); + return; + } + + const wasReadonly = configTextarea.hasAttribute('readonly'); + if (wasReadonly) { + configTextarea.removeAttribute('readonly'); + } + + configTextarea.value = JSON.stringify(config, null, 4); + + const saveButton = document.getElementById('save_config_btn'); + if (saveButton && !saveButton.disabled) { + saveButton.click(); + + setTimeout(() => { + if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) { + window.AmmCounterManager.fetchAmmStatus(); + } + if (window.SummaryManager && window.SummaryManager.fetchSummaryData) { + window.SummaryManager.fetchSummaryData(); + } + }, 1000); + } else { + const form = configTextarea.closest('form'); + if (form) { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = 'save_config'; + hiddenInput.value = 'true'; + form.appendChild(hiddenInput); + form.submit(); + } else { + if (window.showErrorModal) { + window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.'); + } else { + alert('Error: Could not save the configuration.'); + } + } + } + + if (wasReadonly) { + configTextarea.setAttribute('readonly', ''); + } + } catch (error) { + alert(`Error processing the configuration: ${error.message}`); + debugLog('Error deleting item:', error); + } + } + + const adaptor_sig_only_coins = ['6', '9', '8', '7', '13', '18', '17', 'Monero', 'Firo', 'Pivx', 'Dash', 'Namecoin', 'Wownero', 'Zano']; + const secret_hash_only_coins = ['11', '12', 'Ethereum', 'Dogecoin']; + + function updateSwapTypeOptions(coinFromValue, coinToValue, swapTypeSelect) { + if (!swapTypeSelect) return; + + coinFromValue = String(coinFromValue); + coinToValue = String(coinToValue); + + let disableSelect = false; + + if (adaptor_sig_only_coins.includes(coinFromValue) || adaptor_sig_only_coins.includes(coinToValue)) { + swapTypeSelect.value = 'adaptor_sig'; + disableSelect = true; + } else if (secret_hash_only_coins.includes(coinFromValue) || secret_hash_only_coins.includes(coinToValue)) { + swapTypeSelect.value = 'seller_first'; + disableSelect = true; + } else { + swapTypeSelect.value = 'adaptor_sig'; + disableSelect = false; + } + + swapTypeSelect.disabled = disableSelect; + + if (disableSelect) { + swapTypeSelect.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed'); + } else { + swapTypeSelect.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed'); + } + } + + function initializeCustomSelects() { + const coinSelects = [ + document.getElementById('add-amm-coin-from'), + document.getElementById('add-amm-coin-to'), + document.getElementById('edit-amm-coin-from'), + document.getElementById('edit-amm-coin-to') + ]; + + const swapTypeSelects = [ + document.getElementById('add-offer-swap-type'), + document.getElementById('edit-offer-swap-type') + ]; + + function createCoinDropdown(select) { + if (!select) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'relative'; + + const display = document.createElement('div'); + display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer'; + + const icon = document.createElement('img'); + icon.className = 'w-5 h-5 mr-2'; + icon.alt = ''; + + const text = document.createElement('span'); + text.className = 'flex-grow'; + + const arrow = document.createElement('span'); + arrow.className = 'ml-2'; + arrow.innerHTML = ` + + + + `; + + display.appendChild(icon); + display.appendChild(text); + display.appendChild(arrow); + + const dropdown = document.createElement('div'); + dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto'; + + Array.from(select.options).forEach(option => { + const item = document.createElement('div'); + item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white'; + item.setAttribute('data-value', option.value); + item.setAttribute('data-symbol', option.getAttribute('data-symbol') || ''); + + const optionIcon = document.createElement('img'); + optionIcon.className = 'w-5 h-5 mr-2'; + optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`; + optionIcon.alt = ''; + + const optionText = document.createElement('span'); + optionText.textContent = option.textContent.trim(); + + item.appendChild(optionIcon); + item.appendChild(optionText); + + item.addEventListener('click', function() { + select.value = this.getAttribute('data-value'); + + text.textContent = optionText.textContent; + icon.src = optionIcon.src; + + dropdown.classList.add('hidden'); + + const event = new Event('change', { bubbles: true }); + select.dispatchEvent(event); + + if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') { + const coinFrom = document.getElementById('add-amm-coin-from'); + const coinTo = document.getElementById('add-amm-coin-to'); + const swapType = document.getElementById('add-offer-swap-type'); + + if (coinFrom && coinTo && swapType) { + updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType); + } + } else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') { + const coinFrom = document.getElementById('edit-amm-coin-from'); + const coinTo = document.getElementById('edit-amm-coin-to'); + const swapType = document.getElementById('edit-offer-swap-type'); + + if (coinFrom && coinTo && swapType) { + updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType); + } + } + }); + + dropdown.appendChild(item); + }); + + const selectedOption = select.options[select.selectedIndex]; + text.textContent = selectedOption.textContent.trim(); + icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`; + + display.addEventListener('click', function(e) { + e.stopPropagation(); + dropdown.classList.toggle('hidden'); + }); + + document.addEventListener('click', function() { + dropdown.classList.add('hidden'); + }); + + wrapper.appendChild(display); + wrapper.appendChild(dropdown); + select.parentNode.insertBefore(wrapper, select); + + select.style.display = 'none'; + + select.addEventListener('change', function() { + const selectedOption = this.options[this.selectedIndex]; + text.textContent = selectedOption.textContent.trim(); + icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`; + }); + } + + function createSwapTypeDropdown(select) { + if (!select) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'relative'; + + const display = document.createElement('div'); + display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer'; + + const text = document.createElement('span'); + text.className = 'flex-grow'; + + const arrow = document.createElement('span'); + arrow.className = 'ml-2'; + arrow.innerHTML = ` + + + + `; + + display.appendChild(text); + display.appendChild(arrow); + + const dropdown = document.createElement('div'); + dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto'; + + Array.from(select.options).forEach(option => { + const item = document.createElement('div'); + item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white'; + item.setAttribute('data-value', option.value); + + const optionText = document.createElement('span'); + const displayText = option.getAttribute('data-desc') || option.textContent.trim(); + optionText.textContent = displayText; + + item.appendChild(optionText); + + item.addEventListener('click', function() { + if (select.disabled) return; + + select.value = this.getAttribute('data-value'); + text.textContent = displayText; + dropdown.classList.add('hidden'); + + const event = new Event('change', { bubbles: true }); + select.dispatchEvent(event); + }); + + dropdown.appendChild(item); + }); + + const selectedOption = select.options[select.selectedIndex]; + text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim(); + + display.addEventListener('click', function(e) { + if (select.disabled) return; + e.stopPropagation(); + dropdown.classList.toggle('hidden'); + }); + + document.addEventListener('click', function() { + dropdown.classList.add('hidden'); + }); + + wrapper.appendChild(display); + wrapper.appendChild(dropdown); + select.parentNode.insertBefore(wrapper, select); + + select.style.display = 'none'; + + select.addEventListener('change', function() { + const selectedOption = this.options[this.selectedIndex]; + text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim(); + }); + + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.attributeName === 'disabled') { + if (select.disabled) { + display.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed'); + } else { + display.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed'); + } + } + }); + }); + + observer.observe(select, { attributes: true }); + + if (select.disabled) { + display.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed'); + } + } + + coinSelects.forEach(select => createCoinDropdown(select)); + + swapTypeSelects.forEach(select => createSwapTypeDropdown(select)); + } + + function setupBiddingControls(modalType) { + const checkbox = document.getElementById(`${modalType}-offer-attempt-bids-first`); + const optionsDiv = document.getElementById(`${modalType}-offer-bidding-options`); + + if (checkbox && optionsDiv) { + checkbox.addEventListener('change', function() { + if (this.checked) { + optionsDiv.classList.remove('hidden'); + } else { + optionsDiv.classList.add('hidden'); + } + }); + + if (checkbox.checked) { + optionsDiv.classList.remove('hidden'); + } else { + optionsDiv.classList.add('hidden'); + } + } + } + + function populateBiddingControls(modalType, item) { + if (!item) return; + + const attemptBidsFirst = document.getElementById(`${modalType}-offer-attempt-bids-first`); + const bidStrategy = document.getElementById(`${modalType}-offer-bid-strategy`); + const maxBidPercentage = document.getElementById(`${modalType}-offer-max-bid-percentage`); + const bidRateTolerance = document.getElementById(`${modalType}-offer-bid-rate-tolerance`); + const minRemainingOffer = document.getElementById(`${modalType}-offer-min-remaining-offer`); + + if (attemptBidsFirst) { + attemptBidsFirst.checked = item.attempt_bids_first || false; + } + + if (bidStrategy) { + bidStrategy.value = item.bid_strategy || 'balanced'; + } + + if (maxBidPercentage) { + maxBidPercentage.value = item.max_bid_percentage || '50'; + } + + if (bidRateTolerance) { + bidRateTolerance.value = item.bid_rate_tolerance || '2.0'; + } + + if (minRemainingOffer) { + minRemainingOffer.value = item.min_remaining_offer || '0.001'; + } + + if (attemptBidsFirst) { + attemptBidsFirst.dispatchEvent(new Event('change')); + } + } + + function getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput) { + const coinFromOption = coinFromSelect.options[coinFromSelect.selectedIndex]; + const coinToOption = coinToSelect.options[coinToSelect.selectedIndex]; + + if (!coinFromOption || !coinToOption) { + if (window.showErrorModal) { + window.showErrorModal('Rate Lookup Error', 'Please select both coins before getting the rate.'); + } else { + alert('Coins from and to must be set first.'); + } + return; + } + + const coinFromSymbol = coinFromOption.getAttribute('data-symbol'); + const coinToSymbol = coinToOption.getAttribute('data-symbol'); + + if (!coinFromSymbol || !coinToSymbol) { + if (window.showErrorModal) { + window.showErrorModal('Rate Lookup Error', 'Coin information is incomplete. Please try selecting the coins again.'); + } else { + alert('Coin symbols not found.'); + } + return; + } + + const originalValue = rateInput.value; + rateInput.value = 'Loading...'; + rateInput.disabled = true; + + const getRateButton = rateInput.parentElement.querySelector('button'); + let originalButtonText = ''; + if (getRateButton) { + originalButtonText = getRateButton.textContent; + getRateButton.disabled = true; + getRateButton.innerHTML = ` + + + + + Loading... + `; + } + + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/json/rates'); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.onload = function() { + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + debugLog('Rate response:', response); + + if (response.coingecko && response.coingecko.rate_inferred) { + rateInput.value = response.coingecko.rate_inferred; + + if (getRateButton && originalButtonText) { + getRateButton.disabled = false; + getRateButton.textContent = originalButtonText; + } + } else if (response.error) { + console.error('API error:', response.error); + rateInput.value = originalValue || ''; + if (window.showErrorModal) { + window.showErrorModal('Rate Service Error', `Unable to retrieve rate information: ${response.error}\n\nThis could be due to:\n• Temporary service unavailability\n• Network connectivity issues\n• Invalid coin pair\n\nPlease try again in a few moments.`); + } else { + alert('Error: ' + response.error); + } + } else if (response.coingecko_error) { + console.error('CoinGecko error:', response.coingecko_error); + rateInput.value = originalValue || ''; + + let userMessage = 'Unable to get current market rate from CoinGecko.'; + let details = ''; + + if (typeof response.coingecko_error === 'number') { + switch(response.coingecko_error) { + case 8: + details = 'This usually means:\n• One or both coins are not supported by CoinGecko\n• The trading pair is not available\n• Temporary API limitations\n\nYou can manually enter a rate or try again later.'; + break; + case 429: + details = 'Rate limit exceeded. Please wait a moment and try again.'; + break; + case 404: + details = 'The requested coin pair was not found on CoinGecko.'; + break; + case 500: + details = 'CoinGecko service is temporarily unavailable. Please try again later.'; + break; + default: + details = `Error code: ${response.coingecko_error}\n\nThis may be a temporary issue. Please try again or enter the rate manually.`; + } + } else { + details = `${response.coingecko_error}\n\nPlease try again or enter the rate manually.`; + } + + if (window.showErrorModal) { + window.showErrorModal('Market Rate Unavailable', `${userMessage}\n\n${details}`); + } else { + alert('Unable to get rate from CoinGecko: ' + response.coingecko_error); + } + } else { + rateInput.value = originalValue || ''; + if (window.showErrorModal) { + window.showErrorModal('Rate Not Available', `No current market rate is available for this ${coinFromSymbol}/${coinToSymbol} trading pair.\n\nThis could mean:\n• The coins are not traded together on major exchanges\n• CoinGecko doesn't have data for this pair\n• The coins may not be supported\n\nPlease enter a rate manually based on your research.`); + } else { + alert('No rate available from CoinGecko for this pair.'); + } + } + } catch (e) { + console.error('Error parsing rate data:', e); + rateInput.value = originalValue || ''; + if (window.showErrorModal) { + window.showErrorModal('Data Processing Error', 'Unable to process the rate information received from the server.\n\nThis could be due to:\n• Temporary server issues\n• Data format problems\n• Network interference\n\nPlease try again in a moment.'); + } else { + alert('Error retrieving rate information. Please try again later.'); + } + } + } else { + console.error('Error fetching rate data:', xhr.status, xhr.statusText); + rateInput.value = originalValue || ''; + let errorMessage = 'Unable to retrieve rate information from the server.'; + let details = ''; + + switch(xhr.status) { + case 404: + details = 'The rate service endpoint was not found. This may be a configuration issue.'; + break; + case 500: + details = 'The server encountered an internal error. Please try again later.'; + break; + case 503: + details = 'The rate service is temporarily unavailable. Please try again in a few minutes.'; + break; + case 429: + details = 'Too many requests. Please wait a moment before trying again.'; + break; + default: + details = `Server returned error ${xhr.status}. The rate service may be temporarily unavailable.`; + } + + if (window.showErrorModal) { + window.showErrorModal('Rate Service Unavailable', `${errorMessage}\n\n${details}\n\nYou can enter the rate manually if needed.`); + } else { + alert(`Unable to retrieve rate information (HTTP ${xhr.status}). The rate service may be unavailable.`); + } + } + rateInput.disabled = false; + + if (getRateButton && originalButtonText) { + getRateButton.disabled = false; + getRateButton.textContent = originalButtonText; + } + }; + xhr.onerror = function(e) { + console.error('Network error when fetching rate data:', e); + rateInput.value = originalValue || ''; + rateInput.disabled = false; + + if (getRateButton && originalButtonText) { + getRateButton.disabled = false; + getRateButton.textContent = originalButtonText; + } + + if (window.showErrorModal) { + window.showErrorModal('Network Connection Error', 'Unable to connect to the rate service.\n\nPlease check:\n• Your internet connection\n• BasicSwap server status\n• Firewall settings\n\nTry again once your connection is stable, or enter the rate manually.'); + } else { + alert('Unable to connect to the rate service. Please check your network connection and try again.'); + } + }; + + const params = `coin_from=${encodeURIComponent(coinFromSymbol)}&coin_to=${encodeURIComponent(coinToSymbol)}`; + debugLog('Sending rate request with params:', params); + xhr.send(params); + } + + function setupRateButtons() { + const addGetRateButton = document.getElementById('add-get-rate-button'); + if (addGetRateButton) { + addGetRateButton.addEventListener('click', function() { + const coinFromSelect = document.getElementById('add-amm-coin-from'); + const coinToSelect = document.getElementById('add-amm-coin-to'); + const rateInput = document.getElementById('add-amm-rate'); + + if (coinFromSelect && coinToSelect && rateInput) { + getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput); + } else { + console.error('Missing required elements for rate lookup'); + } + }); + } + + const editGetRateButton = document.getElementById('edit-get-rate-button'); + if (editGetRateButton) { + editGetRateButton.addEventListener('click', function() { + const coinFromSelect = document.getElementById('edit-amm-coin-from'); + const coinToSelect = document.getElementById('edit-amm-coin-to'); + const rateInput = document.getElementById('edit-amm-rate'); + + if (coinFromSelect && coinToSelect && rateInput) { + getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput); + } else { + console.error('Missing required elements for rate lookup'); + } + }); + } + } + + async function initialize(options = {}) { + Object.assign(config, options); + + initializeTabs(); + setupButtonHandlers(); + initializeCustomSelects(); + setupRateButtons(); + + await initializePrices(); + + if (refreshButton) { + refreshButton.addEventListener('click', async function() { + const icon = refreshButton.querySelector('svg'); + if (icon) { + icon.classList.add('animate-spin'); + } + + await initializePrices(); + updateTables(); + + setTimeout(() => { + if (icon) { + icon.classList.remove('animate-spin'); + } + }, 1000); + }); + } + + setupConfigFormListener(); + updateTables(); + startRefreshTimer(); + + debugLog('AMM Tables Manager initialized'); + + return { + updateTables, + startRefreshTimer, + stopRefreshTimer + }; + } + + return { + initialize + }; +})(); + +document.addEventListener('DOMContentLoaded', async function() { + if (typeof AmmTablesManager !== 'undefined') { + window.ammTablesManager = await AmmTablesManager.initialize(); + } +}); diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index ae4effd..b15e2a4 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -30,7 +30,7 @@ const state = { document.addEventListener('tabactivated', function(event) { if (event.detail && event.detail.tabId) { - const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' : + const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' : (event.detail.tabId === '#sent' ? 'sent' : 'received')); //console.log('Tab activation event received for:', tabType); state.currentTab = tabType; @@ -555,7 +555,7 @@ function filterAndSortData(bids) { const coinName = selectedOption?.textContent.trim(); if (coinName) { - const coinToMatch = state.currentTab === 'all' + const coinToMatch = state.currentTab === 'all' ? (bid.source === 'sent' ? bid.coin_to : bid.coin_from) : (state.currentTab === 'sent' ? bid.coin_to : bid.coin_from); if (!coinMatches(coinToMatch, coinName)) { @@ -614,7 +614,7 @@ function filterAndSortData(bids) { let matchesDisplayedLabel = false; if (!matchesLabel && document) { try { - const tableId = state.currentTab === 'sent' ? 'sent' : + const tableId = state.currentTab === 'sent' ? 'sent' : (state.currentTab === 'received' ? 'received' : 'all'); const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`); @@ -1056,6 +1056,24 @@ async function fetchAllBids() { receivedResponse.json() ]); + if (sentData.error || receivedData.error) { + const errorData = sentData.error ? sentData : receivedData; + if (errorData.locked) { + const tbody = elements.allBidsBody; + if (tbody) { + tbody.innerHTML = ` + + + ${errorData.error} + + `; + } + return []; + } else { + throw new Error(errorData.error); + } + } + const filteredSentData = filterAndSortData(sentData).map(bid => ({ ...bid, source: 'sent' })); const filteredReceivedData = filterAndSortData(receivedData).map(bid => ({ ...bid, source: 'received' })); @@ -1090,7 +1108,7 @@ const createTableRow = async (bid) => { const timeColor = getTimeStrokeColor(bid.expire_at); const currentTabIsAll = state.currentTab === 'all'; const isSent = currentTabIsAll ? (bid.source === 'sent') : (state.currentTab === 'sent'); - const sourceIndicator = currentTabIsAll ? + const sourceIndicator = currentTabIsAll ? ` ${isSent ? 'Sent' : 'Received'} ` : ''; @@ -1321,6 +1339,24 @@ async function fetchBids(type = state.currentTab) { } const data = await response.json(); + + if (data.error) { + if (data.locked) { + const tbody = elements[`${type}BidsBody`]; + if (tbody) { + tbody.innerHTML = ` + + + ${data.error} + + `; + } + return []; + } else { + throw new Error(data.error); + } + } + //console.log(`Received raw ${type} data:`, data.length, 'bids'); state.filters.with_expired = includeExpired; @@ -1509,14 +1545,13 @@ const updateBidsTable = async () => { updateLoadingState(true); let bids; - + if (state.currentTab === 'all') { bids = await fetchAllBids(); } else { bids = await fetchBids(); } - // Add identity preloading if we're searching if (state.filters.searchQuery && state.filters.searchQuery.length > 0) { await preloadIdentitiesForSearch(bids); } @@ -1774,7 +1809,7 @@ const setupRefreshButtons = () => { state.data[lowerType] = data; } - + await updateTableContent(lowerType); updatePaginationControls(lowerType); @@ -1802,7 +1837,7 @@ const switchTab = (tabId) => { tooltipIdsToCleanup.clear(); - state.currentTab = tabId === '#all' ? 'all' : + state.currentTab = tabId === '#all' ? 'all' : (tabId === '#sent' ? 'sent' : 'received'); elements.allContent.classList.add('hidden'); @@ -1888,7 +1923,7 @@ const setupEventListeners = () => { elements.sentContent.classList.toggle('hidden', targetId !== '#sent'); elements.receivedContent.classList.toggle('hidden', targetId !== '#received'); - state.currentTab = targetId === '#all' ? 'all' : + state.currentTab = targetId === '#all' ? 'all' : (targetId === '#sent' ? 'sent' : 'received'); state.currentPage[state.currentTab] = 1; @@ -2101,7 +2136,7 @@ function initialize() { }, 100); setupMemoryMonitoring(); - + window.cleanupBidsTable = cleanup; } diff --git a/basicswap/static/js/modules/summary-manager.js b/basicswap/static/js/modules/summary-manager.js index 77317b7..6f25360 100644 --- a/basicswap/static/js/modules/summary-manager.js +++ b/basicswap/static/js/modules/summary-manager.js @@ -4,7 +4,8 @@ const SummaryManager = (function() { summaryEndpoint: '/json', retryDelay: 5000, maxRetries: 3, - requestTimeout: 15000 + requestTimeout: 15000, + debug: false }; let refreshTimer = null; @@ -60,12 +61,15 @@ const SummaryManager = (function() { updateElement('network-offers-counter', data.num_network_offers); updateElement('offers-counter', data.num_sent_active_offers); + updateElement('offers-counter-mobile', data.num_sent_active_offers); updateElement('sent-bids-counter', data.num_sent_active_bids); updateElement('recv-bids-counter', data.num_recv_active_bids); updateElement('bid-requests-counter', data.num_available_bids); updateElement('swaps-counter', data.num_swapping); updateElement('watched-outputs-counter', data.num_watched_outputs); + updateTooltips(data); + const shutdownButtons = document.querySelectorAll('.shutdown-button'); shutdownButtons.forEach(button => { button.setAttribute('data-active-swaps', data.num_swapping); @@ -81,6 +85,100 @@ const SummaryManager = (function() { }); } + function updateTooltips(data) { + debugLog(`updateTooltips called with data:`, data); + + const yourOffersTooltip = document.getElementById('tooltip-your-offers'); + debugLog('Looking for tooltip-your-offers element:', yourOffersTooltip); + + if (yourOffersTooltip) { + const newContent = ` +

Total offers: ${data.num_sent_offers || 0}

+

Active offers: ${data.num_sent_active_offers || 0}

+ `; + + const totalParagraph = yourOffersTooltip.querySelector('p:first-child'); + const activeParagraph = yourOffersTooltip.querySelector('p:last-child'); + + debugLog('Found paragraphs:', { totalParagraph, activeParagraph }); + + if (totalParagraph && activeParagraph) { + totalParagraph.innerHTML = `Total offers: ${data.num_sent_offers || 0}`; + activeParagraph.innerHTML = `Active offers: ${data.num_sent_active_offers || 0}`; + debugLog(`Updated Your Offers tooltip paragraphs: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`); + } else { + yourOffersTooltip.innerHTML = newContent; + debugLog(`Replaced Your Offers tooltip content: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`); + } + + refreshTooltipInstances('tooltip-your-offers', newContent); + } else { + debugLog('Your Offers tooltip element not found - checking all tooltip elements'); + const allTooltips = document.querySelectorAll('[id*="tooltip"]'); + debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id)); + } + + const bidsTooltip = document.getElementById('tooltip-bids'); + if (bidsTooltip) { + const newBidsContent = ` +

Sent bids: ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)

+

Received bids: ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)

+ `; + + const sentParagraph = bidsTooltip.querySelector('p:first-child'); + const recvParagraph = bidsTooltip.querySelector('p:last-child'); + + if (sentParagraph && recvParagraph) { + sentParagraph.innerHTML = `Sent bids: ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)`; + recvParagraph.innerHTML = `Received bids: ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)`; + debugLog(`Updated Bids tooltip: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`); + } else { + bidsTooltip.innerHTML = newBidsContent; + debugLog(`Replaced Bids tooltip content: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`); + } + + refreshTooltipInstances('tooltip-bids', newBidsContent); + } else { + debugLog('Bids tooltip element not found'); + } + } + + function refreshTooltipInstances(tooltipId, newContent) { + const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`); + + triggers.forEach(trigger => { + if (trigger._tippy) { + trigger._tippy.setContent(newContent); + debugLog(`Updated Tippy instance content for ${tooltipId}`); + } else { + if (window.TooltipManager && typeof window.TooltipManager.create === 'function') { + window.TooltipManager.create(trigger, newContent, { + placement: trigger.getAttribute('data-tooltip-placement') || 'top' + }); + debugLog(`Created new Tippy instance for ${tooltipId}`); + } + } + }); + + if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') { + window.TooltipManager.refreshTooltip(tooltipId, newContent); + debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`); + } + + if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') { + setTimeout(() => { + window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`); + debugLog(`Re-initialized tooltips for ${tooltipId}`); + }, 50); + } + } + + function debugLog(message) { + if (config.debug && console && console.log) { + console.log(`[SummaryManager] ${message}`); + } + } + function cacheSummaryData(data) { if (!data) return; @@ -303,6 +401,14 @@ const SummaryManager = (function() { }); }, + updateTooltips: function(data) { + updateTooltips(data || lastSuccessfulData); + }, + + updateUI: function(data) { + updateUIFromData(data || lastSuccessfulData); + }, + startRefreshTimer: function() { startRefreshTimer(); }, diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 18ba205..37bc082 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -217,8 +217,8 @@ function filterAndSortData() { const sentFromFilter = filters.sent_from || 'any'; filteredData = filteredData.filter(offer => { - const isMatch = sentFromFilter === 'public' ? offer.is_public : - sentFromFilter === 'private' ? !offer.is_public : + const isMatch = sentFromFilter === 'public' ? offer.is_public : + sentFromFilter === 'private' ? !offer.is_public : true; return isMatch; }); @@ -232,7 +232,7 @@ function filterAndSortData() { const coinToSelect = document.getElementById('coin_to'); const selectedOption = coinToSelect?.querySelector(`option[value="${filters.coin_to}"]`); const coinName = selectedOption?.textContent.trim(); - + if (coinName && !coinMatches(offer.coin_to, coinName)) { return false; } @@ -254,13 +254,13 @@ function filterAndSortData() { let statusMatch = false; switch (filters.status) { - case 'active': + case 'active': statusMatch = !isExpired && !isRevoked; break; - case 'expired': + case 'expired': statusMatch = isExpired && !isRevoked; break; - case 'revoked': + case 'revoked': statusMatch = isRevoked; break; } @@ -275,7 +275,7 @@ function filterAndSortData() { if (currentSortColumn === 7) { const offersWithPercentages = []; - + for (const offer of filteredData) { const fromAmount = parseFloat(offer.amount_from) || 0; const toAmount = parseFloat(offer.amount_to) || 0; @@ -293,7 +293,7 @@ function filterAndSortData() { if (fromPriceUSD && toPriceUSD && !isNaN(fromPriceUSD) && !isNaN(toPriceUSD)) { const fromValueUSD = fromAmount * fromPriceUSD; const toValueUSD = toAmount * toPriceUSD; - + if (fromValueUSD && toValueUSD) { if (offer.is_own_offer || isSentOffers) { percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; @@ -302,7 +302,7 @@ function filterAndSortData() { } } } - + offersWithPercentages.push({ offer: offer, percentDiff: percentDiff @@ -353,7 +353,7 @@ function filterAndSortData() { const bRate = parseFloat(b.rate) || 0; const aPriceUSD = latestPrices && aSymbol ? latestPrices[aSymbol]?.usd : null; const bPriceUSD = latestPrices && bSymbol ? latestPrices[bSymbol]?.usd : null; - + aValue = aPriceUSD && !isNaN(aPriceUSD) ? aRate * aPriceUSD : 0; bValue = bPriceUSD && !isNaN(bPriceUSD) ? bRate * bPriceUSD : 0; break; @@ -367,8 +367,8 @@ function filterAndSortData() { } if (typeof aValue === 'string' && typeof bValue === 'string') { - return currentSortDirection === 'asc' - ? aValue.localeCompare(bValue) + return currentSortDirection === 'asc' + ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } @@ -395,7 +395,7 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin; } else { if (normalizedCoin === 'zcoin') normalizedCoin = 'firo'; - if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash') + if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash') normalizedCoin = 'bitcoin-cash'; if (normalizedCoin.includes('particl')) normalizedCoin = 'particl'; } @@ -481,6 +481,29 @@ async function fetchOffers() { } const data = await offersResponse.json(); + + if (data.error) { + if (data.locked) { + if (typeof ui !== 'undefined' && ui.displayErrorMessage) { + ui.displayErrorMessage(data.error); + } else { + offersBody.innerHTML = ` + + +
+ + + + ${data.error} +
+ + `; + } + return; + } else { + throw new Error(data.error); + } + } const processedData = Array.isArray(data) ? data : Object.values(data); jsonData = formatInitialData(processedData); @@ -980,7 +1003,7 @@ function createTableRow(offer, identity = null) { } = offer; let coinFromSymbol, coinToSymbol; - + if (window.CoinManager) { coinFromSymbol = window.CoinManager.getSymbol(coinFrom) || coinFrom.toLowerCase(); coinToSymbol = window.CoinManager.getSymbol(coinTo) || coinTo.toLowerCase(); @@ -1572,7 +1595,7 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) { const getPriceKey = (coin) => { if (!coin) return null; - + const lowerCoin = coin.toLowerCase(); if (lowerCoin === 'zcoin') return 'firo'; @@ -1807,7 +1830,7 @@ function getPriceKey(coin) { } if (!coin) return null; - + const lowerCoin = coin.toLowerCase(); if (lowerCoin === 'zcoin') { @@ -1818,7 +1841,7 @@ function getPriceKey(coin) { return 'bitcoin-cash'; } - if (lowerCoin === 'part' || lowerCoin === 'particl' || + if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) { return 'particl'; } @@ -2221,7 +2244,7 @@ document.addEventListener('DOMContentLoaded', async function() { } await updateOffersTable(); - + updateProfitLossDisplays(); document.querySelectorAll('.usd-value').forEach(usdValue => { diff --git a/basicswap/templates/amm.html b/basicswap/templates/amm.html new file mode 100644 index 0000000..f77d602 --- /dev/null +++ b/basicswap/templates/amm.html @@ -0,0 +1,2019 @@ +{% include 'header.html' %} +{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg, arrow_right_svg, input_time_svg %} + + + +
+
+
+ + +
+
+

Automated Market Maker

+ +
+
+
+
+
+ + +
+{% include 'inc_messages.html' %} + +
+
+
+
+
+
+
+ + {% if debug_ui_mode %} + + {% endif %} + +
+
+ +
+
    + + {% if debug_ui_mode %} + + {% endif %} +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
+
+ Name +
+
+
+ Swap +
+
+
+ Amount & Min +
+
+
+ Rate & Receive +
+
+
+ Settings +
+
+
+ Status +
+
+
+ Actions +
+
+ Loading offers data... +
+
+
+ + {% if debug_ui_mode %} + + {% endif %} +
+
+
+
+
+
+ +
+
+
+
+
+

Control

+
+
+ +
+ + {{ current_status|capitalize }} + +
+
+ + + + {% if debug_ui_mode %} +
+ + +
+ +
+ + +
+ {% else %} + + + {% endif %} + +
+ + Automatically start AMM when BasicSwap starts +
+ + {% if debug_ui_mode %} +
+ +
+ {% endif %} + +
+ + +
+ + {% if debug_ui_mode %} +
+

Process Management

+
+ + + +
+

+ Use "Check Processes" to see running AMM processes. Use "Kill Orphans" to clean up duplicate processes. Use "Force Start" to automatically clean up and start fresh. +

+
+ {% endif %} +
+
+ + {% if debug_ui_mode %} +
+

Files

+
+

+ AMM Directory: {{ amm_dir }} +

+

+ Config File: {{ config_path }} + {% if config_exists %} + Exists + {% else %} + Missing + {% endif %} +

+

+ State File: {{ state_path }} + {% if state_exists %} + Exists + {% else %} + Will be created + {% endif %} +

+

+ Script Module: {{ script_path }} + Integrated +

+
+ +
+ {% endif %} +
+ +
+
+

Configuration

+ +
+
    + + + +
+
+ +
+ +
+
+
+ + + + +
+ {% if debug_mode %} +
+ + +
+ {% endif %} +
+ + {% if debug_ui_mode %} + + {% endif %} +
+
+
+
+ + + + +
+ + + +
+ + {% if state_exists and debug_ui_mode %} +
+

State File (JSON)

+
+
{{ state_content }}
+
+
+ + +
+
+
+
+ {% endif %} +
+
+ + {% if debug_ui_mode %} +
+
+

AMM Logs

+
+ {% if logs %} + {% for log in logs %} +
{{ log }}
+ {% endfor %} + {% else %} +
No logs available
+ {% endif %} +
+
+
+ {% endif %} +
+
+
+ + + + + + + + + + + + +{% include 'footer.html' %} diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html index 2d083a7..c9c87b1 100644 --- a/basicswap/templates/bid_xmr.html +++ b/basicswap/templates/bid_xmr.html @@ -833,6 +833,27 @@ + @@ -840,9 +861,74 @@ {% include 'footer.html' %} diff --git a/basicswap/templates/donation.html b/basicswap/templates/donation.html new file mode 100644 index 0000000..5809d16 --- /dev/null +++ b/basicswap/templates/donation.html @@ -0,0 +1,334 @@ +{% include 'header.html' %} +{% from 'style.html' import breadcrumb_line_svg, love_svg %} + +
+
+
+ dots-red + wave +
+
+

+ Support BasicSwap Development +

+

+ Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth. +

+
+
+
+
+
+ +
+ +
+
+
+

Why Your Support Matters

+
+ {{ love_svg | safe }} +
+
+

+ BasicSwap is completely free and open-source software that charges no fees for its use. The project is entirely funded by generous community donations from users who believe in decentralized, censorship-resistant trading. +

+

+ Your donations are vital to keeping this project alive, accelerating development, and expanding our reach to more users who value financial freedom and privacy. +

+ +
+
+
+ + + +
+

Core Development

+

New features and improvements

+
+
+
+ + + +
+

Security Audits

+

Testing and security infrastructure

+
+
+
+ + + +
+

Documentation

+

Educational resources and guides

+
+
+
+ + + +
+

Community Growth

+

Outreach and adoption initiatives

+
+
+ +

+ Together, we're building financial tools that empower individuals and resist censorship. Thank you for being part of this movement toward true financial freedom. +

+
+
+
+
+ +
+
+
+
+
+
+
+
+

+ Donation Addresses +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Cryptocurrency +
+
+
+ Donation Address +
+
+ + Monero + + Monero (XMR) + + +
+ + Litecoin + + Litecoin (LTC) + + +
+ + Litecoin MWEB + + Litecoin MWEB + + +
+ + Bitcoin + + Bitcoin (BTC) + + +
+ + Particl + + Particl (PART) + + +
+
+ +
+

+ Every contribution helps make decentralized trading more accessible to everyone. +

+
+
+
+
+
+
+
+
+
+ + + +{% include 'footer.html' %} diff --git a/basicswap/templates/footer.html b/basicswap/templates/footer.html index 3ab89eb..dd5c88a 100644 --- a/basicswap/templates/footer.html +++ b/basicswap/templates/footer.html @@ -8,6 +8,7 @@
+ @@ -25,7 +26,7 @@

© 2025~ (BSX) BasicSwap

BSX: v{{ version }}

-

GUI: v3.2.1

+

GUI: v3.2.2

Made with

{{ love_svg | safe }}
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 0f84a8d..5041143 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -1,13 +1,12 @@ - -{% from 'style.html' import change_password_svg, notifications_network_offer_svg, - notifications_bid_accepted_svg, notifications_unknow_event_svg, - notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, - wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, - cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, - shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, - wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, - swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, - bids_sent_svg, header_arrow_down_svg, love_svg %} +{% from 'style.html' import change_password_svg, notifications_network_offer_svg, + notifications_bid_accepted_svg, notifications_unknow_event_svg, + notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, + wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, + cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, + shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, + wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, + swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, + bids_sent_svg, header_arrow_down_svg, love_svg, mobile_love_svg, amm_active_svg, amm_inactive_svg %} @@ -47,7 +46,7 @@ })(); (function() { - const isDarkMode = localStorage.getItem('color-theme') === 'dark' || + const isDarkMode = localStorage.getItem('color-theme') === 'dark' || (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); if (!localStorage.getItem('color-theme')) { @@ -78,6 +77,7 @@ + {% if current_page == 'wallets' or current_page == 'wallet' %} {% endif %} @@ -93,9 +93,9 @@
-

@@ -107,14 +107,14 @@

This action will shut down the application. Are you sure you want to proceed?

- - @@ -136,7 +136,7 @@ - + + + + -

- diff --git a/basicswap/templates/style.html b/basicswap/templates/style.html index 4bf0cbc..2abccd9 100644 --- a/basicswap/templates/style.html +++ b/basicswap/templates/style.html @@ -471,14 +471,48 @@ - + - + + + + +' %} + +{% set amm_active_svg = ' + + + + + + + + + + + + + + +' %} + +{% set amm_inactive_svg = ' + + + + + + + + + + + @@ -502,6 +536,20 @@ ' %} +{% set mobile_love_svg = ' + + + + + +' %} +{% set donation_svg = ' + + + + + +' %} {% set github_svg = ' @@ -607,7 +655,7 @@ - + diff --git a/basicswap/templates/unlock.html b/basicswap/templates/unlock.html index 40270bc..136e43a 100644 --- a/basicswap/templates/unlock.html +++ b/basicswap/templates/unlock.html @@ -8,7 +8,7 @@ {% endif %} (BSX) BasicSwap - v{{ version }} - > + @@ -37,7 +37,7 @@ })(); (function() { - const isDarkMode = localStorage.getItem('color-theme') === 'dark' || + const isDarkMode = localStorage.getItem('color-theme') === 'dark' || (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); if (!localStorage.getItem('color-theme')) { @@ -77,9 +77,9 @@