mirror of
https://github.com/basicswap/basicswap.git
synced 2026-04-08 18:37:23 +02:00
Litewallets
This commit is contained in:
@@ -5,10 +5,12 @@
|
|||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import socks
|
import socks
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -55,7 +57,7 @@ class BaseApp(DBMethods):
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.coin_clients = {}
|
self.coin_clients = {}
|
||||||
self.coin_interfaces = {}
|
self.coin_interfaces = {}
|
||||||
self.mxDB = threading.Lock()
|
self.mxDB = threading.RLock()
|
||||||
self.debug = self.settings.get("debug", False)
|
self.debug = self.settings.get("debug", False)
|
||||||
self.delay_event = threading.Event()
|
self.delay_event = threading.Event()
|
||||||
self.chainstate_delay_event = threading.Event()
|
self.chainstate_delay_event = threading.Event()
|
||||||
@@ -156,6 +158,71 @@ class BaseApp(DBMethods):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def getElectrumAddressIndex(self, coin_name: str) -> tuple:
|
||||||
|
try:
|
||||||
|
chain_settings = self.settings["chainclients"].get(coin_name, {})
|
||||||
|
ext_idx = chain_settings.get("electrum_address_index", 0)
|
||||||
|
int_idx = chain_settings.get("electrum_internal_address_index", 0)
|
||||||
|
return (ext_idx, int_idx)
|
||||||
|
except Exception:
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
def updateElectrumAddressIndex(
|
||||||
|
self, coin_name: str, ext_idx: int, int_idx: int
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
if coin_name not in self.settings["chainclients"]:
|
||||||
|
self.log.debug(
|
||||||
|
f"updateElectrumAddressIndex: {coin_name} not in chainclients"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
chain_settings = self.settings["chainclients"][coin_name]
|
||||||
|
current_ext = chain_settings.get("electrum_address_index", 0)
|
||||||
|
current_int = chain_settings.get("electrum_internal_address_index", 0)
|
||||||
|
|
||||||
|
if ext_idx <= current_ext and int_idx <= current_int:
|
||||||
|
return
|
||||||
|
|
||||||
|
if ext_idx > current_ext:
|
||||||
|
chain_settings["electrum_address_index"] = ext_idx
|
||||||
|
if int_idx > current_int:
|
||||||
|
chain_settings["electrum_internal_address_index"] = int_idx
|
||||||
|
|
||||||
|
self.log.debug(
|
||||||
|
f"Persisting electrum address index for {coin_name}: ext={ext_idx}, int={int_idx}"
|
||||||
|
)
|
||||||
|
self._saveSettings()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning(
|
||||||
|
f"Failed to update electrum address index for {coin_name}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _normalizeSettingsPaths(self, settings: dict) -> dict:
|
||||||
|
if "chainclients" in settings:
|
||||||
|
for coin_name, cc in settings["chainclients"].items():
|
||||||
|
for path_key in ("datadir", "bindir", "walletsdir"):
|
||||||
|
if path_key in cc and isinstance(cc[path_key], str):
|
||||||
|
cc[path_key] = os.path.normpath(cc[path_key])
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def _saveSettings(self) -> None:
|
||||||
|
from basicswap import config as cfg
|
||||||
|
|
||||||
|
self._normalizeSettingsPaths(self.settings)
|
||||||
|
|
||||||
|
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
|
||||||
|
settings_path_new = settings_path + ".new"
|
||||||
|
try:
|
||||||
|
if os.path.exists(settings_path):
|
||||||
|
shutil.copyfile(settings_path, settings_path + ".last")
|
||||||
|
with open(settings_path_new, "w") as fp:
|
||||||
|
json.dump(self.settings, fp, indent=4)
|
||||||
|
shutil.move(settings_path_new, settings_path)
|
||||||
|
self.log.debug(f"Settings saved to {settings_path}")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning(f"Failed to save settings: {e}")
|
||||||
|
|
||||||
def setDaemonPID(self, name, pid) -> None:
|
def setDaemonPID(self, name, pid) -> None:
|
||||||
if isinstance(name, Coins):
|
if isinstance(name, Coins):
|
||||||
self.coin_clients[name]["pid"] = pid
|
self.coin_clients[name]["pid"] = pid
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -212,6 +212,10 @@ class EventLogTypes(IntEnum):
|
|||||||
BCH_MERCY_TX_FOUND = auto()
|
BCH_MERCY_TX_FOUND = auto()
|
||||||
LOCK_TX_A_IN_MEMPOOL = auto()
|
LOCK_TX_A_IN_MEMPOOL = auto()
|
||||||
LOCK_TX_A_CONFLICTS = auto()
|
LOCK_TX_A_CONFLICTS = auto()
|
||||||
|
LOCK_TX_B_RPC_ERROR = auto()
|
||||||
|
LOCK_TX_A_SPEND_TX_SEEN = auto()
|
||||||
|
LOCK_TX_B_SPEND_TX_SEEN = auto()
|
||||||
|
LOCK_TX_B_REFUND_TX_SEEN = auto()
|
||||||
|
|
||||||
|
|
||||||
class XmrSplitMsgTypes(IntEnum):
|
class XmrSplitMsgTypes(IntEnum):
|
||||||
@@ -247,6 +251,7 @@ class NotificationTypes(IntEnum):
|
|||||||
BID_ACCEPTED = auto()
|
BID_ACCEPTED = auto()
|
||||||
SWAP_COMPLETED = auto()
|
SWAP_COMPLETED = auto()
|
||||||
UPDATE_AVAILABLE = auto()
|
UPDATE_AVAILABLE = auto()
|
||||||
|
SWEEP_COMPLETED = auto()
|
||||||
|
|
||||||
|
|
||||||
class ConnectionRequestTypes(IntEnum):
|
class ConnectionRequestTypes(IntEnum):
|
||||||
@@ -458,6 +463,8 @@ def describeEventEntry(event_type, event_msg):
|
|||||||
return "Failed to publish lock tx B refund"
|
return "Failed to publish lock tx B refund"
|
||||||
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
||||||
return "Detected invalid lock Tx B"
|
return "Detected invalid lock Tx B"
|
||||||
|
if event_type == EventLogTypes.LOCK_TX_B_RPC_ERROR:
|
||||||
|
return "Temporary RPC error checking lock tx B: " + event_msg
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
||||||
return "Lock tx A pre-refund tx published"
|
return "Lock tx A pre-refund tx published"
|
||||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
||||||
@@ -498,6 +505,12 @@ def describeEventEntry(event_type, event_msg):
|
|||||||
return "BCH mercy tx found"
|
return "BCH mercy tx found"
|
||||||
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
|
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
|
||||||
return "Lock tx B mercy tx published"
|
return "Lock tx B mercy tx published"
|
||||||
|
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_SEEN:
|
||||||
|
return "Lock tx A spend tx seen in chain"
|
||||||
|
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_SEEN:
|
||||||
|
return "Lock tx B spend tx seen in chain"
|
||||||
|
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_SEEN:
|
||||||
|
return "Lock tx B refund tx seen in chain"
|
||||||
|
|
||||||
|
|
||||||
def getVoutByAddress(txjs, p2sh):
|
def getVoutByAddress(txjs, p2sh):
|
||||||
|
|||||||
@@ -1748,6 +1748,13 @@ def printHelp():
|
|||||||
)
|
)
|
||||||
print("--client-auth-password= Set or update the password to protect the web UI.")
|
print("--client-auth-password= Set or update the password to protect the web UI.")
|
||||||
print("--disable-client-auth Remove password protection from the web UI.")
|
print("--disable-client-auth Remove password protection from the web UI.")
|
||||||
|
print(
|
||||||
|
"--light Use light wallet mode (Electrum) for all supported coins."
|
||||||
|
)
|
||||||
|
print("--btc-mode=MODE Set BTC connection mode: rpc, electrum, or remote.")
|
||||||
|
print("--ltc-mode=MODE Set LTC connection mode: rpc, electrum, or remote.")
|
||||||
|
print("--btc-electrum-server= Custom Electrum server for BTC (host:port:ssl).")
|
||||||
|
print("--ltc-electrum-server= Custom Electrum server for LTC (host:port:ssl).")
|
||||||
|
|
||||||
active_coins = []
|
active_coins = []
|
||||||
for coin_name in known_coins.keys():
|
for coin_name in known_coins.keys():
|
||||||
@@ -1955,6 +1962,11 @@ def initialise_wallets(
|
|||||||
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
|
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
|
||||||
createDCRWallet(args, hex_seed, logger, threading.Event())
|
createDCRWallet(args, hex_seed, logger, threading.Event())
|
||||||
continue
|
continue
|
||||||
|
if coin_settings.get("connection_type") == "electrum":
|
||||||
|
logger.info(
|
||||||
|
f"Skipping RPC wallet creation for {getCoinName(c)} (electrum mode)."
|
||||||
|
)
|
||||||
|
continue
|
||||||
swap_client.waitForDaemonRPC(c, with_wallet=False)
|
swap_client.waitForDaemonRPC(c, with_wallet=False)
|
||||||
# Create wallet if it doesn't exist yet
|
# Create wallet if it doesn't exist yet
|
||||||
wallets = swap_client.callcoinrpc(c, "listwallets")
|
wallets = swap_client.callcoinrpc(c, "listwallets")
|
||||||
@@ -2052,7 +2064,11 @@ def initialise_wallets(
|
|||||||
c = swap_client.getCoinIdFromName(coin_name)
|
c = swap_client.getCoinIdFromName(coin_name)
|
||||||
if c in (Coins.PART,):
|
if c in (Coins.PART,):
|
||||||
continue
|
continue
|
||||||
if c not in (Coins.DCR,):
|
if coin_settings.get("connection_type") == "electrum":
|
||||||
|
logger.info(
|
||||||
|
f"Skipping daemon RPC wait for {getCoinName(c)} (electrum mode)."
|
||||||
|
)
|
||||||
|
elif c not in (Coins.DCR,):
|
||||||
# initialiseWallet only sets main_wallet_seedid_
|
# initialiseWallet only sets main_wallet_seedid_
|
||||||
swap_client.waitForDaemonRPC(c)
|
swap_client.waitForDaemonRPC(c)
|
||||||
try:
|
try:
|
||||||
@@ -2279,6 +2295,9 @@ def main():
|
|||||||
tor_control_password = None
|
tor_control_password = None
|
||||||
client_auth_pwd_value = None
|
client_auth_pwd_value = None
|
||||||
disable_client_auth_flag = False
|
disable_client_auth_flag = False
|
||||||
|
light_mode = False
|
||||||
|
coin_modes = {}
|
||||||
|
electrum_servers = {}
|
||||||
extra_opts = {}
|
extra_opts = {}
|
||||||
|
|
||||||
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
|
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
|
||||||
@@ -2433,6 +2452,32 @@ def main():
|
|||||||
if name == "disable-client-auth":
|
if name == "disable-client-auth":
|
||||||
disable_client_auth_flag = True
|
disable_client_auth_flag = True
|
||||||
continue
|
continue
|
||||||
|
if name == "light":
|
||||||
|
light_mode = True
|
||||||
|
continue
|
||||||
|
if name.endswith("-mode") and len(s) == 2:
|
||||||
|
coin_prefix = name[:-5]
|
||||||
|
mode_value = s[1].strip().lower()
|
||||||
|
if mode_value not in ("rpc", "electrum", "remote"):
|
||||||
|
exitWithError(
|
||||||
|
f"Invalid mode '{mode_value}' for {coin_prefix}. Use: rpc, electrum, or remote"
|
||||||
|
)
|
||||||
|
coin_modes[coin_prefix] = mode_value
|
||||||
|
continue
|
||||||
|
if name.endswith("-electrum-server") and len(s) == 2:
|
||||||
|
coin_prefix = name[:-16]
|
||||||
|
server_str = s[1].strip()
|
||||||
|
parts = server_str.split(":")
|
||||||
|
if len(parts) >= 2:
|
||||||
|
server = {
|
||||||
|
"host": parts[0],
|
||||||
|
"port": int(parts[1]),
|
||||||
|
"ssl": parts[2].lower() == "true" if len(parts) > 2 else True,
|
||||||
|
}
|
||||||
|
if coin_prefix not in electrum_servers:
|
||||||
|
electrum_servers[coin_prefix] = []
|
||||||
|
electrum_servers[coin_prefix].append(server)
|
||||||
|
continue
|
||||||
if len(s) != 2:
|
if len(s) != 2:
|
||||||
exitWithError("Unknown argument {}".format(v))
|
exitWithError("Unknown argument {}".format(v))
|
||||||
exitWithError("Unknown argument {}".format(v))
|
exitWithError("Unknown argument {}".format(v))
|
||||||
@@ -2791,6 +2836,32 @@ def main():
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
electrum_supported_coins = {
|
||||||
|
"bitcoin": "btc",
|
||||||
|
"litecoin": "ltc",
|
||||||
|
}
|
||||||
|
|
||||||
|
for coin_name, coin_prefix in electrum_supported_coins.items():
|
||||||
|
if coin_name not in chainclients:
|
||||||
|
continue
|
||||||
|
|
||||||
|
use_electrum = False
|
||||||
|
if light_mode and coin_name != "particl":
|
||||||
|
use_electrum = True
|
||||||
|
if coin_prefix in coin_modes:
|
||||||
|
if coin_modes[coin_prefix] == "electrum":
|
||||||
|
use_electrum = True
|
||||||
|
elif coin_modes[coin_prefix] == "rpc":
|
||||||
|
use_electrum = False
|
||||||
|
|
||||||
|
if use_electrum:
|
||||||
|
chainclients[coin_name]["connection_type"] = "electrum"
|
||||||
|
chainclients[coin_name]["manage_daemon"] = False
|
||||||
|
if coin_prefix in electrum_servers:
|
||||||
|
chainclients[coin_name]["electrum_servers"] = electrum_servers[
|
||||||
|
coin_prefix
|
||||||
|
]
|
||||||
|
|
||||||
for coin_name, coin_settings in chainclients.items():
|
for coin_name, coin_settings in chainclients.items():
|
||||||
coin_id = getCoinIdFromName(coin_name)
|
coin_id = getCoinIdFromName(coin_name)
|
||||||
coin_params = chainparams[coin_id]
|
coin_params = chainparams[coin_id]
|
||||||
|
|||||||
@@ -303,21 +303,24 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
|
|||||||
if prepare is False and use_tor_proxy:
|
if prepare is False and use_tor_proxy:
|
||||||
if coin_id == Coins.BCH:
|
if coin_id == Coins.BCH:
|
||||||
# Without this BCH (27.1) will bind to the default BTC port, even with proxy set
|
# Without this BCH (27.1) will bind to the default BTC port, even with proxy set
|
||||||
extra_args.append("--bind=127.0.0.1:" + str(int(coin_settings["port"])))
|
port: int = int(coin_settings["port"])
|
||||||
|
onionport: int = coin_settings.get("onionport", 8335)
|
||||||
|
extra_args.append(f"--bind=127.0.0.1:{port}")
|
||||||
|
extra_args.append(f"--bind=127.0.0.1:{onionport}=onion")
|
||||||
else:
|
else:
|
||||||
extra_args.append("--port=" + str(int(coin_settings["port"])))
|
extra_args.append("--port=" + str(int(coin_settings["port"])))
|
||||||
|
|
||||||
# BTC versions from v28 fail to start if the onionport is in use.
|
# BTC versions from v28 fail to start if the onionport is in use.
|
||||||
# As BCH may use port 8334, disable it here.
|
# As BCH may use port 8334, disable it here.
|
||||||
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
|
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
|
||||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
|
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
|
||||||
if (
|
if prepare is False and coin_id in (Coins.BTC, Coins.NMC):
|
||||||
prepare is False
|
|
||||||
and use_tor_proxy is False
|
|
||||||
and coin_id in (Coins.BTC, Coins.NMC)
|
|
||||||
):
|
|
||||||
port: int = coin_settings.get("port", 8333)
|
port: int = coin_settings.get("port", 8333)
|
||||||
extra_args.append(f"--bind=0.0.0.0:{port}")
|
if use_tor_proxy:
|
||||||
|
onionport: int = coin_settings.get("onionport", 8334)
|
||||||
|
extra_args.append(f"--bind=0.0.0.0:{port}")
|
||||||
|
extra_args.append(f"--bind=127.0.0.1:{onionport}=onion")
|
||||||
|
else:
|
||||||
|
extra_args.append(f"--bind=0.0.0.0:{port}")
|
||||||
return extra_args
|
return extra_args
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from enum import IntEnum, auto
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
CURRENT_DB_VERSION = 33
|
CURRENT_DB_VERSION = 34
|
||||||
CURRENT_DB_DATA_VERSION = 7
|
CURRENT_DB_DATA_VERSION = 7
|
||||||
|
|
||||||
|
|
||||||
@@ -772,8 +772,13 @@ class NetworkPortal(Table):
|
|||||||
created_at = Column("integer")
|
created_at = Column("integer")
|
||||||
|
|
||||||
|
|
||||||
def extract_schema(input_globals=None) -> dict:
|
def extract_schema(extra_tables: list = None) -> dict:
|
||||||
g = globals() if input_globals is None else input_globals
|
g = globals().copy()
|
||||||
|
|
||||||
|
if extra_tables:
|
||||||
|
for table_class in extra_tables:
|
||||||
|
g[table_class.__name__] = table_class
|
||||||
|
|
||||||
tables = {}
|
tables = {}
|
||||||
for name, obj in g.items():
|
for name, obj in g.items():
|
||||||
if not inspect.isclass(obj):
|
if not inspect.isclass(obj):
|
||||||
@@ -893,18 +898,18 @@ def create_table(c, table_name, table) -> None:
|
|||||||
c.execute(query)
|
c.execute(query)
|
||||||
|
|
||||||
|
|
||||||
def create_db_(con, log) -> None:
|
def create_db_(con, log, extra_tables: list = None) -> None:
|
||||||
db_schema = extract_schema()
|
db_schema = extract_schema(extra_tables=extra_tables)
|
||||||
c = con.cursor()
|
c = con.cursor()
|
||||||
for table_name, table in db_schema.items():
|
for table_name, table in db_schema.items():
|
||||||
create_table(c, table_name, table)
|
create_table(c, table_name, table)
|
||||||
|
|
||||||
|
|
||||||
def create_db(db_path: str, log) -> None:
|
def create_db(db_path: str, log, extra_tables: list = None) -> None:
|
||||||
con = None
|
con = None
|
||||||
try:
|
try:
|
||||||
con = sqlite3.connect(db_path)
|
con = sqlite3.connect(db_path)
|
||||||
create_db_(con, log)
|
create_db_(con, log, extra_tables=extra_tables)
|
||||||
con.commit()
|
con.commit()
|
||||||
finally:
|
finally:
|
||||||
if con:
|
if con:
|
||||||
@@ -912,42 +917,63 @@ def create_db(db_path: str, log) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class DBMethods:
|
class DBMethods:
|
||||||
|
_db_lock_depth = 0
|
||||||
|
|
||||||
|
def _db_lock_held(self) -> bool:
|
||||||
|
|
||||||
|
if hasattr(self.mxDB, "_is_owned"):
|
||||||
|
return self.mxDB._is_owned()
|
||||||
|
return self.mxDB.locked()
|
||||||
|
|
||||||
def openDB(self, cursor=None):
|
def openDB(self, cursor=None):
|
||||||
if cursor:
|
if cursor:
|
||||||
# assert(self._thread_debug == threading.get_ident())
|
assert self._db_lock_held()
|
||||||
assert self.mxDB.locked()
|
|
||||||
return cursor
|
return cursor
|
||||||
|
|
||||||
|
if self._db_lock_held():
|
||||||
|
self._db_lock_depth += 1
|
||||||
|
return self._db_con.cursor()
|
||||||
|
|
||||||
self.mxDB.acquire()
|
self.mxDB.acquire()
|
||||||
# self._thread_debug = threading.get_ident()
|
self._db_lock_depth = 1
|
||||||
self._db_con = sqlite3.connect(self.sqlite_file)
|
self._db_con = sqlite3.connect(self.sqlite_file)
|
||||||
|
|
||||||
|
self._db_con.execute("PRAGMA busy_timeout = 30000")
|
||||||
return self._db_con.cursor()
|
return self._db_con.cursor()
|
||||||
|
|
||||||
def getNewDBCursor(self):
|
def getNewDBCursor(self):
|
||||||
assert self.mxDB.locked()
|
assert self._db_lock_held()
|
||||||
return self._db_con.cursor()
|
return self._db_con.cursor()
|
||||||
|
|
||||||
def commitDB(self):
|
def commitDB(self):
|
||||||
assert self.mxDB.locked()
|
assert self._db_lock_held()
|
||||||
self._db_con.commit()
|
self._db_con.commit()
|
||||||
|
|
||||||
def rollbackDB(self):
|
def rollbackDB(self):
|
||||||
assert self.mxDB.locked()
|
assert self._db_lock_held()
|
||||||
self._db_con.rollback()
|
self._db_con.rollback()
|
||||||
|
|
||||||
def closeDBCursor(self, cursor):
|
def closeDBCursor(self, cursor):
|
||||||
assert self.mxDB.locked()
|
assert self._db_lock_held()
|
||||||
if cursor:
|
if cursor:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
def closeDB(self, cursor, commit=True):
|
def closeDB(self, cursor, commit=True):
|
||||||
assert self.mxDB.locked()
|
assert self._db_lock_held()
|
||||||
|
|
||||||
|
if self._db_lock_depth > 1:
|
||||||
|
if commit:
|
||||||
|
self._db_con.commit()
|
||||||
|
cursor.close()
|
||||||
|
self._db_lock_depth -= 1
|
||||||
|
return
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self._db_con.commit()
|
self._db_con.commit()
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
self._db_con.close()
|
self._db_con.close()
|
||||||
|
self._db_lock_depth = 0
|
||||||
self.mxDB.release()
|
self.mxDB.release()
|
||||||
|
|
||||||
def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None:
|
def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None:
|
||||||
@@ -1037,7 +1063,7 @@ class DBMethods:
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if cursor is None:
|
if cursor is None:
|
||||||
self.closeDB(use_cursor, commit=False)
|
self.closeDB(use_cursor, commit=True)
|
||||||
|
|
||||||
def add(self, obj, cursor, upsert: bool = False, columns_list=None):
|
def add(self, obj, cursor, upsert: bool = False, columns_list=None):
|
||||||
if cursor is None:
|
if cursor is None:
|
||||||
@@ -1201,6 +1227,9 @@ class DBMethods:
|
|||||||
query: str = f"UPDATE {table_name} SET "
|
query: str = f"UPDATE {table_name} SET "
|
||||||
|
|
||||||
values = {}
|
values = {}
|
||||||
|
constraint_values = {}
|
||||||
|
set_columns = []
|
||||||
|
|
||||||
for mc in inspect.getmembers(obj.__class__):
|
for mc in inspect.getmembers(obj.__class__):
|
||||||
mc_name, mc_obj = mc
|
mc_name, mc_obj = mc
|
||||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||||
@@ -1212,18 +1241,19 @@ class DBMethods:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if mc_name in constraints:
|
if mc_name in constraints:
|
||||||
values[mc_name] = m_obj
|
constraint_values[mc_name] = m_obj
|
||||||
continue
|
continue
|
||||||
if columns_list is not None and mc_name not in columns_list:
|
if columns_list is not None and mc_name not in columns_list:
|
||||||
continue
|
continue
|
||||||
if len(values) > 0:
|
|
||||||
query += ", "
|
set_columns.append(f"{mc_name} = :{mc_name}")
|
||||||
query += f"{mc_name} = :{mc_name}"
|
|
||||||
values[mc_name] = m_obj
|
values[mc_name] = m_obj
|
||||||
|
|
||||||
|
query += ", ".join(set_columns)
|
||||||
query += " WHERE 1=1 "
|
query += " WHERE 1=1 "
|
||||||
|
|
||||||
for ck in constraints:
|
for ck in constraints:
|
||||||
query += f" AND {ck} = :{ck} "
|
query += f" AND {ck} = :{ck} "
|
||||||
|
|
||||||
|
values.update(constraint_values)
|
||||||
cursor.execute(query, values)
|
cursor.execute(query, values)
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ from .db import (
|
|||||||
extract_schema,
|
extract_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .db_wallet import (
|
||||||
|
WalletAddress,
|
||||||
|
WalletLockedUTXO,
|
||||||
|
WalletPendingTx,
|
||||||
|
WalletState,
|
||||||
|
WalletTxCache,
|
||||||
|
WalletWatchOnly,
|
||||||
|
)
|
||||||
|
|
||||||
from .basicswap_util import (
|
from .basicswap_util import (
|
||||||
BidStates,
|
BidStates,
|
||||||
canAcceptBidState,
|
canAcceptBidState,
|
||||||
@@ -260,7 +269,16 @@ def upgradeDatabase(self, db_version: int):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
expect_schema = extract_schema()
|
wallet_tables = [
|
||||||
|
WalletAddress,
|
||||||
|
WalletLockedUTXO,
|
||||||
|
WalletPendingTx,
|
||||||
|
WalletState,
|
||||||
|
WalletTxCache,
|
||||||
|
WalletWatchOnly,
|
||||||
|
]
|
||||||
|
expect_schema = extract_schema(extra_tables=wallet_tables)
|
||||||
|
have_tables = {}
|
||||||
try:
|
try:
|
||||||
cursor = self.openDB()
|
cursor = self.openDB()
|
||||||
for rename_column in rename_columns:
|
for rename_column in rename_columns:
|
||||||
@@ -269,7 +287,93 @@ def upgradeDatabase(self, db_version: int):
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
|
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
|
||||||
)
|
)
|
||||||
upgradeDatabaseFromSchema(self, cursor, expect_schema)
|
|
||||||
|
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
||||||
|
tables = cursor.execute(query).fetchall()
|
||||||
|
for table in tables:
|
||||||
|
table_name = table[0]
|
||||||
|
if table_name in ("sqlite_sequence",):
|
||||||
|
continue
|
||||||
|
|
||||||
|
have_table = {}
|
||||||
|
have_columns = {}
|
||||||
|
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
|
||||||
|
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
|
||||||
|
for column in columns:
|
||||||
|
cid, name, data_type, notnull, default_value, primary_key = column
|
||||||
|
have_columns[name] = {"type": data_type, "primary_key": primary_key}
|
||||||
|
|
||||||
|
have_table["columns"] = have_columns
|
||||||
|
|
||||||
|
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
|
||||||
|
indices = cursor.fetchall()
|
||||||
|
for index in indices:
|
||||||
|
seq, index_name, unique, origin, partial = index
|
||||||
|
|
||||||
|
if origin == "pk":
|
||||||
|
continue
|
||||||
|
|
||||||
|
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
|
||||||
|
index_info = cursor.fetchall()
|
||||||
|
|
||||||
|
add_index = {"index_name": index_name}
|
||||||
|
for index_columns in index_info:
|
||||||
|
seqno, cid, name = index_columns
|
||||||
|
if origin == "u":
|
||||||
|
have_columns[name]["unique"] = 1
|
||||||
|
else:
|
||||||
|
if "column_1" not in add_index:
|
||||||
|
add_index["column_1"] = name
|
||||||
|
elif "column_2" not in add_index:
|
||||||
|
add_index["column_2"] = name
|
||||||
|
elif "column_3" not in add_index:
|
||||||
|
add_index["column_3"] = name
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Add more index columns.")
|
||||||
|
if origin == "c":
|
||||||
|
if "indices" not in have_table:
|
||||||
|
have_table["indices"] = []
|
||||||
|
have_table["indices"].append(add_index)
|
||||||
|
|
||||||
|
have_tables[table_name] = have_table
|
||||||
|
|
||||||
|
for table_name, table in expect_schema.items():
|
||||||
|
if table_name not in have_tables:
|
||||||
|
self.log.info(f"Creating table {table_name}.")
|
||||||
|
create_table(cursor, table_name, table)
|
||||||
|
continue
|
||||||
|
|
||||||
|
have_table = have_tables[table_name]
|
||||||
|
have_columns = have_table["columns"]
|
||||||
|
for colname, column in table["columns"].items():
|
||||||
|
if colname not in have_columns:
|
||||||
|
col_type = column["type"]
|
||||||
|
self.log.info(f"Adding column {colname} to table {table_name}.")
|
||||||
|
cursor.execute(
|
||||||
|
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
|
||||||
|
)
|
||||||
|
indices = table.get("indices", [])
|
||||||
|
have_indices = have_table.get("indices", [])
|
||||||
|
for index in indices:
|
||||||
|
index_name = index["index_name"]
|
||||||
|
if not any(
|
||||||
|
have_idx.get("index_name") == index_name
|
||||||
|
for have_idx in have_indices
|
||||||
|
):
|
||||||
|
self.log.info(f"Adding index {index_name} to table {table_name}.")
|
||||||
|
column_1 = index["column_1"]
|
||||||
|
column_2 = index.get("column_2", None)
|
||||||
|
column_3 = index.get("column_3", None)
|
||||||
|
query: str = (
|
||||||
|
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||||
|
)
|
||||||
|
if column_2:
|
||||||
|
query += f", {column_2}"
|
||||||
|
if column_3:
|
||||||
|
query += f", {column_3}"
|
||||||
|
query += ")"
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
if CURRENT_DB_VERSION != db_version:
|
if CURRENT_DB_VERSION != db_version:
|
||||||
self.db_version = CURRENT_DB_VERSION
|
self.db_version = CURRENT_DB_VERSION
|
||||||
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
|
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
|
||||||
|
|||||||
122
basicswap/db_wallet.py
Normal file
122
basicswap/db_wallet.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024-2026 The Basicswap developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
|
||||||
|
from .db import Column, Index, Table, UniqueConstraint
|
||||||
|
|
||||||
|
|
||||||
|
class WalletAddress(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_addresses"
|
||||||
|
|
||||||
|
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||||
|
coin_type = Column("integer")
|
||||||
|
derivation_index = Column("integer")
|
||||||
|
is_internal = Column("bool")
|
||||||
|
derivation_path = Column("string")
|
||||||
|
address = Column("string")
|
||||||
|
scripthash = Column("string")
|
||||||
|
pubkey = Column("blob")
|
||||||
|
is_funded = Column("bool")
|
||||||
|
cached_balance = Column("integer")
|
||||||
|
cached_balance_time = Column("integer")
|
||||||
|
first_seen_height = Column("integer")
|
||||||
|
created_at = Column("integer")
|
||||||
|
|
||||||
|
__unique_1__ = UniqueConstraint("coin_type", "derivation_index", "is_internal")
|
||||||
|
__index_address__ = Index("idx_wallet_address", "address")
|
||||||
|
__index_scripthash__ = Index("idx_wallet_scripthash", "scripthash")
|
||||||
|
__index_funded__ = Index("idx_wallet_funded", "coin_type", "is_funded")
|
||||||
|
|
||||||
|
|
||||||
|
class WalletState(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_state"
|
||||||
|
|
||||||
|
coin_type = Column("integer", primary_key=True)
|
||||||
|
last_external_index = Column("integer")
|
||||||
|
last_internal_index = Column("integer")
|
||||||
|
derivation_path_type = Column("string")
|
||||||
|
last_sync_height = Column("integer")
|
||||||
|
migration_complete = Column("bool")
|
||||||
|
created_at = Column("integer")
|
||||||
|
updated_at = Column("integer")
|
||||||
|
|
||||||
|
|
||||||
|
class WalletWatchOnly(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_watch_only"
|
||||||
|
|
||||||
|
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||||
|
coin_type = Column("integer")
|
||||||
|
address = Column("string")
|
||||||
|
scripthash = Column("string")
|
||||||
|
label = Column("string")
|
||||||
|
source = Column("string")
|
||||||
|
is_funded = Column("bool")
|
||||||
|
cached_balance = Column("integer")
|
||||||
|
cached_balance_time = Column("integer")
|
||||||
|
private_key_encrypted = Column("blob")
|
||||||
|
created_at = Column("integer")
|
||||||
|
|
||||||
|
__unique_1__ = UniqueConstraint("coin_type", "address")
|
||||||
|
__index_watch_address__ = Index("idx_watch_address", "address")
|
||||||
|
__index_watch_scripthash__ = Index("idx_watch_scripthash", "scripthash")
|
||||||
|
|
||||||
|
|
||||||
|
class WalletLockedUTXO(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_locked_utxos"
|
||||||
|
|
||||||
|
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||||
|
coin_type = Column("integer")
|
||||||
|
txid = Column("string")
|
||||||
|
vout = Column("integer")
|
||||||
|
value = Column("integer")
|
||||||
|
address = Column("string")
|
||||||
|
bid_id = Column("blob")
|
||||||
|
locked_at = Column("integer")
|
||||||
|
expires_at = Column("integer")
|
||||||
|
|
||||||
|
__unique_1__ = UniqueConstraint("coin_type", "txid", "vout")
|
||||||
|
__index_locked_coin__ = Index("idx_locked_coin", "coin_type")
|
||||||
|
__index_locked_bid__ = Index("idx_locked_bid", "bid_id")
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTxCache(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_tx_cache"
|
||||||
|
|
||||||
|
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||||
|
coin_type = Column("integer")
|
||||||
|
txid = Column("string")
|
||||||
|
block_height = Column("integer")
|
||||||
|
confirmations = Column("integer")
|
||||||
|
tx_data = Column("blob")
|
||||||
|
cached_at = Column("integer")
|
||||||
|
expires_at = Column("integer")
|
||||||
|
|
||||||
|
__unique_1__ = UniqueConstraint("coin_type", "txid")
|
||||||
|
__index_tx_cache__ = Index("idx_tx_cache", "coin_type", "txid")
|
||||||
|
|
||||||
|
|
||||||
|
class WalletPendingTx(Table):
|
||||||
|
|
||||||
|
__tablename__ = "wallet_pending_txs"
|
||||||
|
|
||||||
|
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||||
|
coin_type = Column("integer")
|
||||||
|
txid = Column("string")
|
||||||
|
tx_type = Column("string")
|
||||||
|
amount = Column("integer")
|
||||||
|
fee = Column("integer")
|
||||||
|
addresses = Column("string")
|
||||||
|
bid_id = Column("blob")
|
||||||
|
first_seen = Column("integer")
|
||||||
|
confirmed_at = Column("integer")
|
||||||
|
|
||||||
|
__unique_1__ = UniqueConstraint("coin_type", "txid")
|
||||||
|
__index_pending_coin__ = Index("idx_pending_coin", "coin_type", "confirmed_at")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -632,6 +632,15 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
# TODO: filter errors
|
# TODO: filter errors
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
|
||||||
|
try:
|
||||||
|
return self.rpc_wallet(
|
||||||
|
"listtransactions", ["*", count, skip, include_watchonly]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.error(f"listWalletTransactions failed: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||||
# TODO: Lock unspent and use same output/s to fund bid
|
# TODO: Lock unspent and use same output/s to fund bid
|
||||||
|
|
||||||
|
|||||||
1131
basicswap/interface/electrumx.py
Normal file
1131
basicswap/interface/electrumx.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,12 +27,21 @@ class LTCInterface(BTCInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
|
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||||
|
if self.useBackend():
|
||||||
|
raise ValueError("MWEB addresses not supported in electrum mode")
|
||||||
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
|
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
|
||||||
|
|
||||||
def getNewStealthAddress(self, label=""):
|
def getNewStealthAddress(self, label=""):
|
||||||
|
if self.useBackend():
|
||||||
|
raise ValueError("MWEB addresses not supported in electrum mode")
|
||||||
return self.getNewMwebAddress(False, label)
|
return self.getNewMwebAddress(False, label)
|
||||||
|
|
||||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||||
|
if self.useBackend():
|
||||||
|
if type_from == "mweb":
|
||||||
|
raise ValueError("MWEB withdrawals not supported in electrum mode")
|
||||||
|
return self._withdrawCoinElectrum(value, addr_to, subfee)
|
||||||
|
|
||||||
params = [addr_to, value, "", "", subfee, True, self._conf_target]
|
params = [addr_to, value, "", "", subfee, True, self._conf_target]
|
||||||
if type_from == "mweb":
|
if type_from == "mweb":
|
||||||
return self.rpc_wallet_mweb("sendtoaddress", params)
|
return self.rpc_wallet_mweb("sendtoaddress", params)
|
||||||
@@ -53,14 +62,27 @@ class LTCInterface(BTCInterface):
|
|||||||
|
|
||||||
def getWalletInfo(self):
|
def getWalletInfo(self):
|
||||||
rv = super(LTCInterface, self).getWalletInfo()
|
rv = super(LTCInterface, self).getWalletInfo()
|
||||||
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
if not self.useBackend():
|
||||||
rv["mweb_balance"] = mweb_info["balance"]
|
try:
|
||||||
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
||||||
rv["mweb_immature"] = mweb_info["immature_balance"]
|
rv["mweb_balance"] = mweb_info["balance"]
|
||||||
|
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
||||||
|
rv["mweb_immature"] = mweb_info["immature_balance"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def getUnspentsByAddr(self):
|
def getUnspentsByAddr(self):
|
||||||
unspent_addr = dict()
|
unspent_addr = dict()
|
||||||
|
|
||||||
|
if self.useBackend():
|
||||||
|
wm = self.getWalletManager()
|
||||||
|
if wm:
|
||||||
|
addresses = wm.getAllAddresses(self.coin_type())
|
||||||
|
if addresses:
|
||||||
|
return self._backend.getBalance(addresses)
|
||||||
|
return unspent_addr
|
||||||
|
|
||||||
unspent = self.rpc_wallet("listunspent")
|
unspent = self.rpc_wallet("listunspent")
|
||||||
for u in unspent:
|
for u in unspent:
|
||||||
if u.get("spendable", False) is False:
|
if u.get("spendable", False) is False:
|
||||||
@@ -152,6 +174,9 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
return
|
return
|
||||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
|
if self.useBackend():
|
||||||
|
return
|
||||||
|
|
||||||
if not self.has_mweb_wallet():
|
if not self.has_mweb_wallet():
|
||||||
self.init_wallet(password)
|
self.init_wallet(password)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ class XMRInterface(CoinInterface):
|
|||||||
"failed to get output distribution",
|
"failed to get output distribution",
|
||||||
"request-sent",
|
"request-sent",
|
||||||
"idle",
|
"idle",
|
||||||
|
"busy",
|
||||||
|
"responsenotready",
|
||||||
|
"connection",
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@@ -832,3 +835,25 @@ class XMRInterface(CoinInterface):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
|
||||||
|
try:
|
||||||
|
with self._mx_wallet:
|
||||||
|
self.openWallet(self._wallet_filename)
|
||||||
|
rv = self.rpc_wallet(
|
||||||
|
"get_transfers",
|
||||||
|
{"in": True, "out": True, "pending": True, "failed": True},
|
||||||
|
)
|
||||||
|
transactions = []
|
||||||
|
for tx_type in ["in", "out", "pending", "failed"]:
|
||||||
|
if tx_type in rv:
|
||||||
|
for tx in rv[tx_type]:
|
||||||
|
tx["type"] = tx_type
|
||||||
|
transactions.append(tx)
|
||||||
|
transactions.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
|
||||||
|
return (
|
||||||
|
transactions[skip : skip + count] if count else transactions[skip:]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.error(f"listWalletTransactions failed: {e}")
|
||||||
|
return []
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
|||||||
for k, v in swap_client.coin_clients.items():
|
for k, v in swap_client.coin_clients.items():
|
||||||
if k not in chainparams:
|
if k not in chainparams:
|
||||||
continue
|
continue
|
||||||
if v["connection_type"] == "rpc":
|
if v["connection_type"] in ("rpc", "electrum"):
|
||||||
|
|
||||||
balance = "0.0"
|
balance = "0.0"
|
||||||
if k in wallets:
|
if k in wallets:
|
||||||
@@ -168,8 +168,20 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
|||||||
"balance": balance,
|
"balance": balance,
|
||||||
"pending": pending,
|
"pending": pending,
|
||||||
"ticker": chainparams[k]["ticker"],
|
"ticker": chainparams[k]["ticker"],
|
||||||
|
"connection_type": v["connection_type"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ci = swap_client.ci(k)
|
||||||
|
if hasattr(ci, "getScanStatus"):
|
||||||
|
coin_entry["scan_status"] = ci.getScanStatus()
|
||||||
|
if hasattr(ci, "getElectrumServer"):
|
||||||
|
server = ci.getElectrumServer()
|
||||||
|
if server:
|
||||||
|
coin_entry["electrum_server"] = server
|
||||||
|
version = ci.getDaemonVersion()
|
||||||
|
if version:
|
||||||
|
coin_entry["version"] = version
|
||||||
|
|
||||||
coins_with_balances.append(coin_entry)
|
coins_with_balances.append(coin_entry)
|
||||||
|
|
||||||
if k == Coins.PART:
|
if k == Coins.PART:
|
||||||
@@ -293,6 +305,9 @@ def js_wallets(self, url_split, post_string, is_json):
|
|||||||
elif cmd == "reseed":
|
elif cmd == "reseed":
|
||||||
swap_client.reseedWallet(coin_type)
|
swap_client.reseedWallet(coin_type)
|
||||||
return bytes(json.dumps({"reseeded": True}), "UTF-8")
|
return bytes(json.dumps({"reseeded": True}), "UTF-8")
|
||||||
|
elif cmd == "rescan":
|
||||||
|
result = swap_client.rescanWalletAddresses(coin_type)
|
||||||
|
return bytes(json.dumps(result), "UTF-8")
|
||||||
elif cmd == "newstealthaddress":
|
elif cmd == "newstealthaddress":
|
||||||
if coin_type != Coins.PART:
|
if coin_type != Coins.PART:
|
||||||
raise ValueError("Invalid coin for command")
|
raise ValueError("Invalid coin for command")
|
||||||
@@ -306,6 +321,31 @@ def js_wallets(self, url_split, post_string, is_json):
|
|||||||
return bytes(
|
return bytes(
|
||||||
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
|
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
|
||||||
)
|
)
|
||||||
|
elif cmd == "watchaddress":
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
address = get_data_entry(post_data, "address")
|
||||||
|
label = get_data_entry_or(post_data, "label", "manual_import")
|
||||||
|
wm = swap_client.getWalletManager()
|
||||||
|
if wm is None:
|
||||||
|
raise ValueError("WalletManager not available")
|
||||||
|
wm.importWatchOnlyAddress(
|
||||||
|
coin_type, address, label=label, source="manual_import"
|
||||||
|
)
|
||||||
|
return bytes(json.dumps({"success": True, "address": address}), "UTF-8")
|
||||||
|
elif cmd == "listaddresses":
|
||||||
|
wm = swap_client.getWalletManager()
|
||||||
|
if wm is None:
|
||||||
|
raise ValueError("WalletManager not available")
|
||||||
|
addresses = wm.getAllAddresses(coin_type)
|
||||||
|
return bytes(json.dumps({"addresses": addresses}), "UTF-8")
|
||||||
|
elif cmd == "fixseedid":
|
||||||
|
root_key = swap_client.getWalletKey(coin_type, 1)
|
||||||
|
swap_client.storeSeedIDForCoin(root_key, coin_type)
|
||||||
|
swap_client.checkWalletSeed(coin_type)
|
||||||
|
return bytes(
|
||||||
|
json.dumps({"success": True, "message": "Seed IDs updated"}),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
raise ValueError("Unknown command")
|
raise ValueError("Unknown command")
|
||||||
|
|
||||||
if coin_type == Coins.LTC_MWEB:
|
if coin_type == Coins.LTC_MWEB:
|
||||||
@@ -1526,6 +1566,71 @@ def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def js_wallettransactions(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
from basicswap.ui.page_wallet import format_transactions
|
||||||
|
import time
|
||||||
|
|
||||||
|
TX_CACHE_DURATION = 30
|
||||||
|
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
|
||||||
|
if len(url_split) < 4:
|
||||||
|
return bytes(json.dumps({"error": "No coin specified"}), "UTF-8")
|
||||||
|
|
||||||
|
ticker_str = url_split[3]
|
||||||
|
coin_id = getCoinIdFromTicker(ticker_str)
|
||||||
|
|
||||||
|
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
page_no = 1
|
||||||
|
limit = 30
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
if have_data_entry(post_data, "page_no"):
|
||||||
|
page_no = int(get_data_entry(post_data, "page_no"))
|
||||||
|
if page_no < 1:
|
||||||
|
page_no = 1
|
||||||
|
|
||||||
|
if page_no > 1:
|
||||||
|
offset = (page_no - 1) * limit
|
||||||
|
|
||||||
|
try:
|
||||||
|
ci = swap_client.ci(coin_id)
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
cache_entry = swap_client._tx_cache.get(coin_id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
cache_entry is None
|
||||||
|
or (current_time - cache_entry["time"]) > TX_CACHE_DURATION
|
||||||
|
):
|
||||||
|
all_txs = ci.listWalletTransactions(count=10000, skip=0)
|
||||||
|
all_txs = list(reversed(all_txs)) if all_txs else []
|
||||||
|
swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time}
|
||||||
|
else:
|
||||||
|
all_txs = cache_entry["txs"]
|
||||||
|
|
||||||
|
total_transactions = len(all_txs)
|
||||||
|
raw_txs = all_txs[offset : offset + limit] if all_txs else []
|
||||||
|
transactions = format_transactions(ci, raw_txs, coin_id)
|
||||||
|
|
||||||
|
return bytes(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"transactions": transactions,
|
||||||
|
"page_no": page_no,
|
||||||
|
"total": total_transactions,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": (total_transactions + limit - 1) // limit,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||||
|
|
||||||
|
|
||||||
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||||
@@ -1569,10 +1674,107 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
|||||||
return bytes(json.dumps(message_routes), "UTF-8")
|
return bytes(json.dumps(message_routes), "UTF-8")
|
||||||
|
|
||||||
|
|
||||||
|
def js_electrum_discover(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
coin_str = get_data_entry(post_data, "coin")
|
||||||
|
do_ping = toBool(get_data_entry_or(post_data, "ping", "false"))
|
||||||
|
|
||||||
|
coin_type = None
|
||||||
|
try:
|
||||||
|
coin_id = int(coin_str)
|
||||||
|
coin_type = Coins(coin_id)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
coin_type = getCoinIdFromName(coin_str)
|
||||||
|
except ValueError:
|
||||||
|
coin_type = getCoinType(coin_str)
|
||||||
|
|
||||||
|
electrum_supported = ["bitcoin", "litecoin"]
|
||||||
|
coin_name = chainparams.get(coin_type, {}).get("name", "").lower()
|
||||||
|
if coin_name not in electrum_supported:
|
||||||
|
return bytes(
|
||||||
|
json.dumps(
|
||||||
|
{"error": f"Electrum not supported for {coin_name}", "servers": []}
|
||||||
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
ci = swap_client.ci(coin_type)
|
||||||
|
connection_type = getattr(ci, "_connection_type", "rpc")
|
||||||
|
|
||||||
|
discovered_servers = []
|
||||||
|
current_server = None
|
||||||
|
|
||||||
|
if connection_type == "electrum":
|
||||||
|
backend = ci.getBackend()
|
||||||
|
if backend and hasattr(backend, "_server"):
|
||||||
|
server = backend._server
|
||||||
|
current_server = server.get_current_server_info()
|
||||||
|
discovered_servers = server.discover_peers()
|
||||||
|
|
||||||
|
if do_ping and discovered_servers:
|
||||||
|
for srv in discovered_servers[:10]:
|
||||||
|
latency = server.ping_server(
|
||||||
|
srv["host"], srv["port"], srv.get("ssl", True)
|
||||||
|
)
|
||||||
|
srv["latency_ms"] = latency
|
||||||
|
srv["online"] = latency is not None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from .interface.electrumx import ElectrumServer
|
||||||
|
|
||||||
|
temp_server = ElectrumServer(
|
||||||
|
coin_name,
|
||||||
|
log=swap_client.log,
|
||||||
|
)
|
||||||
|
temp_server.connect()
|
||||||
|
current_server = temp_server.get_current_server_info()
|
||||||
|
discovered_servers = temp_server.discover_peers()
|
||||||
|
|
||||||
|
if do_ping and discovered_servers:
|
||||||
|
for srv in discovered_servers[:10]:
|
||||||
|
latency = temp_server.ping_server(
|
||||||
|
srv["host"], srv["port"], srv.get("ssl", True)
|
||||||
|
)
|
||||||
|
srv["latency_ms"] = latency
|
||||||
|
srv["online"] = latency is not None
|
||||||
|
|
||||||
|
temp_server.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
return bytes(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"error": f"Failed to connect to electrum server: {str(e)}",
|
||||||
|
"servers": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
onion_servers = [s for s in discovered_servers if s.get("is_onion")]
|
||||||
|
clearnet_servers = [s for s in discovered_servers if not s.get("is_onion")]
|
||||||
|
|
||||||
|
return bytes(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"coin": coin_name,
|
||||||
|
"current_server": current_server,
|
||||||
|
"clearnet_servers": clearnet_servers,
|
||||||
|
"onion_servers": onion_servers,
|
||||||
|
"total_discovered": len(discovered_servers),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"UTF-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
endpoints = {
|
endpoints = {
|
||||||
"coins": js_coins,
|
"coins": js_coins,
|
||||||
"walletbalances": js_walletbalances,
|
"walletbalances": js_walletbalances,
|
||||||
"wallets": js_wallets,
|
"wallets": js_wallets,
|
||||||
|
"wallettransactions": js_wallettransactions,
|
||||||
"offers": js_offers,
|
"offers": js_offers,
|
||||||
"sentoffers": js_sentoffers,
|
"sentoffers": js_sentoffers,
|
||||||
"bids": js_bids,
|
"bids": js_bids,
|
||||||
@@ -1602,6 +1804,7 @@ endpoints = {
|
|||||||
"coinvolume": js_coinvolume,
|
"coinvolume": js_coinvolume,
|
||||||
"coinhistory": js_coinhistory,
|
"coinhistory": js_coinhistory,
|
||||||
"messageroutes": js_messageroutes,
|
"messageroutes": js_messageroutes,
|
||||||
|
"electrumdiscover": js_electrum_discover,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,42 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const EventHandlers = {
|
const EventHandlers = {
|
||||||
|
|
||||||
confirmPopup: function(action = 'proceed', coinName = '') {
|
showConfirmModal: function(title, message, callback) {
|
||||||
const message = action === 'Accept'
|
const modal = document.getElementById('confirmModal');
|
||||||
? 'Are you sure you want to accept this bid?'
|
if (!modal) {
|
||||||
: coinName
|
if (callback) callback();
|
||||||
? `Are you sure you want to ${action} ${coinName}?`
|
return;
|
||||||
: 'Are you sure you want to proceed?';
|
}
|
||||||
|
|
||||||
return confirm(message);
|
const titleEl = document.getElementById('confirmTitle');
|
||||||
|
const messageEl = document.getElementById('confirmMessage');
|
||||||
|
const yesBtn = document.getElementById('confirmYes');
|
||||||
|
const noBtn = document.getElementById('confirmNo');
|
||||||
|
const bidDetails = document.getElementById('bidDetailsSection');
|
||||||
|
|
||||||
|
if (titleEl) titleEl.textContent = title;
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = message;
|
||||||
|
messageEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
if (bidDetails) bidDetails.classList.add('hidden');
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
const newYesBtn = yesBtn.cloneNode(true);
|
||||||
|
yesBtn.parentNode.replaceChild(newYesBtn, yesBtn);
|
||||||
|
|
||||||
|
newYesBtn.addEventListener('click', function() {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
const newNoBtn = noBtn.cloneNode(true);
|
||||||
|
noBtn.parentNode.replaceChild(newNoBtn, noBtn);
|
||||||
|
newNoBtn.addEventListener('click', function() {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmReseed: function() {
|
confirmReseed: function() {
|
||||||
@@ -18,7 +45,6 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
confirmWithdrawal: function() {
|
confirmWithdrawal: function() {
|
||||||
|
|
||||||
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
|
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
|
||||||
return window.WalletPage.confirmWithdrawal();
|
return window.WalletPage.confirmWithdrawal();
|
||||||
}
|
}
|
||||||
@@ -67,14 +93,36 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
|
let coinFromId;
|
||||||
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
|
if (inputId === 'add-amm-amount') {
|
||||||
|
coinFromId = 'add-amm-coin-from';
|
||||||
|
} else if (inputId === 'edit-amm-amount') {
|
||||||
|
coinFromId = 'edit-amm-coin-from';
|
||||||
|
} else {
|
||||||
|
const form = amountInput.closest('form') || amountInput.closest('.modal-content') || amountInput.closest('[id*="modal"]');
|
||||||
|
const select = form?.querySelector('select[id*="coin-from"]');
|
||||||
|
coinFromId = select?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinFromSelect = coinFromId ? document.getElementById(coinFromId) : null;
|
||||||
|
if (!coinFromSelect) {
|
||||||
|
console.error('EventHandlers: Coin-from dropdown not found for:', inputId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||||
|
if (!selectedOption) {
|
||||||
|
console.error('EventHandlers: No option selected in coin-from dropdown');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = parseFloat(selectedOption.getAttribute('data-balance') || '0');
|
||||||
|
|
||||||
if (balance > 0) {
|
if (balance > 0) {
|
||||||
const calculatedAmount = balance * percent;
|
const calculatedAmount = balance * percent;
|
||||||
amountInput.value = calculatedAmount.toFixed(8);
|
amountInput.value = calculatedAmount.toFixed(8);
|
||||||
} else {
|
} else {
|
||||||
console.warn('EventHandlers: No balance found for AMM amount calculation');
|
console.warn('EventHandlers: No balance found for selected coin');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,13 +180,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
hideConfirmModal: function() {
|
hideConfirmModal: function() {
|
||||||
if (window.DOMCache) {
|
const modal = document.getElementById('confirmModal');
|
||||||
window.DOMCache.hide('confirmModal');
|
if (modal) {
|
||||||
} else {
|
modal.classList.add('hidden');
|
||||||
const modal = document.getElementById('confirmModal');
|
modal.style.display = '';
|
||||||
if (modal) {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -187,17 +232,43 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const target = e.target.closest('[data-confirm]');
|
const target = e.target.closest('[data-confirm]');
|
||||||
if (target) {
|
if (target) {
|
||||||
|
if (target.dataset.confirmHandled) {
|
||||||
|
delete target.dataset.confirmHandled;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
const action = target.getAttribute('data-confirm-action') || 'proceed';
|
const action = target.getAttribute('data-confirm-action') || 'proceed';
|
||||||
const coinName = target.getAttribute('data-confirm-coin') || '';
|
const coinName = target.getAttribute('data-confirm-coin') || '';
|
||||||
|
|
||||||
if (!this.confirmPopup(action, coinName)) {
|
const message = action === 'Accept'
|
||||||
e.preventDefault();
|
? 'Are you sure you want to accept this bid?'
|
||||||
return false;
|
: coinName
|
||||||
}
|
? `Are you sure you want to ${action} ${coinName}?`
|
||||||
|
: 'Are you sure you want to proceed?';
|
||||||
|
|
||||||
|
const title = `Confirm ${action}`;
|
||||||
|
|
||||||
|
this.showConfirmModal(title, message, function() {
|
||||||
|
target.dataset.confirmHandled = 'true';
|
||||||
|
|
||||||
|
if (target.form) {
|
||||||
|
const hiddenInput = document.createElement('input');
|
||||||
|
hiddenInput.type = 'hidden';
|
||||||
|
hiddenInput.name = target.name;
|
||||||
|
hiddenInput.value = target.value;
|
||||||
|
target.form.appendChild(hiddenInput);
|
||||||
|
target.form.submit();
|
||||||
|
} else {
|
||||||
|
target.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,8 +397,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.EventHandlers = EventHandlers;
|
window.EventHandlers = EventHandlers;
|
||||||
|
|
||||||
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
|
|
||||||
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
|
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
|
||||||
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
|
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
|
||||||
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
|
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ function ensureToastContainer() {
|
|||||||
'new_bid': 'bg-green-500',
|
'new_bid': 'bg-green-500',
|
||||||
'bid_accepted': 'bg-purple-500',
|
'bid_accepted': 'bg-purple-500',
|
||||||
'swap_completed': 'bg-green-600',
|
'swap_completed': 'bg-green-600',
|
||||||
|
'sweep_completed': 'bg-orange-500',
|
||||||
'balance_change': 'bg-yellow-500',
|
'balance_change': 'bg-yellow-500',
|
||||||
'update_available': 'bg-blue-600',
|
'update_available': 'bg-blue-600',
|
||||||
'success': 'bg-blue-500'
|
'success': 'bg-blue-500'
|
||||||
@@ -735,6 +736,17 @@ function ensureToastContainer() {
|
|||||||
shouldShowToast = config.showUpdateNotifications;
|
shouldShowToast = config.showUpdateNotifications;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'sweep_completed':
|
||||||
|
const sweepAmount = parseFloat(data.amount || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||||
|
const sweepFee = parseFloat(data.fee || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||||
|
toastTitle = `Swept ${sweepAmount} ${data.coin_name} to RPC wallet`;
|
||||||
|
toastOptions.subtitle = `Fee: ${sweepFee} ${data.coin_name} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
||||||
|
toastOptions.coinSymbol = data.coin_name;
|
||||||
|
toastOptions.txid = data.txid;
|
||||||
|
toastType = 'sweep_completed';
|
||||||
|
shouldShowToast = true;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'coin_balance_updated':
|
case 'coin_balance_updated':
|
||||||
if (data.coin && config.showBalanceChanges) {
|
if (data.coin && config.showBalanceChanges) {
|
||||||
this.handleBalanceUpdate(data);
|
this.handleBalanceUpdate(data);
|
||||||
|
|||||||
459
basicswap/static/js/pages/bid-page.js
Normal file
459
basicswap/static/js/pages/bid-page.js
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
const BidPage = {
|
||||||
|
bidId: null,
|
||||||
|
bidStateInd: null,
|
||||||
|
createdAtTimestamp: null,
|
||||||
|
autoRefreshInterval: null,
|
||||||
|
elapsedTimeInterval: null,
|
||||||
|
AUTO_REFRESH_SECONDS: 15,
|
||||||
|
|
||||||
|
INACTIVE_STATES: [8, 17, 18, 19, 21, 22, 23, 25, 31], // Completed, Failed variants, Timed-out, Abandoned, Error, Rejected, Expired
|
||||||
|
|
||||||
|
STATE_TOOLTIPS: {
|
||||||
|
'Bid Sent': 'Your bid has been broadcast to the network',
|
||||||
|
'Bid Receiving': 'Receiving partial bid message from the network',
|
||||||
|
'Bid Received': 'Bid received and waiting for decision to accept or reject',
|
||||||
|
'Bid Receiving accept': 'Receiving acceptance message from the other party',
|
||||||
|
'Bid Accepted': 'Bid accepted. The atomic swap process is starting',
|
||||||
|
'Bid Initiated': 'Swap initiated. First lock transaction is being created',
|
||||||
|
'Bid Participating': 'Participating in the swap. Second lock transaction is being created',
|
||||||
|
'Bid Completed': 'Swap completed successfully! Both parties received their coins',
|
||||||
|
'Bid Script coin locked': 'Your coins are locked in the atomic swap contract on the script chain (e.g., BTC/LTC)',
|
||||||
|
'Bid Script coin spend tx valid': 'The spend transaction for the script coin has been validated and is ready',
|
||||||
|
'Bid Scriptless coin locked': 'The other party\'s coins are locked using adaptor signatures (e.g., XMR)',
|
||||||
|
'Bid Script coin lock released': 'Secret key revealed. The script coin can now be claimed',
|
||||||
|
'Bid Script tx redeemed': 'Script coin has been successfully claimed',
|
||||||
|
'Bid Script pre-refund tx in chain': 'Pre-refund transaction detected. Swap may be failing',
|
||||||
|
'Bid Scriptless tx redeemed': 'Scriptless coin (e.g., XMR) has been successfully claimed',
|
||||||
|
'Bid Scriptless tx recovered': 'Scriptless coin recovered after swap failure',
|
||||||
|
'Bid Failed, refunded': 'Swap failed but your coins have been refunded',
|
||||||
|
'Bid Failed, swiped': 'Swap failed due to an unexpected issue. Please check the event log for details',
|
||||||
|
'Bid Failed': 'Swap failed. Check events for details',
|
||||||
|
'Bid Delaying': 'Brief delay between swap steps to ensure network propagation',
|
||||||
|
'Bid Timed-out': 'Swap timed out waiting for the other party',
|
||||||
|
'Bid Abandoned': 'Swap was manually abandoned. Locked coins will be refunded after timelock',
|
||||||
|
'Bid Error': 'An error occurred. Check events for details',
|
||||||
|
'Bid Rejected': 'Bid was rejected by the offer owner',
|
||||||
|
'Bid Stalled (debug)': 'Debug mode: swap intentionally stalled for testing',
|
||||||
|
'Bid Exchanged script lock tx sigs msg': 'Exchanging cryptographic signatures needed for lock transactions',
|
||||||
|
'Bid Exchanged script lock spend tx msg': 'Exchanging signed spend transaction for locked coins',
|
||||||
|
'Bid Request sent': 'Connection request sent to the other party',
|
||||||
|
'Bid Request accepted': 'Connection request accepted',
|
||||||
|
'Bid Expired': 'Bid expired before being accepted',
|
||||||
|
'Bid Auto accept delay': 'Waiting for automation delay before auto-accepting',
|
||||||
|
'Bid Auto accept failed': 'Automation failed to accept this bid',
|
||||||
|
'Bid Connect request sent': 'Sent connection request to peer',
|
||||||
|
'Bid Unknown bid state': 'Unknown state - please check the swap details',
|
||||||
|
|
||||||
|
'ITX Sent': 'Initiate transaction has been broadcast to the network',
|
||||||
|
'ITX Confirmed': 'Initiate transaction has been confirmed by miners',
|
||||||
|
'ITX Redeemed': 'Initiate transaction has been successfully claimed',
|
||||||
|
'ITX Refunded': 'Initiate transaction has been refunded',
|
||||||
|
'ITX In Mempool': 'Initiate transaction is in the mempool (unconfirmed)',
|
||||||
|
'ITX In Chain': 'Initiate transaction is included in a block',
|
||||||
|
'PTX Sent': 'Participate transaction has been broadcast to the network',
|
||||||
|
'PTX Confirmed': 'Participate transaction has been confirmed by miners',
|
||||||
|
'PTX Redeemed': 'Participate transaction has been successfully claimed',
|
||||||
|
'PTX Refunded': 'Participate transaction has been refunded',
|
||||||
|
'PTX In Mempool': 'Participate transaction is in the mempool (unconfirmed)',
|
||||||
|
'PTX In Chain': 'Participate transaction is included in a block'
|
||||||
|
},
|
||||||
|
|
||||||
|
EVENT_TOOLTIPS: {
|
||||||
|
'Lock tx A published': 'First lock transaction broadcast to the blockchain network',
|
||||||
|
'Lock tx A seen in mempool': 'First lock transaction detected in mempool (unconfirmed)',
|
||||||
|
'Lock tx A seen in chain': 'First lock transaction included in a block',
|
||||||
|
'Lock tx A confirmed in chain': 'First lock transaction has enough confirmations',
|
||||||
|
'Lock tx B published': 'Second lock transaction broadcast to the blockchain network',
|
||||||
|
'Lock tx B seen in mempool': 'Second lock transaction detected in mempool (unconfirmed)',
|
||||||
|
'Lock tx B seen in chain': 'Second lock transaction included in a block',
|
||||||
|
'Lock tx B confirmed in chain': 'Second lock transaction has enough confirmations',
|
||||||
|
'Lock tx A spend tx published': 'Transaction to claim coins from first lock has been broadcast',
|
||||||
|
'Lock tx A spend tx seen in chain': 'First lock spend transaction included in a block',
|
||||||
|
'Lock tx B spend tx published': 'Transaction to claim coins from second lock has been broadcast',
|
||||||
|
'Lock tx B spend tx seen in chain': 'Second lock spend transaction included in a block',
|
||||||
|
'Failed to publish lock tx B': 'ERROR: Could not broadcast second lock transaction',
|
||||||
|
'Failed to publish lock tx B spend': 'ERROR: Could not broadcast spend transaction for second lock',
|
||||||
|
'Failed to publish lock tx B refund': 'ERROR: Could not broadcast refund transaction',
|
||||||
|
'Detected invalid lock Tx B': 'ERROR: Second lock transaction is invalid or malformed',
|
||||||
|
'Lock tx A pre-refund tx published': 'Pre-refund transaction broadcast. Swap is being cancelled',
|
||||||
|
'Lock tx A refund spend tx published': 'Refund transaction for first lock has been broadcast',
|
||||||
|
'Lock tx A refund swipe tx published': 'Other party claimed your refund (swiped)',
|
||||||
|
'Lock tx B refund tx published': 'Refund transaction for second lock has been broadcast',
|
||||||
|
'Lock tx A conflicting txn/s': 'WARNING: Conflicting transaction detected for first lock',
|
||||||
|
'Lock tx A pre-refund tx seen in chain': 'Pre-refund transaction detected in blockchain',
|
||||||
|
'Lock tx A refund spend tx seen in chain': 'Refund spend transaction detected in blockchain',
|
||||||
|
'Initiate tx published': 'Secret-hash swap: Initiate transaction broadcast',
|
||||||
|
'Initiate tx redeem tx published': 'Secret-hash swap: Initiate transaction claimed',
|
||||||
|
'Initiate tx refund tx published': 'Secret-hash swap: Initiate transaction refunded',
|
||||||
|
'Participate tx published': 'Secret-hash swap: Participate transaction broadcast',
|
||||||
|
'Participate tx redeem tx published': 'Secret-hash swap: Participate transaction claimed',
|
||||||
|
'Participate tx refund tx published': 'Secret-hash swap: Participate transaction refunded',
|
||||||
|
'BCH mercy tx found': 'BCH specific: Mercy transaction detected',
|
||||||
|
'Lock tx B mercy tx published': 'BCH specific: Mercy transaction broadcast',
|
||||||
|
'Auto accepting': 'Automation is accepting this bid',
|
||||||
|
'Failed auto accepting': 'Automation constraints prevented accepting this bid',
|
||||||
|
'Debug tweak applied': 'Debug mode: A test tweak was applied'
|
||||||
|
},
|
||||||
|
|
||||||
|
STATE_PHASES: {
|
||||||
|
1: { phase: 'negotiation', order: 1, label: 'Negotiation' }, // BID_SENT
|
||||||
|
2: { phase: 'negotiation', order: 2, label: 'Negotiation' }, // BID_RECEIVING
|
||||||
|
3: { phase: 'negotiation', order: 3, label: 'Negotiation' }, // BID_RECEIVED
|
||||||
|
4: { phase: 'negotiation', order: 4, label: 'Negotiation' }, // BID_RECEIVING_ACC
|
||||||
|
5: { phase: 'accepted', order: 5, label: 'Accepted' }, // BID_ACCEPTED
|
||||||
|
6: { phase: 'locking', order: 6, label: 'Locking' }, // SWAP_INITIATED
|
||||||
|
7: { phase: 'locking', order: 7, label: 'Locking' }, // SWAP_PARTICIPATING
|
||||||
|
8: { phase: 'complete', order: 100, label: 'Complete' }, // SWAP_COMPLETED
|
||||||
|
9: { phase: 'locking', order: 8, label: 'Locking' }, // XMR_SWAP_SCRIPT_COIN_LOCKED
|
||||||
|
10: { phase: 'locking', order: 9, label: 'Locking' }, // XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX
|
||||||
|
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
|
||||||
|
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
|
||||||
|
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED
|
||||||
|
14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
|
||||||
|
15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
|
||||||
|
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
|
||||||
|
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
|
||||||
|
18: { phase: 'failed', order: 93, label: 'Failed' }, // XMR_SWAP_FAILED_SWIPED
|
||||||
|
19: { phase: 'failed', order: 94, label: 'Failed' }, // XMR_SWAP_FAILED
|
||||||
|
20: { phase: 'locking', order: 7.5, label: 'Locking' }, // SWAP_DELAYING
|
||||||
|
21: { phase: 'failed', order: 95, label: 'Failed' }, // SWAP_TIMEDOUT
|
||||||
|
22: { phase: 'failed', order: 96, label: 'Abandoned' }, // BID_ABANDONED
|
||||||
|
23: { phase: 'failed', order: 97, label: 'Error' }, // BID_ERROR
|
||||||
|
25: { phase: 'failed', order: 98, label: 'Rejected' }, // BID_REJECTED
|
||||||
|
27: { phase: 'accepted', order: 5.5, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS
|
||||||
|
28: { phase: 'accepted', order: 5.6, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX
|
||||||
|
29: { phase: 'negotiation', order: 0.5, label: 'Negotiation' }, // BID_REQUEST_SENT
|
||||||
|
30: { phase: 'negotiation', order: 0.6, label: 'Negotiation' }, // BID_REQUEST_ACCEPTED
|
||||||
|
31: { phase: 'failed', order: 99, label: 'Expired' }, // BID_EXPIRED
|
||||||
|
32: { phase: 'negotiation', order: 3.5, label: 'Negotiation' }, // BID_AACCEPT_DELAY
|
||||||
|
33: { phase: 'failed', order: 89, label: 'Failed' }, // BID_AACCEPT_FAIL
|
||||||
|
34: { phase: 'negotiation', order: 0.4, label: 'Negotiation' } // CONNECT_REQ_SENT
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function(bidId, bidStateInd, createdAtTimestamp, stateTimeTimestamp) {
|
||||||
|
this.bidId = bidId;
|
||||||
|
this.bidStateInd = bidStateInd;
|
||||||
|
this.createdAtTimestamp = createdAtTimestamp;
|
||||||
|
this.stateTimeTimestamp = stateTimeTimestamp;
|
||||||
|
this.tooltipCounter = 0;
|
||||||
|
|
||||||
|
this.applyStateTooltips();
|
||||||
|
this.applyEventTooltips();
|
||||||
|
this.createProgressBar();
|
||||||
|
this.startElapsedTimeUpdater();
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
isActiveState: function() {
|
||||||
|
return !this.INACTIVE_STATES.includes(this.bidStateInd);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupAutoRefresh: function() {
|
||||||
|
if (!this.isActiveState()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshBtn = document.getElementById('refresh');
|
||||||
|
if (!refreshBtn) return;
|
||||||
|
|
||||||
|
let countdown = this.AUTO_REFRESH_SECONDS;
|
||||||
|
const originalSpan = refreshBtn.querySelector('span');
|
||||||
|
if (!originalSpan) return;
|
||||||
|
|
||||||
|
const updateCountdown = () => {
|
||||||
|
originalSpan.textContent = `Auto-refresh in ${countdown}s`;
|
||||||
|
countdown--;
|
||||||
|
if (countdown < 0) {
|
||||||
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCountdown();
|
||||||
|
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('mouseenter', () => {
|
||||||
|
if (this.autoRefreshInterval) {
|
||||||
|
clearInterval(this.autoRefreshInterval);
|
||||||
|
originalSpan.textContent = 'Click to refresh (paused)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('mouseleave', () => {
|
||||||
|
countdown = this.AUTO_REFRESH_SECONDS;
|
||||||
|
updateCountdown();
|
||||||
|
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createTooltip: function(element, tooltipText) {
|
||||||
|
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
||||||
|
try {
|
||||||
|
const tooltipContent = `
|
||||||
|
<div class="py-1 px-2 text-sm text-white">
|
||||||
|
${tooltipText}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
window.TooltipManager.create(element, tooltipContent, {
|
||||||
|
placement: 'top'
|
||||||
|
});
|
||||||
|
element.classList.add('cursor-help');
|
||||||
|
} catch (e) {
|
||||||
|
element.setAttribute('title', tooltipText);
|
||||||
|
element.classList.add('cursor-help');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element.setAttribute('title', tooltipText);
|
||||||
|
element.classList.add('cursor-help');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStateTooltips: function() {
|
||||||
|
const sections = document.querySelectorAll('section');
|
||||||
|
let oldStatesSection = null;
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const h4 = section.querySelector('h4');
|
||||||
|
if (h4 && h4.textContent.includes('Old states')) {
|
||||||
|
oldStatesSection = section.nextElementSibling;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldStatesSection) {
|
||||||
|
const table = oldStatesSection.querySelector('table');
|
||||||
|
if (table) {
|
||||||
|
const rows = table.querySelectorAll('tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
const stateCell = cells[cells.length - 1];
|
||||||
|
const stateText = stateCell.textContent.trim();
|
||||||
|
const tooltip = this.STATE_TOOLTIPS[stateText];
|
||||||
|
if (tooltip) {
|
||||||
|
this.addHelpIcon(stateCell, tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRows = document.querySelectorAll('table tr');
|
||||||
|
allRows.forEach(row => {
|
||||||
|
const firstCell = row.querySelector('td');
|
||||||
|
if (firstCell) {
|
||||||
|
const labelText = firstCell.textContent.trim();
|
||||||
|
if (labelText === 'Bid State') {
|
||||||
|
const valueCell = row.querySelectorAll('td')[1];
|
||||||
|
if (valueCell) {
|
||||||
|
const stateText = valueCell.textContent.trim();
|
||||||
|
const tooltip = this.STATE_TOOLTIPS[stateText] || this.STATE_TOOLTIPS['Bid ' + stateText];
|
||||||
|
if (tooltip) {
|
||||||
|
this.addHelpIcon(valueCell, tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addHelpIcon: function(cell, tooltipText) {
|
||||||
|
if (cell.querySelector('.help-icon')) return;
|
||||||
|
|
||||||
|
const helpIcon = document.createElement('span');
|
||||||
|
helpIcon.className = 'help-icon cursor-help inline-flex items-center justify-center w-4 h-4 ml-2 text-xs font-medium text-white bg-blue-500 dark:bg-blue-600 rounded-full hover:bg-blue-600 dark:hover:bg-blue-500';
|
||||||
|
helpIcon.textContent = '?';
|
||||||
|
helpIcon.style.fontSize = '10px';
|
||||||
|
helpIcon.style.verticalAlign = 'middle';
|
||||||
|
helpIcon.style.flexShrink = '0';
|
||||||
|
|
||||||
|
cell.appendChild(helpIcon);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.createTooltip(helpIcon, tooltipText);
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
applyEventTooltips: function() {
|
||||||
|
const sections = document.querySelectorAll('section');
|
||||||
|
let eventsSection = null;
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const h4 = section.querySelector('h4');
|
||||||
|
if (h4 && h4.textContent.includes('Events')) {
|
||||||
|
eventsSection = section.nextElementSibling;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventsSection) {
|
||||||
|
const table = eventsSection.querySelector('table');
|
||||||
|
if (table) {
|
||||||
|
const rows = table.querySelectorAll('tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
const eventCell = cells[cells.length - 1];
|
||||||
|
const eventText = eventCell.textContent.trim();
|
||||||
|
|
||||||
|
let tooltip = this.EVENT_TOOLTIPS[eventText];
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
for (const [key, value] of Object.entries(this.EVENT_TOOLTIPS)) {
|
||||||
|
if (eventText.startsWith(key.replace(':', ''))) {
|
||||||
|
tooltip = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tooltip && eventText.startsWith('Warning:')) {
|
||||||
|
tooltip = 'System warning - check message for details';
|
||||||
|
}
|
||||||
|
if (!tooltip && eventText.startsWith('Error:')) {
|
||||||
|
tooltip = 'Error occurred - check message for details';
|
||||||
|
}
|
||||||
|
if (!tooltip && eventText.startsWith('Temporary RPC error')) {
|
||||||
|
tooltip = 'Temporary error checking transaction. Will retry automatically';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
this.addHelpIcon(eventCell, tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createProgressBar: function() {
|
||||||
|
const phaseInfo = this.STATE_PHASES[this.bidStateInd];
|
||||||
|
if (!phaseInfo) return;
|
||||||
|
|
||||||
|
let progressPercent = 0;
|
||||||
|
const phase = phaseInfo.phase;
|
||||||
|
|
||||||
|
if (phase === 'negotiation') progressPercent = 15;
|
||||||
|
else if (phase === 'accepted') progressPercent = 30;
|
||||||
|
else if (phase === 'locking') progressPercent = 55;
|
||||||
|
else if (phase === 'redemption') progressPercent = 80;
|
||||||
|
else if (phase === 'complete') progressPercent = 100;
|
||||||
|
else if (phase === 'failed' || phase === 'error') progressPercent = 100;
|
||||||
|
|
||||||
|
const bidStateRow = document.querySelector('td.bold');
|
||||||
|
if (!bidStateRow) return;
|
||||||
|
|
||||||
|
let targetRow = null;
|
||||||
|
const rows = document.querySelectorAll('table tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const firstTd = row.querySelector('td.bold');
|
||||||
|
if (firstTd && firstTd.textContent.trim() === 'Bid State') {
|
||||||
|
targetRow = row;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetRow) return;
|
||||||
|
|
||||||
|
const progressRow = document.createElement('tr');
|
||||||
|
progressRow.className = 'opacity-100 text-gray-500 dark:text-gray-100';
|
||||||
|
|
||||||
|
const isError = ['failed', 'error'].includes(phase);
|
||||||
|
const isComplete = phase === 'complete';
|
||||||
|
const barColor = isError ? 'bg-red-500' : (isComplete ? 'bg-green-500' : 'bg-blue-500');
|
||||||
|
const phaseLabel = isError ? phaseInfo.label : (isComplete ? 'Complete' : `${phaseInfo.label} (${progressPercent}%)`);
|
||||||
|
|
||||||
|
progressRow.innerHTML = `
|
||||||
|
<td class="py-3 px-6 bold">Swap Progress</td>
|
||||||
|
<td class="py-3 px-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex-1 bg-gray-200 dark:bg-gray-600 rounded-full h-2.5 max-w-xs">
|
||||||
|
<div class="${barColor} h-2.5 rounded-full transition-all duration-500" style="width: ${progressPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-white">${phaseLabel}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
targetRow.parentNode.insertBefore(progressRow, targetRow.nextSibling);
|
||||||
|
},
|
||||||
|
|
||||||
|
startElapsedTimeUpdater: function() {
|
||||||
|
if (!this.createdAtTimestamp) return;
|
||||||
|
|
||||||
|
let createdAtRow = null;
|
||||||
|
const rows = document.querySelectorAll('table tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const firstTd = row.querySelector('td');
|
||||||
|
if (firstTd && firstTd.textContent.includes('Created At')) {
|
||||||
|
createdAtRow = row;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createdAtRow) return;
|
||||||
|
|
||||||
|
const isCompleted = !this.isActiveState() && this.stateTimeTimestamp;
|
||||||
|
|
||||||
|
const elapsedRow = document.createElement('tr');
|
||||||
|
elapsedRow.className = 'opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
|
||||||
|
|
||||||
|
const labelText = isCompleted ? 'Swap Duration' : 'Time Elapsed';
|
||||||
|
const iconColor = isCompleted ? '#10B981' : '#3B82F6';
|
||||||
|
|
||||||
|
elapsedRow.innerHTML = `
|
||||||
|
<td class="flex items-center px-46 whitespace-nowrap">
|
||||||
|
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||||
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${iconColor}" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="11"></circle>
|
||||||
|
<polyline points="12,6 12,12 18,12" stroke="${iconColor}"></polyline>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div class="py-3 pl-2 bold">
|
||||||
|
<div>${labelText}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-6" id="elapsed-time-display">Calculating...</td>
|
||||||
|
`;
|
||||||
|
createdAtRow.parentNode.insertBefore(elapsedRow, createdAtRow.nextSibling);
|
||||||
|
|
||||||
|
const elapsedDisplay = document.getElementById('elapsed-time-display');
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
const duration = this.stateTimeTimestamp - this.createdAtTimestamp;
|
||||||
|
elapsedDisplay.textContent = this.formatDuration(duration);
|
||||||
|
} else {
|
||||||
|
const updateElapsed = () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const elapsed = now - this.createdAtTimestamp;
|
||||||
|
elapsedDisplay.textContent = this.formatDuration(elapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateElapsed();
|
||||||
|
this.elapsedTimeInterval = setInterval(updateElapsed, 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDuration: function(seconds) {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) {
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
if (remainingSeconds > 0) {
|
||||||
|
return `${minutes} min ${remainingSeconds} sec`;
|
||||||
|
}
|
||||||
|
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
if (hours < 24) {
|
||||||
|
if (remainingMinutes > 0) {
|
||||||
|
return `${hours} hr ${remainingMinutes} min`;
|
||||||
|
}
|
||||||
|
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const remainingHours = hours % 24;
|
||||||
|
if (remainingHours > 0) {
|
||||||
|
return `${days} day${days !== 1 ? 's' : ''} ${remainingHours} hr`;
|
||||||
|
}
|
||||||
|
return `${days} day${days !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -41,12 +41,29 @@
|
|||||||
setupEventListeners: function() {
|
setupEventListeners: function() {
|
||||||
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
|
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
|
||||||
if (sendBidBtn) {
|
if (sendBidBtn) {
|
||||||
sendBidBtn.onclick = this.showConfirmModal.bind(this);
|
sendBidBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.showConfirmModal();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
|
const modalCancelBtn = document.querySelector('#confirmModal [data-hide-modal]');
|
||||||
if (modalCancelBtn) {
|
if (modalCancelBtn) {
|
||||||
modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
|
modalCancelBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.hideConfirmModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmModal = document.getElementById('confirmModal');
|
||||||
|
if (confirmModal) {
|
||||||
|
confirmModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === confirmModal || e.target.classList.contains('bg-opacity-50')) {
|
||||||
|
this.hideConfirmModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainCancelBtn = document.querySelector('button[name="cancel"]');
|
const mainCancelBtn = document.querySelector('button[name="cancel"]');
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
confirmCallback: null,
|
confirmCallback: null,
|
||||||
triggerElement: null,
|
triggerElement: null,
|
||||||
|
|
||||||
|
originalConnectionTypes: {},
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this.setupTabs();
|
this.setupTabs();
|
||||||
this.setupCoinHeaders();
|
this.setupCoinHeaders();
|
||||||
this.setupConfirmModal();
|
this.setupConfirmModal();
|
||||||
this.setupNotificationSettings();
|
this.setupNotificationSettings();
|
||||||
|
this.setupMigrationIndicator();
|
||||||
|
this.setupServerDiscovery();
|
||||||
},
|
},
|
||||||
|
|
||||||
setupTabs: function() {
|
setupTabs: function() {
|
||||||
@@ -61,6 +65,144 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pendingModeSwitch: null,
|
||||||
|
|
||||||
|
setupMigrationIndicator: function() {
|
||||||
|
const connectionTypeSelects = document.querySelectorAll('select[name^="connection_type_"]');
|
||||||
|
connectionTypeSelects.forEach(select => {
|
||||||
|
const originalValue = select.dataset.originalValue || select.value;
|
||||||
|
this.originalConnectionTypes[select.name] = originalValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupWalletModeModal();
|
||||||
|
|
||||||
|
const coinsForm = document.getElementById('coins-form');
|
||||||
|
if (coinsForm) {
|
||||||
|
coinsForm.addEventListener('submit', (e) => {
|
||||||
|
const submitter = e.submitter;
|
||||||
|
if (!submitter || !submitter.name.startsWith('apply_')) return;
|
||||||
|
|
||||||
|
const coinName = submitter.name.replace('apply_', '');
|
||||||
|
const select = document.querySelector(`select[name="connection_type_${coinName}"]`);
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
const original = this.originalConnectionTypes[select.name];
|
||||||
|
const current = select.value;
|
||||||
|
|
||||||
|
if (original && current && original !== current) {
|
||||||
|
e.preventDefault();
|
||||||
|
const direction = (original === 'rpc' && current === 'electrum') ? 'lite' : 'rpc';
|
||||||
|
this.showWalletModeConfirmation(coinName, direction, submitter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupWalletModeModal: function() {
|
||||||
|
const confirmBtn = document.getElementById('walletModeConfirm');
|
||||||
|
const cancelBtn = document.getElementById('walletModeCancel');
|
||||||
|
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', () => {
|
||||||
|
this.hideWalletModeModal();
|
||||||
|
if (this.pendingModeSwitch) {
|
||||||
|
const { coinName, direction, submitter } = this.pendingModeSwitch;
|
||||||
|
this.showMigrationModal(coinName.toUpperCase(), direction);
|
||||||
|
const form = submitter.form;
|
||||||
|
const hiddenInput = document.createElement('input');
|
||||||
|
hiddenInput.type = 'hidden';
|
||||||
|
hiddenInput.name = submitter.name;
|
||||||
|
hiddenInput.value = submitter.value;
|
||||||
|
form.appendChild(hiddenInput);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
this.hideWalletModeModal();
|
||||||
|
if (this.pendingModeSwitch) {
|
||||||
|
const { coinName } = this.pendingModeSwitch;
|
||||||
|
const select = document.querySelector(`select[name="connection_type_${coinName}"]`);
|
||||||
|
if (select) {
|
||||||
|
select.value = this.originalConnectionTypes[select.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pendingModeSwitch = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showWalletModeConfirmation: function(coinName, direction, submitter) {
|
||||||
|
const modal = document.getElementById('walletModeModal');
|
||||||
|
const title = document.getElementById('walletModeTitle');
|
||||||
|
const message = document.getElementById('walletModeMessage');
|
||||||
|
const details = document.getElementById('walletModeDetails');
|
||||||
|
|
||||||
|
if (!modal || !title || !message || !details) return;
|
||||||
|
|
||||||
|
this.pendingModeSwitch = { coinName, direction, submitter };
|
||||||
|
|
||||||
|
const displayName = coinName.charAt(0).toUpperCase() + coinName.slice(1).toLowerCase();
|
||||||
|
|
||||||
|
if (direction === 'lite') {
|
||||||
|
title.textContent = `Switch ${displayName} to Lite Wallet Mode`;
|
||||||
|
message.textContent = 'Please confirm you want to switch to lite wallet mode.';
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2"><strong>Before switching:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li>Active swaps must be completed first</li>
|
||||||
|
<li>Wait for any pending transactions to confirm</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-3 text-green-600 dark:text-green-400">
|
||||||
|
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
title.textContent = `Switch ${displayName} to Full Node Mode`;
|
||||||
|
message.textContent = 'Please confirm you want to switch to full node mode.';
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2"><strong>Switching to full node mode:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li>Requires synced ${displayName} blockchain</li>
|
||||||
|
<li>Your wallet addresses will be synced</li>
|
||||||
|
<li>Active swaps must be completed first</li>
|
||||||
|
<li>Restart required after switch</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-3 text-green-600 dark:text-green-400">
|
||||||
|
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
},
|
||||||
|
|
||||||
|
hideWalletModeModal: function() {
|
||||||
|
const modal = document.getElementById('walletModeModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showMigrationModal: function(coinName, direction) {
|
||||||
|
const modal = document.getElementById('migrationModal');
|
||||||
|
const title = document.getElementById('migrationTitle');
|
||||||
|
const message = document.getElementById('migrationMessage');
|
||||||
|
|
||||||
|
if (modal && title && message) {
|
||||||
|
if (direction === 'lite') {
|
||||||
|
title.textContent = `Migrating ${coinName} to Lite Wallet`;
|
||||||
|
message.textContent = 'Checking wallet balance and migrating addresses. Please wait...';
|
||||||
|
} else {
|
||||||
|
title.textContent = `Switching ${coinName} to Full Node`;
|
||||||
|
message.textContent = 'Syncing wallet indices. Please wait...';
|
||||||
|
}
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setupConfirmModal: function() {
|
setupConfirmModal: function() {
|
||||||
const confirmYesBtn = document.getElementById('confirmYes');
|
const confirmYesBtn = document.getElementById('confirmYes');
|
||||||
if (confirmYesBtn) {
|
if (confirmYesBtn) {
|
||||||
@@ -307,6 +449,166 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SettingsPage.setupServerDiscovery = function() {
|
||||||
|
const discoverBtns = document.querySelectorAll('.discover-servers-btn');
|
||||||
|
discoverBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const coin = btn.dataset.coin;
|
||||||
|
this.discoverServers(coin, btn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeBtns = document.querySelectorAll('.close-discovered-btn');
|
||||||
|
closeBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const coin = btn.dataset.coin;
|
||||||
|
const panel = document.getElementById(`discovered-servers-${coin}`);
|
||||||
|
if (panel) panel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsPage.discoverServers = function(coin, button) {
|
||||||
|
const originalHtml = button.innerHTML;
|
||||||
|
button.innerHTML = `<svg class="w-3.5 h-3.5 mr-1 animate-spin inline-block" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Discovering...`;
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
const panel = document.getElementById(`discovered-servers-${coin}`);
|
||||||
|
const listContainer = document.getElementById(`discovered-list-${coin}`);
|
||||||
|
|
||||||
|
fetch('/json/electrumdiscover', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ coin: coin, ping: true })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
listContainer.innerHTML = `<div class="text-sm text-red-500">${data.error}</div>`;
|
||||||
|
} else {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (data.current_server) {
|
||||||
|
html += `
|
||||||
|
<div class="flex items-center mb-4 p-3 bg-gray-100 dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-3 animate-pulse"></span>
|
||||||
|
<span class="text-sm text-gray-900 dark:text-white">
|
||||||
|
Connected to: <span class="font-mono font-medium">${data.current_server.host}:${data.current_server.port}</span>
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.clearnet_servers && data.clearnet_servers.length > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-semibold text-gray-900 dark:text-white mb-2 pb-2 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
Clearnet
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">`;
|
||||||
|
data.clearnet_servers.forEach(srv => {
|
||||||
|
const statusClass = srv.online ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500';
|
||||||
|
const statusText = srv.online ? (srv.latency_ms ? srv.latency_ms.toFixed(0) + 'ms' : 'online') : 'offline';
|
||||||
|
const statusDot = srv.online ? 'bg-green-500' : 'bg-gray-400';
|
||||||
|
html += `
|
||||||
|
<div class="flex items-center justify-between py-2 px-3 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg cursor-pointer add-server-btn transition-colors border border-transparent hover:border-blue-500"
|
||||||
|
data-coin="${coin}" data-host="${srv.host}" data-port="${srv.port}" data-type="clearnet">
|
||||||
|
<div class="flex items-center flex-1 min-w-0">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="font-mono text-sm text-gray-900 dark:text-white truncate">${srv.host}:${srv.port}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center ml-3">
|
||||||
|
<span class="w-2 h-2 ${statusDot} rounded-full mr-2"></span>
|
||||||
|
<span class="text-xs ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.onion_servers && data.onion_servers.length > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-semibold text-gray-900 dark:text-white mb-2 pb-2 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
TOR (.onion)
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">`;
|
||||||
|
data.onion_servers.forEach(srv => {
|
||||||
|
const statusClass = srv.online ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500';
|
||||||
|
const statusText = srv.online ? (srv.latency_ms ? srv.latency_ms.toFixed(0) + 'ms' : 'online') : 'offline';
|
||||||
|
const statusDot = srv.online ? 'bg-green-500' : 'bg-gray-400';
|
||||||
|
html += `
|
||||||
|
<div class="flex items-center justify-between py-2 px-3 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg cursor-pointer add-server-btn transition-colors border border-transparent hover:border-blue-500"
|
||||||
|
data-coin="${coin}" data-host="${srv.host}" data-port="${srv.port}" data-type="onion">
|
||||||
|
<div class="flex items-center flex-1 min-w-0">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="font-mono text-sm text-gray-900 dark:text-white truncate" title="${srv.host}">${srv.host.substring(0, 24)}...:${srv.port}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center ml-3">
|
||||||
|
<span class="w-2 h-2 ${statusDot} rounded-full mr-2"></span>
|
||||||
|
<span class="text-xs ${statusClass}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.clearnet_servers?.length && !data.onion_servers?.length) {
|
||||||
|
html = '<div class="text-sm text-gray-500 dark:text-gray-400 py-4 text-center">No servers discovered. The connected server may not support peer discovery.</div>';
|
||||||
|
} else {
|
||||||
|
html += `<div class="text-xs text-gray-500 dark:text-gray-400 pt-3 border-t border-gray-200 dark:border-gray-600">Click a server to add it to your list</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
listContainer.innerHTML = html;
|
||||||
|
|
||||||
|
listContainer.querySelectorAll('.add-server-btn').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const host = item.dataset.host;
|
||||||
|
const port = item.dataset.port;
|
||||||
|
const type = item.dataset.type;
|
||||||
|
const coinName = item.dataset.coin;
|
||||||
|
|
||||||
|
const textareaId = type === 'onion' ?
|
||||||
|
`electrum_onion_${coinName}` : `electrum_clearnet_${coinName}`;
|
||||||
|
const textarea = document.getElementById(textareaId);
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
const serverLine = `${host}:${port}`;
|
||||||
|
const currentValue = textarea.value.trim();
|
||||||
|
|
||||||
|
if (currentValue.split('\n').some(line => line.trim() === serverLine)) {
|
||||||
|
item.classList.add('bg-yellow-100', 'dark:bg-yellow-800/30');
|
||||||
|
setTimeout(() => item.classList.remove('bg-yellow-100', 'dark:bg-yellow-800/30'), 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.value = currentValue ? currentValue + '\n' + serverLine : serverLine;
|
||||||
|
item.classList.add('bg-green-100', 'dark:bg-green-800/30');
|
||||||
|
setTimeout(() => item.classList.remove('bg-green-100', 'dark:bg-green-800/30'), 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
listContainer.innerHTML = `<div class="text-xs text-red-500">Failed to discover servers: ${err.message}</div>`;
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
button.innerHTML = originalHtml;
|
||||||
|
button.disabled = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
SettingsPage.cleanup = function() {
|
SettingsPage.cleanup = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
this.setupWithdrawalConfirmation();
|
this.setupWithdrawalConfirmation();
|
||||||
this.setupTransactionDisplay();
|
this.setupTransactionDisplay();
|
||||||
this.setupWebSocketUpdates();
|
this.setupWebSocketUpdates();
|
||||||
|
this.setupTransactionPagination();
|
||||||
},
|
},
|
||||||
|
|
||||||
setupAddressCopy: function() {
|
setupAddressCopy: function() {
|
||||||
@@ -340,13 +341,289 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleBalanceUpdate: function(balanceData) {
|
handleBalanceUpdate: function(balanceData) {
|
||||||
|
if (!balanceData || !Array.isArray(balanceData)) return;
|
||||||
console.log('Balance updated:', balanceData);
|
|
||||||
|
const coinId = this.currentCoinId;
|
||||||
|
if (!coinId) return;
|
||||||
|
|
||||||
|
const matchingCoins = balanceData.filter(coin =>
|
||||||
|
coin.ticker && coin.ticker.toLowerCase() === coinId.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
matchingCoins.forEach(coinData => {
|
||||||
|
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||||
|
balanceElements.forEach(element => {
|
||||||
|
const elementCoinName = element.getAttribute('data-coinname');
|
||||||
|
if (elementCoinName === coinData.name) {
|
||||||
|
const currentText = element.textContent;
|
||||||
|
const ticker = coinData.ticker || coinId.toUpperCase();
|
||||||
|
const newBalance = `${coinData.balance} ${ticker}`;
|
||||||
|
if (currentText !== newBalance) {
|
||||||
|
element.textContent = newBalance;
|
||||||
|
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updatePendingForCoin(coinData);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refreshTransactions();
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePendingForCoin: function(coinData) {
|
||||||
|
const pendingAmount = parseFloat(coinData.pending || '0');
|
||||||
|
|
||||||
|
|
||||||
|
const pendingElements = document.querySelectorAll('.inline-block.py-1.px-2.rounded-full.bg-green-100');
|
||||||
|
|
||||||
|
pendingElements.forEach(el => {
|
||||||
|
const text = el.textContent || '';
|
||||||
|
|
||||||
|
if (text.includes('Pending:') && text.includes(coinData.ticker)) {
|
||||||
|
if (pendingAmount > 0) {
|
||||||
|
el.textContent = `Pending: +${coinData.pending} ${coinData.ticker}`;
|
||||||
|
el.style.display = '';
|
||||||
|
} else {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTransactions: function() {
|
||||||
|
const txTable = document.querySelector('#transaction-history-section tbody');
|
||||||
|
if (txTable) {
|
||||||
|
const pathParts = window.location.pathname.split('/');
|
||||||
|
const ticker = pathParts[pathParts.length - 1];
|
||||||
|
|
||||||
|
fetch(`/json/wallettransactions/${ticker}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ page_no: 1 })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.transactions && data.transactions.length > 0) {
|
||||||
|
const currentPageSpan = document.getElementById('currentPageTx');
|
||||||
|
const totalPagesSpan = document.getElementById('totalPagesTx');
|
||||||
|
if (currentPageSpan) currentPageSpan.textContent = data.page_no;
|
||||||
|
if (totalPagesSpan) totalPagesSpan.textContent = data.total_pages;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error refreshing transactions:', error));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSwapEvent: function(eventData) {
|
handleSwapEvent: function(eventData) {
|
||||||
|
if (window.BalanceUpdatesManager) {
|
||||||
console.log('Swap event:', eventData);
|
window.BalanceUpdatesManager.fetchBalanceData()
|
||||||
|
.then(data => this.handleBalanceUpdate(data))
|
||||||
|
.catch(error => console.error('Error updating balance after swap:', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupTransactionPagination: function() {
|
||||||
|
const txContainer = document.getElementById('tx-container');
|
||||||
|
if (!txContainer) return;
|
||||||
|
|
||||||
|
const pathParts = window.location.pathname.split('/');
|
||||||
|
const ticker = pathParts[pathParts.length - 1];
|
||||||
|
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
const prevBtn = document.getElementById('prevPageTx');
|
||||||
|
const nextBtn = document.getElementById('nextPageTx');
|
||||||
|
const currentPageSpan = document.getElementById('currentPageTx');
|
||||||
|
const totalPagesSpan = document.getElementById('totalPagesTx');
|
||||||
|
const paginationControls = document.getElementById('tx-pagination-section');
|
||||||
|
|
||||||
|
const copyToClipboard = (text, button) => {
|
||||||
|
const showSuccess = () => {
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.innerHTML = `<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(showSuccess).catch(err => {
|
||||||
|
console.error('Clipboard API failed:', err);
|
||||||
|
fallbackCopy(text, showSuccess);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text, showSuccess);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackCopy = (text, onSuccess) => {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
onSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback copy failed:', err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTransactions = async (page) => {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/json/wallettransactions/${ticker}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ page_no: page })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error loading transactions:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage = data.page_no;
|
||||||
|
totalPages = data.total_pages;
|
||||||
|
|
||||||
|
currentPageSpan.textContent = currentPage;
|
||||||
|
totalPagesSpan.textContent = totalPages;
|
||||||
|
|
||||||
|
txContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.transactions && data.transactions.length > 0) {
|
||||||
|
data.transactions.forEach(tx => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'bg-white dark:bg-gray-600 rounded-lg border border-gray-200 dark:border-gray-500 p-4 hover:shadow-md transition-shadow';
|
||||||
|
|
||||||
|
let typeClass = 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300';
|
||||||
|
let amountClass = 'text-gray-700 dark:text-gray-200';
|
||||||
|
let typeIcon = '';
|
||||||
|
let amountPrefix = '';
|
||||||
|
if (tx.type === 'Incoming') {
|
||||||
|
typeClass = 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-300';
|
||||||
|
amountClass = 'text-green-600 dark:text-green-400';
|
||||||
|
typeIcon = '↓';
|
||||||
|
amountPrefix = '+';
|
||||||
|
} else if (tx.type === 'Outgoing') {
|
||||||
|
typeClass = 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300';
|
||||||
|
amountClass = 'text-red-600 dark:text-red-400';
|
||||||
|
typeIcon = '↑';
|
||||||
|
amountPrefix = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmClass = 'text-gray-600 dark:text-gray-300';
|
||||||
|
if (tx.confirmations === 0) {
|
||||||
|
confirmClass = 'text-yellow-600 dark:text-yellow-400 font-medium';
|
||||||
|
} else if (tx.confirmations >= 1 && tx.confirmations <= 5) {
|
||||||
|
confirmClass = 'text-blue-600 dark:text-blue-400';
|
||||||
|
} else if (tx.confirmations >= 6) {
|
||||||
|
confirmClass = 'text-green-600 dark:text-green-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="inline-flex items-center gap-1 py-1 px-2 rounded-full text-xs font-semibold ${typeClass}">
|
||||||
|
${typeIcon} ${tx.type}
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold ${amountClass}">
|
||||||
|
${amountPrefix}${tx.amount} ${ticker.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 text-sm">
|
||||||
|
<span class="${confirmClass}">${tx.confirmations} Confirmations</span>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">${tx.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${tx.address ? `
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400 w-16 flex-shrink-0">Address:</span>
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-200 break-all flex-1">${tx.address}</span>
|
||||||
|
<button class="copy-address-btn p-1.5 hover:bg-gray-100 dark:hover:bg-gray-500 rounded flex-shrink-0 focus:outline-none focus:ring-0" title="Copy Address">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400 w-16 flex-shrink-0">Txid:</span>
|
||||||
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-200 break-all flex-1">${tx.txid}</span>
|
||||||
|
<button class="copy-txid-btn p-1.5 hover:bg-gray-100 dark:hover:bg-gray-500 rounded flex-shrink-0 focus:outline-none focus:ring-0" title="Copy Transaction ID">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const copyAddressBtn = card.querySelector('.copy-address-btn');
|
||||||
|
if (copyAddressBtn) {
|
||||||
|
copyAddressBtn.addEventListener('click', () => copyToClipboard(tx.address, copyAddressBtn));
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyTxidBtn = card.querySelector('.copy-txid-btn');
|
||||||
|
if (copyTxidBtn) {
|
||||||
|
copyTxidBtn.addEventListener('click', () => copyToClipboard(tx.txid, copyTxidBtn));
|
||||||
|
}
|
||||||
|
|
||||||
|
txContainer.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalPages > 1 && paginationControls) {
|
||||||
|
paginationControls.style.display = 'block';
|
||||||
|
} else if (paginationControls) {
|
||||||
|
paginationControls.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txContainer.innerHTML = '<div class="text-center py-8 text-gray-500 dark:text-gray-400">No transactions found</div>';
|
||||||
|
if (paginationControls) paginationControls.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
prevBtn.style.display = currentPage > 1 ? 'inline-flex' : 'none';
|
||||||
|
nextBtn.style.display = currentPage < totalPages ? 'inline-flex' : 'none';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching transactions:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
loadTransactions(currentPage - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
loadTransactions(currentPage + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTransactions(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coinData.scan_status) {
|
||||||
|
this.updateScanStatus(coinData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coinData.version) {
|
||||||
|
const versionEl = document.querySelector(`.electrum-version[data-coin="${coinData.name}"]`);
|
||||||
|
if (versionEl && versionEl.textContent !== coinData.version) {
|
||||||
|
versionEl.textContent = coinData.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (coinData.electrum_server) {
|
||||||
|
const serverEl = document.querySelector(`.electrum-server[data-coin="${coinData.name}"]`);
|
||||||
|
if (serverEl && serverEl.textContent !== coinData.electrum_server) {
|
||||||
|
serverEl.textContent = coinData.electrum_server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateScanStatus: function(coinData) {
|
||||||
|
const scanStatusEl = document.querySelector(`.scan-status[data-coin="${coinData.name}"]`);
|
||||||
|
if (!scanStatusEl) return;
|
||||||
|
|
||||||
|
const status = coinData.scan_status;
|
||||||
|
if (status.in_progress) {
|
||||||
|
scanStatusEl.innerHTML = `
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-blue-600 dark:text-blue-300">
|
||||||
|
<svg class="inline-block w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Scanning ${status.status}
|
||||||
|
</span>
|
||||||
|
<span class="text-blue-500 dark:text-blue-200 font-medium">${status.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-blue-200 dark:bg-gray-700 rounded-full h-1 mt-1">
|
||||||
|
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full transition-all" style="width: ${status.progress}%"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
scanStatusEl.innerHTML = `
|
||||||
|
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||||
|
<svg class="inline-block w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
Electrum Wallet Synced
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
||||||
@@ -102,12 +152,13 @@
|
|||||||
const currentLabel = labelElement.textContent.trim();
|
const currentLabel = labelElement.textContent.trim();
|
||||||
|
|
||||||
if (currentLabel === labelText) {
|
if (currentLabel === labelText) {
|
||||||
|
const cleanBalance = balance.toString().replace(/^\+/, '');
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
const cleanBalance = balance.toString().replace(/^\+/, '');
|
|
||||||
element.textContent = `+${cleanBalance} ${ticker}`;
|
element.textContent = `+${cleanBalance} ${ticker}`;
|
||||||
} else {
|
} else {
|
||||||
element.textContent = `${balance} ${ticker}`;
|
element.textContent = `${balance} ${ticker}`;
|
||||||
}
|
}
|
||||||
|
element.setAttribute('data-original-value', `${cleanBalance} ${ticker}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +190,7 @@
|
|||||||
if (pendingSpan) {
|
if (pendingSpan) {
|
||||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||||
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
||||||
|
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${coinData.ticker || coinData.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialUSD = '$0.00';
|
let initialUSD = '$0.00';
|
||||||
@@ -218,7 +270,7 @@
|
|||||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||||
for (const element of balanceElements) {
|
for (const element of balanceElements) {
|
||||||
if (element.getAttribute('data-coinname') === coinName) {
|
if (element.getAttribute('data-coinname') === coinName) {
|
||||||
return element.closest('.bg-white, .dark\\:bg-gray-500');
|
return element.closest('.bg-gray-50, .dark\\:bg-gray-500');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -330,6 +382,7 @@
|
|||||||
if (pendingSpan) {
|
if (pendingSpan) {
|
||||||
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
||||||
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
|
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
|
||||||
|
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${ticker}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,14 +525,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if data.can_abandon == true and not edit_bid %}
|
{% if data.can_abandon == true and not edit_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
|
<button name="abandon_bid" type="submit" value="Abandon Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
|
<button name="accept_bid" value="Accept Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -689,6 +689,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/static/js/pages/bid-page.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
BidPage.init('{{ bid_id }}', {{ data.bid_state_ind }}, {{ data.created_at_timestamp }}, {{ data.state_time_timestamp or 'null' }});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -801,14 +801,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if data.can_abandon == true %}
|
{% if data.can_abandon == true %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
|
<button name="abandon_bid" type="submit" value="Abandon Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
|
<button name="accept_bid" value="Accept Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -965,6 +965,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/static/js/pages/bid-page.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
BidPage.init('{{ bid_id }}', {{ data.bid_state_ind }}, {{ data.created_at_timestamp }}, {{ data.state_time_timestamp or 'null' }});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||||
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||||
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.3.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.4.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||||
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
|
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
|
||||||
{{ love_svg | safe }}
|
{{ love_svg | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -105,17 +105,31 @@
|
|||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
Connection
|
Connection
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Connection Type</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Connection Type</label>
|
||||||
|
{% if c.supports_electrum %}
|
||||||
|
<div class="relative">
|
||||||
|
<select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none" name="connection_type_{{ c.name }}" data-original-value="{{ c.connection_type }}">
|
||||||
|
<option value="rpc" {% if c.connection_type == 'rpc' %} selected{% endif %}>Full Node (RPC)</option>
|
||||||
|
<option value="electrum" {% if c.connection_type == 'electrum' %} selected{% endif %}>Light Wallet (Electrum)</option>
|
||||||
|
</select>
|
||||||
|
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||||
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg text-sm text-gray-900 dark:text-gray-100">
|
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg text-sm text-gray-900 dark:text-gray-100">
|
||||||
{{ c.connection_type }}
|
{{ c.connection_type }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if c.manage_daemon is defined %}
|
{% if c.manage_daemon is defined %}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manage Daemon</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manage Daemon</label>
|
||||||
@@ -138,12 +152,164 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if c.supports_electrum %}
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
Mode Information
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 space-y-4">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">To Light Wallet:</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• Your full node stops running</li>
|
||||||
|
<li>• Light wallet uses your seed to access existing funds</li>
|
||||||
|
<li>• No transfer needed - same seed, same funds</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">While in Light Wallet mode:</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• Light wallet generates NEW addresses (BIP84 format: bc1q.../ltc1q...)</li>
|
||||||
|
<li>• Any funds you RECEIVE go to these new addresses</li>
|
||||||
|
<li>• Your full node doesn't know about these addresses</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">To Full Node:</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• Full node can't see funds on light wallet addresses</li>
|
||||||
|
<li>• These funds must be SENT back to your node wallet (real transaction, network fee applies based on current rate)</li>
|
||||||
|
<li>• Enable "Auto-transfer" in Fund Transfer section to do this automatically on unlock</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<p class="text-xs text-red-600 dark:text-red-400"><strong>Active Swaps:</strong> Complete all swaps before switching modes.</p>
|
||||||
|
{% if c.name == 'litecoin' %}
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-200 mt-1"><strong>MWEB:</strong> Not supported in light wallet mode.</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1"><strong>If TOR enabled:</strong> Electrum connections routed through TOR.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if c.connection_type == 'electrum' %}
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
Electrum Servers
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h5 class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-3">Clearnet</h5>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
|
<textarea class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none font-mono" name="electrum_clearnet_{{ c.name }}" id="electrum_clearnet_{{ c.name }}" rows="3" placeholder="electrum.blockstream.info:50002 electrum.emzy.de:50002">{{ c.clearnet_servers_text }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">One per line. Format: host:port (50002=SSL, 50001=non-SSL)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 pt-2">
|
||||||
|
<h5 class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-3">TOR (.onion)</h5>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
|
<textarea class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none font-mono text-xs" name="electrum_onion_{{ c.name }}" id="electrum_onion_{{ c.name }}" rows="3" placeholder="explorerzyd...onion:110 lksvbmwwi2b...onion:50001">{{ c.onion_servers_text }}</textarea>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">One per line. Used when TOR is enabled.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discover Servers Button -->
|
||||||
|
<div class="mb-4 flex justify-end">
|
||||||
|
<button type="button" class="discover-servers-btn inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-md transition-colors focus:outline-none focus:ring-0" data-coin="{{ c.name }}">
|
||||||
|
Discover {{ c.name }} electrum servers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discovered Servers Panel -->
|
||||||
|
<div id="discovered-servers-{{ c.name }}" class="hidden mb-4">
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<h5 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
Discovered Servers
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="close-discovered-btn text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-white transition-colors" data-coin="{{ c.name }}">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<div id="discovered-list-{{ c.name }}" class="space-y-1 max-h-64 overflow-y-auto">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">Click "Discover Servers" to find available servers...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
Fund Transfer
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
|
{% if c.lite_wallet_balance %}
|
||||||
|
<div class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-700 rounded-lg">
|
||||||
|
<p class="text-sm font-medium text-orange-800 dark:text-orange-200 mb-2">
|
||||||
|
<svg class="inline w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
||||||
|
Light Wallet Balance Detected
|
||||||
|
</p>
|
||||||
|
<div class="text-xs text-orange-700 dark:text-orange-300 space-y-1">
|
||||||
|
<p><strong>Confirmed:</strong> {{ "%.8f"|format(c.lite_wallet_balance.confirmed) }} {{ c.display_name }}</p>
|
||||||
|
{% if c.lite_wallet_balance.unconfirmed > 0 %}
|
||||||
|
<p><strong>Unconfirmed:</strong> {{ "%.8f"|format(c.lite_wallet_balance.unconfirmed) }} {{ c.display_name }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-xs text-orange-600 dark:text-orange-400 mt-2">
|
||||||
|
{% if c.lite_wallet_balance.is_pending_sweep %}
|
||||||
|
<span class="inline-flex items-center"><svg class="animate-spin h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Sweep pending - waiting for confirmations...</span>
|
||||||
|
{% else %}
|
||||||
|
These funds will be swept to your RPC wallet automatically.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if c.lite_wallet_balance.confirmed > 0 %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="submit" name="force_sweep_{{ c.name }}" value="1" class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 dark:focus:ring-offset-gray-800" onclick="return confirm('Sweep {{ '%.8f'|format(c.lite_wallet_balance.confirmed) }} {{ c.display_name }} to your RPC wallet now? Network fee will apply.');">
|
||||||
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||||
|
Force Sweep Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="auto_transfer_{{ c.name }}" value="true" {% if c.auto_transfer_on_mode_switch != false %}checked{% endif %} class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||||
|
<span class="ml-2 text-sm font-medium text-red-600 dark:text-red-400">Auto-transfer funds when switching to Full Node</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2 ml-6">Funds in light wallet addresses will be swept to your RPC wallet after switching. Network fee applies based on current rate.</p>
|
||||||
|
|
||||||
|
{% if general_settings.debug %}
|
||||||
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address Gap Limit</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="number" name="gap_limit_{{ c.name }}" value="{{ c.address_gap_limit }}" min="5" max="100" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-24 p-2.5 focus:ring-1 focus:outline-none" placeholder="20">
|
||||||
|
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(5-100)</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Number of consecutive unfunded addresses to scan. Increase if you generated many unused addresses.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if c.name in ('wownero', 'monero') %}
|
{% if c.name in ('wownero', 'monero') %}
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
@@ -656,6 +822,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="migrationModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
|
||||||
|
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
|
||||||
|
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<svg class="animate-spin h-12 w-12 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2" id="migrationTitle">Migrating Wallet</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-200" id="migrationMessage">Extracting addresses from wallet. Please wait...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="walletModeModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
|
||||||
|
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
|
||||||
|
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-lg w-full p-6 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<svg class="h-12 w-12 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="walletModeTitle">Switch Wallet Mode</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-200 mb-4" id="walletModeMessage">Are you sure you want to switch wallet modes?</p>
|
||||||
|
<div id="walletModeDetails" class="text-left bg-gray-100 dark:bg-gray-600 rounded-lg p-4 mb-4 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<button type="button" id="walletModeConfirm"
|
||||||
|
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||||
|
Switch Mode
|
||||||
|
</button>
|
||||||
|
<button type="button" id="walletModeCancel"
|
||||||
|
class="px-4 py-2.5 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-700 font-medium text-sm text-gray-700 dark:text-white rounded-md focus:ring-0 focus:outline-none">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/pages/settings-page.js"></script>
|
<script src="/static/js/pages/settings-page.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="caps-warning" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center">
|
<div id="caps-warning" class="hidden mt-2 text-sm text-red-600 dark:text-white flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -111,6 +111,9 @@
|
|||||||
{% if w.pending %}
|
{% if w.pending %}
|
||||||
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
|
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if w.pending_out %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-yellow-600 dark:bg-gray-500 dark:text-yellow-400">Unconfirmed: -{{ w.pending_out }} {{ w.ticker }} </span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if w.cid == '1' %} {# PART #}
|
{% if w.cid == '1' %} {# PART #}
|
||||||
@@ -139,11 +142,15 @@
|
|||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td>
|
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td>
|
||||||
<td class="py-3 px-6 bold">
|
<td class="py-3 px-6 bold">
|
||||||
|
{% if is_electrum_mode %}
|
||||||
|
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
|
||||||
|
{% else %}
|
||||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
||||||
(<span class="usd-value"></span>)
|
(<span class="usd-value"></span>)
|
||||||
{% if w.mweb_pending %}
|
{% if w.mweb_pending %}
|
||||||
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
|
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -163,6 +170,19 @@
|
|||||||
<td class="py-3 px-6 bold">{{ w.name }} Version:</td>
|
<td class="py-3 px-6 bold">{{ w.name }} Version:</td>
|
||||||
<td class="py-3 px-6">{{ w.version }}</td>
|
<td class="py-3 px-6">{{ w.version }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
|
<td class="py-3 px-6 bold">Wallet Mode:</td>
|
||||||
|
<td class="py-3 px-6">
|
||||||
|
{% if w.connection_type == 'electrum' %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Light Wallet (Electrum)</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">Full Node</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if use_tor %}
|
||||||
|
<span class="inline-block py-1 px-2 ml-2 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" title="Electrum connections routed through TOR">TOR</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
<td class="py-3 px-6 bold">Blockheight:</td>
|
<td class="py-3 px-6 bold">Blockheight:</td>
|
||||||
<td class="py-3 px-6">{{ w.blocks }}
|
<td class="py-3 px-6">{{ w.blocks }}
|
||||||
@@ -178,8 +198,66 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
<td class="py-3 px-6 bold">Synced:</td>
|
<td class="py-3 px-6 bold">Synced:</td>
|
||||||
<td class="py-3 px-6">{{ w.synced }}</td>
|
<td class="py-3 px-6">
|
||||||
|
{% if is_electrum_mode %}
|
||||||
|
<span class="text-green-600 dark:text-green-400">Electrum Wallet Synced</span>
|
||||||
|
{% else %}
|
||||||
|
{{ w.synced }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if is_electrum_mode and w.electrum_server %}
|
||||||
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
|
<td class="py-3 px-6 bold">Server:</td>
|
||||||
|
<td class="py-3 px-6 font-mono text-sm">
|
||||||
|
{{ w.electrum_server }}
|
||||||
|
{% if w.electrum_status == 'connected' %}
|
||||||
|
<span class="ml-2 inline-flex items-center">
|
||||||
|
<span class="text-xs text-green-600 dark:text-green-400">(Connected)</span>
|
||||||
|
</span>
|
||||||
|
{% elif w.electrum_status == 'all_failed' %}
|
||||||
|
<span class="ml-2 inline-flex items-center">
|
||||||
|
<span class="text-xs text-red-600 dark:text-red-400">(All Servers Failed)</span>
|
||||||
|
</span>
|
||||||
|
{% elif w.electrum_status == 'disconnected' %}
|
||||||
|
<span class="ml-2 inline-flex items-center">
|
||||||
|
<span class="text-xs text-red-600 dark:text-red-400">(Disconnected - Reconnecting...)</span>
|
||||||
|
</span>
|
||||||
|
{% elif w.electrum_status == 'error' %}
|
||||||
|
<span class="ml-2 inline-flex items-center">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-yellow-500 mr-1"></span>
|
||||||
|
<span class="text-xs text-yellow-600 dark:text-yellow-400">(Connection Error)</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_electrum_mode and w.electrum_all_failed and w.electrum_using_defaults %}
|
||||||
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||||
|
<td colspan="2" class="py-3 px-6">
|
||||||
|
<div class="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded-lg">
|
||||||
|
<p class="text-sm font-medium text-red-800 dark:text-red-200 mb-1">
|
||||||
|
<svg class="inline w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
|
||||||
|
Default Electrum Servers Unavailable
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-red-700 dark:text-red-300">
|
||||||
|
All default servers failed to connect. Please configure custom Electrum servers in
|
||||||
|
<a href="/settings" class="underline font-medium hover:text-red-900 dark:hover:text-red-100">Settings</a>
|
||||||
|
under the {{ w.name }} section.
|
||||||
|
</p>
|
||||||
|
{% if w.electrum_last_error %}
|
||||||
|
<p class="text-xs text-red-600 dark:text-red-400 mt-1 font-mono">Last error: {{ w.electrum_last_error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_electrum_mode and w.electrum_version %}
|
||||||
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
|
<td class="py-3 px-6 bold">Server Version:</td>
|
||||||
|
<td class="py-3 px-6">{{ w.electrum_version }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if w.bootstrapping %}
|
{% if w.bootstrapping %}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
<td class="py-3 px-6 bold">Bootstrapping:</td>
|
<td class="py-3 px-6 bold">Bootstrapping:</td>
|
||||||
@@ -319,13 +397,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{# / PART #}
|
{# / PART #}
|
||||||
{% elif w.cid == '3' %}
|
{% elif w.cid == '3' %}
|
||||||
{# LTC #}
|
{# LTC - MWEB not available in light mode #}
|
||||||
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div>
|
{% if not is_electrum_mode %}
|
||||||
|
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
|
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
|
||||||
<div class="text-center relative">
|
<div class="text-center relative">
|
||||||
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.mweb_address }}</div>
|
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="mweb_address">{{ w.mweb_address }}</div>
|
||||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
|
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
|
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
|
||||||
@@ -333,6 +412,7 @@
|
|||||||
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newmwebaddr_{{ w.cid }}" value="New MWEB Address"> {{ circular_arrows_svg }} New MWEB Address </button>
|
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newmwebaddr_{{ w.cid }}" value="New MWEB Address"> {{ circular_arrows_svg }} New MWEB Address </button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{# / LTC #}
|
{# / LTC #}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -349,10 +429,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Address copy functionality handled by external JS -->
|
|
||||||
|
|
||||||
<section class="p-6">
|
<section class="p-6">
|
||||||
<div class="lg:container mx-auto">
|
<div class="lg:container mx-auto">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -393,8 +469,12 @@
|
|||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||||
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td>
|
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td>
|
||||||
<td class="py-3 px-6">
|
<td class="py-3 px-6">
|
||||||
|
{% if is_electrum_mode %}
|
||||||
|
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
|
||||||
|
{% else %}
|
||||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
||||||
(<span class="usd-value"></span>)
|
(<span class="usd-value"></span>)
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif w.cid == '1' %}
|
{% elif w.cid == '1' %}
|
||||||
@@ -541,7 +621,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{# / PART #}
|
{# / PART #}
|
||||||
{% elif w.cid == '3' %} {# LTC #}
|
{% elif w.cid == '3' and not is_electrum_mode %} {# LTC - only show in full node mode #}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||||
<td class="py-3 px-6 bold">Type From:</td>
|
<td class="py-3 px-6 bold">Type From:</td>
|
||||||
<td class="py-3 px-6">
|
<td class="py-3 px-6">
|
||||||
@@ -593,6 +673,8 @@
|
|||||||
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div>
|
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div>
|
||||||
{# / XMR | WOW #}
|
{# / XMR | WOW #}
|
||||||
{% elif w.show_utxo_groups %}
|
{% elif w.show_utxo_groups %}
|
||||||
|
{% elif is_electrum_mode %}
|
||||||
|
{# Hide UTXO Groups button in electrum/lite wallet mode #}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div>
|
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -680,6 +762,96 @@
|
|||||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if w.havedata and not w.error %}
|
||||||
|
<section class="p-6">
|
||||||
|
<div class="lg:container mx-auto">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h4 class="font-semibold text-2xl text-black dark:text-white">Transaction History</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="px-6 py-0 h-full overflow-hidden">
|
||||||
|
<div class="border-coolGray-100">
|
||||||
|
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||||
|
<div class="w-full pt-2">
|
||||||
|
<div class="lg:container mt-5 mx-auto">
|
||||||
|
<div id="transaction-history-section" class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||||
|
<div class="px-6">
|
||||||
|
{% if is_electrum_mode %}
|
||||||
|
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
|
Transaction history is not available in Light Wallet mode.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div id="tx-container" class="space-y-3 pb-6">
|
||||||
|
<div class="text-center py-8 text-gray-500 dark:text-gray-400">Loading transactions...</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if not is_electrum_mode %}
|
||||||
|
<section id="tx-pagination-section" style="display: none;">
|
||||||
|
<div class="px-6 py-0 h-full overflow-hidden">
|
||||||
|
<div class="pb-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||||
|
<div class="w-full pt-2">
|
||||||
|
<div class="lg:container mx-auto">
|
||||||
|
<div class="py-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
|
||||||
|
<div class="px-6">
|
||||||
|
<div class="flex flex-wrap justify-end items-center space-x-4">
|
||||||
|
<button id="prevPageTx" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-md transition duration-200 focus:ring-0 focus:outline-none">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||||
|
</svg>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<p class="text-sm font-heading dark:text-white">
|
||||||
|
Page <span id="currentPageTx">1</span> of <span id="totalPagesTx">1</span>
|
||||||
|
</p>
|
||||||
|
<button id="nextPageTx" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-md transition duration-200 focus:ring-0 focus:outline-none">
|
||||||
|
Next
|
||||||
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_electrum_mode %}
|
||||||
|
<section id="tx-pagination-section">
|
||||||
|
<div class="px-6 py-0 h-full overflow-hidden">
|
||||||
|
<div class="pb-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||||
|
<div class="w-full pt-2">
|
||||||
|
<div class="lg:container mx-auto">
|
||||||
|
<div class="py-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
|
||||||
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
|
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
|
||||||
|
|||||||
@@ -48,8 +48,18 @@
|
|||||||
<div class="px-6 mb-6">
|
<div class="px-6 mb-6">
|
||||||
<h4 class="text-xl font-bold dark:text-white">{{ w.name }}
|
<h4 class="text-xl font-bold dark:text-white">{{ w.name }}
|
||||||
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
|
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
|
||||||
|
{% if w.connection_type == 'electrum' %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-xs text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Light Wallet (Electrum)</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-700 dark:bg-green-900 dark:text-green-300">Full Node</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if use_tor %}
|
||||||
|
<span class="inline-block py-1 px-2 rounded-full bg-purple-100 text-xs text-purple-700 dark:bg-purple-900 dark:text-purple-300">TOR</span>
|
||||||
|
{% endif %}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
|
<p class="pt-2 text-xs text-gray-500 dark:text-gray-200">Version: <span class="electrum-version" data-coin="{{ w.name }}">{{ w.version }}</span> {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span>{% endif %}</p>
|
||||||
|
{% if w.electrum_server %}
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-200">Server: <span class="electrum-server" data-coin="{{ w.name }}">{{ w.electrum_server }}</span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
|
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
|
||||||
@@ -71,6 +81,12 @@
|
|||||||
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
|
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if w.pending_out %}
|
||||||
|
<div class="flex mb-2 justify-between items-center">
|
||||||
|
<h4 class="text-xs font-bold text-yellow-600 dark:text-yellow-400">Unconfirmed:</h4>
|
||||||
|
<span class="bold inline-block py-1 px-2 rounded-full bg-yellow-100 text-xs text-yellow-600 dark:bg-gray-500 dark:text-yellow-400 coinname-value" data-coinname="{{ w.name }}">-{{ w.pending_out }} {{ w.ticker }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if w.cid == '1' %} {# PART #}
|
{% if w.cid == '1' %} {# PART #}
|
||||||
<div class="flex mb-2 justify-between items-center">
|
<div class="flex mb-2 justify-between items-center">
|
||||||
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4>
|
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4>
|
||||||
@@ -110,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %} {# / PART #}
|
{% endif %} {# / PART #}
|
||||||
{% if w.cid == '3' %} {# LTC #}
|
{% if w.cid == '3' and w.connection_type != 'electrum' %} {# LTC - MWEB not available in electrum mode #}
|
||||||
<div class="flex mb-2 justify-between items-center">
|
<div class="flex mb-2 justify-between items-center">
|
||||||
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4>
|
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4>
|
||||||
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
||||||
@@ -159,6 +175,33 @@
|
|||||||
<h4 class="text-xs font-medium dark:text-white">Expected Seed:</h4>
|
<h4 class="text-xs font-medium dark:text-white">Expected Seed:</h4>
|
||||||
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.expected_seed }}</span>
|
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.expected_seed }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if w.connection_type == 'electrum' %}
|
||||||
|
<div class="scan-status mt-10 p-2 rounded" data-coin="{{ w.name }}">
|
||||||
|
{% if w.scan_status and w.scan_status.in_progress %}
|
||||||
|
<div class="bg-blue-50 dark:bg-gray-500 p-2 rounded">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-blue-600 dark:text-blue-300">
|
||||||
|
<svg class="inline-block w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Scanning {{ w.scan_status.status }}
|
||||||
|
</span>
|
||||||
|
<span class="text-blue-500 dark:text-blue-200 font-medium">{{ w.scan_status.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-blue-200 dark:bg-gray-700 rounded-full h-1 mt-1">
|
||||||
|
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full" style="width: {{ w.scan_status.progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
||||||
|
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||||
|
Electrum Wallet Synced
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div class="flex justify-between mb-1 mt-10">
|
<div class="flex justify-between mb-1 mt-10">
|
||||||
<span class="text-xs font-medium dark:text-gray-200">Blockchain</span>
|
<span class="text-xs font-medium dark:text-gray-200">Blockchain</span>
|
||||||
<span class="text-xs font-medium dark:text-gray-200">{{ w.synced }}%</span>
|
<span class="text-xs font-medium dark:text-gray-200">{{ w.synced }}%</span>
|
||||||
@@ -179,6 +222,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1253,7 +1253,8 @@ def amm_autostart_api(swap_client, post_string, params=None):
|
|||||||
|
|
||||||
settings_path = os.path.join(swap_client.data_dir, cfg.CONFIG_FILENAME)
|
settings_path = os.path.join(swap_client.data_dir, cfg.CONFIG_FILENAME)
|
||||||
settings_path_new = settings_path + ".new"
|
settings_path_new = settings_path + ".new"
|
||||||
shutil.copyfile(settings_path, settings_path + ".last")
|
if os.path.exists(settings_path):
|
||||||
|
shutil.copyfile(settings_path, settings_path + ".last")
|
||||||
with open(settings_path_new, "w") as fp:
|
with open(settings_path_new, "w") as fp:
|
||||||
json.dump(swap_client.settings, fp, indent=4)
|
json.dump(swap_client.settings, fp, indent=4)
|
||||||
shutil.move(settings_path_new, settings_path)
|
shutil.move(settings_path_new, settings_path)
|
||||||
|
|||||||
@@ -218,6 +218,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
|
|||||||
if have_data_entry(form_data, "fee_from_extra"):
|
if have_data_entry(form_data, "fee_from_extra"):
|
||||||
page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra"))
|
page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra"))
|
||||||
parsed_data["fee_from_extra"] = page_data["fee_from_extra"]
|
parsed_data["fee_from_extra"] = page_data["fee_from_extra"]
|
||||||
|
else:
|
||||||
|
page_data["fee_from_extra"] = 0
|
||||||
|
|
||||||
if have_data_entry(form_data, "fee_to_conf"):
|
if have_data_entry(form_data, "fee_to_conf"):
|
||||||
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
|
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
|
||||||
@@ -226,6 +228,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
|
|||||||
if have_data_entry(form_data, "fee_to_extra"):
|
if have_data_entry(form_data, "fee_to_extra"):
|
||||||
page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra"))
|
page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra"))
|
||||||
parsed_data["fee_to_extra"] = page_data["fee_to_extra"]
|
parsed_data["fee_to_extra"] = page_data["fee_to_extra"]
|
||||||
|
else:
|
||||||
|
page_data["fee_to_extra"] = 0
|
||||||
|
|
||||||
if have_data_entry(form_data, "check_offer"):
|
if have_data_entry(form_data, "check_offer"):
|
||||||
page_data["check_offer"] = True
|
page_data["check_offer"] = True
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ def page_settings(self, url_split, post_string):
|
|||||||
"TODO: If running in docker see doc/tor.md to enable/disable tor."
|
"TODO: If running in docker see doc/tor.md to enable/disable tor."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
electrum_supported_coins = (
|
||||||
|
"bitcoin",
|
||||||
|
"litecoin",
|
||||||
|
)
|
||||||
for name, c in swap_client.settings["chainclients"].items():
|
for name, c in swap_client.settings["chainclients"].items():
|
||||||
if have_data_entry(form_data, "apply_" + name):
|
if have_data_entry(form_data, "apply_" + name):
|
||||||
data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
|
data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
|
||||||
@@ -138,10 +142,67 @@ def page_settings(self, url_split, post_string):
|
|||||||
data["anon_tx_ring_size"] = int(
|
data["anon_tx_ring_size"] = int(
|
||||||
get_data_entry(form_data, "rct_ring_size_" + name)
|
get_data_entry(form_data, "rct_ring_size_" + name)
|
||||||
)
|
)
|
||||||
|
if name in electrum_supported_coins:
|
||||||
|
new_connection_type = get_data_entry_or(
|
||||||
|
form_data, "connection_type_" + name, None
|
||||||
|
)
|
||||||
|
if new_connection_type and new_connection_type != c.get(
|
||||||
|
"connection_type"
|
||||||
|
):
|
||||||
|
coin_id = swap_client.getCoinIdFromName(name)
|
||||||
|
has_active_swaps = False
|
||||||
|
for bid_id, (bid, offer) in list(
|
||||||
|
swap_client.swaps_in_progress.items()
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
offer.coin_from == coin_id
|
||||||
|
or offer.coin_to == coin_id
|
||||||
|
):
|
||||||
|
has_active_swaps = True
|
||||||
|
break
|
||||||
|
if has_active_swaps:
|
||||||
|
display_name = getCoinName(coin_id)
|
||||||
|
err_messages.append(
|
||||||
|
f"Cannot change {display_name} connection mode while swaps are in progress. "
|
||||||
|
f"Please wait for all {display_name} swaps to complete."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data["connection_type"] = new_connection_type
|
||||||
|
if new_connection_type == "electrum":
|
||||||
|
data["manage_daemon"] = False
|
||||||
|
elif new_connection_type == "rpc":
|
||||||
|
data["manage_daemon"] = True
|
||||||
|
clearnet_servers = get_data_entry_or(
|
||||||
|
form_data, "electrum_clearnet_" + name, ""
|
||||||
|
).strip()
|
||||||
|
data["electrum_clearnet_servers"] = clearnet_servers
|
||||||
|
onion_servers = get_data_entry_or(
|
||||||
|
form_data, "electrum_onion_" + name, ""
|
||||||
|
).strip()
|
||||||
|
data["electrum_onion_servers"] = onion_servers
|
||||||
|
auto_transfer = have_data_entry(
|
||||||
|
form_data, "auto_transfer_" + name
|
||||||
|
)
|
||||||
|
data["auto_transfer_on_mode_switch"] = auto_transfer
|
||||||
|
# Address gap limit for scanning
|
||||||
|
gap_limit_str = get_data_entry_or(
|
||||||
|
form_data, "gap_limit_" + name, "20"
|
||||||
|
).strip()
|
||||||
|
try:
|
||||||
|
gap_limit = int(gap_limit_str)
|
||||||
|
if gap_limit < 5:
|
||||||
|
gap_limit = 5
|
||||||
|
elif gap_limit > 100:
|
||||||
|
gap_limit = 100
|
||||||
|
data["address_gap_limit"] = gap_limit
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
settings_changed, suggest_reboot = swap_client.editSettings(
|
settings_changed, suggest_reboot, migration_message = (
|
||||||
name, data
|
swap_client.editSettings(name, data)
|
||||||
)
|
)
|
||||||
|
if migration_message:
|
||||||
|
messages.append(migration_message)
|
||||||
if settings_changed is True:
|
if settings_changed is True:
|
||||||
messages.append("Settings applied.")
|
messages.append("Settings applied.")
|
||||||
if suggest_reboot is True:
|
if suggest_reboot is True:
|
||||||
@@ -156,19 +217,65 @@ def page_settings(self, url_split, post_string):
|
|||||||
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
||||||
messages.append(display_name + " disabled, shutting down.")
|
messages.append(display_name + " disabled, shutting down.")
|
||||||
swap_client.stopRunning()
|
swap_client.stopRunning()
|
||||||
|
elif have_data_entry(form_data, "force_sweep_" + name):
|
||||||
|
coin_id = swap_client.getCoinIdFromName(name)
|
||||||
|
display_name = getCoinName(coin_id)
|
||||||
|
try:
|
||||||
|
result = swap_client.sweepLiteWalletFunds(coin_id)
|
||||||
|
if result.get("success"):
|
||||||
|
amount = result.get("amount", 0)
|
||||||
|
fee = result.get("fee", 0)
|
||||||
|
txid = result.get("txid", "")
|
||||||
|
messages.append(
|
||||||
|
f"Successfully swept {amount:.8f} {display_name} to RPC wallet. "
|
||||||
|
f"Fee: {fee:.8f}. TXID: {txid} (1 confirmation required)"
|
||||||
|
)
|
||||||
|
elif result.get("skipped"):
|
||||||
|
messages.append(
|
||||||
|
f"{display_name}: {result.get('reason', 'Sweep skipped')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err_messages.append(
|
||||||
|
f"{display_name}: Sweep failed - {result.get('error', 'Unknown error')}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
err_messages.append(f"{display_name}: Sweep failed - {str(e)}")
|
||||||
except InactiveCoin as ex:
|
except InactiveCoin as ex:
|
||||||
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
|
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_messages.append(str(e))
|
err_messages.append(str(e))
|
||||||
chains_formatted = []
|
chains_formatted = []
|
||||||
|
electrum_supported_coins = (
|
||||||
|
"bitcoin",
|
||||||
|
"litecoin",
|
||||||
|
)
|
||||||
|
|
||||||
sorted_names = sorted(swap_client.settings["chainclients"].keys())
|
sorted_names = sorted(swap_client.settings["chainclients"].keys())
|
||||||
|
from basicswap.interface.electrumx import (
|
||||||
|
DEFAULT_ELECTRUM_SERVERS,
|
||||||
|
DEFAULT_ONION_SERVERS,
|
||||||
|
)
|
||||||
|
|
||||||
for name in sorted_names:
|
for name in sorted_names:
|
||||||
c = swap_client.settings["chainclients"][name]
|
c = swap_client.settings["chainclients"][name]
|
||||||
try:
|
try:
|
||||||
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
||||||
except Exception:
|
except Exception:
|
||||||
display_name = name
|
display_name = name
|
||||||
|
|
||||||
|
clearnet_servers = c.get("electrum_clearnet_servers", None)
|
||||||
|
onion_servers = c.get("electrum_onion_servers", None)
|
||||||
|
|
||||||
|
if clearnet_servers is None:
|
||||||
|
default_clearnet = DEFAULT_ELECTRUM_SERVERS.get(name, [])
|
||||||
|
clearnet_servers = [f"{s['host']}:{s['port']}" for s in default_clearnet]
|
||||||
|
if onion_servers is None:
|
||||||
|
default_onion = DEFAULT_ONION_SERVERS.get(name, [])
|
||||||
|
onion_servers = [f"{s['host']}:{s['port']}" for s in default_onion]
|
||||||
|
|
||||||
|
clearnet_text = "\n".join(clearnet_servers) if clearnet_servers else ""
|
||||||
|
onion_text = "\n".join(onion_servers) if onion_servers else ""
|
||||||
|
|
||||||
chains_formatted.append(
|
chains_formatted.append(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -176,6 +283,13 @@ def page_settings(self, url_split, post_string):
|
|||||||
"lookups": c.get("chain_lookups", "local"),
|
"lookups": c.get("chain_lookups", "local"),
|
||||||
"manage_daemon": c.get("manage_daemon", "Unknown"),
|
"manage_daemon": c.get("manage_daemon", "Unknown"),
|
||||||
"connection_type": c.get("connection_type", "Unknown"),
|
"connection_type": c.get("connection_type", "Unknown"),
|
||||||
|
"supports_electrum": name in electrum_supported_coins,
|
||||||
|
"clearnet_servers_text": clearnet_text,
|
||||||
|
"onion_servers_text": onion_text,
|
||||||
|
"auto_transfer_on_mode_switch": c.get(
|
||||||
|
"auto_transfer_on_mode_switch", True
|
||||||
|
),
|
||||||
|
"address_gap_limit": c.get("address_gap_limit", 20),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if name in ("monero", "wownero"):
|
if name in ("monero", "wownero"):
|
||||||
@@ -203,6 +317,14 @@ def page_settings(self, url_split, post_string):
|
|||||||
else:
|
else:
|
||||||
chains_formatted[-1]["can_disable"] = True
|
chains_formatted[-1]["can_disable"] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
coin_id = swap_client.getCoinIdFromName(name)
|
||||||
|
lite_balance_info = swap_client.getLiteWalletBalanceInfo(coin_id)
|
||||||
|
if lite_balance_info:
|
||||||
|
chains_formatted[-1]["lite_wallet_balance"] = lite_balance_info
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
general_settings = {
|
general_settings = {
|
||||||
"debug": swap_client.debug,
|
"debug": swap_client.debug,
|
||||||
"debug_ui": swap_client.debug_ui,
|
"debug_ui": swap_client.debug_ui,
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ DONATION_ADDRESSES = {
|
|||||||
|
|
||||||
|
|
||||||
def format_wallet_data(swap_client, ci, w):
|
def format_wallet_data(swap_client, ci, w):
|
||||||
|
coin_id = ci.coin_type()
|
||||||
|
connection_type = swap_client.coin_clients.get(coin_id, {}).get(
|
||||||
|
"connection_type", w.get("connection_type", "rpc")
|
||||||
|
)
|
||||||
wf = {
|
wf = {
|
||||||
"name": ci.coin_name(),
|
"name": ci.coin_name(),
|
||||||
"version": w.get("version", "?"),
|
"version": w.get("version", "?"),
|
||||||
"ticker": ci.ticker_mainnet(),
|
"ticker": ci.ticker_mainnet(),
|
||||||
"cid": str(int(ci.coin_type())),
|
"cid": str(int(coin_id)),
|
||||||
"balance": w.get("balance", "?"),
|
"balance": w.get("balance", "?"),
|
||||||
"blocks": w.get("blocks", "?"),
|
"blocks": w.get("blocks", "?"),
|
||||||
"synced": w.get("synced", "?"),
|
"synced": w.get("synced", "?"),
|
||||||
@@ -45,6 +49,7 @@ def format_wallet_data(swap_client, ci, w):
|
|||||||
"locked": w.get("locked", "?"),
|
"locked": w.get("locked", "?"),
|
||||||
"updating": w.get("updating", "?"),
|
"updating": w.get("updating", "?"),
|
||||||
"havedata": True,
|
"havedata": True,
|
||||||
|
"connection_type": connection_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
if "wallet_blocks" in w:
|
if "wallet_blocks" in w:
|
||||||
@@ -70,6 +75,9 @@ def format_wallet_data(swap_client, ci, w):
|
|||||||
if pending > 0.0:
|
if pending > 0.0:
|
||||||
wf["pending"] = ci.format_amount(pending)
|
wf["pending"] = ci.format_amount(pending)
|
||||||
|
|
||||||
|
if "unconfirmed" in w and float(w["unconfirmed"]) < 0.0:
|
||||||
|
wf["pending_out"] = ci.format_amount(abs(ci.make_int(w["unconfirmed"])))
|
||||||
|
|
||||||
if ci.coin_type() == Coins.PART:
|
if ci.coin_type() == Coins.PART:
|
||||||
wf["stealth_address"] = w.get("stealth_address", "?")
|
wf["stealth_address"] = w.get("stealth_address", "?")
|
||||||
wf["blind_balance"] = w.get("blind_balance", "?")
|
wf["blind_balance"] = w.get("blind_balance", "?")
|
||||||
@@ -83,10 +91,85 @@ def format_wallet_data(swap_client, ci, w):
|
|||||||
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
||||||
wf["mweb_pending"] = w.get("mweb_pending", "?")
|
wf["mweb_pending"] = w.get("mweb_pending", "?")
|
||||||
|
|
||||||
|
if hasattr(ci, "getScanStatus"):
|
||||||
|
wf["scan_status"] = ci.getScanStatus()
|
||||||
|
|
||||||
|
if connection_type == "electrum" and hasattr(ci, "_backend") and ci._backend:
|
||||||
|
backend = ci._backend
|
||||||
|
wf["electrum_server"] = backend.getServerHost()
|
||||||
|
wf["electrum_version"] = backend.getServerVersion()
|
||||||
|
try:
|
||||||
|
conn_status = backend.getConnectionStatus()
|
||||||
|
wf["electrum_connected"] = conn_status.get("connected", False)
|
||||||
|
wf["electrum_failures"] = conn_status.get("failures", 0)
|
||||||
|
wf["electrum_using_defaults"] = conn_status.get("using_defaults", True)
|
||||||
|
wf["electrum_all_failed"] = conn_status.get("all_failed", False)
|
||||||
|
wf["electrum_last_error"] = conn_status.get("last_error")
|
||||||
|
if conn_status.get("connected"):
|
||||||
|
wf["electrum_status"] = "connected"
|
||||||
|
elif conn_status.get("all_failed"):
|
||||||
|
wf["electrum_status"] = "all_failed"
|
||||||
|
else:
|
||||||
|
wf["electrum_status"] = "disconnected"
|
||||||
|
except Exception:
|
||||||
|
wf["electrum_connected"] = False
|
||||||
|
wf["electrum_status"] = "error"
|
||||||
|
|
||||||
checkAddressesOwned(swap_client, ci, wf)
|
checkAddressesOwned(swap_client, ci, wf)
|
||||||
return wf
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
def format_transactions(ci, transactions, coin_id):
|
||||||
|
formatted_txs = []
|
||||||
|
|
||||||
|
if coin_id in (Coins.XMR, Coins.WOW):
|
||||||
|
for tx in transactions:
|
||||||
|
tx_type = tx.get("type", "")
|
||||||
|
direction = (
|
||||||
|
"Incoming"
|
||||||
|
if tx_type == "in"
|
||||||
|
else "Outgoing" if tx_type == "out" else tx_type.capitalize()
|
||||||
|
)
|
||||||
|
|
||||||
|
formatted_txs.append(
|
||||||
|
{
|
||||||
|
"txid": tx.get("txid", ""),
|
||||||
|
"type": direction,
|
||||||
|
"amount": ci.format_amount(tx.get("amount", 0)),
|
||||||
|
"confirmations": tx.get("confirmations", 0),
|
||||||
|
"timestamp": format_timestamp(tx.get("timestamp", 0)),
|
||||||
|
"height": tx.get("height", 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for tx in transactions:
|
||||||
|
category = tx.get("category", "")
|
||||||
|
if category == "send":
|
||||||
|
direction = "Outgoing"
|
||||||
|
amount = abs(tx.get("amount", 0))
|
||||||
|
elif category == "receive":
|
||||||
|
direction = "Incoming"
|
||||||
|
amount = tx.get("amount", 0)
|
||||||
|
else:
|
||||||
|
direction = category.capitalize()
|
||||||
|
amount = abs(tx.get("amount", 0))
|
||||||
|
|
||||||
|
formatted_txs.append(
|
||||||
|
{
|
||||||
|
"txid": tx.get("txid", ""),
|
||||||
|
"type": direction,
|
||||||
|
"amount": ci.format_amount(ci.make_int(amount)),
|
||||||
|
"confirmations": tx.get("confirmations", 0),
|
||||||
|
"timestamp": format_timestamp(
|
||||||
|
tx.get("time", tx.get("timereceived", 0))
|
||||||
|
),
|
||||||
|
"address": tx.get("address", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return formatted_txs
|
||||||
|
|
||||||
|
|
||||||
def page_wallets(self, url_split, post_string):
|
def page_wallets(self, url_split, post_string):
|
||||||
server = self.server
|
server = self.server
|
||||||
swap_client = server.swap_client
|
swap_client = server.swap_client
|
||||||
@@ -131,6 +214,7 @@ def page_wallets(self, url_split, post_string):
|
|||||||
"err_messages": err_messages,
|
"err_messages": err_messages,
|
||||||
"wallets": wallets_formatted,
|
"wallets": wallets_formatted,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
"use_tor": getattr(swap_client, "use_tor_proxy", False),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,8 +235,25 @@ def page_wallet(self, url_split, post_string):
|
|||||||
show_utxo_groups: bool = False
|
show_utxo_groups: bool = False
|
||||||
withdrawal_successful: bool = False
|
withdrawal_successful: bool = False
|
||||||
force_refresh: bool = False
|
force_refresh: bool = False
|
||||||
|
|
||||||
|
tx_filters = {
|
||||||
|
"page_no": 1,
|
||||||
|
"limit": 30,
|
||||||
|
"offset": 0,
|
||||||
|
}
|
||||||
|
|
||||||
form_data = self.checkForm(post_string, "wallet", err_messages)
|
form_data = self.checkForm(post_string, "wallet", err_messages)
|
||||||
if form_data:
|
if form_data:
|
||||||
|
if have_data_entry(form_data, "pageback"):
|
||||||
|
tx_filters["page_no"] = int(form_data[b"pageno"][0]) - 1
|
||||||
|
if tx_filters["page_no"] < 1:
|
||||||
|
tx_filters["page_no"] = 1
|
||||||
|
elif have_data_entry(form_data, "pageforwards"):
|
||||||
|
tx_filters["page_no"] = int(form_data[b"pageno"][0]) + 1
|
||||||
|
|
||||||
|
if tx_filters["page_no"] > 1:
|
||||||
|
tx_filters["offset"] = (tx_filters["page_no"] - 1) * 30
|
||||||
|
|
||||||
cid = str(int(coin_id))
|
cid = str(int(coin_id))
|
||||||
|
|
||||||
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
|
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
|
||||||
@@ -170,6 +271,22 @@ def page_wallet(self, url_split, post_string):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
err_messages.append("Reseed failed " + str(ex))
|
err_messages.append("Reseed failed " + str(ex))
|
||||||
swap_client.updateWalletsInfo(True, coin_id)
|
swap_client.updateWalletsInfo(True, coin_id)
|
||||||
|
elif have_data_entry(form_data, "importkey_" + cid):
|
||||||
|
try:
|
||||||
|
wif_key = form_data[bytes("wifkey_" + cid, "utf-8")][0].decode("utf-8")
|
||||||
|
if wif_key:
|
||||||
|
result = swap_client.importWIFKey(coin_id, wif_key)
|
||||||
|
if result.get("success"):
|
||||||
|
messages.append(
|
||||||
|
f"Imported key for address: {result['address']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err_messages.append(f"Import failed: {result.get('error')}")
|
||||||
|
else:
|
||||||
|
err_messages.append("Missing WIF key")
|
||||||
|
except Exception as ex:
|
||||||
|
err_messages.append(f"Import failed: {ex}")
|
||||||
|
swap_client.updateWalletsInfo(True, coin_id)
|
||||||
elif withdraw or estimate_fee:
|
elif withdraw or estimate_fee:
|
||||||
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
|
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
|
||||||
page_data["wd_subfee_" + cid] = subfee
|
page_data["wd_subfee_" + cid] = subfee
|
||||||
@@ -215,7 +332,14 @@ def page_wallet(self, url_split, post_string):
|
|||||||
].decode("utf-8")
|
].decode("utf-8")
|
||||||
page_data["wd_type_from_" + cid] = type_from
|
page_data["wd_type_from_" + cid] = type_from
|
||||||
except Exception as e: # noqa: F841
|
except Exception as e: # noqa: F841
|
||||||
err_messages.append("Missing type")
|
if (
|
||||||
|
swap_client.coin_clients[coin_id].get("connection_type")
|
||||||
|
== "electrum"
|
||||||
|
):
|
||||||
|
type_from = "plain"
|
||||||
|
page_data["wd_type_from_" + cid] = type_from
|
||||||
|
else:
|
||||||
|
err_messages.append("Missing type")
|
||||||
|
|
||||||
if len(err_messages) == 0:
|
if len(err_messages) == 0:
|
||||||
ci = swap_client.ci(coin_id)
|
ci = swap_client.ci(coin_id)
|
||||||
@@ -328,6 +452,9 @@ def page_wallet(self, url_split, post_string):
|
|||||||
cid = str(int(coin_id))
|
cid = str(int(coin_id))
|
||||||
|
|
||||||
wallet_data = format_wallet_data(swap_client, ci, w)
|
wallet_data = format_wallet_data(swap_client, ci, w)
|
||||||
|
wallet_data["is_electrum_mode"] = (
|
||||||
|
getattr(ci, "_connection_type", "rpc") == "electrum"
|
||||||
|
)
|
||||||
|
|
||||||
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
|
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
|
||||||
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
|
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
|
||||||
@@ -400,6 +527,26 @@ def page_wallet(self, url_split, post_string):
|
|||||||
"coin_name": wallet_data.get("name", ticker),
|
"coin_name": wallet_data.get("name", ticker),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transactions = []
|
||||||
|
total_transactions = 0
|
||||||
|
is_electrum_mode = False
|
||||||
|
if wallet_data.get("havedata", False) and not wallet_data.get("error"):
|
||||||
|
try:
|
||||||
|
ci = swap_client.ci(coin_id)
|
||||||
|
is_electrum_mode = getattr(ci, "_connection_type", "rpc") == "electrum"
|
||||||
|
if not is_electrum_mode:
|
||||||
|
count = tx_filters.get("limit", 30)
|
||||||
|
skip = tx_filters.get("offset", 0)
|
||||||
|
|
||||||
|
all_txs = ci.listWalletTransactions(count=10000, skip=0)
|
||||||
|
all_txs = list(reversed(all_txs)) if all_txs else []
|
||||||
|
total_transactions = len(all_txs)
|
||||||
|
|
||||||
|
raw_txs = all_txs[skip : skip + count] if all_txs else []
|
||||||
|
transactions = format_transactions(ci, raw_txs, coin_id)
|
||||||
|
except Exception as e:
|
||||||
|
swap_client.log.warning(f"Failed to fetch transactions for {ticker}: {e}")
|
||||||
|
|
||||||
template = server.env.get_template("wallet.html")
|
template = server.env.get_template("wallet.html")
|
||||||
return self.render_template(
|
return self.render_template(
|
||||||
template,
|
template,
|
||||||
@@ -411,5 +558,11 @@ def page_wallet(self, url_split, post_string):
|
|||||||
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
|
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
|
||||||
"donation_info": donation_info,
|
"donation_info": donation_info,
|
||||||
"debug_ui": swap_client.debug_ui,
|
"debug_ui": swap_client.debug_ui,
|
||||||
|
"transactions": transactions,
|
||||||
|
"tx_page_no": tx_filters.get("page_no", 1),
|
||||||
|
"tx_total": total_transactions,
|
||||||
|
"tx_limit": tx_filters.get("limit", 30),
|
||||||
|
"is_electrum_mode": is_electrum_mode,
|
||||||
|
"use_tor": getattr(swap_client, "use_tor_proxy", False),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ def describeBid(
|
|||||||
"ticker_from": ci_from.ticker(),
|
"ticker_from": ci_from.ticker(),
|
||||||
"ticker_to": ci_to.ticker(),
|
"ticker_to": ci_to.ticker(),
|
||||||
"bid_state": strBidState(bid.state),
|
"bid_state": strBidState(bid.state),
|
||||||
|
"bid_state_ind": int(bid.state),
|
||||||
"state_description": state_description,
|
"state_description": state_description,
|
||||||
"itx_state": strTxState(bid.getITxState()),
|
"itx_state": strTxState(bid.getITxState()),
|
||||||
"ptx_state": strTxState(bid.getPTxState()),
|
"ptx_state": strTxState(bid.getPTxState()),
|
||||||
@@ -343,6 +344,8 @@ def describeBid(
|
|||||||
if for_api
|
if for_api
|
||||||
else format_timestamp(bid.created_at, with_seconds=True)
|
else format_timestamp(bid.created_at, with_seconds=True)
|
||||||
),
|
),
|
||||||
|
"created_at_timestamp": bid.created_at,
|
||||||
|
"state_time_timestamp": getLastStateTimestamp(bid),
|
||||||
"expired_at": (
|
"expired_at": (
|
||||||
bid.expire_at
|
bid.expire_at
|
||||||
if for_api
|
if for_api
|
||||||
@@ -623,6 +626,14 @@ def listOldBidStates(bid):
|
|||||||
return old_states
|
return old_states
|
||||||
|
|
||||||
|
|
||||||
|
def getLastStateTimestamp(bid):
|
||||||
|
if not bid.states or len(bid.states) < 12:
|
||||||
|
return None
|
||||||
|
num_states = len(bid.states) // 12
|
||||||
|
last_entry = struct.unpack_from("<iq", bid.states[(num_states - 1) * 12 :])
|
||||||
|
return last_entry[1]
|
||||||
|
|
||||||
|
|
||||||
def getCoinName(c):
|
def getCoinName(c):
|
||||||
if c == Coins.PART_ANON:
|
if c == Coins.PART_ANON:
|
||||||
return chainparams[Coins.PART]["name"].capitalize() + " Anon"
|
return chainparams[Coins.PART]["name"].capitalize() + " Anon"
|
||||||
@@ -643,7 +654,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
|||||||
for k, v in swap_client.coin_clients.items():
|
for k, v in swap_client.coin_clients.items():
|
||||||
if k not in chainparams:
|
if k not in chainparams:
|
||||||
continue
|
continue
|
||||||
if v["connection_type"] == "rpc":
|
if v["connection_type"] in ("rpc", "electrum"):
|
||||||
coins.append((int(k), getCoinName(k)))
|
coins.append((int(k), getCoinName(k)))
|
||||||
if split_from:
|
if split_from:
|
||||||
coins_from.append(coins[-1])
|
coins_from.append(coins[-1])
|
||||||
@@ -670,7 +681,7 @@ def listAvailableCoinsWithBalances(swap_client, with_variants=True, split_from=F
|
|||||||
for k, v in swap_client.coin_clients.items():
|
for k, v in swap_client.coin_clients.items():
|
||||||
if k not in chainparams:
|
if k not in chainparams:
|
||||||
continue
|
continue
|
||||||
if v["connection_type"] == "rpc":
|
if v["connection_type"] in ("rpc", "electrum"):
|
||||||
|
|
||||||
balance = "0.0"
|
balance = "0.0"
|
||||||
if k in wallets:
|
if k in wallets:
|
||||||
@@ -735,10 +746,23 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
|
|||||||
|
|
||||||
if wallet_info["stealth_address"] != "?":
|
if wallet_info["stealth_address"] != "?":
|
||||||
if not ci.isAddressMine(wallet_info["stealth_address"]):
|
if not ci.isAddressMine(wallet_info["stealth_address"]):
|
||||||
ci._log.error(
|
ci._log.warning(
|
||||||
"Unowned stealth address: {}".format(wallet_info["stealth_address"])
|
"Unowned stealth address: {} - clearing cache and regenerating".format(
|
||||||
|
wallet_info["stealth_address"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
wallet_info["stealth_address"] = "Error: unowned address"
|
key_str = "stealth_addr_" + ci.coin_name().lower()
|
||||||
|
swap_client.clearStringKV(key_str)
|
||||||
|
try:
|
||||||
|
new_addr = ci.getNewStealthAddress()
|
||||||
|
swap_client.setStringKV(key_str, new_addr)
|
||||||
|
wallet_info["stealth_address"] = new_addr
|
||||||
|
ci._log.info(
|
||||||
|
"Regenerated stealth address for {}".format(ci.coin_name())
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ci._log.error("Failed to regenerate stealth address: {}".format(e))
|
||||||
|
wallet_info["stealth_address"] = "Error: unowned address"
|
||||||
elif (
|
elif (
|
||||||
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
||||||
):
|
):
|
||||||
@@ -747,10 +771,24 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
|
|||||||
if "deposit_address" in wallet_info:
|
if "deposit_address" in wallet_info:
|
||||||
if wallet_info["deposit_address"] != "Refresh necessary":
|
if wallet_info["deposit_address"] != "Refresh necessary":
|
||||||
if not ci.isAddressMine(wallet_info["deposit_address"]):
|
if not ci.isAddressMine(wallet_info["deposit_address"]):
|
||||||
ci._log.error(
|
ci._log.warning(
|
||||||
"Unowned deposit address: {}".format(wallet_info["deposit_address"])
|
"Unowned deposit address: {} - clearing cache and regenerating".format(
|
||||||
|
wallet_info["deposit_address"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
wallet_info["deposit_address"] = "Error: unowned address"
|
key_str = "receive_addr_" + ci.coin_name().lower()
|
||||||
|
swap_client.clearStringKV(key_str)
|
||||||
|
try:
|
||||||
|
coin_type = ci.coin_type()
|
||||||
|
new_addr = swap_client.getReceiveAddressForCoin(coin_type)
|
||||||
|
swap_client.setStringKV(key_str, new_addr)
|
||||||
|
wallet_info["deposit_address"] = new_addr
|
||||||
|
ci._log.info(
|
||||||
|
"Regenerated deposit address for {}".format(ci.coin_name())
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ci._log.error("Failed to regenerate deposit address: {}".format(e))
|
||||||
|
wallet_info["deposit_address"] = "Error: unowned address"
|
||||||
elif (
|
elif (
|
||||||
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -190,11 +190,14 @@ def format_amount(i: int, display_scale: int, scale: int = None) -> str:
|
|||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
def format_timestamp(value: int, with_seconds: bool = False) -> str:
|
def format_timestamp(
|
||||||
|
value: int, with_seconds: bool = False, with_timezone: bool = False
|
||||||
|
) -> str:
|
||||||
str_format = "%Y-%m-%d %H:%M"
|
str_format = "%Y-%m-%d %H:%M"
|
||||||
if with_seconds:
|
if with_seconds:
|
||||||
str_format += ":%S"
|
str_format += ":%S"
|
||||||
str_format += " %z"
|
if with_timezone:
|
||||||
|
str_format += " %z"
|
||||||
return time.strftime(str_format, time.localtime(value))
|
return time.strftime(str_format, time.localtime(value))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
752
basicswap/wallet_backend.py
Normal file
752
basicswap/wallet_backend.py
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2024-2026 The Basicswap developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import time
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class WalletBackend(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def findAddressWithBalance(
|
||||||
|
self, addresses: List[str], min_balance: int
|
||||||
|
) -> Optional[tuple]:
|
||||||
|
balances = self.getBalance(addresses)
|
||||||
|
for addr, balance in balances.items():
|
||||||
|
if balance >= min_balance:
|
||||||
|
return (addr, balance)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getUnspentOutputs(
|
||||||
|
self, addresses: List[str], min_confirmations: int = 0
|
||||||
|
) -> List[dict]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def broadcastTransaction(self, tx_hex: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getTransaction(self, txid: str) -> Optional[dict]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getTransactionRaw(self, txid: str) -> Optional[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getBlockHeight(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def estimateFee(self, blocks: int = 6) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def isConnected(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def getAddressHistory(self, address: str) -> List[dict]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FullNodeBackend(WalletBackend):
|
||||||
|
|
||||||
|
def __init__(self, rpc_client, coin_type, log):
|
||||||
|
self._rpc = rpc_client
|
||||||
|
self._coin_type = coin_type
|
||||||
|
self._log = log
|
||||||
|
|
||||||
|
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
|
||||||
|
result = {}
|
||||||
|
for addr in addresses:
|
||||||
|
result[addr] = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
utxos = self._rpc("listunspent", [0, 9999999, addresses])
|
||||||
|
for utxo in utxos:
|
||||||
|
addr = utxo.get("address")
|
||||||
|
if addr in result:
|
||||||
|
result[addr] += int(utxo.get("amount", 0) * 1e8)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"FullNodeBackend.getBalance error: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getUnspentOutputs(
|
||||||
|
self, addresses: List[str], min_confirmations: int = 0
|
||||||
|
) -> List[dict]:
|
||||||
|
try:
|
||||||
|
utxos = self._rpc("listunspent", [min_confirmations, 9999999, addresses])
|
||||||
|
result = []
|
||||||
|
for utxo in utxos:
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"txid": utxo.get("txid"),
|
||||||
|
"vout": utxo.get("vout"),
|
||||||
|
"value": int(utxo.get("amount", 0) * 1e8),
|
||||||
|
"address": utxo.get("address"),
|
||||||
|
"confirmations": utxo.get("confirmations", 0),
|
||||||
|
"scriptPubKey": utxo.get("scriptPubKey"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"FullNodeBackend.getUnspentOutputs error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def broadcastTransaction(self, tx_hex: str) -> str:
|
||||||
|
return self._rpc("sendrawtransaction", [tx_hex])
|
||||||
|
|
||||||
|
def getTransaction(self, txid: str) -> Optional[dict]:
|
||||||
|
try:
|
||||||
|
return self._rpc("getrawtransaction", [txid, True])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getTransactionRaw(self, txid: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
return self._rpc("getrawtransaction", [txid, False])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getBlockHeight(self) -> int:
|
||||||
|
return self._rpc("getblockcount")
|
||||||
|
|
||||||
|
def estimateFee(self, blocks: int = 6) -> int:
|
||||||
|
try:
|
||||||
|
result = self._rpc("estimatesmartfee", [blocks])
|
||||||
|
if "feerate" in result:
|
||||||
|
return int(result["feerate"] * 1e8 / 1000)
|
||||||
|
return 1
|
||||||
|
except Exception:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def isConnected(self) -> bool:
|
||||||
|
try:
|
||||||
|
self._rpc("getblockchaininfo")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAddressHistory(self, address: str) -> List[dict]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def importAddress(self, address: str, label: str = "", rescan: bool = False):
|
||||||
|
try:
|
||||||
|
self._rpc("importaddress", [address, label, rescan])
|
||||||
|
except Exception as e:
|
||||||
|
if "already in wallet" not in str(e).lower():
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class ElectrumBackend(WalletBackend):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coin_type,
|
||||||
|
log,
|
||||||
|
clearnet_servers=None,
|
||||||
|
onion_servers=None,
|
||||||
|
chain="mainnet",
|
||||||
|
proxy_host=None,
|
||||||
|
proxy_port=None,
|
||||||
|
):
|
||||||
|
from basicswap.interface.electrumx import ElectrumServer
|
||||||
|
from basicswap.chainparams import Coins, chainparams
|
||||||
|
|
||||||
|
self._coin_type = coin_type
|
||||||
|
self._log = log
|
||||||
|
self._subscribed_scripthashes = set()
|
||||||
|
|
||||||
|
coin_params = chainparams.get(coin_type, chainparams.get(Coins.BTC))
|
||||||
|
self._network_params = coin_params.get(chain, coin_params.get("mainnet", {}))
|
||||||
|
|
||||||
|
coin_name_map = {
|
||||||
|
Coins.BTC: "bitcoin",
|
||||||
|
Coins.LTC: "litecoin",
|
||||||
|
}
|
||||||
|
coin_name = coin_name_map.get(coin_type, "bitcoin")
|
||||||
|
|
||||||
|
self._host = "localhost"
|
||||||
|
self._port = 50002
|
||||||
|
self._use_ssl = True
|
||||||
|
|
||||||
|
self._server = ElectrumServer(
|
||||||
|
coin_name,
|
||||||
|
clearnet_servers=clearnet_servers,
|
||||||
|
onion_servers=onion_servers,
|
||||||
|
log=log,
|
||||||
|
proxy_host=proxy_host,
|
||||||
|
proxy_port=proxy_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._realtime_callback = None
|
||||||
|
self._address_to_scripthash = {}
|
||||||
|
|
||||||
|
self._cached_height = 0
|
||||||
|
self._cached_height_time = 0
|
||||||
|
self._height_cache_ttl = 5
|
||||||
|
|
||||||
|
self._max_batch_size = 10
|
||||||
|
self._background_mode = False
|
||||||
|
|
||||||
|
def setBackgroundMode(self, enabled: bool):
|
||||||
|
self._background_mode = enabled
|
||||||
|
|
||||||
|
def _call(self, method: str, params: list = None, timeout: int = 10):
|
||||||
|
if self._background_mode and hasattr(self._server, "call_background"):
|
||||||
|
return self._server.call_background(method, params, timeout)
|
||||||
|
return self._server.call(method, params, timeout)
|
||||||
|
|
||||||
|
def _call_batch(self, calls: list, timeout: int = 15):
|
||||||
|
if self._background_mode and hasattr(self._server, "call_batch_background"):
|
||||||
|
return self._server.call_batch_background(calls, timeout)
|
||||||
|
return self._server.call_batch(calls, timeout)
|
||||||
|
|
||||||
|
def _split_batch_call(
|
||||||
|
self, scripthashes: list, method: str, batch_size: int = None
|
||||||
|
) -> list:
|
||||||
|
if batch_size is None:
|
||||||
|
batch_size = self._max_batch_size
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for i in range(0, len(scripthashes), batch_size):
|
||||||
|
chunk = scripthashes[i : i + batch_size]
|
||||||
|
try:
|
||||||
|
calls = [(method, [sh]) for sh in chunk]
|
||||||
|
results = self._call_batch(calls)
|
||||||
|
all_results.extend(results)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"Batch chunk failed ({len(chunk)} items): {e}")
|
||||||
|
for sh in chunk:
|
||||||
|
try:
|
||||||
|
result = self._call(method, [sh])
|
||||||
|
all_results.append(result)
|
||||||
|
except Exception as e2:
|
||||||
|
self._log.debug(f"Individual call failed for {sh[:8]}...: {e2}")
|
||||||
|
all_results.append(None)
|
||||||
|
return all_results
|
||||||
|
|
||||||
|
def _isUnsupportedAddress(self, address: str) -> bool:
|
||||||
|
if address.startswith("ltcmweb1"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _addressToScripthash(self, address: str) -> str:
|
||||||
|
from basicswap.interface.electrumx import scripthash_from_address
|
||||||
|
|
||||||
|
return scripthash_from_address(address, self._network_params)
|
||||||
|
|
||||||
|
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
|
||||||
|
result = {}
|
||||||
|
for addr in addresses:
|
||||||
|
result[addr] = 0
|
||||||
|
|
||||||
|
if not addresses:
|
||||||
|
return result
|
||||||
|
|
||||||
|
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
|
||||||
|
if not addr_list:
|
||||||
|
return result
|
||||||
|
|
||||||
|
addr_to_scripthash = {}
|
||||||
|
for addr in addr_list:
|
||||||
|
try:
|
||||||
|
addr_to_scripthash[addr] = self._addressToScripthash(addr)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"getBalance: scripthash error for {addr[:10]}...: {e}")
|
||||||
|
|
||||||
|
if not addr_to_scripthash:
|
||||||
|
return result
|
||||||
|
|
||||||
|
scripthashes = list(addr_to_scripthash.values())
|
||||||
|
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
|
||||||
|
|
||||||
|
batch_results = self._split_batch_call(
|
||||||
|
scripthashes, "blockchain.scripthash.get_balance"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, balance in enumerate(batch_results):
|
||||||
|
if balance and isinstance(balance, dict):
|
||||||
|
addr = scripthash_to_addr.get(scripthashes[i])
|
||||||
|
if addr:
|
||||||
|
confirmed = balance.get("confirmed", 0)
|
||||||
|
unconfirmed = balance.get("unconfirmed", 0)
|
||||||
|
result[addr] = confirmed + unconfirmed
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getDetailedBalance(self, addresses: List[str]) -> Dict[str, dict]:
|
||||||
|
result = {}
|
||||||
|
for addr in addresses:
|
||||||
|
result[addr] = {"confirmed": 0, "unconfirmed": 0}
|
||||||
|
|
||||||
|
if not addresses:
|
||||||
|
return result
|
||||||
|
|
||||||
|
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
|
||||||
|
if not addr_list:
|
||||||
|
return result
|
||||||
|
|
||||||
|
batch_size = 10
|
||||||
|
for batch_start in range(0, len(addr_list), batch_size):
|
||||||
|
batch = addr_list[batch_start : batch_start + batch_size]
|
||||||
|
|
||||||
|
addr_to_scripthash = {}
|
||||||
|
for addr in batch:
|
||||||
|
try:
|
||||||
|
addr_to_scripthash[addr] = self._addressToScripthash(addr)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(
|
||||||
|
f"getDetailedBalance: scripthash error for {addr[:10]}...: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not addr_to_scripthash:
|
||||||
|
continue
|
||||||
|
|
||||||
|
scripthashes = list(addr_to_scripthash.values())
|
||||||
|
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
|
||||||
|
batch_success = False
|
||||||
|
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
batch_results = self._server.get_balance_batch(scripthashes)
|
||||||
|
for i, balance in enumerate(batch_results):
|
||||||
|
if balance and isinstance(balance, dict):
|
||||||
|
addr = scripthash_to_addr.get(scripthashes[i])
|
||||||
|
if addr:
|
||||||
|
result[addr] = {
|
||||||
|
"confirmed": balance.get("confirmed", 0),
|
||||||
|
"unconfirmed": balance.get("unconfirmed", 0),
|
||||||
|
}
|
||||||
|
batch_success = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == 0:
|
||||||
|
self._log.debug(
|
||||||
|
f"Batch detailed balance query failed, reconnecting: {e}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._server.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
self._log.debug(
|
||||||
|
f"Batch detailed balance query failed after retry, falling back: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not batch_success:
|
||||||
|
for addr, scripthash in addr_to_scripthash.items():
|
||||||
|
try:
|
||||||
|
balance = self._call(
|
||||||
|
"blockchain.scripthash.get_balance", [scripthash]
|
||||||
|
)
|
||||||
|
if balance and isinstance(balance, dict):
|
||||||
|
result[addr] = {
|
||||||
|
"confirmed": balance.get("confirmed", 0),
|
||||||
|
"unconfirmed": balance.get("unconfirmed", 0),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(
|
||||||
|
f"ElectrumBackend.getDetailedBalance error for {addr[:10]}...: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def findAddressWithBalance(
|
||||||
|
self, addresses: List[str], min_balance: int
|
||||||
|
) -> Optional[tuple]:
|
||||||
|
if not addresses:
|
||||||
|
return None
|
||||||
|
|
||||||
|
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
|
||||||
|
if not addr_list:
|
||||||
|
return None
|
||||||
|
|
||||||
|
batch_size = 50
|
||||||
|
for batch_start in range(0, len(addr_list), batch_size):
|
||||||
|
batch = addr_list[batch_start : batch_start + batch_size]
|
||||||
|
|
||||||
|
addr_to_scripthash = {}
|
||||||
|
for addr in batch:
|
||||||
|
try:
|
||||||
|
addr_to_scripthash[addr] = self._addressToScripthash(addr)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not addr_to_scripthash:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
scripthashes = list(addr_to_scripthash.values())
|
||||||
|
batch_results = self._server.get_balance_batch(scripthashes)
|
||||||
|
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
|
||||||
|
|
||||||
|
for i, balance in enumerate(batch_results):
|
||||||
|
if balance and isinstance(balance, dict):
|
||||||
|
confirmed = balance.get("confirmed", 0)
|
||||||
|
unconfirmed = balance.get("unconfirmed", 0)
|
||||||
|
total = confirmed + unconfirmed
|
||||||
|
if total >= min_balance:
|
||||||
|
addr = scripthash_to_addr.get(scripthashes[i])
|
||||||
|
if addr:
|
||||||
|
return (addr, total)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"findAddressWithBalance batch error: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getUnspentOutputs(
|
||||||
|
self, addresses: List[str], min_confirmations: int = 0
|
||||||
|
) -> List[dict]:
|
||||||
|
result = []
|
||||||
|
if not addresses:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_height = self.getBlockHeight()
|
||||||
|
|
||||||
|
for addr in addresses:
|
||||||
|
if self._isUnsupportedAddress(addr):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
scripthash = self._addressToScripthash(addr)
|
||||||
|
utxos = self._call(
|
||||||
|
"blockchain.scripthash.listunspent", [scripthash]
|
||||||
|
)
|
||||||
|
if utxos:
|
||||||
|
for utxo in utxos:
|
||||||
|
height = utxo.get("height", 0)
|
||||||
|
if height <= 0:
|
||||||
|
confirmations = 0
|
||||||
|
else:
|
||||||
|
confirmations = current_height - height + 1
|
||||||
|
|
||||||
|
if confirmations >= min_confirmations:
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"txid": utxo.get("tx_hash"),
|
||||||
|
"vout": utxo.get("tx_pos"),
|
||||||
|
"value": utxo.get("value", 0),
|
||||||
|
"address": addr,
|
||||||
|
"confirmations": confirmations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(
|
||||||
|
f"ElectrumBackend.getUnspentOutputs error for {addr[:10]}...: {e}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"ElectrumBackend.getUnspentOutputs error: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def broadcastTransaction(self, tx_hex: str) -> str:
|
||||||
|
import time
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
retry_delay = 0.5
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
result = self._server.call("blockchain.transaction.broadcast", [tx_hex])
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
if any(
|
||||||
|
pattern in error_msg
|
||||||
|
for pattern in [
|
||||||
|
"missing inputs",
|
||||||
|
"bad-txns",
|
||||||
|
"txn-mempool-conflict",
|
||||||
|
"already in block chain",
|
||||||
|
"transaction already exists",
|
||||||
|
"insufficient fee",
|
||||||
|
"dust",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
self._log.debug(
|
||||||
|
f"broadcastTransaction retry {attempt + 1}/{max_retries}: {e}"
|
||||||
|
)
|
||||||
|
time.sleep(retry_delay * (2**attempt)) # Exponential backoff
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getTransaction(self, txid: str) -> Optional[dict]:
|
||||||
|
try:
|
||||||
|
return self._call("blockchain.transaction.get", [txid, True])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getTransactionRaw(self, txid: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
tx_hex = self._call("blockchain.transaction.get", [txid, False])
|
||||||
|
return tx_hex
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"getTransactionRaw failed for {txid[:16]}...: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getTransactionBatch(self, txids: List[str]) -> Dict[str, Optional[dict]]:
|
||||||
|
result = {}
|
||||||
|
if not txids:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
calls = [("blockchain.transaction.get", [txid, True]) for txid in txids]
|
||||||
|
responses = self._call_batch(calls)
|
||||||
|
for txid, tx_info in zip(txids, responses):
|
||||||
|
result[txid] = tx_info if tx_info else None
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"getTransactionBatch error: {e}")
|
||||||
|
for txid in txids:
|
||||||
|
result[txid] = self.getTransaction(txid)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getTransactionBatchRaw(self, txids: List[str]) -> Dict[str, Optional[str]]:
|
||||||
|
result = {}
|
||||||
|
if not txids:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
calls = [("blockchain.transaction.get", [txid, False]) for txid in txids]
|
||||||
|
responses = self._call_batch(calls)
|
||||||
|
for txid, tx_hex in zip(txids, responses):
|
||||||
|
result[txid] = tx_hex if tx_hex else None
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"getTransactionBatchRaw error: {e}")
|
||||||
|
for txid in txids:
|
||||||
|
result[txid] = self.getTransactionRaw(txid)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getBlockHeight(self) -> int:
|
||||||
|
import time
|
||||||
|
|
||||||
|
if hasattr(self._server, "get_subscribed_height"):
|
||||||
|
subscribed_height = self._server.get_subscribed_height()
|
||||||
|
if subscribed_height > 0:
|
||||||
|
if subscribed_height > self._cached_height:
|
||||||
|
self._cached_height = subscribed_height
|
||||||
|
self._cached_height_time = time.time()
|
||||||
|
return subscribed_height
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if (
|
||||||
|
self._cached_height > 0
|
||||||
|
and (now - self._cached_height_time) < self._height_cache_ttl
|
||||||
|
):
|
||||||
|
return self._cached_height
|
||||||
|
|
||||||
|
try:
|
||||||
|
header = self._call("blockchain.headers.subscribe", [])
|
||||||
|
if header:
|
||||||
|
height = header.get("height", 0)
|
||||||
|
if height > 0:
|
||||||
|
self._cached_height = height
|
||||||
|
self._cached_height_time = now
|
||||||
|
return height
|
||||||
|
return self._cached_height if self._cached_height > 0 else 0
|
||||||
|
except Exception:
|
||||||
|
return self._cached_height if self._cached_height > 0 else 0
|
||||||
|
|
||||||
|
def estimateFee(self, blocks: int = 6) -> int:
|
||||||
|
try:
|
||||||
|
fee = self._call("blockchain.estimatefee", [blocks])
|
||||||
|
if fee and fee > 0:
|
||||||
|
return int(fee * 1e8 / 1000)
|
||||||
|
return 1
|
||||||
|
except Exception:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def isConnected(self) -> bool:
|
||||||
|
try:
|
||||||
|
self._call("server.ping", [])
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getServerVersion(self) -> str:
|
||||||
|
version = self._server.get_server_version()
|
||||||
|
if not version:
|
||||||
|
try:
|
||||||
|
self._call("server.ping", [])
|
||||||
|
version = self._server.get_server_version()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return version or "electrum"
|
||||||
|
|
||||||
|
def getServerHost(self) -> str:
|
||||||
|
host, port = self._server.get_current_server()
|
||||||
|
if host and port:
|
||||||
|
return f"{host}:{port}"
|
||||||
|
return f"{self._host}:{self._port}"
|
||||||
|
|
||||||
|
def getConnectionStatus(self) -> dict:
|
||||||
|
if hasattr(self._server, "getConnectionStatus"):
|
||||||
|
status = self._server.getConnectionStatus()
|
||||||
|
else:
|
||||||
|
status = {
|
||||||
|
"connected": self.isConnected(),
|
||||||
|
"failures": 0,
|
||||||
|
"last_error": None,
|
||||||
|
"all_failed": False,
|
||||||
|
"using_defaults": True,
|
||||||
|
"server_count": 1,
|
||||||
|
}
|
||||||
|
status["server"] = self.getServerHost()
|
||||||
|
status["version"] = self.getServerVersion()
|
||||||
|
return status
|
||||||
|
|
||||||
|
def getAddressHistory(self, address: str) -> List[dict]:
|
||||||
|
if self._isUnsupportedAddress(address):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
scripthash = self._addressToScripthash(address)
|
||||||
|
history = self._call("blockchain.scripthash.get_history", [scripthash])
|
||||||
|
if history:
|
||||||
|
return [
|
||||||
|
{"txid": h.get("tx_hash"), "height": h.get("height", 0)}
|
||||||
|
for h in history
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getAddressHistoryBackground(self, address: str) -> List[dict]:
|
||||||
|
if self._isUnsupportedAddress(address):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
scripthash = self._addressToScripthash(address)
|
||||||
|
history = self._server.call_background(
|
||||||
|
"blockchain.scripthash.get_history", [scripthash]
|
||||||
|
)
|
||||||
|
if history:
|
||||||
|
return [
|
||||||
|
{"txid": h.get("tx_hash"), "height": h.get("height", 0)}
|
||||||
|
for h in history
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getBatchBalance(self, scripthashes: List[str]) -> Dict[str, int]:
|
||||||
|
result = {}
|
||||||
|
for sh in scripthashes:
|
||||||
|
result[sh] = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
calls = [("blockchain.scripthash.get_balance", [sh]) for sh in scripthashes]
|
||||||
|
responses = self._call_batch(calls)
|
||||||
|
for sh, balance in zip(scripthashes, responses):
|
||||||
|
if balance:
|
||||||
|
confirmed = balance.get("confirmed", 0)
|
||||||
|
unconfirmed = balance.get("unconfirmed", 0)
|
||||||
|
result[sh] = confirmed + unconfirmed
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"ElectrumBackend.getBatchBalance error: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getBatchUnspent(
|
||||||
|
self, scripthashes: List[str], min_confirmations: int = 0
|
||||||
|
) -> Dict[str, List[dict]]:
|
||||||
|
result = {}
|
||||||
|
for sh in scripthashes:
|
||||||
|
result[sh] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_height = self.getBlockHeight()
|
||||||
|
|
||||||
|
calls = [("blockchain.scripthash.listunspent", [sh]) for sh in scripthashes]
|
||||||
|
responses = self._call_batch(calls)
|
||||||
|
for sh, utxos in zip(scripthashes, responses):
|
||||||
|
if utxos:
|
||||||
|
for utxo in utxos:
|
||||||
|
height = utxo.get("height", 0)
|
||||||
|
if height <= 0:
|
||||||
|
confirmations = 0
|
||||||
|
else:
|
||||||
|
confirmations = current_height - height + 1
|
||||||
|
|
||||||
|
if confirmations >= min_confirmations:
|
||||||
|
result[sh].append(
|
||||||
|
{
|
||||||
|
"txid": utxo.get("tx_hash"),
|
||||||
|
"vout": utxo.get("tx_pos"),
|
||||||
|
"value": utxo.get("value", 0),
|
||||||
|
"confirmations": confirmations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.warning(f"ElectrumBackend.getBatchUnspent error: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def enableRealtimeNotifications(self, callback) -> None:
|
||||||
|
self._realtime_callback = callback
|
||||||
|
self._server.enable_realtime_notifications()
|
||||||
|
self._log.info(f"Real-time notifications enabled for {self._coin_type}")
|
||||||
|
|
||||||
|
def _create_scripthash_callback(self, scripthash):
|
||||||
|
|
||||||
|
def callback(sh, new_status):
|
||||||
|
self._handle_scripthash_notification(sh, new_status)
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def _handle_scripthash_notification(self, scripthash, new_status):
|
||||||
|
if not self._realtime_callback:
|
||||||
|
return
|
||||||
|
|
||||||
|
address = None
|
||||||
|
for addr, sh in self._address_to_scripthash.items():
|
||||||
|
if sh == scripthash:
|
||||||
|
address = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._realtime_callback(
|
||||||
|
self._coin_type, address, scripthash, "balance_change"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"Error in realtime callback: {e}")
|
||||||
|
|
||||||
|
def subscribeAddressWithCallback(self, address: str) -> str:
|
||||||
|
if self._isUnsupportedAddress(address):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
scripthash = self._addressToScripthash(address)
|
||||||
|
self._address_to_scripthash[address] = scripthash
|
||||||
|
|
||||||
|
if self._realtime_callback:
|
||||||
|
status = self._server.subscribe_with_callback(
|
||||||
|
scripthash, self._create_scripthash_callback(scripthash)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status = self._call("blockchain.scripthash.subscribe", [scripthash])
|
||||||
|
|
||||||
|
self._subscribed_scripthashes.add(scripthash)
|
||||||
|
return status
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"Failed to subscribe to {address}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getServer(self):
|
||||||
|
return self._server
|
||||||
2028
basicswap/wallet_manager.py
Normal file
2028
basicswap/wallet_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -676,7 +676,7 @@ class Test(unittest.TestCase):
|
|||||||
def test_db(self):
|
def test_db(self):
|
||||||
db_test = DBMethods()
|
db_test = DBMethods()
|
||||||
db_test.sqlite_file = ":memory:"
|
db_test.sqlite_file = ":memory:"
|
||||||
db_test.mxDB = threading.Lock()
|
db_test.mxDB = threading.RLock()
|
||||||
cursor = db_test.openDB()
|
cursor = db_test.openDB()
|
||||||
try:
|
try:
|
||||||
create_db_(db_test._db_con, logger)
|
create_db_(db_test._db_con, logger)
|
||||||
|
|||||||
Reference in New Issue
Block a user