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 `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ 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'}
+
+
+ ${activeOffersCount} Running Offer${activeOffersCount !== 1 ? 's' : ''}
+
+
+
+
+
+
+
+ `;
+ });
+
+ 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'}
+
+
+ ${activeBidsCount} Running Bid${activeBidsCount !== 1 ? 's' : ''}
+
+
+
+
+
+
+
+ `;
+ });
+
+ 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 = `
+
+
+
+
+ `;
+ }
+ 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
+
+ Automatically create offers and bids based on your configuration.
+
+
+
+
+
+
+
+
+
+{% include 'inc_messages.html' %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Offer
+
+ {% if debug_ui_mode %}
+
+
+
+
+ Add Bid
+
+ {% endif %}
+
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+ Offers (0)
+
+
+ {% if debug_ui_mode %}
+
+
+ Bids (0)
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+ Swap
+
+
+
+
+ Amount & Min
+
+
+
+
+ Rate & Receive
+
+
+
+
+ Settings
+
+
+
+
+ Status
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+ Loading offers data...
+
+
+
+
+
+
+
+ {% if debug_ui_mode %}
+
+
+
+
+
+
+
+ Name
+
+
+
+
+ Swap
+
+
+
+
+ Amount & Balance
+
+
+
+
+ Rate & Send
+
+
+
+
+ Settings
+
+
+
+
+ Status
+
+
+
+
+ Actions
+
+
+
+
+
+
+
+ Loading bids data...
+
+
+
+
+
+
+ {% 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
+
+
+
+
+ JSON View
+
+
+ Global Settings
+
+
+ Settings FAQ
+
+
+
+
+
+
+
+ {% if state_exists and debug_ui_mode %}
+
+
State File (JSON)
+
+
{{ state_content }}
+
+
+
+ Clear AMM State
+
+
+
+
+
+
+ {% endif %}
+
+
+
+ {% if debug_ui_mode %}
+
+
+
AMM Logs
+
+ {% if logs %}
+ {% for log in logs %}
+
{{ log }}
+ {% endfor %}
+ {% else %}
+
No logs available
+ {% endif %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
Add New Item
+
+
+
+
+
+ Name *
+
+
+
+
+
+ Enabled
+
+
+
+
+
Maker
+
+
+
+
+
+ {% for c in coins %}
+
+ {{ c[1] }}
+
+ {% endfor %}
+
+
+
+
+
+
Taker
+
+
+
+
+
+ {% for c in coins %}
+
+ {{ c[1] }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+ Offer Amount *
+
+
+
+
+
Minimum Rate *
+
+
+ Get Rate
+
+
+
+
+
+
+
+
+
+
+
+
Swap Type
+
+
+
+
+
+ Adaptor Sig (default)
+ Secret Hash
+
+
+
+
+
+ Min Swap Amount
+
+
+
+
+
+
+ Offer Size Increment *
+
+
+ How much to adjust offer size when partially filled. Must be between 0.001 and your offer amount.
+
+
+
+ {% if debug_ui_mode %}
+
+
Auto Accept Bids
+
+
+
+
+
+ Accept All Bids
+ Accept Known Identities
+ No Auto Accept
+
+
+
+ {% endif %}
+
+
+
+
Adjust Rates Based on Market
+
+
+
+
+
+ Coingecko - Use CoinGecko, fail if rates not found
+ Orderbook - Use Orderbook auto-accept offers only, fail if rates not found
+ Either - Use greater of CoinGecko + Orderbook, fail if BOTH unavailable
+ Both - Use greater of CoinGecko + Orderbook, fail if EITHER unavailable
+ Orderbook (Fallback) - Fallback to Minimum Rate
+ Static - Exclusively use Minimum Rate + Tweak
+
+
+
+
+ {% if debug_ui_mode %}
+
+
+
+ Attempt Bids First
+
+
Before creating an AMM offer, attempt to fill existing offers by placing bids, then create offer for remainder.
+
+
+
+
+
Bidding Strategy
+
+
+
+
+
+ Aggressive - Fill as much as possible
+ Balanced - Mix of bids and offers
+ Conservative - Only obvious wins
+ Best rates from auto-accept offers only
+
+
+
+
+
+ Max Bid Percentage
+
+ Maximum % of offer amount to use for bidding (0-100)
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
Offers to Bid On
+
+
+
+
+
+ All Offers
+ Auto-Accept Offers Only
+ Known Identities Only
+
+
+
+
+
+ Address
+
+
+
+
+
+
+ Min Swap Amount
+
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Edit Item
+
+
+
+
+
+
+
+ Name *
+
+
+
+
+
+ Enabled
+
+
+
+
+
Maker
+
+
+
+
+
+ {% for c in coins %}
+
+ {{ c[1] }}
+
+ {% endfor %}
+
+
+
+
+
+
Taker
+
+
+
+
+
+ {% for c in coins %}
+
+ {{ c[1] }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+ Offer Amount *
+
+
+
+
+
Minimum Rate *
+
+
+ Get Rate
+
+
+
+
+
+
+
+
+
+
+
+
Swap Type
+
+
+
+
+
+ Adaptor Sig
+ Secret Hash
+
+
+
+
+
+ Minimum Swap Amount
+
+
+
+
+
+
+ Offer Size Increment *
+
+
+ How much to adjust offer size when partially filled. Must be between 0.001 and your offer amount.
+
+
+
+ {% if debug_ui_mode %}
+
+
Auto Accept Bids
+
+
+
+
+
+ Accept All Bids
+ Accept Known Identities
+ No Auto Accept
+
+
+
+ {% endif %}
+
+
+
+
Adjust Rates Based on Market
+
+
+
+
+
+ Coingecko - Use CoinGecko, fail if rates not found
+ Orderbook - Use Orderbook auto-accept offers only, fail if rates not found
+ Either - Use greater of CoinGecko + Orderbook, fail if BOTH unavailable
+ Both - Use greater of CoinGecko + Orderbook, fail if EITHER unavailable
+ Orderbook (Fallback) - Fallback to Minimum Rate
+ Static - Exclusively use Minimum Rate + Tweak
+
+
+
+
+ {% if debug_ui_mode %}
+
+
+
+ Attempt Bids First
+
+
Before creating an AMM offer, attempt to fill existing offers by placing bids, then create offer for remainder.
+
+
+
+
+
Bidding Strategy
+
+
+
+
+
+ Aggressive - Fill as much as possible
+ Balanced - Mix of bids and offers
+ Conservative - Only obvious wins
+ Best rates from auto-accept offers only
+
+
+
+
+
+ Max Bid Percentage
+
+ Maximum % of offer amount to use for bidding (0-100)
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
Offers to Bid On
+
+
+
+
+
+ All Offers
+ Auto-Accept Offers Only
+ Known Identities Only
+
+
+
+
+
+ Address
+
+
+
+
+
+
+ Min Swap Amount
+
+
+
+
+
+
+
+
+
+
+
+
+ Save Changes
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
Error
+
An error occurred
+
+
+ OK
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Action
+
Are you sure?
+
+
+ Confirm
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+{% 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 @@
+
+
+
+
+
+
Confirm Action
+
Are you sure?
+
+
+ Confirm
+
+
+ Cancel
+
+
+
+
+
+
@@ -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 %}
+
+
+
+
+
+
+
+
+
+ Support BasicSwap Development
+
+
+ Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth.
+
+
+
+
+
+
+
+