mirror of
https://github.com/basicswap/basicswap.git
synced 2026-04-17 13:57:22 +02:00
Compare commits
44 Commits
v0.15.3
...
3c76454e68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c76454e68 | ||
|
|
c58e637987 | ||
|
|
61da9d703c | ||
|
|
45e0f85cf0 | ||
|
|
303fe59d7b | ||
|
|
1102ff1ddf | ||
|
|
e5e0e6e911 | ||
|
|
129a5bb9b7 | ||
|
|
3258b76a49 | ||
|
|
54ece5dff9 | ||
|
|
1ca454b269 | ||
|
|
7d592a520b | ||
|
|
7b0925de46 | ||
|
|
c9029a5e34 | ||
|
|
f4b645bccd | ||
|
|
9b9078b153 | ||
|
|
807880547e | ||
|
|
604171c3eb | ||
|
|
8fa0668079 | ||
|
|
bbc5e64db0 | ||
|
|
5213ddd173 | ||
|
|
79b45d59db | ||
|
|
d5a6d63e0b | ||
|
|
a456a15b8d | ||
|
|
44c77f162b | ||
|
|
e737ba7e27 | ||
|
|
1afe1316d0 | ||
|
|
e67e37b801 | ||
|
|
a2c37a13f8 | ||
|
|
8a86c494ee | ||
|
|
f4f3fa63f2 | ||
|
|
12e3d3bab8 | ||
|
|
d1552717ae | ||
|
|
8f1382d00d | ||
|
|
60dd5d43e7 | ||
|
|
eee45858b5 | ||
|
|
162254c537 | ||
|
|
e719ba3d6f | ||
|
|
75f058dad6 | ||
|
|
39e134c46c | ||
|
|
3fcd70098a | ||
|
|
38c03a3abf | ||
|
|
8f076c7bfb | ||
|
|
afae62ae38 |
@@ -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):
|
||||||
@@ -628,6 +641,7 @@ def canTimeoutBidState(state):
|
|||||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||||
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
||||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||||
|
BidStates.BID_REQUEST_ACCEPTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
@@ -1834,6 +1841,7 @@ def initialise_wallets(
|
|||||||
daemons = []
|
daemons = []
|
||||||
daemon_args = ["-noconnect", "-nodnsseed"]
|
daemon_args = ["-noconnect", "-nodnsseed"]
|
||||||
generated_mnemonic: bool = False
|
generated_mnemonic: bool = False
|
||||||
|
extended_keys = {}
|
||||||
|
|
||||||
coins_failed_to_initialise = []
|
coins_failed_to_initialise = []
|
||||||
|
|
||||||
@@ -1955,6 +1963,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 +2065,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:
|
||||||
@@ -2082,6 +2099,20 @@ def initialise_wallets(
|
|||||||
except Exception as e: # noqa: F841
|
except Exception as e: # noqa: F841
|
||||||
logger.warning(f"changeWalletPassword failed for {coin_name}.")
|
logger.warning(f"changeWalletPassword failed for {coin_name}.")
|
||||||
|
|
||||||
|
zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC
|
||||||
|
for coin_name in with_coins:
|
||||||
|
c = swap_client.getCoinIdFromName(coin_name)
|
||||||
|
if c == Coins.PART:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ci = swap_client.ci(c)
|
||||||
|
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
|
||||||
|
seed_key = swap_client.getWalletKey(c, 1)
|
||||||
|
account_key = ci.getAccountKey(seed_key, zprv_prefix)
|
||||||
|
extended_keys[getCoinName(c)] = account_key
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Could not generate extended key for {coin_name}: {e}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if swap_client:
|
if swap_client:
|
||||||
swap_client.finalise()
|
swap_client.finalise()
|
||||||
@@ -2113,6 +2144,18 @@ def initialise_wallets(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if extended_keys:
|
||||||
|
print("Extended private keys (for external wallet import):")
|
||||||
|
for coin_name, key in extended_keys.items():
|
||||||
|
print(f" {coin_name}: {key}")
|
||||||
|
print("")
|
||||||
|
print(
|
||||||
|
"NOTE: These keys can be imported into Electrum using 'Use a master key'."
|
||||||
|
)
|
||||||
|
print("WARNING: Write these down NOW. They will not be shown again.\n")
|
||||||
|
|
||||||
|
return extended_keys
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path):
|
def load_config(config_path):
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
@@ -2279,6 +2322,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 +2479,31 @@ 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:
|
||||||
|
if len(parts) >= 3:
|
||||||
|
server = f"{parts[0]}:{parts[1]}:{parts[2]}"
|
||||||
|
else:
|
||||||
|
server = f"{parts[0]}:{parts[1]}"
|
||||||
|
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 +2862,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_clearnet_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]
|
||||||
@@ -3001,7 +3098,7 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if particl_wallet_mnemonic != "none":
|
if particl_wallet_mnemonic != "none":
|
||||||
initialise_wallets(
|
extended_keys = initialise_wallets(
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
add_coin,
|
add_coin,
|
||||||
@@ -3013,6 +3110,18 @@ def main():
|
|||||||
extra_opts=extra_opts,
|
extra_opts=extra_opts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if extended_keys:
|
||||||
|
print("\nExtended private key (for external wallet import):")
|
||||||
|
for coin_name, key in extended_keys.items():
|
||||||
|
print(f" {coin_name}: {key}")
|
||||||
|
print("")
|
||||||
|
print(
|
||||||
|
"NOTE: This key can be imported into Electrum using 'Use a master key'."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"WARNING: Write this down NOW. It will not be shown again.\n"
|
||||||
|
)
|
||||||
|
|
||||||
save_config(config_path, settings)
|
save_config(config_path, settings)
|
||||||
finally:
|
finally:
|
||||||
if "particl_daemon" in extra_opts:
|
if "particl_daemon" in extra_opts:
|
||||||
|
|||||||
@@ -36,22 +36,25 @@ def signal_handler(sig, frame):
|
|||||||
os.write(
|
os.write(
|
||||||
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
||||||
)
|
)
|
||||||
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
try:
|
||||||
try:
|
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
try:
|
||||||
|
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||||
|
|
||||||
amm_status = get_amm_status()
|
amm_status = get_amm_status()
|
||||||
if amm_status == "running":
|
if amm_status == "running":
|
||||||
logger.info("Signal handler stopping AMM process...")
|
logger.info("Signal handler stopping AMM process...")
|
||||||
success, msg = stop_amm_process(swap_client)
|
success, msg = stop_amm_process(swap_client)
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"AMM signal shutdown: {msg}")
|
logger.info(f"AMM signal shutdown: {msg}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error stopping AMM in signal handler: {e}")
|
logger.error(f"Error stopping AMM in signal handler: {e}")
|
||||||
|
|
||||||
swap_client.stopRunning()
|
swap_client.stopRunning()
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||||
@@ -632,7 +635,7 @@ def runClient(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fail_code: int = swap_client.fail_code
|
fail_code: int = swap_client.fail_code
|
||||||
del swap_client
|
swap_client = None
|
||||||
|
|
||||||
if os.path.exists(pids_path):
|
if os.path.exists(pids_path):
|
||||||
with open(pids_path) as fd:
|
with open(pids_path) as fd:
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ 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 = 8
|
||||||
|
|
||||||
|
|
||||||
class Concepts(IntEnum):
|
class Concepts(IntEnum):
|
||||||
@@ -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,
|
||||||
@@ -129,6 +138,14 @@ def upgradeDatabaseData(self, data_version):
|
|||||||
"state_id": int(state),
|
"state_id": int(state),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if data_version > 0 and data_version < 8:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE bidstates SET can_timeout = :can_timeout WHERE state_id = :state_id",
|
||||||
|
{
|
||||||
|
"can_timeout": 1,
|
||||||
|
"state_id": int(BidStates.BID_REQUEST_ACCEPTED),
|
||||||
|
},
|
||||||
|
)
|
||||||
if data_version > 0 and data_version < 4:
|
if data_version > 0 and data_version < 4:
|
||||||
for state in (
|
for state in (
|
||||||
BidStates.BID_REQUEST_SENT,
|
BidStates.BID_REQUEST_SENT,
|
||||||
@@ -260,7 +277,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 +295,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")
|
||||||
@@ -6,8 +6,10 @@
|
|||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import shlex
|
import shlex
|
||||||
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
@@ -19,6 +21,7 @@ from jinja2 import Environment, PackageLoader
|
|||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
@@ -802,7 +805,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if page == "static":
|
if page == "static":
|
||||||
try:
|
try:
|
||||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||||
content = None
|
|
||||||
mime_type = ""
|
mime_type = ""
|
||||||
filepath = ""
|
filepath = ""
|
||||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||||
@@ -835,9 +837,71 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if mime_type == "" or not filepath:
|
if mime_type == "" or not filepath:
|
||||||
raise ValueError("Unknown file type or path")
|
raise ValueError("Unknown file type or path")
|
||||||
|
|
||||||
|
file_stat = os.stat(filepath)
|
||||||
|
mtime = file_stat.st_mtime
|
||||||
|
file_size = file_stat.st_size
|
||||||
|
|
||||||
|
etag_hash = hashlib.md5(f"{file_size}-{mtime}".encode()).hexdigest()
|
||||||
|
etag = f'"{etag_hash}"'
|
||||||
|
last_modified = formatdate(mtime, usegmt=True)
|
||||||
|
|
||||||
|
if_none_match = self.headers.get("If-None-Match")
|
||||||
|
if if_none_match:
|
||||||
|
if if_none_match.strip() == "*" or etag in [
|
||||||
|
t.strip() for t in if_none_match.split(",")
|
||||||
|
]:
|
||||||
|
self.send_response(304)
|
||||||
|
self.send_header("ETag", etag)
|
||||||
|
self.send_header("Cache-Control", "public")
|
||||||
|
self.end_headers()
|
||||||
|
return b""
|
||||||
|
|
||||||
|
if_modified_since = self.headers.get("If-Modified-Since")
|
||||||
|
if if_modified_since and not if_none_match:
|
||||||
|
try:
|
||||||
|
ims_time = parsedate_to_datetime(if_modified_since)
|
||||||
|
file_time = datetime.fromtimestamp(int(mtime), tz=timezone.utc)
|
||||||
|
if file_time <= ims_time:
|
||||||
|
self.send_response(304)
|
||||||
|
self.send_header("Last-Modified", last_modified)
|
||||||
|
self.send_header("Cache-Control", "public")
|
||||||
|
self.end_headers()
|
||||||
|
return b""
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
is_lib = len(url_split) > 4 and url_split[3] == "libs"
|
||||||
|
if is_lib:
|
||||||
|
cache_control = "public, max-age=31536000, immutable"
|
||||||
|
elif url_split[2] in ("css", "js"):
|
||||||
|
cache_control = "public, max-age=3600, must-revalidate"
|
||||||
|
elif url_split[2] in ("images", "sequence_diagrams"):
|
||||||
|
cache_control = "public, max-age=86400"
|
||||||
|
else:
|
||||||
|
cache_control = "public, max-age=3600"
|
||||||
|
|
||||||
with open(filepath, "rb") as fp:
|
with open(filepath, "rb") as fp:
|
||||||
content = fp.read()
|
content = fp.read()
|
||||||
self.putHeaders(status_code, mime_type)
|
|
||||||
|
extra_headers = [
|
||||||
|
("Cache-Control", cache_control),
|
||||||
|
("Last-Modified", last_modified),
|
||||||
|
("ETag", etag),
|
||||||
|
]
|
||||||
|
|
||||||
|
is_compressible = mime_type in (
|
||||||
|
"text/css; charset=utf-8",
|
||||||
|
"application/javascript",
|
||||||
|
"image/svg+xml",
|
||||||
|
)
|
||||||
|
accept_encoding = self.headers.get("Accept-Encoding", "")
|
||||||
|
if is_compressible and "gzip" in accept_encoding:
|
||||||
|
content = gzip.compress(content)
|
||||||
|
extra_headers.append(("Content-Encoding", "gzip"))
|
||||||
|
extra_headers.append(("Vary", "Accept-Encoding"))
|
||||||
|
|
||||||
|
extra_headers.append(("Content-Length", str(len(content))))
|
||||||
|
self.putHeaders(status_code, mime_type, extra_headers=extra_headers)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -132,7 +132,7 @@ class DASHInterface(BTCInterface):
|
|||||||
self.unlockWallet(old_password, check_seed=False)
|
self.unlockWallet(old_password, check_seed=False)
|
||||||
seed_id_before: str = self.getWalletSeedID()
|
seed_id_before: str = self.getWalletSeedID()
|
||||||
|
|
||||||
self.rpc_wallet("encryptwallet", [new_password])
|
self.rpc_wallet("encryptwallet", [new_password], timeout=120)
|
||||||
|
|
||||||
if check_seed is False or seed_id_before == "Not found":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
@@ -156,4 +156,6 @@ class DASHInterface(BTCInterface):
|
|||||||
if self.isWalletEncrypted():
|
if self.isWalletEncrypted():
|
||||||
raise ValueError("Old password must be set")
|
raise ValueError("Old password must be set")
|
||||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
||||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
self.rpc_wallet(
|
||||||
|
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||||
|
)
|
||||||
|
|||||||
@@ -188,6 +188,10 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
def coin_type():
|
def coin_type():
|
||||||
return Coins.DCR
|
return Coins.DCR
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def useBackend() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def exp() -> int:
|
def exp() -> int:
|
||||||
return 8
|
return 8
|
||||||
@@ -364,7 +368,9 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
# Read initial pwd from settings
|
# Read initial pwd from settings
|
||||||
settings = self._sc.getChainClientSettings(self.coin_type())
|
settings = self._sc.getChainClientSettings(self.coin_type())
|
||||||
old_password = settings["wallet_pwd"]
|
old_password = settings["wallet_pwd"]
|
||||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
self.rpc_wallet(
|
||||||
|
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||||
|
)
|
||||||
|
|
||||||
# Lock wallet to match other coins
|
# Lock wallet to match other coins
|
||||||
self.rpc_wallet("walletlock")
|
self.rpc_wallet("walletlock")
|
||||||
@@ -378,7 +384,7 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
if check_seed:
|
if check_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
@@ -632,6 +638,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
|
||||||
|
|
||||||
@@ -1055,6 +1070,9 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
def describeTx(self, tx_hex: str):
|
def describeTx(self, tx_hex: str):
|
||||||
return self.rpc("decoderawtransaction", [tx_hex])
|
return self.rpc("decoderawtransaction", [tx_hex])
|
||||||
|
|
||||||
|
def decodeRawTransaction(self, tx_hex: str):
|
||||||
|
return self.rpc("decoderawtransaction", [tx_hex])
|
||||||
|
|
||||||
def fundTx(self, tx: bytes, feerate) -> bytes:
|
def fundTx(self, tx: bytes, feerate) -> bytes:
|
||||||
feerate_str = float(self.format_amount(feerate))
|
feerate_str = float(self.format_amount(feerate))
|
||||||
# TODO: unlock unspents if bid cancelled
|
# TODO: unlock unspents if bid cancelled
|
||||||
@@ -1723,15 +1741,19 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
tx.vout.append(self.txoType()(output_amount, script_pk))
|
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||||
return tx.serialize()
|
return tx.serialize()
|
||||||
|
|
||||||
def publishBLockTx(
|
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
|
||||||
) -> bytes:
|
|
||||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||||
|
|
||||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
||||||
|
|
||||||
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
funded_tx = self.loadTx(b_lock_tx)
|
||||||
|
lock_vout = findOutput(funded_tx, script_pk)
|
||||||
|
|
||||||
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
||||||
|
|
||||||
return bytes.fromhex(self.publishTx(b_lock_tx))
|
txid = bytes.fromhex(self.publishTx(b_lock_tx))
|
||||||
|
return txid, lock_vout
|
||||||
|
|
||||||
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||||
witness_bytes = 115
|
witness_bytes = 115
|
||||||
@@ -1755,26 +1777,53 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
lock_tx_vout=None,
|
lock_tx_vout=None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
||||||
locked_n = lock_tx_vout
|
|
||||||
|
|
||||||
Kbs = self.getPubkey(kbs)
|
Kbs = self.getPubkey(kbs)
|
||||||
script_pk = self.getPkDest(Kbs)
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
|
||||||
if locked_n is None:
|
locked_n = None
|
||||||
self._log.debug(
|
actual_value = None
|
||||||
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}"
|
wtx = self.rpc_wallet(
|
||||||
|
"gettransaction",
|
||||||
|
[
|
||||||
|
chain_b_lock_txid.hex(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||||
|
locked_n = findOutput(lock_tx, script_pk)
|
||||||
|
if locked_n is not None:
|
||||||
|
actual_value = lock_tx.vout[locked_n].value
|
||||||
|
else:
|
||||||
|
self._log.error(
|
||||||
|
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
|
||||||
|
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
|
||||||
)
|
)
|
||||||
# When refunding a lock tx, it should be in the wallet as a sent tx
|
for i, out in enumerate(lock_tx.vout):
|
||||||
wtx = self.rpc_wallet(
|
self._log.debug(
|
||||||
"gettransaction",
|
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||||
[
|
)
|
||||||
chain_b_lock_txid.hex(),
|
|
||||||
],
|
if (
|
||||||
|
locked_n is not None
|
||||||
|
and lock_tx_vout is not None
|
||||||
|
and locked_n != lock_tx_vout
|
||||||
|
):
|
||||||
|
self._log.warning(
|
||||||
|
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
|
||||||
|
f"for tx {chain_b_lock_txid.hex()}"
|
||||||
)
|
)
|
||||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
|
||||||
locked_n = findOutput(lock_tx, script_pk)
|
|
||||||
|
|
||||||
ensure(locked_n is not None, "Output not found in tx")
|
ensure(locked_n is not None, "Output not found in tx")
|
||||||
|
|
||||||
|
spend_value = cb_swap_value
|
||||||
|
if spend_actual_balance and actual_value is not None:
|
||||||
|
if actual_value != cb_swap_value:
|
||||||
|
self._log.warning(
|
||||||
|
f"spendBLockTx: Spending actual balance {actual_value}, "
|
||||||
|
f"not expected swap value {cb_swap_value}."
|
||||||
|
)
|
||||||
|
spend_value = actual_value
|
||||||
|
|
||||||
pkh_to = self.decodeAddress(address_to)
|
pkh_to = self.decodeAddress(address_to)
|
||||||
|
|
||||||
tx = CTransaction()
|
tx = CTransaction()
|
||||||
@@ -1783,10 +1832,10 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
||||||
|
|
||||||
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
|
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
|
||||||
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to)))
|
tx.vout.append(self.txoType()(spend_value, self.getPubkeyHashDest(pkh_to)))
|
||||||
|
|
||||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||||
tx.vout[0].value = cb_swap_value - pay_fee
|
tx.vout[0].value = spend_value - pay_fee
|
||||||
|
|
||||||
b_lock_spend_tx = tx.serialize()
|
b_lock_spend_tx = tx.serialize()
|
||||||
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
||||||
|
|||||||
1305
basicswap/interface/electrumx.py
Normal file
1305
basicswap/interface/electrumx.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@ class FIROInterface(BTCInterface):
|
|||||||
# Firo shuts down after encryptwallet
|
# Firo shuts down after encryptwallet
|
||||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
||||||
|
|
||||||
self.rpc_wallet("encryptwallet", [password])
|
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||||
|
|
||||||
if check_seed is False or seed_id_before == "Not found":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,13 +26,103 @@ class LTCInterface(BTCInterface):
|
|||||||
wallet=self._rpc_wallet_mweb,
|
wallet=self._rpc_wallet_mweb,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def checkWallets(self) -> int:
|
||||||
|
if self._connection_type == "electrum":
|
||||||
|
wm = self.getWalletManager()
|
||||||
|
if wm and wm.isInitialized(self.coin_type()):
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
|
||||||
|
if self._rpc_wallet not in wallets:
|
||||||
|
self._log.debug(
|
||||||
|
f"Wallet: {self._rpc_wallet} not active, attempting to load."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.rpc(
|
||||||
|
"loadwallet",
|
||||||
|
[
|
||||||
|
self._rpc_wallet,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f'Error loading wallet "{self._rpc_wallet}": {e}.')
|
||||||
|
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||||
|
try:
|
||||||
|
wallet_dirs = self.rpc("listwalletdir")
|
||||||
|
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
|
||||||
|
except Exception:
|
||||||
|
existing = []
|
||||||
|
if len(existing) == 0:
|
||||||
|
self._log.info(
|
||||||
|
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||||
|
self.rpc(
|
||||||
|
"createwallet",
|
||||||
|
[
|
||||||
|
self._rpc_wallet,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
"",
|
||||||
|
False,
|
||||||
|
self._use_descriptors,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
if self.getWalletSeedID() == "Not found":
|
||||||
|
self._log.info(
|
||||||
|
f"Initializing HD seed for {self.coin_name()}."
|
||||||
|
)
|
||||||
|
self._sc.initialiseWallet(self.coin_type())
|
||||||
|
except Exception as create_e:
|
||||||
|
self._log.error(f"Error creating wallet: {create_e}")
|
||||||
|
|
||||||
|
if self._rpc_wallet not in wallets and len(wallets) > 0:
|
||||||
|
self._log.warning(f"Changing {self.ticker()} wallet name.")
|
||||||
|
for wallet_name in wallets:
|
||||||
|
if wallet_name in ("mweb",):
|
||||||
|
continue
|
||||||
|
|
||||||
|
change_watchonly_wallet: bool = (
|
||||||
|
self._rpc_wallet_watch == self._rpc_wallet
|
||||||
|
)
|
||||||
|
|
||||||
|
self._rpc_wallet = wallet_name
|
||||||
|
self._log.info(
|
||||||
|
f"Switched {self.ticker()} wallet name to {self._rpc_wallet}."
|
||||||
|
)
|
||||||
|
self.rpc_wallet = make_rpc_func(
|
||||||
|
self._rpcport,
|
||||||
|
self._rpcauth,
|
||||||
|
host=self._rpc_host,
|
||||||
|
wallet=self._rpc_wallet,
|
||||||
|
)
|
||||||
|
if change_watchonly_wallet:
|
||||||
|
self.rpc_wallet_watch = self.rpc_wallet
|
||||||
|
break
|
||||||
|
|
||||||
|
return len(wallets)
|
||||||
|
|
||||||
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 +143,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:
|
||||||
@@ -86,6 +189,82 @@ class LTCInterface(BTCInterface):
|
|||||||
) + self.make_int(u["amount"], r=1)
|
) + self.make_int(u["amount"], r=1)
|
||||||
return unspent_addr
|
return unspent_addr
|
||||||
|
|
||||||
|
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||||
|
if password == "":
|
||||||
|
return
|
||||||
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
|
if self.useBackend():
|
||||||
|
return
|
||||||
|
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
if self._rpc_wallet not in wallets:
|
||||||
|
try:
|
||||||
|
self.rpc("loadwallet", [self._rpc_wallet])
|
||||||
|
except Exception as e:
|
||||||
|
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||||
|
try:
|
||||||
|
wallet_dirs = self.rpc("listwalletdir")
|
||||||
|
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
|
||||||
|
except Exception:
|
||||||
|
existing = []
|
||||||
|
if len(existing) == 0:
|
||||||
|
self._log.info(
|
||||||
|
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||||
|
)
|
||||||
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||||
|
self.rpc(
|
||||||
|
"createwallet",
|
||||||
|
[
|
||||||
|
self._rpc_wallet,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
password,
|
||||||
|
False,
|
||||||
|
self._use_descriptors,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
seed_id = self.getWalletSeedID()
|
||||||
|
needs_seed_init = seed_id == "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||||
|
needs_seed_init = True
|
||||||
|
if needs_seed_init:
|
||||||
|
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||||
|
self._sc.initialiseWallet(self.coin_type())
|
||||||
|
if password:
|
||||||
|
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||||
|
try:
|
||||||
|
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"encryptwallet returned: {e}")
|
||||||
|
import time
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
time.sleep(1)
|
||||||
|
try:
|
||||||
|
self.rpc("listwallets")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
self._log.debug(
|
||||||
|
f"Waiting for wallet after encryption... {i + 1}/10"
|
||||||
|
)
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
if self._rpc_wallet not in wallets:
|
||||||
|
self.rpc("loadwallet", [self._rpc_wallet])
|
||||||
|
self.setWalletSeedWarning(False)
|
||||||
|
check_seed = False
|
||||||
|
|
||||||
|
if self.isWalletEncrypted():
|
||||||
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
|
|
||||||
|
if check_seed:
|
||||||
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
|
|
||||||
class LTCInterfaceMWEB(LTCInterface):
|
class LTCInterfaceMWEB(LTCInterface):
|
||||||
|
|
||||||
@@ -129,13 +308,50 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
|
|
||||||
self._log.info("init_wallet - {}".format(self.ticker()))
|
self._log.info("init_wallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
|
wallets = self.rpc("listwallets")
|
||||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
if self._rpc_wallet not in wallets:
|
||||||
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
|
try:
|
||||||
|
self.rpc("loadwallet", [self._rpc_wallet])
|
||||||
|
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".')
|
||||||
|
except Exception as e:
|
||||||
|
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||||
|
self._log.info(
|
||||||
|
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||||
|
)
|
||||||
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||||
|
self.rpc(
|
||||||
|
"createwallet",
|
||||||
|
[
|
||||||
|
self._rpc_wallet,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
password,
|
||||||
|
False,
|
||||||
|
self._use_descriptors,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
wallets = self.rpc("listwallets")
|
||||||
|
if "mweb" not in wallets:
|
||||||
|
try:
|
||||||
|
self.rpc("loadwallet", ["mweb"])
|
||||||
|
self._log.debug("Loaded existing MWEB wallet.")
|
||||||
|
except Exception as e:
|
||||||
|
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||||
|
self._log.info(f"Creating MWEB wallet for {self.coin_name()}.")
|
||||||
|
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||||
|
self.rpc(
|
||||||
|
"createwallet",
|
||||||
|
["mweb", False, True, password, False, False, True],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
|
|
||||||
if self.getWalletSeedID() == "Not found":
|
if self.getWalletSeedID() == "Not found":
|
||||||
self._sc.initialiseWallet(self.interface_type())
|
self._sc.initialiseWallet(self.interface_type())
|
||||||
@@ -144,7 +360,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
self.rpc("unloadwallet", ["mweb"])
|
self.rpc("unloadwallet", ["mweb"])
|
||||||
self.rpc("loadwallet", ["mweb"])
|
self.rpc("loadwallet", ["mweb"])
|
||||||
if password is not None:
|
if password is not None:
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
self.rpc_wallet("keypoolrefill")
|
self.rpc_wallet("keypoolrefill")
|
||||||
|
|
||||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||||
@@ -152,10 +368,22 @@ 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:
|
||||||
# Max timeout value, ~3 years
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
try:
|
||||||
|
seed_id = self.getWalletSeedID()
|
||||||
|
needs_seed_init = seed_id == "Not found"
|
||||||
|
except Exception as e:
|
||||||
|
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||||
|
needs_seed_init = True
|
||||||
|
if needs_seed_init:
|
||||||
|
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||||
|
self._sc.initialiseWallet(self.interface_type())
|
||||||
|
|
||||||
if check_seed:
|
if check_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.interface_type())
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class PIVXInterface(BTCInterface):
|
|||||||
|
|
||||||
seed_id_before: str = self.getWalletSeedID()
|
seed_id_before: str = self.getWalletSeedID()
|
||||||
|
|
||||||
self.rpc_wallet("encryptwallet", [password])
|
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||||
|
|
||||||
if check_seed is False or seed_id_before == "Not found":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
|||||||
@@ -137,7 +137,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:
|
||||||
@@ -170,8 +170,29 @@ 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
|
||||||
|
if (
|
||||||
|
v["connection_type"] == "electrum"
|
||||||
|
and hasattr(ci, "_backend")
|
||||||
|
and ci._backend
|
||||||
|
and hasattr(ci._backend, "getSyncStatus")
|
||||||
|
):
|
||||||
|
sync_status = ci._backend.getSyncStatus()
|
||||||
|
coin_entry["electrum_synced"] = sync_status.get("synced", False)
|
||||||
|
coin_entry["electrum_height"] = sync_status.get("height", 0)
|
||||||
|
|
||||||
coins_with_balances.append(coin_entry)
|
coins_with_balances.append(coin_entry)
|
||||||
|
|
||||||
if k == Coins.PART:
|
if k == Coins.PART:
|
||||||
@@ -295,6 +316,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")
|
||||||
@@ -308,6 +332,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:
|
||||||
@@ -601,8 +650,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if have_data_entry(post_data, "debugind"):
|
if have_data_entry(post_data, "debugind"):
|
||||||
|
main_debug_ind: bool = toBool(
|
||||||
|
get_data_entry_or(post_data, "maindebugind", True)
|
||||||
|
)
|
||||||
swap_client.setBidDebugInd(
|
swap_client.setBidDebugInd(
|
||||||
bid_id, int(get_data_entry(post_data, "debugind"))
|
bid_id,
|
||||||
|
int(get_data_entry(post_data, "debugind")),
|
||||||
|
add_to_bid=main_debug_ind,
|
||||||
)
|
)
|
||||||
|
|
||||||
rv = {"bid_id": bid_id.hex()}
|
rv = {"bid_id": bid_id.hex()}
|
||||||
@@ -620,8 +674,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
|||||||
elif have_data_entry(post_data, "abandon"):
|
elif have_data_entry(post_data, "abandon"):
|
||||||
swap_client.abandonBid(bid_id)
|
swap_client.abandonBid(bid_id)
|
||||||
elif have_data_entry(post_data, "debugind"):
|
elif have_data_entry(post_data, "debugind"):
|
||||||
|
main_debug_ind: bool = toBool(
|
||||||
|
get_data_entry_or(post_data, "maindebugind", True)
|
||||||
|
)
|
||||||
swap_client.setBidDebugInd(
|
swap_client.setBidDebugInd(
|
||||||
bid_id, int(get_data_entry(post_data, "debugind"))
|
bid_id,
|
||||||
|
int(get_data_entry(post_data, "debugind")),
|
||||||
|
add_to_bid=main_debug_ind,
|
||||||
)
|
)
|
||||||
|
|
||||||
if have_data_entry(post_data, "show_extra"):
|
if have_data_entry(post_data, "show_extra"):
|
||||||
@@ -630,7 +689,9 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
|||||||
with_events = True
|
with_events = True
|
||||||
|
|
||||||
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
||||||
assert bid, "Unknown bid ID"
|
if bid is None:
|
||||||
|
swap_client.log.debug(f"js_bids: Unknown bid id {bid_id.hex()}")
|
||||||
|
return bytes(json.dumps({"error": "Unknown bid id"}), "UTF-8")
|
||||||
|
|
||||||
if post_string != "":
|
if post_string != "":
|
||||||
if have_data_entry(post_data, "chainbkeysplit"):
|
if have_data_entry(post_data, "chainbkeysplit"):
|
||||||
@@ -1210,10 +1271,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
|||||||
"current_seed_id": wallet_seed_id,
|
"current_seed_id": wallet_seed_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
|
|
||||||
rv.update(
|
if hasattr(ci, "getAccountKey"):
|
||||||
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
|
try:
|
||||||
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
|
rv.update({"account_key": ci.getAccountKey(seed_key, extkey_prefix)})
|
||||||
|
except Exception as e:
|
||||||
|
rv.update({"account_key_error": str(e)})
|
||||||
|
|
||||||
return bytes(
|
return bytes(
|
||||||
json.dumps(rv),
|
json.dumps(rv),
|
||||||
@@ -1528,6 +1591,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)
|
||||||
@@ -1571,10 +1699,221 @@ 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_modeswitchinfo(self, url_split, post_string, is_json) -> bytes:
|
||||||
|
swap_client = self.server.swap_client
|
||||||
|
swap_client.checkSystemStatus()
|
||||||
|
post_data = getFormData(post_string, is_json)
|
||||||
|
|
||||||
|
coin_str = get_data_entry(post_data, "coin")
|
||||||
|
direction = get_data_entry_or(post_data, "direction", "lite")
|
||||||
|
|
||||||
|
try:
|
||||||
|
coin_type = getCoinIdFromName(coin_str)
|
||||||
|
except Exception:
|
||||||
|
coin_type = getCoinIdFromTicker(coin_str.upper())
|
||||||
|
|
||||||
|
ci = swap_client.ci(coin_type)
|
||||||
|
ticker = ci.ticker()
|
||||||
|
|
||||||
|
try:
|
||||||
|
wallet_info = ci.getWalletInfo()
|
||||||
|
balance = wallet_info.get("balance", 0)
|
||||||
|
balance_sats = ci.make_int(balance)
|
||||||
|
except Exception as e:
|
||||||
|
return bytes(json.dumps({"error": f"Failed to get balance: {e}"}), "UTF-8")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fee_rate, rate_src = ci.get_fee_rate(ci._conf_target)
|
||||||
|
est_vsize = 180
|
||||||
|
if isinstance(fee_rate, int):
|
||||||
|
fee_per_vbyte = max(1, fee_rate // 1000)
|
||||||
|
else:
|
||||||
|
fee_per_vbyte = max(1, int(fee_rate * 100000))
|
||||||
|
estimated_fee_sats = est_vsize * fee_per_vbyte
|
||||||
|
except Exception:
|
||||||
|
estimated_fee_sats = 180
|
||||||
|
rate_src = "default"
|
||||||
|
|
||||||
|
min_viable = estimated_fee_sats * 2
|
||||||
|
can_transfer = balance_sats > min_viable
|
||||||
|
|
||||||
|
rv = {
|
||||||
|
"coin": ticker,
|
||||||
|
"direction": direction,
|
||||||
|
"balance": balance,
|
||||||
|
"balance_sats": balance_sats,
|
||||||
|
"estimated_fee_sats": estimated_fee_sats,
|
||||||
|
"estimated_fee": ci.format_amount(estimated_fee_sats),
|
||||||
|
"fee_rate_src": rate_src,
|
||||||
|
"can_transfer": can_transfer,
|
||||||
|
"min_viable_sats": min_viable,
|
||||||
|
}
|
||||||
|
|
||||||
|
if direction == "lite":
|
||||||
|
non_bip84_balance_sats = 0
|
||||||
|
has_non_bip84_funds = False
|
||||||
|
try:
|
||||||
|
if hasattr(ci, "rpc_wallet"):
|
||||||
|
unspent = ci.rpc_wallet("listunspent")
|
||||||
|
|
||||||
|
wm = swap_client.getWalletManager()
|
||||||
|
|
||||||
|
bip84_addresses = set()
|
||||||
|
if wm:
|
||||||
|
try:
|
||||||
|
all_addrs = wm.getAllAddresses(
|
||||||
|
coin_type, include_watch_only=False
|
||||||
|
)
|
||||||
|
bip84_addresses = set(all_addrs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for u in unspent:
|
||||||
|
addr = u.get("address")
|
||||||
|
if not addr:
|
||||||
|
continue
|
||||||
|
amount_sats = ci.make_int(u.get("amount", 0))
|
||||||
|
if amount_sats <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if addr not in bip84_addresses:
|
||||||
|
non_bip84_balance_sats += amount_sats
|
||||||
|
has_non_bip84_funds = True
|
||||||
|
except Exception as e:
|
||||||
|
swap_client.log.debug(f"Error checking non-BIP84 addresses: {e}")
|
||||||
|
|
||||||
|
if has_non_bip84_funds and non_bip84_balance_sats > min_viable:
|
||||||
|
rv["show_transfer_option"] = True
|
||||||
|
rv["require_transfer"] = True
|
||||||
|
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||||
|
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||||
|
rv["message"] = (
|
||||||
|
"Funds on non-derivable addresses must be transferred for external wallet compatibility"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rv["show_transfer_option"] = False
|
||||||
|
rv["require_transfer"] = False
|
||||||
|
if has_non_bip84_funds:
|
||||||
|
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||||
|
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||||
|
rv["message"] = "Non-derivable balance too low to transfer"
|
||||||
|
else:
|
||||||
|
rv["legacy_balance_sats"] = 0
|
||||||
|
rv["legacy_balance"] = "0"
|
||||||
|
rv["message"] = "All funds on BIP84 addresses"
|
||||||
|
else:
|
||||||
|
rv["show_transfer_option"] = can_transfer
|
||||||
|
if balance_sats == 0:
|
||||||
|
rv["message"] = "No funds to transfer"
|
||||||
|
elif not can_transfer:
|
||||||
|
rv["message"] = "Balance too low to transfer (fee would exceed funds)"
|
||||||
|
else:
|
||||||
|
rv["message"] = ""
|
||||||
|
|
||||||
|
return bytes(json.dumps(rv), "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,
|
||||||
@@ -1604,6 +1943,8 @@ endpoints = {
|
|||||||
"coinvolume": js_coinvolume,
|
"coinvolume": js_coinvolume,
|
||||||
"coinhistory": js_coinhistory,
|
"coinhistory": js_coinhistory,
|
||||||
"messageroutes": js_messageroutes,
|
"messageroutes": js_messageroutes,
|
||||||
|
"electrumdiscover": js_electrum_discover,
|
||||||
|
"modeswitchinfo": js_modeswitchinfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -152,15 +152,17 @@ class Jsonrpc:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
def callrpc(
|
||||||
|
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
|
||||||
|
):
|
||||||
if _use_rpc_pooling:
|
if _use_rpc_pooling:
|
||||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
|
return callrpc_pooled(rpc_port, auth, method, params, wallet, host, timeout)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
url += "wallet/" + urllib.parse.quote(wallet)
|
url += "wallet/" + urllib.parse.quote(wallet)
|
||||||
x = Jsonrpc(url)
|
x = Jsonrpc(url, timeout=timeout if timeout else 10)
|
||||||
|
|
||||||
v = x.json_request(method, params)
|
v = x.json_request(method, params)
|
||||||
x.close()
|
x.close()
|
||||||
@@ -174,7 +176,9 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
|||||||
return r["result"]
|
return r["result"]
|
||||||
|
|
||||||
|
|
||||||
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
def callrpc_pooled(
|
||||||
|
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
|
||||||
|
):
|
||||||
from .rpc_pool import get_rpc_pool
|
from .rpc_pool import get_rpc_pool
|
||||||
import http.client
|
import http.client
|
||||||
import socket
|
import socket
|
||||||
@@ -183,6 +187,20 @@ def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0
|
|||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
url += "wallet/" + urllib.parse.quote(wallet)
|
url += "wallet/" + urllib.parse.quote(wallet)
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
try:
|
||||||
|
conn = Jsonrpc(url, timeout=timeout)
|
||||||
|
v = conn.json_request(method, params)
|
||||||
|
r = json.loads(v.decode("utf-8"))
|
||||||
|
conn.close()
|
||||||
|
if "error" in r and r["error"] is not None:
|
||||||
|
raise ValueError("RPC error " + str(r["error"]))
|
||||||
|
return r["result"]
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
|
except Exception as ex:
|
||||||
|
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||||
|
|
||||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||||
pool = get_rpc_pool(url, max_connections)
|
pool = get_rpc_pool(url, max_connections)
|
||||||
|
|
||||||
@@ -247,7 +265,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
|||||||
wallet = wallet
|
wallet = wallet
|
||||||
host = host
|
host = host
|
||||||
|
|
||||||
def rpc_func(method, params=None, wallet_override=None):
|
def rpc_func(method, params=None, wallet_override=None, timeout=None):
|
||||||
return callrpc(
|
return callrpc(
|
||||||
port,
|
port,
|
||||||
auth,
|
auth,
|
||||||
@@ -255,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
|||||||
params,
|
params,
|
||||||
wallet if wallet_override is None else wallet_override,
|
wallet if wallet_override is None else wallet_override,
|
||||||
host,
|
host,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|||||||
@@ -3,14 +3,41 @@
|
|||||||
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -191,13 +236,39 @@
|
|||||||
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'
|
||||||
@@ -609,7 +610,7 @@ function ensureToastContainer() {
|
|||||||
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
||||||
cursorStyle = 'cursor-pointer';
|
cursorStyle = 'cursor-pointer';
|
||||||
} else if (options.coinSymbol) {
|
} else if (options.coinSymbol) {
|
||||||
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
|
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol.toLowerCase()}'"`;
|
||||||
cursorStyle = 'cursor-pointer';
|
cursorStyle = 'cursor-pointer';
|
||||||
} else if (options.releaseUrl) {
|
} else if (options.releaseUrl) {
|
||||||
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
|
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
|
||||||
@@ -735,6 +736,18 @@ 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+$/, '');
|
||||||
|
const sweepTicker = data.ticker || data.coin_name;
|
||||||
|
toastTitle = `Swept ${sweepAmount} ${sweepTicker} to RPC wallet`;
|
||||||
|
toastOptions.subtitle = `Fee: ${sweepFee} ${sweepTicker} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
||||||
|
toastOptions.coinSymbol = sweepTicker;
|
||||||
|
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);
|
||||||
|
|||||||
614
basicswap/static/js/pages/bid-page.js
Normal file
614
basicswap/static/js/pages/bid-page.js
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
const BidPage = {
|
||||||
|
bidId: null,
|
||||||
|
bidStateInd: null,
|
||||||
|
createdAtTimestamp: null,
|
||||||
|
autoRefreshInterval: null,
|
||||||
|
elapsedTimeInterval: null,
|
||||||
|
AUTO_REFRESH_SECONDS: 60,
|
||||||
|
refreshPaused: false,
|
||||||
|
swapType: null,
|
||||||
|
coinFrom: null,
|
||||||
|
coinTo: null,
|
||||||
|
previousStateInd: null,
|
||||||
|
|
||||||
|
INACTIVE_STATES: [8, 17, 18, 19, 21, 22, 23, 25, 31], // Completed, Failed variants, Timed-out, Abandoned, Error, Rejected, Expired
|
||||||
|
|
||||||
|
DELAYING_STATE: 20,
|
||||||
|
|
||||||
|
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': null,
|
||||||
|
'Bid Script coin spend tx valid': null,
|
||||||
|
'Bid Scriptless coin locked': null,
|
||||||
|
'Bid Script coin lock released': 'Adaptor signature revealed. The script coin can now be claimed',
|
||||||
|
'Bid Script tx redeemed': null,
|
||||||
|
'Bid Script pre-refund tx in chain': 'Pre-refund transaction detected. Swap may be failing',
|
||||||
|
'Bid Scriptless tx redeemed': null,
|
||||||
|
'Bid Scriptless tx recovered': null,
|
||||||
|
'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 adaptor 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'
|
||||||
|
},
|
||||||
|
|
||||||
|
getStateTooltip: function(stateText) {
|
||||||
|
const staticTooltip = this.STATE_TOOLTIPS[stateText];
|
||||||
|
if (staticTooltip !== null && staticTooltip !== undefined) {
|
||||||
|
return staticTooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptlessCoins = ['XMR', 'WOW'];
|
||||||
|
let scriptCoin, scriptlessCoin;
|
||||||
|
|
||||||
|
if (scriptlessCoins.includes(this.coinFrom)) {
|
||||||
|
scriptlessCoin = this.coinFrom;
|
||||||
|
scriptCoin = this.coinTo;
|
||||||
|
} else if (scriptlessCoins.includes(this.coinTo)) {
|
||||||
|
scriptlessCoin = this.coinTo;
|
||||||
|
scriptCoin = this.coinFrom;
|
||||||
|
} else {
|
||||||
|
scriptCoin = this.coinFrom;
|
||||||
|
scriptlessCoin = this.coinTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicTooltips = {
|
||||||
|
'Bid Script coin locked': `${scriptCoin} is locked in the atomic swap contract`,
|
||||||
|
'Bid Script coin spend tx valid': `The ${scriptCoin} spend transaction has been validated and is ready`,
|
||||||
|
'Bid Scriptless coin locked': `${scriptlessCoin} is locked using adaptor signatures`,
|
||||||
|
'Bid Script tx redeemed': `${scriptCoin} has been successfully claimed`,
|
||||||
|
'Bid Scriptless tx redeemed': `${scriptlessCoin} has been successfully claimed`,
|
||||||
|
'Bid Scriptless tx recovered': `${scriptlessCoin} recovered after swap failure`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return dynamicTooltips[stateText] || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
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, options) {
|
||||||
|
this.bidId = bidId;
|
||||||
|
this.bidStateInd = bidStateInd;
|
||||||
|
this.createdAtTimestamp = createdAtTimestamp;
|
||||||
|
this.stateTimeTimestamp = stateTimeTimestamp;
|
||||||
|
this.tooltipCounter = 0;
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
this.swapType = options.swapType || 'secret-hash';
|
||||||
|
this.coinFrom = options.coinFrom || '';
|
||||||
|
this.coinTo = options.coinTo || '';
|
||||||
|
|
||||||
|
if (this.bidStateInd === this.DELAYING_STATE) {
|
||||||
|
this.previousStateInd = this.findPreviousState();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyStateTooltips();
|
||||||
|
this.applyEventTooltips();
|
||||||
|
this.createProgressBar();
|
||||||
|
this.startElapsedTimeUpdater();
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
findPreviousState: 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');
|
||||||
|
|
||||||
|
for (let i = rows.length - 1; i >= 0; i--) {
|
||||||
|
const cells = rows[i].querySelectorAll('td');
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
const stateText = cells[cells.length - 1].textContent.trim();
|
||||||
|
if (!stateText.includes('Delaying')) {
|
||||||
|
return this.stateTextToIndex(stateText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
stateTextToIndex: function(stateText) {
|
||||||
|
const stateMap = {
|
||||||
|
'Sent': 1, 'Receiving': 2, 'Received': 3, 'Receiving accept': 4,
|
||||||
|
'Accepted': 5, 'Initiated': 6, 'Participating': 7, 'Completed': 8,
|
||||||
|
'Script coin locked': 9, 'Script coin spend tx valid': 10,
|
||||||
|
'Scriptless coin locked': 11, 'Script coin lock released': 12,
|
||||||
|
'Script tx redeemed': 13, 'Script pre-refund tx in chain': 14,
|
||||||
|
'Scriptless tx redeemed': 15, 'Scriptless tx recovered': 16,
|
||||||
|
'Failed, refunded': 17, 'Failed, swiped': 18, 'Failed': 19,
|
||||||
|
'Delaying': 20, 'Timed-out': 21, 'Abandoned': 22, 'Error': 23,
|
||||||
|
'Rejected': 25, 'Exchanged script lock tx sigs msg': 27,
|
||||||
|
'Exchanged script lock spend tx msg': 28, 'Request sent': 29,
|
||||||
|
'Request accepted': 30, 'Expired': 31
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(stateMap)) {
|
||||||
|
if (stateText.includes(key)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
isActiveState: function() {
|
||||||
|
return !this.INACTIVE_STATES.includes(this.bidStateInd);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupAutoRefresh: function() {
|
||||||
|
const refreshBtn = document.getElementById('refresh');
|
||||||
|
if (!refreshBtn) return;
|
||||||
|
|
||||||
|
if (!this.isActiveState()) {
|
||||||
|
refreshBtn.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSpan = refreshBtn.querySelector('span');
|
||||||
|
if (!originalSpan) return;
|
||||||
|
|
||||||
|
let countdown = this.AUTO_REFRESH_SECONDS;
|
||||||
|
let isRefreshing = false;
|
||||||
|
let isPersistentlyPaused = false;
|
||||||
|
|
||||||
|
const updateCountdown = () => {
|
||||||
|
if (this.refreshPaused || isPersistentlyPaused || isRefreshing) return;
|
||||||
|
|
||||||
|
originalSpan.textContent = `Auto-refresh in ${countdown}s`;
|
||||||
|
countdown--;
|
||||||
|
|
||||||
|
if (countdown < 0 && !isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
if (this.autoRefreshInterval) {
|
||||||
|
clearInterval(this.autoRefreshInterval);
|
||||||
|
this.autoRefreshInterval = null;
|
||||||
|
}
|
||||||
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCountdown();
|
||||||
|
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isPersistentlyPaused) {
|
||||||
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
|
} else {
|
||||||
|
isPersistentlyPaused = true;
|
||||||
|
if (this.autoRefreshInterval) {
|
||||||
|
clearInterval(this.autoRefreshInterval);
|
||||||
|
this.autoRefreshInterval = null;
|
||||||
|
}
|
||||||
|
originalSpan.textContent = 'Paused (click to refresh)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('mouseenter', () => {
|
||||||
|
if (!isPersistentlyPaused) {
|
||||||
|
this.refreshPaused = true;
|
||||||
|
if (this.autoRefreshInterval) {
|
||||||
|
clearInterval(this.autoRefreshInterval);
|
||||||
|
this.autoRefreshInterval = null;
|
||||||
|
}
|
||||||
|
originalSpan.textContent = 'Click to pause';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('mouseleave', () => {
|
||||||
|
if (!isPersistentlyPaused) {
|
||||||
|
this.refreshPaused = false;
|
||||||
|
countdown = this.AUTO_REFRESH_SECONDS;
|
||||||
|
if (!this.autoRefreshInterval) {
|
||||||
|
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.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + 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.getStateTooltip(stateText) || this.getStateTooltip('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() {
|
||||||
|
let stateForProgress = this.bidStateInd;
|
||||||
|
let isDelaying = false;
|
||||||
|
|
||||||
|
if (this.bidStateInd === this.DELAYING_STATE && this.previousStateInd) {
|
||||||
|
stateForProgress = this.previousStateInd;
|
||||||
|
isDelaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseInfo = this.STATE_PHASES[stateForProgress];
|
||||||
|
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');
|
||||||
|
|
||||||
|
let phaseLabel;
|
||||||
|
if (isError) {
|
||||||
|
phaseLabel = phaseInfo.label;
|
||||||
|
} else if (isComplete) {
|
||||||
|
phaseLabel = 'Complete';
|
||||||
|
} else if (isDelaying) {
|
||||||
|
phaseLabel = `${phaseInfo.label} (${progressPercent}%) - Delaying`;
|
||||||
|
} else {
|
||||||
|
phaseLabel = `${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' : ''}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
dentities: new Map(),
|
identities: new Map(),
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
jsonData: [],
|
jsonData: [],
|
||||||
|
|||||||
@@ -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,410 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
select.addEventListener('change', (e) => {
|
||||||
|
const coinName = select.name.replace('connection_type_', '');
|
||||||
|
const electrumSection = document.getElementById(`electrum-section-${coinName}`);
|
||||||
|
const fundTransferSection = document.getElementById(`fund-transfer-section-${coinName}`);
|
||||||
|
const originalValue = this.originalConnectionTypes[select.name];
|
||||||
|
|
||||||
|
if (e.target.value === 'electrum') {
|
||||||
|
if (electrumSection) {
|
||||||
|
electrumSection.classList.remove('hidden');
|
||||||
|
|
||||||
|
const clearnetTextarea = document.getElementById(`electrum_clearnet_${coinName}`);
|
||||||
|
const onionTextarea = document.getElementById(`electrum_onion_${coinName}`);
|
||||||
|
|
||||||
|
if (clearnetTextarea && !clearnetTextarea.value.trim()) {
|
||||||
|
clearnetTextarea.value = electrumSection.dataset.defaultClearnet || '';
|
||||||
|
}
|
||||||
|
if (onionTextarea && !onionTextarea.value.trim()) {
|
||||||
|
onionTextarea.value = electrumSection.dataset.defaultOnion || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fundTransferSection) {
|
||||||
|
fundTransferSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (electrumSection) {
|
||||||
|
electrumSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (fundTransferSection && originalValue === 'electrum') {
|
||||||
|
fundTransferSection.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let transferValue = null;
|
||||||
|
const transferRadio = document.querySelector('input[name="transfer_choice"]:checked');
|
||||||
|
const transferHidden = document.querySelector('input[name="transfer_choice"][type="hidden"]');
|
||||||
|
if (transferRadio) {
|
||||||
|
transferValue = transferRadio.value;
|
||||||
|
} else if (transferHidden) {
|
||||||
|
transferValue = transferHidden.value;
|
||||||
|
}
|
||||||
|
if (transferValue) {
|
||||||
|
const transferInput = document.createElement('input');
|
||||||
|
transferInput.type = 'hidden';
|
||||||
|
transferInput.name = `auto_transfer_now_${coinName}`;
|
||||||
|
transferInput.value = transferValue === 'auto' ? 'true' : 'false';
|
||||||
|
form.appendChild(transferInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfirmButtonState: function() {
|
||||||
|
const confirmBtn = document.getElementById('walletModeConfirm');
|
||||||
|
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
|
||||||
|
if (confirmBtn && checkbox) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
} else {
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showWalletModeConfirmation: async function(coinName, direction, submitter) {
|
||||||
|
const modal = document.getElementById('walletModeModal');
|
||||||
|
const title = document.getElementById('walletModeTitle');
|
||||||
|
const message = document.getElementById('walletModeMessage');
|
||||||
|
const details = document.getElementById('walletModeDetails');
|
||||||
|
const confirmBtn = document.getElementById('walletModeConfirm');
|
||||||
|
|
||||||
|
if (!modal || !title || !message || !details) return;
|
||||||
|
|
||||||
|
this.pendingModeSwitch = { coinName, direction, submitter };
|
||||||
|
|
||||||
|
const displayName = coinName.charAt(0).toUpperCase() + coinName.slice(1).toLowerCase();
|
||||||
|
|
||||||
|
details.innerHTML = `
|
||||||
|
<div class="flex items-center justify-center py-4">
|
||||||
|
<svg class="animate-spin h-5 w-5 text-blue-500 mr-2" 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>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (direction === 'lite') {
|
||||||
|
title.textContent = `Switch ${displayName} to Lite Wallet Mode`;
|
||||||
|
message.textContent = 'Write down this key before switching. It will only be shown ONCE.';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [infoResponse, seedResponse] = await Promise.all([
|
||||||
|
fetch('/json/modeswitchinfo', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ coin: coinName, direction: 'lite' })
|
||||||
|
}),
|
||||||
|
fetch('/json/getcoinseed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ coin: coinName })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const info = await infoResponse.json();
|
||||||
|
const data = await seedResponse.json();
|
||||||
|
|
||||||
|
let transferSection = '';
|
||||||
|
if (info.require_transfer && info.legacy_balance_sats > 0) {
|
||||||
|
transferSection = `
|
||||||
|
<div class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
|
||||||
|
<p class="text-sm font-medium text-gray-900 dark:text-white mb-2">Funds Transfer Required</p>
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-200 mb-2">
|
||||||
|
<strong>${info.legacy_balance} ${info.coin}</strong> on non-derivable addresses will be automatically transferred to a BIP84 address.
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">
|
||||||
|
Est. fee: ${info.estimated_fee} ${info.coin}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-200">
|
||||||
|
This ensures your funds are recoverable using the extended key backup in external Electrum wallets.
|
||||||
|
</p>
|
||||||
|
<input type="hidden" name="transfer_choice" value="auto">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (info.legacy_balance_sats > 0 && !info.show_transfer_option) {
|
||||||
|
transferSection = `
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-xs mb-3">
|
||||||
|
Some funds on non-derivable addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.account_key) {
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2 text-red-600 dark:text-red-300 font-semibold">
|
||||||
|
IMPORTANT: Write down this key NOW. It will not be shown again.
|
||||||
|
</p>
|
||||||
|
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Extended Private Key (for external wallet import):</strong></p>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded p-2 mb-3">
|
||||||
|
<code id="extendedKeyDisplay" class="text-xs break-all font-mono text-gray-900 dark:text-gray-100">${'*'.repeat(Math.min(data.account_key.length, 80))}</code>
|
||||||
|
<code id="extendedKeyActual" class="text-xs break-all select-all font-mono text-gray-900 dark:text-gray-100 hidden">${data.account_key}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button" id="toggleKeyVisibility" class="px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">
|
||||||
|
Show Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-300 mb-3 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded p-2">
|
||||||
|
<p class="font-medium mb-1 text-gray-800 dark:text-gray-100">To import in Electrum wallet:</p>
|
||||||
|
<ol class="list-decimal list-inside space-y-0.5">
|
||||||
|
<li>Open Electrum → File → New/Restore</li>
|
||||||
|
<li>Choose "Standard wallet" → "Use a master key"</li>
|
||||||
|
<li>Paste this key (starts with zprv... or yprv...)</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
${transferSection}
|
||||||
|
<div class="border-t border-gray-300 dark:border-gray-500 pt-3">
|
||||||
|
<label class="flex items-center cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-500 rounded p-1 -m-1">
|
||||||
|
<input type="checkbox" id="walletModeKeyConfirmCheckbox" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-500 focus:ring-blue-500 dark:bg-gray-700">
|
||||||
|
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">I have written down this key</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const toggleBtn = document.getElementById('toggleKeyVisibility');
|
||||||
|
const keyDisplay = document.getElementById('extendedKeyDisplay');
|
||||||
|
const keyActual = document.getElementById('extendedKeyActual');
|
||||||
|
if (toggleBtn && keyDisplay && keyActual) {
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
if (keyDisplay.classList.contains('hidden')) {
|
||||||
|
keyDisplay.classList.remove('hidden');
|
||||||
|
keyActual.classList.add('hidden');
|
||||||
|
toggleBtn.textContent = 'Show Key';
|
||||||
|
} else {
|
||||||
|
keyDisplay.classList.add('hidden');
|
||||||
|
keyActual.classList.remove('hidden');
|
||||||
|
toggleBtn.textContent = 'Hide Key';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
|
||||||
|
<li>Active swaps must be completed first</li>
|
||||||
|
<li>Wait for any pending transactions to confirm</li>
|
||||||
|
</ul>
|
||||||
|
${transferSection}
|
||||||
|
<p class="mt-3 text-green-700 dark:text-green-300">
|
||||||
|
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch coin seed:', error);
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="text-red-600 dark:text-red-300 mb-2">Failed to retrieve extended key. Please try again.</p>
|
||||||
|
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
|
||||||
|
<li>Active swaps must be completed first</li>
|
||||||
|
<li>Wait for any pending transactions to confirm</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title.textContent = `Switch ${displayName} to Full Node Mode`;
|
||||||
|
message.textContent = 'Please confirm you want to switch to full node mode.';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/json/modeswitchinfo', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ coin: coinName, direction: 'rpc' })
|
||||||
|
});
|
||||||
|
const info = await response.json();
|
||||||
|
|
||||||
|
let transferSection = '';
|
||||||
|
if (info.error) {
|
||||||
|
transferSection = `<p class="text-yellow-700 dark:text-yellow-300 text-sm">${info.error}</p>`;
|
||||||
|
} else if (info.balance_sats === 0) {
|
||||||
|
transferSection = `<p class="text-gray-600 dark:text-gray-300 text-sm">No funds to transfer.</p>`;
|
||||||
|
} else if (!info.can_transfer) {
|
||||||
|
transferSection = `
|
||||||
|
<p class="text-yellow-700 dark:text-yellow-300 text-sm">
|
||||||
|
Balance (${info.balance} ${info.coin}) is too low to transfer - fee would exceed funds.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
transferSection = `
|
||||||
|
<div class="bg-gray-200 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
|
||||||
|
<p class="text-sm font-medium text-gray-900 dark:text-white mb-2">Fund Transfer Options</p>
|
||||||
|
<p class="text-xs text-gray-700 dark:text-gray-300 mb-3">
|
||||||
|
Balance: ${info.balance} ${info.coin} | Est. fee: ${info.estimated_fee} ${info.coin}
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
|
||||||
|
<input type="radio" name="transfer_choice" value="auto" checked class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-white">Auto-transfer funds to RPC wallet</span>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300">Recommended. Ensures all funds visible in full node wallet.</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
|
||||||
|
<input type="radio" name="transfer_choice" value="manual" class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-white">Keep funds on current addresses</span>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300">Transfer manually later if needed.</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-3">
|
||||||
|
If you skip transfer, you will need to manually send funds from lite wallet addresses to your RPC wallet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1 mb-3 text-gray-700 dark:text-gray-200">
|
||||||
|
<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>
|
||||||
|
${transferSection}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch mode switch info:', error);
|
||||||
|
details.innerHTML = `
|
||||||
|
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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 +715,167 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const serverName = data.current_server ? `${data.current_server.host}:${data.current_server.port}` : 'The connected server';
|
||||||
|
html = `<div class="text-sm text-gray-500 dark:text-gray-400 py-4 text-center">No servers discovered. <span class="font-mono">${serverName}</span> does not return peer lists.</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) {
|
||||||
|
window.BalanceUpdatesManager.fetchBalanceData()
|
||||||
|
.then(data => this.handleBalanceUpdate(data))
|
||||||
|
.catch(error => console.error('Error updating balance after swap:', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
console.log('Swap event:', eventData);
|
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,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coinData.scan_status || coinData.electrum_synced !== undefined) {
|
||||||
|
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 && 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 if (coinData.electrum_synced) {
|
||||||
|
const height = coinData.electrum_height || '';
|
||||||
|
scanStatusEl.innerHTML = `
|
||||||
|
<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 (${height})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (coinData.electrum_synced === false) {
|
||||||
|
scanStatusEl.innerHTML = `
|
||||||
|
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
|
||||||
|
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
|
||||||
|
Waiting for Electrum Server...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
scanStatusEl.innerHTML = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
||||||
@@ -102,12 +168,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 +206,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 +286,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 +398,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,16 @@ 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' }}, {
|
||||||
|
swapType: 'secret-hash',
|
||||||
|
coinFrom: '{{ data.ticker_from }}',
|
||||||
|
coinTo: '{{ data.ticker_to }}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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,16 @@ 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' }}, {
|
||||||
|
swapType: 'adaptor-sig',
|
||||||
|
coinFrom: '{{ data.ticker_from }}',
|
||||||
|
coinTo: '{{ data.ticker_to }}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
<div class="w-full md:w-1/2 mb-6 md:mb-0">
|
<div class="w-full md:w-1/2 mb-6 md:mb-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<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">© 2026~ (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.1</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>
|
||||||
|
|||||||
@@ -111,9 +111,23 @@
|
|||||||
|
|
||||||
<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 %}
|
||||||
@@ -144,6 +158,163 @@
|
|||||||
</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">Light Wallet Mode (Electrum):</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• No blockchain download needed - connect via external Electrum servers</li>
|
||||||
|
<li>• Uses BIP84 derivation (native SegWit) - lower fees, modern addresses (bc1q.../ltc1q...)</li>
|
||||||
|
<li>• You receive an extended private key (zprv/...) that can be imported into external wallets</li>
|
||||||
|
<li>• Best for: fresh installs, low storage, quick setup, mobile-friendly</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">Full Node Mode (RPC):</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• Maximum privacy - no external servers, your node validates everything</li>
|
||||||
|
<li>• More wallet features: coin control, RBF, CPFP, raw transactions</li>
|
||||||
|
<li>• Supports legacy address types and coin-specific features (e.g. MWEB for LTC)</li>
|
||||||
|
<li>• Best for: existing node users, power users, maximum control</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">When switching modes:</p>
|
||||||
|
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||||
|
<li>• To Light: Save your BIP84 key shown during switch (for external wallet import)</li>
|
||||||
|
<li>• To Full Node: Funds on light wallet addresses must be transferred (network fee applies)</li>
|
||||||
|
<li>• Both modes share the same seed - switching is safe, just save keys when shown</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.supports_electrum %}
|
||||||
|
<div id="electrum-section-{{ c.name }}" class="mb-6 {% if c.connection_type != 'electrum' %}hidden{% endif %}"
|
||||||
|
data-default-clearnet="{{ c.clearnet_servers_text }}"
|
||||||
|
data-default-onion="{{ c.onion_servers_text }}">
|
||||||
|
<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">{% if c.connection_type == 'electrum' %}{{ c.clearnet_servers_text }}{% endif %}</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">{% if c.connection_type == 'electrum' %}{{ c.onion_servers_text }}{% endif %}</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 id="fund-transfer-section-{{ c.name }}" class="mb-6 hidden">
|
||||||
|
{% if c.lite_wallet_balance %}
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
Pending Balance
|
||||||
|
</h4>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
|
<div class="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>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if general_settings.debug %}
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4 {% if c.lite_wallet_balance %}mt-6{% endif %}">
|
||||||
|
Advanced
|
||||||
|
</h4>
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
|
<div>
|
||||||
|
<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="50">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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 +827,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-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 font-medium text-sm text-gray-800 dark:text-white rounded-md border border-gray-300 dark:border-gray-500 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>
|
||||||
|
|||||||
@@ -84,6 +84,36 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if legacy_funds_info and legacy_funds_info.has_legacy_funds %}
|
||||||
|
<section class="py-4 px-6" id="legacy_funds_warning">
|
||||||
|
<div class="lg:container mx-auto">
|
||||||
|
<div class="p-6 rounded-lg bg-yellow-50 border border-yellow-400 dark:bg-yellow-900/30 dark:border-yellow-700">
|
||||||
|
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||||
|
<div class="flex-1 p-2">
|
||||||
|
<div class="flex flex-wrap -m-1">
|
||||||
|
<div class="w-auto p-1">
|
||||||
|
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" 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"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<p class="font-semibold text-lg lg:text-sm text-yellow-700 dark:text-yellow-300">Legacy Address Funds</p>
|
||||||
|
<p class="mt-1 text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
{{ legacy_funds_info.legacy_balance }} {{ legacy_funds_info.coin }} on legacy addresses won't be visible in external Electrum wallet.
|
||||||
|
To use funds with external wallets, transfer to a new address.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs text-yellow-500 dark:text-yellow-500">
|
||||||
|
Use the withdraw function below to send funds to a new <code class="bg-yellow-100 dark:bg-yellow-800/50 px-1 rounded">{{ w.ticker | lower }}1...</code> address.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<form method="post" autocomplete="off">
|
<form method="post" autocomplete="off">
|
||||||
<div class="px-6 py-0 h-full overflow-hidden">
|
<div class="px-6 py-0 h-full overflow-hidden">
|
||||||
@@ -113,6 +143,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 #}
|
||||||
@@ -141,11 +174,26 @@
|
|||||||
<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>
|
||||||
|
</tr>
|
||||||
|
{% elif w.cid == '13' %} {# FIRO #}
|
||||||
|
<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 }} Spark"> </span>Spark Balance: </td>
|
||||||
|
<td class="py-3 px-6 bold">
|
||||||
|
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
|
||||||
|
(<span class="usd-value"></span>)
|
||||||
|
{% if w.spark_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.spark_pending }} {{ w.ticker }} </span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif w.cid == '13' %} {# FIRO #}
|
{% elif w.cid == '13' %} {# FIRO #}
|
||||||
@@ -177,6 +225,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 }}
|
||||||
@@ -192,8 +253,70 @@
|
|||||||
{% 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 %}
|
||||||
|
{% if w.electrum_synced %}
|
||||||
|
<span class="text-green-600 dark:text-green-400">Electrum Wallet Synced ({{ w.electrum_height }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-yellow-600 dark:text-yellow-400">Waiting for Electrum Server...</span>
|
||||||
|
{% endif %}
|
||||||
|
{% 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>
|
||||||
@@ -333,13 +456,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">
|
||||||
@@ -347,6 +471,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 #}
|
||||||
{% elif w.cid == '13' %}
|
{% elif w.cid == '13' %}
|
||||||
{# FIRO #}
|
{# FIRO #}
|
||||||
@@ -379,10 +504,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">
|
||||||
@@ -423,8 +544,21 @@
|
|||||||
<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>
|
||||||
|
</tr>
|
||||||
|
{% elif w.cid == '13' %}
|
||||||
|
{# FIRO #}
|
||||||
|
<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>Spark Balance: </td>
|
||||||
|
<td class="py-3 px-6">
|
||||||
|
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
|
||||||
|
(<span class="usd-value"></span>)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif w.cid == '13' %}
|
{% elif w.cid == '13' %}
|
||||||
@@ -588,7 +722,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">
|
||||||
@@ -653,6 +787,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 %}
|
||||||
@@ -740,6 +876,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>
|
||||||
@@ -181,6 +197,39 @@
|
|||||||
<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>
|
||||||
|
{% elif w.electrum_synced %}
|
||||||
|
<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 ({{ w.electrum_height }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
|
||||||
|
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
|
||||||
|
Waiting for Electrum Server...
|
||||||
|
</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>
|
||||||
@@ -201,6 +250,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
|
||||||
@@ -249,6 +253,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
|
|||||||
get_data_entry(form_data, "valid_for_seconds")
|
get_data_entry(form_data, "valid_for_seconds")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if swap_client.debug:
|
||||||
|
if have_data_entry(form_data, "lock_type"):
|
||||||
|
parsed_data["lock_type"] = TxLockTypes(
|
||||||
|
int(get_data_entry(form_data, "lock_type"))
|
||||||
|
)
|
||||||
|
if have_data_entry(form_data, "lock_blocks"):
|
||||||
|
parsed_data["lock_blocks"] = int(get_data_entry(form_data, "lock_blocks"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if len(errors) == 0 and page_data["swap_style"] == "xmr":
|
if len(errors) == 0 and page_data["swap_style"] == "xmr":
|
||||||
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
|
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
|
||||||
@@ -342,7 +354,15 @@ def postNewOfferFromParsed(swap_client, parsed_data):
|
|||||||
lock_type = TxLockTypes.ABS_LOCK_TIME
|
lock_type = TxLockTypes.ABS_LOCK_TIME
|
||||||
|
|
||||||
extra_options = {}
|
extra_options = {}
|
||||||
|
lock_value: int = parsed_data.get("lock_seconds", -1)
|
||||||
|
if swap_client.debug:
|
||||||
|
if "lock_type" in parsed_data:
|
||||||
|
lock_type = parsed_data["lock_type"]
|
||||||
|
if "lock_blocks" in parsed_data:
|
||||||
|
lock_value = parsed_data["lock_blocks"]
|
||||||
|
|
||||||
|
if "fee_from_conf" in parsed_data:
|
||||||
|
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
|
||||||
if "fee_from_conf" in parsed_data:
|
if "fee_from_conf" in parsed_data:
|
||||||
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
|
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
|
||||||
if "from_fee_multiplier_percent" in parsed_data:
|
if "from_fee_multiplier_percent" in parsed_data:
|
||||||
@@ -393,7 +413,7 @@ def postNewOfferFromParsed(swap_client, parsed_data):
|
|||||||
parsed_data["amt_bid_min"],
|
parsed_data["amt_bid_min"],
|
||||||
swap_type,
|
swap_type,
|
||||||
lock_type=lock_type,
|
lock_type=lock_type,
|
||||||
lock_value=parsed_data["lock_seconds"],
|
lock_value=lock_value,
|
||||||
addr_send_from=parsed_data["addr_from"],
|
addr_send_from=parsed_data["addr_from"],
|
||||||
extra_options=extra_options,
|
extra_options=extra_options,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,70 @@ 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_now = have_data_entry(
|
||||||
|
form_data, "auto_transfer_now_" + name
|
||||||
|
)
|
||||||
|
if auto_transfer_now:
|
||||||
|
transfer_value = get_data_entry_or(
|
||||||
|
form_data, "auto_transfer_now_" + name, "false"
|
||||||
|
)
|
||||||
|
data["auto_transfer_now"] = transfer_value == "true"
|
||||||
|
gap_limit_str = get_data_entry_or(
|
||||||
|
form_data, "gap_limit_" + name, "50"
|
||||||
|
).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 +220,71 @@ 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 not clearnet_servers:
|
||||||
|
default_clearnet = DEFAULT_ELECTRUM_SERVERS.get(name, [])
|
||||||
|
clearnet_servers = [
|
||||||
|
f"{s['host']}:{s['port']}:{str(s.get('ssl', True)).lower()}"
|
||||||
|
for s in default_clearnet
|
||||||
|
]
|
||||||
|
if not onion_servers:
|
||||||
|
default_onion = DEFAULT_ONION_SERVERS.get(name, [])
|
||||||
|
onion_servers = [
|
||||||
|
f"{s['host']}:{s['port']}:{str(s.get('ssl', False)).lower()}"
|
||||||
|
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 +292,10 @@ 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,
|
||||||
|
"address_gap_limit": c.get("address_gap_limit", 50),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if name in ("monero", "wownero"):
|
if name in ("monero", "wownero"):
|
||||||
@@ -203,6 +323,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", "?")
|
||||||
@@ -87,10 +95,92 @@ def format_wallet_data(swap_client, ci, w):
|
|||||||
wf["spark_balance"] = w.get("spark_balance", "?")
|
wf["spark_balance"] = w.get("spark_balance", "?")
|
||||||
wf["spark_pending"] = w.get("spark_pending", "?")
|
wf["spark_pending"] = w.get("spark_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"
|
||||||
|
try:
|
||||||
|
sync_status = backend.getSyncStatus()
|
||||||
|
wf["electrum_synced"] = sync_status.get("synced", False)
|
||||||
|
wf["electrum_height"] = sync_status.get("height", 0)
|
||||||
|
except Exception:
|
||||||
|
wf["electrum_synced"] = False
|
||||||
|
wf["electrum_height"] = 0
|
||||||
|
|
||||||
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
|
||||||
@@ -135,6 +225,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),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,8 +246,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)
|
||||||
@@ -176,6 +284,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
|
||||||
@@ -221,7 +345,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)
|
||||||
@@ -311,8 +442,12 @@ def page_wallet(self, url_split, post_string):
|
|||||||
if swap_client.debug is True:
|
if swap_client.debug is True:
|
||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
is_electrum_mode = (
|
||||||
|
swap_client.coin_clients.get(coin_id, {}).get("connection_type") == "electrum"
|
||||||
|
)
|
||||||
|
|
||||||
swap_client.updateWalletsInfo(
|
swap_client.updateWalletsInfo(
|
||||||
force_refresh, only_coin=coin_id, wait_for_complete=True
|
force_refresh, only_coin=coin_id, wait_for_complete=not is_electrum_mode
|
||||||
)
|
)
|
||||||
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
|
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
|
||||||
wallet_data = {}
|
wallet_data = {}
|
||||||
@@ -334,6 +469,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)
|
||||||
@@ -408,6 +546,30 @@ 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
|
||||||
|
legacy_funds_info = None
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
if coin_id in (Coins.BTC, Coins.LTC):
|
||||||
|
legacy_funds_info = swap_client.getElectrumLegacyFundsInfo(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,
|
||||||
@@ -419,5 +581,12 @@ 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,
|
||||||
|
"legacy_funds_info": legacy_funds_info,
|
||||||
|
"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))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
821
basicswap/wallet_backend.py
Normal file
821
basicswap/wallet_backend.py
Normal file
@@ -0,0 +1,821 @@
|
|||||||
|
# -*- 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._cached_fee = {}
|
||||||
|
self._cached_fee_time = {}
|
||||||
|
self._fee_cache_ttl = 300
|
||||||
|
|
||||||
|
self._max_batch_size = 5
|
||||||
|
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)
|
||||||
|
if hasattr(self._server, "call_user"):
|
||||||
|
return self._server.call_user(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)
|
||||||
|
if hasattr(self._server, "call_batch_user"):
|
||||||
|
return self._server.call_batch_user(calls, timeout)
|
||||||
|
return self._server.call_batch(calls, timeout)
|
||||||
|
|
||||||
|
def _is_server_stopping(self) -> bool:
|
||||||
|
return getattr(self._server, "_stopping", False)
|
||||||
|
|
||||||
|
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):
|
||||||
|
if self._is_server_stopping():
|
||||||
|
self._log.debug("_split_batch_call: server stopping, aborting")
|
||||||
|
break
|
||||||
|
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:
|
||||||
|
if self._is_server_stopping():
|
||||||
|
self._log.debug(
|
||||||
|
"_split_batch_call: server stopping after batch failure, aborting"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
for sh in chunk:
|
||||||
|
if self._is_server_stopping():
|
||||||
|
self._log.debug(
|
||||||
|
"_split_batch_call: server stopping during fallback, aborting"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
result = self._call(method, [sh])
|
||||||
|
all_results.append(result)
|
||||||
|
except Exception:
|
||||||
|
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 = self._max_batch_size
|
||||||
|
for batch_start in range(0, len(addr_list), batch_size):
|
||||||
|
if self._is_server_stopping():
|
||||||
|
break
|
||||||
|
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 self._is_server_stopping():
|
||||||
|
break
|
||||||
|
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():
|
||||||
|
if self._is_server_stopping():
|
||||||
|
break
|
||||||
|
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",
|
||||||
|
"non-bip68-final",
|
||||||
|
"non-final",
|
||||||
|
"locktime",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
now = time.time()
|
||||||
|
cache_key = blocks
|
||||||
|
if cache_key in self._cached_fee:
|
||||||
|
if (now - self._cached_fee_time.get(cache_key, 0)) < self._fee_cache_ttl:
|
||||||
|
return self._cached_fee[cache_key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
fee = self._call("blockchain.estimatefee", [blocks])
|
||||||
|
if fee and fee > 0:
|
||||||
|
result = int(fee * 1e8 / 1000)
|
||||||
|
self._cached_fee[cache_key] = result
|
||||||
|
self._cached_fee_time[cache_key] = now
|
||||||
|
return result
|
||||||
|
return self._cached_fee.get(cache_key, 1)
|
||||||
|
except Exception:
|
||||||
|
return self._cached_fee.get(cache_key, 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 recentlyReconnected(self, grace_seconds: int = 30) -> bool:
|
||||||
|
if hasattr(self._server, "recently_reconnected"):
|
||||||
|
return self._server.recently_reconnected(grace_seconds)
|
||||||
|
return False
|
||||||
|
|
||||||
|
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 getSyncStatus(self) -> dict:
|
||||||
|
import time
|
||||||
|
|
||||||
|
height = 0
|
||||||
|
height_time = 0
|
||||||
|
if hasattr(self._server, "get_subscribed_height"):
|
||||||
|
height = self._server.get_subscribed_height()
|
||||||
|
height_time = getattr(self._server, "_subscribed_height_time", 0)
|
||||||
|
|
||||||
|
if self._cached_height > 0:
|
||||||
|
if self._cached_height > height:
|
||||||
|
height = self._cached_height
|
||||||
|
if self._cached_height_time > height_time:
|
||||||
|
height_time = self._cached_height_time
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
stale_threshold = 300
|
||||||
|
is_synced = height > 0 and (now - height_time) < stale_threshold
|
||||||
|
return {
|
||||||
|
"height": height,
|
||||||
|
"synced": is_synced,
|
||||||
|
"last_update": height_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
def getServer(self):
|
||||||
|
return self._server
|
||||||
2034
basicswap/wallet_manager.py
Normal file
2034
basicswap/wallet_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2024 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
# Copyright (c) 2024-2025 The Basicswap developers
|
# Copyright (c) 2024-2026 The Basicswap developers
|
||||||
# Distributed under the MIT software license, see the accompanying
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import subprocess
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from .util import read_json_api
|
from .util import read_json_api
|
||||||
|
from basicswap.basicswap import Coins
|
||||||
from basicswap.rpc import callrpc
|
from basicswap.rpc import callrpc
|
||||||
from basicswap.util import toBool
|
from basicswap.util import toBool
|
||||||
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
|
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
|
||||||
@@ -125,6 +126,65 @@ def prepareDataDir(
|
|||||||
return node_dir
|
return node_dir
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_balance(
|
||||||
|
use_delay_event,
|
||||||
|
coin,
|
||||||
|
amount: float,
|
||||||
|
port_target_node: int,
|
||||||
|
port_take_from_node: int,
|
||||||
|
test_balance: bool = True,
|
||||||
|
) -> None:
|
||||||
|
if coin == Coins.PART_BLIND:
|
||||||
|
coin_ticker: str = "PART"
|
||||||
|
balance_type: str = "blind_balance"
|
||||||
|
address_type: str = "stealth_address"
|
||||||
|
type_to: str = "blind"
|
||||||
|
elif coin == Coins.PART_ANON:
|
||||||
|
coin_ticker: str = "PART"
|
||||||
|
balance_type: str = "anon_balance"
|
||||||
|
address_type: str = "stealth_address"
|
||||||
|
type_to: str = "anon"
|
||||||
|
else:
|
||||||
|
coin_ticker: str = coin.name
|
||||||
|
balance_type: str = "balance"
|
||||||
|
address_type: str = "deposit_address"
|
||||||
|
js_w = read_json_api(port_target_node, "wallets")
|
||||||
|
current_balance: float = float(js_w[coin_ticker][balance_type])
|
||||||
|
|
||||||
|
if test_balance and current_balance >= amount:
|
||||||
|
return
|
||||||
|
post_json = {
|
||||||
|
"value": amount,
|
||||||
|
"address": js_w[coin_ticker][address_type],
|
||||||
|
"subfee": False,
|
||||||
|
}
|
||||||
|
if coin in (Coins.XMR, Coins.WOW):
|
||||||
|
post_json["sweepall"] = False
|
||||||
|
if coin in (Coins.PART_BLIND, Coins.PART_ANON):
|
||||||
|
post_json["type_to"] = type_to
|
||||||
|
json_rv = read_json_api(
|
||||||
|
port_take_from_node,
|
||||||
|
"wallets/{}/withdraw".format(coin_ticker.lower()),
|
||||||
|
post_json,
|
||||||
|
)
|
||||||
|
assert len(json_rv["txid"]) == 64
|
||||||
|
wait_for_amount: float = amount
|
||||||
|
if not test_balance:
|
||||||
|
wait_for_amount += current_balance
|
||||||
|
delay_iterations = 100 if coin == Coins.NAV else 30
|
||||||
|
delay_time = 5 if coin == Coins.NAV else 3
|
||||||
|
wait_for_balance(
|
||||||
|
use_delay_event,
|
||||||
|
"http://127.0.0.1:{}/json/wallets/{}".format(
|
||||||
|
port_target_node, coin_ticker.lower()
|
||||||
|
),
|
||||||
|
balance_type,
|
||||||
|
wait_for_amount,
|
||||||
|
iterations=delay_iterations,
|
||||||
|
delay_time=delay_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def checkForks(ro):
|
def checkForks(ro):
|
||||||
try:
|
try:
|
||||||
if "bip9_softforks" in ro:
|
if "bip9_softforks" in ro:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2020-2024 tecnovert
|
# Copyright (c) 2020-2024 tecnovert
|
||||||
# Copyright (c) 2024-2025 The Basicswap developers
|
# Copyright (c) 2024-2026 The Basicswap developers
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -139,6 +139,7 @@ def run_prepare(
|
|||||||
use_rpcauth=False,
|
use_rpcauth=False,
|
||||||
extra_settings={},
|
extra_settings={},
|
||||||
port_ofs=0,
|
port_ofs=0,
|
||||||
|
extra_args=[],
|
||||||
):
|
):
|
||||||
config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME)
|
config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME)
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ def run_prepare(
|
|||||||
"-noextractover",
|
"-noextractover",
|
||||||
"-noreleasesizecheck",
|
"-noreleasesizecheck",
|
||||||
"-xmrrestoreheight=0",
|
"-xmrrestoreheight=0",
|
||||||
]
|
] + extra_args
|
||||||
if mnemonic_in:
|
if mnemonic_in:
|
||||||
testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
|
testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
|
||||||
|
|
||||||
@@ -645,6 +646,7 @@ class XmrTestBase(TestBase):
|
|||||||
prepare_nodes(3, "monero")
|
prepare_nodes(3, "monero")
|
||||||
|
|
||||||
def start_processes(self):
|
def start_processes(self):
|
||||||
|
multiprocessing.set_start_method("fork")
|
||||||
self.delay_event.clear()
|
self.delay_event.clear()
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
|||||||
@@ -1489,7 +1489,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
test_delay_event.wait(1)
|
test_delay_event.wait(1)
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(True)
|
addr_out = ci.getNewAddress(True)
|
||||||
|
|||||||
@@ -412,7 +412,11 @@ class Test(TestFunctions):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
test_delay_event.wait(1)
|
test_delay_event.wait(1)
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(False)
|
addr_out = ci.getNewAddress(False)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2021-2024 tecnovert
|
# Copyright (c) 2021-2024 tecnovert
|
||||||
# Copyright (c) 2024-2025 The Basicswap developers
|
# Copyright (c) 2024-2026 The Basicswap developers
|
||||||
# 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.
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ def updateThread(cls):
|
|||||||
calldogerpc(0, "generatetoaddress", [1, cls.doge_addr])
|
calldogerpc(0, "generatetoaddress", [1, cls.doge_addr])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("updateThread error", str(e))
|
print("updateThread error", str(e))
|
||||||
cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max))
|
cls.delay_event.wait(random.uniform(cls.update_min, cls.update_max))
|
||||||
|
|
||||||
|
|
||||||
def updateThreadXMR(cls):
|
def updateThreadXMR(cls):
|
||||||
@@ -228,7 +228,7 @@ def updateThreadXMR(cls):
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("updateThreadXMR error", str(e))
|
print("updateThreadXMR error", str(e))
|
||||||
cls.delay_event.wait(random.randrange(cls.xmr_update_min, cls.xmr_update_max))
|
cls.delay_event.wait(random.uniform(cls.xmr_update_min, cls.xmr_update_max))
|
||||||
|
|
||||||
|
|
||||||
def updateThreadDCR(cls):
|
def updateThreadDCR(cls):
|
||||||
@@ -262,7 +262,7 @@ def updateThreadDCR(cls):
|
|||||||
logging.warning("updateThreadDCR generate {}".format(e))
|
logging.warning("updateThreadDCR generate {}".format(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("updateThreadDCR error", str(e))
|
print("updateThreadDCR error", str(e))
|
||||||
cls.delay_event.wait(random.randrange(cls.dcr_update_min, cls.dcr_update_max))
|
cls.delay_event.wait(random.uniform(cls.dcr_update_min, cls.dcr_update_max))
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(self, sig, frame):
|
def signal_handler(self, sig, frame):
|
||||||
@@ -283,6 +283,7 @@ def run_process(client_id):
|
|||||||
|
|
||||||
|
|
||||||
def start_processes(self):
|
def start_processes(self):
|
||||||
|
multiprocessing.set_start_method("spawn")
|
||||||
self.delay_event.clear()
|
self.delay_event.clear()
|
||||||
|
|
||||||
for i in range(NUM_NODES):
|
for i in range(NUM_NODES):
|
||||||
@@ -299,7 +300,7 @@ def start_processes(self):
|
|||||||
|
|
||||||
wallets = read_json_api(UI_PORT + 1, "wallets")
|
wallets = read_json_api(UI_PORT + 1, "wallets")
|
||||||
|
|
||||||
if "monero" in TEST_COINS_LIST:
|
if "monero" in self.test_coins_list:
|
||||||
xmr_auth = None
|
xmr_auth = None
|
||||||
if os.getenv("XMR_RPC_USER", "") != "":
|
if os.getenv("XMR_RPC_USER", "") != "":
|
||||||
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
|
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
|
||||||
@@ -333,7 +334,7 @@ def start_processes(self):
|
|||||||
callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
|
callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
|
||||||
logging.info("BTC blocks: {}".format(callbtcrpc(0, "getblockcount")))
|
logging.info("BTC blocks: {}".format(callbtcrpc(0, "getblockcount")))
|
||||||
|
|
||||||
if "litecoin" in TEST_COINS_LIST:
|
if "litecoin" in self.test_coins_list:
|
||||||
self.ltc_addr = callltcrpc(
|
self.ltc_addr = callltcrpc(
|
||||||
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
|
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
|
||||||
)
|
)
|
||||||
@@ -364,7 +365,7 @@ def start_processes(self):
|
|||||||
wallet="wallet.dat",
|
wallet="wallet.dat",
|
||||||
)
|
)
|
||||||
|
|
||||||
if "decred" in TEST_COINS_LIST:
|
if "decred" in self.test_coins_list:
|
||||||
if RESET_TEST:
|
if RESET_TEST:
|
||||||
_ = calldcrrpc(0, "getnewaddress")
|
_ = calldcrrpc(0, "getnewaddress")
|
||||||
# assert (addr == self.dcr_addr)
|
# assert (addr == self.dcr_addr)
|
||||||
@@ -394,7 +395,7 @@ def start_processes(self):
|
|||||||
self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,))
|
self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,))
|
||||||
self.update_thread_dcr.start()
|
self.update_thread_dcr.start()
|
||||||
|
|
||||||
if "firo" in TEST_COINS_LIST:
|
if "firo" in self.test_coins_list:
|
||||||
self.firo_addr = callfirorpc(0, "getnewaddress", ["mining_addr"])
|
self.firo_addr = callfirorpc(0, "getnewaddress", ["mining_addr"])
|
||||||
num_blocks: int = 200
|
num_blocks: int = 200
|
||||||
have_blocks: int = callfirorpc(0, "getblockcount")
|
have_blocks: int = callfirorpc(0, "getblockcount")
|
||||||
@@ -410,7 +411,7 @@ def start_processes(self):
|
|||||||
[num_blocks - have_blocks, self.firo_addr],
|
[num_blocks - have_blocks, self.firo_addr],
|
||||||
)
|
)
|
||||||
|
|
||||||
if "bitcoincash" in TEST_COINS_LIST:
|
if "bitcoincash" in self.test_coins_list:
|
||||||
self.bch_addr = callbchrpc(
|
self.bch_addr = callbchrpc(
|
||||||
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
|
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
|
||||||
)
|
)
|
||||||
@@ -429,7 +430,7 @@ def start_processes(self):
|
|||||||
wallet="wallet.dat",
|
wallet="wallet.dat",
|
||||||
)
|
)
|
||||||
|
|
||||||
if "dogecoin" in TEST_COINS_LIST:
|
if "dogecoin" in self.test_coins_list:
|
||||||
self.doge_addr = calldogerpc(0, "getnewaddress", ["mining_addr"])
|
self.doge_addr = calldogerpc(0, "getnewaddress", ["mining_addr"])
|
||||||
num_blocks: int = 200
|
num_blocks: int = 200
|
||||||
have_blocks: int = calldogerpc(0, "getblockcount")
|
have_blocks: int = calldogerpc(0, "getblockcount")
|
||||||
@@ -443,7 +444,7 @@ def start_processes(self):
|
|||||||
0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr]
|
0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr]
|
||||||
)
|
)
|
||||||
|
|
||||||
if "namecoin" in TEST_COINS_LIST:
|
if "namecoin" in self.test_coins_list:
|
||||||
self.nmc_addr = callnmcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
|
self.nmc_addr = callnmcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
|
||||||
num_blocks: int = 500
|
num_blocks: int = 500
|
||||||
have_blocks: int = callnmcrpc(0, "getblockcount")
|
have_blocks: int = callnmcrpc(0, "getblockcount")
|
||||||
@@ -536,7 +537,22 @@ class BaseTestWithPrepare(unittest.TestCase):
|
|||||||
firo_addr = None
|
firo_addr = None
|
||||||
bch_addr = None
|
bch_addr = None
|
||||||
doge_addr = None
|
doge_addr = None
|
||||||
initialised = False
|
test_coins_list = TEST_COINS_LIST
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def modifyConfig(cls, test_path, i):
|
||||||
|
modifyConfig(test_path, i)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setupNodes(cls):
|
||||||
|
logging.info(f"Preparing {NUM_NODES} nodes.")
|
||||||
|
prepare_nodes(
|
||||||
|
NUM_NODES,
|
||||||
|
cls.test_coins_list,
|
||||||
|
True,
|
||||||
|
{"min_sequence_lock_seconds": 60},
|
||||||
|
PORT_OFS,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@@ -547,22 +563,18 @@ class BaseTestWithPrepare(unittest.TestCase):
|
|||||||
if os.path.exists(test_path) and not RESET_TEST:
|
if os.path.exists(test_path) and not RESET_TEST:
|
||||||
logging.info(f"Continuing with existing directory: {test_path}")
|
logging.info(f"Continuing with existing directory: {test_path}")
|
||||||
else:
|
else:
|
||||||
logging.info(f"Preparing {NUM_NODES} nodes.")
|
cls.setupNodes()
|
||||||
prepare_nodes(
|
|
||||||
NUM_NODES,
|
|
||||||
TEST_COINS_LIST,
|
|
||||||
True,
|
|
||||||
{"min_sequence_lock_seconds": 60},
|
|
||||||
PORT_OFS,
|
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(NUM_NODES):
|
for i in range(NUM_NODES):
|
||||||
modifyConfig(test_path, i)
|
cls.modifyConfig(test_path, i)
|
||||||
|
|
||||||
signal.signal(
|
signal.signal(
|
||||||
signal.SIGINT, lambda signal, frame: signal_handler(cls, signal, frame)
|
signal.SIGINT, lambda signal, frame: signal_handler(cls, signal, frame)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
start_processes(cls)
|
||||||
|
waitForServer(cls.delay_event, UI_PORT + 0)
|
||||||
|
waitForServer(cls.delay_event, UI_PORT + 1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
logging.info("Stopping test")
|
logging.info("Stopping test")
|
||||||
@@ -582,14 +594,6 @@ class BaseTestWithPrepare(unittest.TestCase):
|
|||||||
cls.update_thread_dcr = None
|
cls.update_thread_dcr = None
|
||||||
cls.processes = []
|
cls.processes = []
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
if self.initialised:
|
|
||||||
return
|
|
||||||
start_processes(self)
|
|
||||||
waitForServer(self.delay_event, UI_PORT + 0)
|
|
||||||
waitForServer(self.delay_event, UI_PORT + 1)
|
|
||||||
self.initialised = True
|
|
||||||
|
|
||||||
|
|
||||||
class Test(BaseTestWithPrepare):
|
class Test(BaseTestWithPrepare):
|
||||||
def test_persistent(self):
|
def test_persistent(self):
|
||||||
|
|||||||
@@ -1579,7 +1579,11 @@ class BasicSwapTest(TestFunctions):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, self.test_fee_rate)
|
result = ci.publishBLockTx(v, S, amount, self.test_fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(True)
|
addr_out = ci.getNewAddress(True)
|
||||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||||
|
|||||||
859
tests/basicswap/test_electrum.py
Normal file
859
tests/basicswap/test_electrum.py
Normal file
@@ -0,0 +1,859 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2026 The Basicswap developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensure Electrumx is installed to a venv in ELECTRUMX_SRC_DIR/venv
|
||||||
|
# Example setup with default paths:
|
||||||
|
|
||||||
|
The leveldb system package may be required to install plyvel:
|
||||||
|
sudo pacman -S leveldb
|
||||||
|
|
||||||
|
cd ~/tmp/
|
||||||
|
git clone git@github.com:spesmilo/electrumx.git
|
||||||
|
cd electrumx
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install ".[ujson]"
|
||||||
|
|
||||||
|
# Run test
|
||||||
|
export TEST_PATH=/tmp/test_electrum
|
||||||
|
mkdir -p ${TEST_PATH}/bin
|
||||||
|
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
|
||||||
|
export ELECTRUMX_SRC_DIR="~/tmp/electrumx"
|
||||||
|
export EXTRA_CONFIG_JSON=$(cat <<EOF | jq -r @json
|
||||||
|
{
|
||||||
|
"btc0":["txindex=1","rpcworkqueue=1100"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
export TEST_COINS_LIST="bitcoin,monero"
|
||||||
|
export PYTHONPATH=$(pwd)
|
||||||
|
pytest -v -s --log-cli-level=DEBUG tests/basicswap/test_electrum.py
|
||||||
|
|
||||||
|
# Run select test
|
||||||
|
pytest -v -s --log-cli-level=DEBUG tests/basicswap/test_electrum.py::Test::test_01_b_full_swap_xmr
|
||||||
|
|
||||||
|
# Optionally copy coin releases to permanent storage for faster subsequent startups
|
||||||
|
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import basicswap.config as cfg
|
||||||
|
from basicswap.basicswap_util import (
|
||||||
|
BidStates,
|
||||||
|
DebugTypes,
|
||||||
|
TxLockTypes,
|
||||||
|
strBidState,
|
||||||
|
)
|
||||||
|
from basicswap.chainparams import (
|
||||||
|
Coins,
|
||||||
|
chainparams,
|
||||||
|
getCoinIdFromName,
|
||||||
|
)
|
||||||
|
from basicswap.util.daemon import Daemon
|
||||||
|
|
||||||
|
from tests.basicswap.common import (
|
||||||
|
prepare_balance,
|
||||||
|
stopDaemons,
|
||||||
|
)
|
||||||
|
from tests.basicswap.common_xmr import run_prepare, TEST_PATH
|
||||||
|
from tests.basicswap.extended.test_xmr_persistent import (
|
||||||
|
BaseTestWithPrepare,
|
||||||
|
NUM_NODES,
|
||||||
|
PORT_OFS,
|
||||||
|
RESET_TEST,
|
||||||
|
)
|
||||||
|
from tests.basicswap.mnemonics import mnemonics
|
||||||
|
from tests.basicswap.util import (
|
||||||
|
read_json_api,
|
||||||
|
post_json_api,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.level = logging.DEBUG
|
||||||
|
if not len(logger.handlers):
|
||||||
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
|
|
||||||
|
|
||||||
|
def modify_config(test_path, i):
|
||||||
|
config_path = os.path.join(test_path, f"client{i}", cfg.CONFIG_FILENAME)
|
||||||
|
with open(config_path) as fp:
|
||||||
|
settings = json.load(fp)
|
||||||
|
|
||||||
|
if i == 1:
|
||||||
|
settings["debug_ui"] = True
|
||||||
|
settings.update(
|
||||||
|
{
|
||||||
|
"fetchpricesthread": False,
|
||||||
|
"check_progress_seconds": 2,
|
||||||
|
"check_watched_seconds": 3,
|
||||||
|
"check_expired_seconds": 60,
|
||||||
|
"check_events_seconds": 1,
|
||||||
|
"check_xmr_swaps_seconds": 1,
|
||||||
|
"min_delay_event": 1,
|
||||||
|
"max_delay_event": 4,
|
||||||
|
"min_delay_event_short": 1,
|
||||||
|
"max_delay_event_short": 3,
|
||||||
|
"min_delay_retry": 2,
|
||||||
|
"max_delay_retry": 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with open(config_path, "w") as fp:
|
||||||
|
json.dump(settings, fp, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_bid_state(
|
||||||
|
delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30
|
||||||
|
) -> None:
|
||||||
|
logger.info(f"TEST: wait_for_bid {bid_id}, node {node_port}, state {state}")
|
||||||
|
|
||||||
|
pass_state_strs = []
|
||||||
|
if isinstance(state, (list, tuple)):
|
||||||
|
for s in state:
|
||||||
|
pass_state_strs.append(strBidState(s))
|
||||||
|
elif state is not None:
|
||||||
|
pass_state_strs.append(strBidState(state))
|
||||||
|
|
||||||
|
for i in range(wait_for):
|
||||||
|
if delay_event.is_set():
|
||||||
|
raise ValueError("Test stopped.")
|
||||||
|
delay_event.wait(1)
|
||||||
|
try:
|
||||||
|
rv = read_json_api(node_port, f"bids/{bid_id}")
|
||||||
|
if rv["bid_state"] in pass_state_strs or state is None:
|
||||||
|
return
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
pass
|
||||||
|
# logger.debug(f"TEST: wait_for_bid {bid_id}, error {e}")
|
||||||
|
raise ValueError(f"wait_for_bid timed out {bid_id}.")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_offer(
|
||||||
|
delay_event, node_port: int, offer_id: str, state=None, wait_for: int = 30
|
||||||
|
) -> None:
|
||||||
|
logger.info(f"TEST: wait_for_offer {offer_id}, node {node_port}, state {state}")
|
||||||
|
|
||||||
|
for i in range(wait_for):
|
||||||
|
if delay_event.is_set():
|
||||||
|
raise ValueError("Test stopped.")
|
||||||
|
delay_event.wait(1)
|
||||||
|
try:
|
||||||
|
rv = read_json_api(node_port, f"offers/{offer_id}")
|
||||||
|
if any(offer["offer_id"] == offer_id for offer in rv):
|
||||||
|
return
|
||||||
|
except Exception as e: # noqa: F841
|
||||||
|
pass
|
||||||
|
# logger.debug(f"TEST: wait_for_offer {offer_id}, error {e}")
|
||||||
|
raise ValueError(f"wait_for_offer timed out {offer_id}.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestFunctions(BaseTestWithPrepare):
|
||||||
|
__test__ = False
|
||||||
|
|
||||||
|
port_node_0 = 12701
|
||||||
|
port_node_1 = 12702
|
||||||
|
|
||||||
|
def do_test_01_full_swap(
|
||||||
|
self,
|
||||||
|
coin_from: Coins,
|
||||||
|
coin_to: Coins,
|
||||||
|
port_node_from: int = port_node_0,
|
||||||
|
port_node_to: int = port_node_1,
|
||||||
|
) -> None:
|
||||||
|
logger.info(
|
||||||
|
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to})"
|
||||||
|
)
|
||||||
|
|
||||||
|
ticker_from: str = chainparams[coin_from]["ticker"]
|
||||||
|
ticker_to: str = chainparams[coin_to]["ticker"]
|
||||||
|
|
||||||
|
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
data = {
|
||||||
|
"addr_from": "-1",
|
||||||
|
"coin_from": ticker_from,
|
||||||
|
"coin_to": ticker_to,
|
||||||
|
"amt_from": amt_from_str,
|
||||||
|
"amt_to": amt_to_str,
|
||||||
|
"lockhrs": "24",
|
||||||
|
"swap_type": "adaptor_sig",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
|
||||||
|
)
|
||||||
|
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
|
||||||
|
wait_for_offer(self.delay_event, port_node_to, offer_id)
|
||||||
|
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
|
||||||
|
assert offer["offer_id"] == offer_id
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
"amount_from": offer["amount_from"],
|
||||||
|
"validmins": 60,
|
||||||
|
}
|
||||||
|
rv = post_json_api(port_node_to, "bids/new", data)
|
||||||
|
bid_id: str = rv["bid_id"]
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
|
||||||
|
)
|
||||||
|
|
||||||
|
rv = post_json_api(port_node_from, f"bids/{bid_id}", {"accept": True})
|
||||||
|
assert rv["bid_state"] in ("Accepted", "Request accepted")
|
||||||
|
|
||||||
|
logger.info("Completing swap")
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_from, bid_id, BidStates.SWAP_COMPLETED, 240
|
||||||
|
)
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_to, bid_id, BidStates.SWAP_COMPLETED, 240
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_test_02_leader_recover_a_lock_tx(
|
||||||
|
self,
|
||||||
|
coin_from: Coins,
|
||||||
|
coin_to: Coins,
|
||||||
|
port_node_from: int = port_node_0,
|
||||||
|
port_node_to: int = port_node_1,
|
||||||
|
lock_value: int = 12,
|
||||||
|
) -> None:
|
||||||
|
logger.info(
|
||||||
|
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to}) leader recovers coin a lock tx"
|
||||||
|
)
|
||||||
|
|
||||||
|
ticker_from: str = chainparams[coin_from]["ticker"]
|
||||||
|
ticker_to: str = chainparams[coin_to]["ticker"]
|
||||||
|
|
||||||
|
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
|
||||||
|
port_offerer: int = port_node_from
|
||||||
|
port_bidder: int = port_node_to
|
||||||
|
port_leader: int = port_bidder if reverse_bid else port_offerer
|
||||||
|
port_follower: int = port_offerer if reverse_bid else port_bidder
|
||||||
|
logger.info(
|
||||||
|
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
|
||||||
|
)
|
||||||
|
|
||||||
|
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
data = {
|
||||||
|
"addr_from": "-1",
|
||||||
|
"coin_from": ticker_from,
|
||||||
|
"coin_to": ticker_to,
|
||||||
|
"amt_from": amt_from_str,
|
||||||
|
"amt_to": amt_to_str,
|
||||||
|
"swap_type": "adaptor_sig",
|
||||||
|
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
|
||||||
|
"lock_blocks": str(lock_value),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
|
||||||
|
)
|
||||||
|
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
|
||||||
|
wait_for_offer(self.delay_event, port_node_to, offer_id)
|
||||||
|
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
|
||||||
|
assert offer["offer_id"] == offer_id
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
"amount_from": offer["amount_from"],
|
||||||
|
"validmins": 60,
|
||||||
|
}
|
||||||
|
rv = post_json_api(port_node_to, "bids/new", data)
|
||||||
|
bid_id: str = rv["bid_id"]
|
||||||
|
|
||||||
|
wait_for_bid_state(self.delay_event, port_follower, bid_id)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"debugind": DebugTypes.BID_STOP_AFTER_COIN_A_LOCK},
|
||||||
|
)
|
||||||
|
assert "bid_state" in rv # Test that the return didn't fail
|
||||||
|
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
|
||||||
|
)
|
||||||
|
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
|
||||||
|
assert rv["bid_state"] in ("Accepted", "Request accepted")
|
||||||
|
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_leader,
|
||||||
|
bid_id,
|
||||||
|
BidStates.XMR_SWAP_FAILED_REFUNDED,
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_follower,
|
||||||
|
bid_id,
|
||||||
|
[BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED],
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_test_03_follower_recover_a_lock_tx(
|
||||||
|
self,
|
||||||
|
coin_from: Coins,
|
||||||
|
coin_to: Coins,
|
||||||
|
port_node_from: int = port_node_0,
|
||||||
|
port_node_to: int = port_node_1,
|
||||||
|
lock_value: int = 12,
|
||||||
|
with_mercy: bool = True,
|
||||||
|
) -> None:
|
||||||
|
logger.info(
|
||||||
|
"---------- Test {} ({}) to {} ({}) follower recovers coin a lock tx{}".format(
|
||||||
|
coin_from.name,
|
||||||
|
port_node_from,
|
||||||
|
coin_to.name,
|
||||||
|
port_node_to,
|
||||||
|
" (with mercy tx)" if with_mercy else "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Leader is too slow to recover the coin a lock tx and follower swipes it
|
||||||
|
# Coin B lock tx remains unspent unless a mercy output revealing the follower's keyshare is sent
|
||||||
|
|
||||||
|
ticker_from: str = chainparams[coin_from]["ticker"]
|
||||||
|
ticker_to: str = chainparams[coin_to]["ticker"]
|
||||||
|
|
||||||
|
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
|
||||||
|
port_offerer: int = port_node_from
|
||||||
|
port_bidder: int = port_node_to
|
||||||
|
port_leader: int = port_bidder if reverse_bid else port_offerer
|
||||||
|
port_follower: int = port_offerer if reverse_bid else port_bidder
|
||||||
|
logger.info(
|
||||||
|
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
|
||||||
|
)
|
||||||
|
|
||||||
|
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
data = {
|
||||||
|
"addr_from": "-1",
|
||||||
|
"coin_from": ticker_from,
|
||||||
|
"coin_to": ticker_to,
|
||||||
|
"amt_from": amt_from_str,
|
||||||
|
"amt_to": amt_to_str,
|
||||||
|
"swap_type": "adaptor_sig",
|
||||||
|
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
|
||||||
|
"lock_blocks": str(lock_value),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
|
||||||
|
)
|
||||||
|
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
|
||||||
|
wait_for_offer(self.delay_event, port_node_to, offer_id)
|
||||||
|
|
||||||
|
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
|
||||||
|
assert offer["offer_id"] == offer_id
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
"amount_from": offer["amount_from"],
|
||||||
|
"validmins": 60,
|
||||||
|
}
|
||||||
|
rv = post_json_api(port_node_to, "bids/new", data)
|
||||||
|
bid_id: str = rv["bid_id"]
|
||||||
|
|
||||||
|
wait_for_bid_state(self.delay_event, port_leader, bid_id)
|
||||||
|
wait_for_bid_state(self.delay_event, port_follower, bid_id)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_leader,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"debugind": DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2},
|
||||||
|
)
|
||||||
|
assert "bid_state" in rv # Test that the return didn't fail
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"debugind": DebugTypes.BID_DONT_SPEND_COIN_B_LOCK},
|
||||||
|
)
|
||||||
|
assert "bid_state" in rv
|
||||||
|
for node_port in (port_leader, port_follower):
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{
|
||||||
|
"debugind": DebugTypes.BID_DONT_SPEND_COIN_B_LOCK,
|
||||||
|
"maindebugind": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "bid_state" in rv
|
||||||
|
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
|
||||||
|
)
|
||||||
|
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
|
||||||
|
assert rv["bid_state"] in ("Accepted", "Request accepted")
|
||||||
|
|
||||||
|
expect_state = (
|
||||||
|
(BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED, BidStates.SWAP_COMPLETED)
|
||||||
|
if with_mercy
|
||||||
|
else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED)
|
||||||
|
)
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_leader,
|
||||||
|
bid_id,
|
||||||
|
expect_state,
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_follower,
|
||||||
|
bid_id,
|
||||||
|
[BidStates.XMR_SWAP_FAILED_SWIPED],
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_leader,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"show_extra": True, "with_events": True},
|
||||||
|
)
|
||||||
|
events = rv["events"]
|
||||||
|
logger.info(f"Initiator events: {events}")
|
||||||
|
if with_mercy:
|
||||||
|
assert any(
|
||||||
|
event["desc"] == "Lock tx B spend tx published" for event in events
|
||||||
|
)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"show_extra": True, "with_events": True},
|
||||||
|
)
|
||||||
|
events = rv["events"]
|
||||||
|
logger.info(f"Participant events: {events}")
|
||||||
|
assert any(
|
||||||
|
event["desc"] == "Lock tx A refund swipe tx published" for event in events
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_test_04_follower_recover_b_lock_tx(
|
||||||
|
self,
|
||||||
|
coin_from: Coins,
|
||||||
|
coin_to: Coins,
|
||||||
|
port_node_from: int = port_node_0,
|
||||||
|
port_node_to: int = port_node_1,
|
||||||
|
lock_value: int = 16,
|
||||||
|
) -> None:
|
||||||
|
logger.info(
|
||||||
|
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to}) follower recovers coin b lock tx"
|
||||||
|
)
|
||||||
|
|
||||||
|
ticker_from: str = chainparams[coin_from]["ticker"]
|
||||||
|
ticker_to: str = chainparams[coin_to]["ticker"]
|
||||||
|
|
||||||
|
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
|
||||||
|
port_offerer: int = port_node_from
|
||||||
|
port_bidder: int = port_node_to
|
||||||
|
port_leader: int = port_bidder if reverse_bid else port_offerer
|
||||||
|
port_follower: int = port_offerer if reverse_bid else port_bidder
|
||||||
|
logger.info(
|
||||||
|
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
|
||||||
|
)
|
||||||
|
|
||||||
|
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
|
||||||
|
data = {
|
||||||
|
"addr_from": "-1",
|
||||||
|
"coin_from": ticker_from,
|
||||||
|
"coin_to": ticker_to,
|
||||||
|
"amt_from": amt_from_str,
|
||||||
|
"amt_to": amt_to_str,
|
||||||
|
"swap_type": "adaptor_sig",
|
||||||
|
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
|
||||||
|
"lock_blocks": str(lock_value),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
|
||||||
|
)
|
||||||
|
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
|
||||||
|
wait_for_offer(self.delay_event, port_node_to, offer_id)
|
||||||
|
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
|
||||||
|
assert offer["offer_id"] == offer_id
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
"amount_from": offer["amount_from"],
|
||||||
|
"validmins": 60,
|
||||||
|
}
|
||||||
|
rv = post_json_api(port_node_to, "bids/new", data)
|
||||||
|
bid_id: str = rv["bid_id"]
|
||||||
|
|
||||||
|
wait_for_bid_state(self.delay_event, port_follower, bid_id)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"debugind": DebugTypes.CREATE_INVALID_COIN_B_LOCK},
|
||||||
|
)
|
||||||
|
assert "bid_state" in rv
|
||||||
|
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
|
||||||
|
)
|
||||||
|
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
|
||||||
|
assert rv["bid_state"] in ("Accepted", "Request accepted")
|
||||||
|
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_leader,
|
||||||
|
bid_id,
|
||||||
|
BidStates.XMR_SWAP_FAILED_REFUNDED,
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
wait_for_bid_state(
|
||||||
|
self.delay_event,
|
||||||
|
port_follower,
|
||||||
|
bid_id,
|
||||||
|
BidStates.XMR_SWAP_FAILED_REFUNDED,
|
||||||
|
240,
|
||||||
|
)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_leader,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"show_extra": True, "with_events": True},
|
||||||
|
)
|
||||||
|
events = rv["events"]
|
||||||
|
logger.info(f"Initiator events: {events}")
|
||||||
|
assert any(event["desc"] == "Detected invalid lock Tx B" for event in events)
|
||||||
|
assert any(
|
||||||
|
event["desc"] == "Lock tx A refund spend tx published" for event in events
|
||||||
|
)
|
||||||
|
rv = post_json_api(
|
||||||
|
port_follower,
|
||||||
|
f"bids/{bid_id}",
|
||||||
|
{"show_extra": True, "with_events": True},
|
||||||
|
)
|
||||||
|
events = rv["events"]
|
||||||
|
logger.info(f"Participant events: {events}")
|
||||||
|
assert any(event["desc"] == "Lock tx B refund tx published" for event in events)
|
||||||
|
|
||||||
|
|
||||||
|
class Test(TestFunctions):
|
||||||
|
__test__ = True
|
||||||
|
update_min = 1.7
|
||||||
|
daemons = []
|
||||||
|
|
||||||
|
test_coin_a = Coins.PART
|
||||||
|
test_coin_b = Coins.BTC
|
||||||
|
test_coin_xmr = Coins.XMR
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def addElectrumxDaemon(cls, coin_name: str, node_rpc_port: int, services_port: int):
|
||||||
|
coin_type: Coins = getCoinIdFromName(coin_name)
|
||||||
|
ticker: str = chainparams[coin_type]["ticker"]
|
||||||
|
ticker_lc: str = ticker.lower()
|
||||||
|
|
||||||
|
logger.info(f"Starting Electrumx for {ticker}")
|
||||||
|
ELECTRUMX_SRC_DIR = os.path.expanduser(os.getenv("ELECTRUMX_SRC_DIR"))
|
||||||
|
if ELECTRUMX_SRC_DIR is None:
|
||||||
|
raise ValueError("Please set ELECTRUMX_SRC_DIR")
|
||||||
|
ELECTRUMX_VENV = os.getenv(
|
||||||
|
"ELECTRUMX_VENV", os.path.join(ELECTRUMX_SRC_DIR, "venv")
|
||||||
|
)
|
||||||
|
ELECTRUMX_DATADIR = os.getenv(
|
||||||
|
f"ELECTRUMX_DATADIR_{ticker}", f"/tmp/electrumx_{ticker_lc}"
|
||||||
|
)
|
||||||
|
SSL_CERTFILE = f"{ELECTRUMX_DATADIR}/certfile.crt"
|
||||||
|
SSL_KEYFILE = f"{ELECTRUMX_DATADIR}/keyfile.key"
|
||||||
|
|
||||||
|
if os.path.isdir(ELECTRUMX_DATADIR):
|
||||||
|
if RESET_TEST:
|
||||||
|
logger.info("Removing " + ELECTRUMX_DATADIR)
|
||||||
|
shutil.rmtree(ELECTRUMX_DATADIR)
|
||||||
|
if not os.path.exists(ELECTRUMX_DATADIR):
|
||||||
|
os.makedirs(os.path.join(ELECTRUMX_DATADIR, "db"))
|
||||||
|
with open(os.path.join(ELECTRUMX_DATADIR, "banner"), "w") as fp:
|
||||||
|
fp.write("TEST BANNER")
|
||||||
|
try:
|
||||||
|
stdout = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"openssl",
|
||||||
|
"req",
|
||||||
|
"-nodes",
|
||||||
|
"-new",
|
||||||
|
"-x509",
|
||||||
|
"-keyout",
|
||||||
|
SSL_KEYFILE,
|
||||||
|
"-out",
|
||||||
|
SSL_CERTFILE,
|
||||||
|
"-subj",
|
||||||
|
'/C=CA/ST=Quebec/L=Montreal/O="Poutine LLC"/OU=devops/CN=*.poutine.net\n',
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
logger.info(f"openssl {stdout}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.info(f"Error openssl {e.output}")
|
||||||
|
|
||||||
|
electrumx_env = {
|
||||||
|
"COIN": coin_name.capitalize(),
|
||||||
|
"NET": "regtest",
|
||||||
|
"LOG_LEVEL": "debug",
|
||||||
|
"SERVICES": f"tcp://:{services_port},ssl://:{services_port + 1},rpc://",
|
||||||
|
"CACHE_MB": "400",
|
||||||
|
"DAEMON_URL": f"http://test_{ticker_lc}_0:test_{ticker_lc}_pwd_0@127.0.0.1:{node_rpc_port}",
|
||||||
|
"DB_DIRECTORY": f"{ELECTRUMX_DATADIR}/db",
|
||||||
|
"SSL_CERTFILE": f"{ELECTRUMX_DATADIR}/certfile.crt",
|
||||||
|
"SSL_KEYFILE": f"{ELECTRUMX_DATADIR}/keyfile.key",
|
||||||
|
"BANNER_FILE": f"{ELECTRUMX_DATADIR}/banner",
|
||||||
|
"DAEMON_POLL_INTERVAL_BLOCKS": "1000",
|
||||||
|
"DAEMON_POLL_INTERVAL_MEMPOOL": "1000",
|
||||||
|
}
|
||||||
|
opened_files = []
|
||||||
|
stdout_dest = open(f"{ELECTRUMX_DATADIR}/electrumx.log", "w")
|
||||||
|
stderr_dest = stdout_dest
|
||||||
|
cls.daemons.append(
|
||||||
|
Daemon(
|
||||||
|
subprocess.Popen(
|
||||||
|
[
|
||||||
|
os.path.join(ELECTRUMX_VENV, "bin", "python"),
|
||||||
|
os.path.join(ELECTRUMX_SRC_DIR, "electrumx_server"),
|
||||||
|
],
|
||||||
|
shell=False,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=stdout_dest,
|
||||||
|
stderr=stderr_dest,
|
||||||
|
cwd=ELECTRUMX_SRC_DIR,
|
||||||
|
env=electrumx_env,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
opened_files,
|
||||||
|
],
|
||||||
|
f"electrumx_{ticker_lc}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.addElectrumxDaemon("bitcoin", 32793, 50001)
|
||||||
|
super(Test, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def modifyConfig(cls, test_path, i):
|
||||||
|
modify_config(test_path, i)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setupNodes(cls):
|
||||||
|
logger.info(f"Preparing {NUM_NODES} nodes.")
|
||||||
|
|
||||||
|
bins_path = os.path.join(TEST_PATH, "bin")
|
||||||
|
for i in range(NUM_NODES):
|
||||||
|
logger.info(f"Preparing node: {i}.")
|
||||||
|
client_path = os.path.join(TEST_PATH, f"client{i}")
|
||||||
|
try:
|
||||||
|
shutil.rmtree(client_path)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(f"setupNodes {ex}")
|
||||||
|
|
||||||
|
extra_args = []
|
||||||
|
if i == 1:
|
||||||
|
extra_args = [
|
||||||
|
"--btc-mode=electrum",
|
||||||
|
"--btc-electrum-server=127.0.0.1:50001",
|
||||||
|
]
|
||||||
|
run_prepare(
|
||||||
|
i,
|
||||||
|
client_path,
|
||||||
|
bins_path,
|
||||||
|
cls.test_coins_list,
|
||||||
|
mnemonics[i] if i < len(mnemonics) else None,
|
||||||
|
num_nodes=NUM_NODES,
|
||||||
|
use_rpcauth=True,
|
||||||
|
extra_settings={"min_sequence_lock_seconds": 10},
|
||||||
|
port_ofs=PORT_OFS,
|
||||||
|
extra_args=extra_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
logger.info("Finalising Test")
|
||||||
|
super().tearDownClass()
|
||||||
|
stopDaemons(cls.daemons)
|
||||||
|
|
||||||
|
def test_01_a_full_swap_xmr(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
1000,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_01_full_swap(self.test_coin_a, self.test_coin_b)
|
||||||
|
|
||||||
|
def test_01_b_full_swap_xmr(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_01_full_swap(self.test_coin_b, self.test_coin_xmr)
|
||||||
|
|
||||||
|
def test_01_c_full_swap_xmr_reverse(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
1000,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_01_full_swap(
|
||||||
|
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_02_a_leader_recover_a_lock_tx(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_02_leader_recover_a_lock_tx(
|
||||||
|
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_02_b_leader_recover_a_lock_tx_reverse(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_02_leader_recover_a_lock_tx(
|
||||||
|
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_03_a_follower_recover_a_lock_tx(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_03_follower_recover_a_lock_tx(
|
||||||
|
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_03_b_follower_recover_a_lock_tx_reverse(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_03_follower_recover_a_lock_tx(
|
||||||
|
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_04_a_follower_recover_b_lock_tx(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_04_follower_recover_b_lock_tx(
|
||||||
|
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_04_b_follower_recover_b_lock_tx_reverse(self):
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_b,
|
||||||
|
100,
|
||||||
|
self.port_node_1,
|
||||||
|
self.port_node_0,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
prepare_balance(
|
||||||
|
self.delay_event,
|
||||||
|
self.test_coin_xmr,
|
||||||
|
100,
|
||||||
|
self.port_node_0,
|
||||||
|
self.port_node_1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.do_test_04_follower_recover_b_lock_tx(
|
||||||
|
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from tests.basicswap.util import (
|
|||||||
from tests.basicswap.common import (
|
from tests.basicswap.common import (
|
||||||
callrpc_cli,
|
callrpc_cli,
|
||||||
prepareDataDir,
|
prepareDataDir,
|
||||||
|
prepare_balance,
|
||||||
make_rpc_func,
|
make_rpc_func,
|
||||||
checkForks,
|
checkForks,
|
||||||
stopDaemons,
|
stopDaemons,
|
||||||
@@ -1055,54 +1056,13 @@ class BaseTest(unittest.TestCase):
|
|||||||
port_take_from_node: int,
|
port_take_from_node: int,
|
||||||
test_balance: bool = True,
|
test_balance: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
delay_iterations = 100 if coin == Coins.NAV else 20
|
prepare_balance(
|
||||||
delay_time = 5 if coin == Coins.NAV else 3
|
|
||||||
if coin == Coins.PART_BLIND:
|
|
||||||
coin_ticker: str = "PART"
|
|
||||||
balance_type: str = "blind_balance"
|
|
||||||
address_type: str = "stealth_address"
|
|
||||||
type_to: str = "blind"
|
|
||||||
elif coin == Coins.PART_ANON:
|
|
||||||
coin_ticker: str = "PART"
|
|
||||||
balance_type: str = "anon_balance"
|
|
||||||
address_type: str = "stealth_address"
|
|
||||||
type_to: str = "anon"
|
|
||||||
else:
|
|
||||||
coin_ticker: str = coin.name
|
|
||||||
balance_type: str = "balance"
|
|
||||||
address_type: str = "deposit_address"
|
|
||||||
js_w = read_json_api(port_target_node, "wallets")
|
|
||||||
current_balance: float = float(js_w[coin_ticker][balance_type])
|
|
||||||
|
|
||||||
if test_balance and current_balance >= amount:
|
|
||||||
return
|
|
||||||
post_json = {
|
|
||||||
"value": amount,
|
|
||||||
"address": js_w[coin_ticker][address_type],
|
|
||||||
"subfee": False,
|
|
||||||
}
|
|
||||||
if coin in (Coins.XMR, Coins.WOW):
|
|
||||||
post_json["sweepall"] = False
|
|
||||||
if coin in (Coins.PART_BLIND, Coins.PART_ANON):
|
|
||||||
post_json["type_to"] = type_to
|
|
||||||
json_rv = read_json_api(
|
|
||||||
port_take_from_node,
|
|
||||||
"wallets/{}/withdraw".format(coin_ticker.lower()),
|
|
||||||
post_json,
|
|
||||||
)
|
|
||||||
assert len(json_rv["txid"]) == 64
|
|
||||||
wait_for_amount: float = amount
|
|
||||||
if not test_balance:
|
|
||||||
wait_for_amount += current_balance
|
|
||||||
wait_for_balance(
|
|
||||||
test_delay_event,
|
test_delay_event,
|
||||||
"http://127.0.0.1:{}/json/wallets/{}".format(
|
coin,
|
||||||
port_target_node, coin_ticker.lower()
|
amount,
|
||||||
),
|
port_target_node,
|
||||||
balance_type,
|
port_take_from_node,
|
||||||
wait_for_amount,
|
test_balance,
|
||||||
iterations=delay_iterations,
|
|
||||||
delay_time=delay_time,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1258,7 +1218,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(True)
|
addr_out = ci.getNewAddress(True)
|
||||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||||
@@ -1287,7 +1251,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(True)
|
addr_out = ci.getNewAddress(True)
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
@@ -2346,7 +2314,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
S = ci.getPubkey(s)
|
||||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
lock_tx_b_txid, lock_tx_b_vout = result
|
||||||
|
else:
|
||||||
|
lock_tx_b_txid = result
|
||||||
|
|
||||||
addr_out = ci.getNewStealthAddress()
|
addr_out = ci.getNewStealthAddress()
|
||||||
lock_tx_b_spend_txid = None
|
lock_tx_b_spend_txid = None
|
||||||
|
|||||||
Reference in New Issue
Block a user