mirror of
https://github.com/basicswap/basicswap.git
synced 2025-12-30 17:11:38 +01:00
Compare commits
1 Commits
v0.14.4
...
cryptoguar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631ccea626 |
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -9,9 +9,6 @@ concurrency:
|
||||
env:
|
||||
BIN_DIR: /tmp/cached_bin
|
||||
TEST_RELOAD_PATH: /tmp/test_basicswap
|
||||
BSX_SELENIUM_DRIVER: firefox-ci
|
||||
XMR_RPC_USER: xmr_user
|
||||
XMR_RPC_PWD: xmr_pwd
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -27,18 +24,8 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
if [ $(dpkg-query -W -f='${Status}' firefox 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
|
||||
install -d -m 0755 /etc/apt/keyrings
|
||||
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
|
||||
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | sudo tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
|
||||
echo "Package: *" | sudo tee /etc/apt/preferences.d/mozilla
|
||||
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y firefox
|
||||
fi
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[dev]
|
||||
pip install flake8 codespell pytest
|
||||
pip install -r requirements.txt --require-hashes
|
||||
- name: Install
|
||||
run: |
|
||||
@@ -46,16 +33,13 @@ jobs:
|
||||
# Print the core versions to a file for caching
|
||||
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
|
||||
cat core_versions.txt
|
||||
- name: Run flake8
|
||||
- name: Running flake8
|
||||
run: |
|
||||
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||
- name: Run codespell
|
||||
- name: Running codespell
|
||||
run: |
|
||||
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
- name: Run black
|
||||
run: |
|
||||
black --check --diff --exclude="contrib" .
|
||||
- name: Run test_other
|
||||
- name: Running test_other
|
||||
run: |
|
||||
pytest tests/basicswap/test_other.py
|
||||
- name: Cache coin cores
|
||||
@@ -71,41 +55,17 @@ jobs:
|
||||
name: Running basicswap-prepare
|
||||
run: |
|
||||
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
|
||||
- name: Run test_prepare
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export TEST_BIN_PATH="$BIN_DIR"
|
||||
export TEST_PATH=/tmp/test_prepare
|
||||
pytest tests/basicswap/extended/test_prepare.py
|
||||
- name: Run test_xmr
|
||||
- name: Running test_xmr
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export PARTICL_BINDIR="$BIN_DIR/particl"
|
||||
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
|
||||
export XMR_BINDIR="$BIN_DIR/monero"
|
||||
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
|
||||
- name: Run test_encrypted_xmr_reload
|
||||
- name: Running test_encrypted_xmr_reload
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export TEST_PATH=${TEST_RELOAD_PATH}
|
||||
mkdir -p ${TEST_PATH}/bin
|
||||
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
|
||||
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
|
||||
- name: Run selenium tests
|
||||
run: |
|
||||
export TEST_PATH=/tmp/test_persistent
|
||||
mkdir -p ${TEST_PATH}/bin
|
||||
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
|
||||
export PYTHONPATH=$(pwd)
|
||||
python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & TEST_NETWORK_PID=$!
|
||||
echo "Starting test_xmr_persistent, PID $TEST_NETWORK_PID"
|
||||
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
|
||||
do
|
||||
tail -n 1 /tmp/log.txt
|
||||
sleep 2
|
||||
done
|
||||
echo "Running test_settings.py"
|
||||
python tests/basicswap/selenium/test_settings.py
|
||||
echo "Running test_swap_direction.py"
|
||||
python tests/basicswap/selenium/test_swap_direction.py
|
||||
kill -9 $TEST_NETWORK_PID
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,8 +8,6 @@ __pycache__
|
||||
/*.eggs
|
||||
.tox
|
||||
.eggs
|
||||
.ruff_cache
|
||||
.pytest_cache
|
||||
*~
|
||||
|
||||
# geckodriver.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.14.4"
|
||||
__version__ = "0.14.3"
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import socket
|
||||
import socks
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import shlex
|
||||
import socks
|
||||
import random
|
||||
import socket
|
||||
import urllib
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
from sockshandler import SocksiPyHandler
|
||||
|
||||
@@ -43,9 +42,9 @@ def getaddrinfo_tor(*args):
|
||||
|
||||
|
||||
class BaseApp(DBMethods):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
self.fp = None
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
self.log_name = log_name
|
||||
self.fp = fp
|
||||
self.fail_code = 0
|
||||
self.mock_time_offset = 0
|
||||
|
||||
@@ -72,33 +71,24 @@ class BaseApp(DBMethods):
|
||||
self.default_socket_timeout = socket.getdefaulttimeout()
|
||||
self.default_socket_getaddrinfo = socket.getaddrinfo
|
||||
|
||||
def __del__(self):
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
|
||||
def stopRunning(self, with_code=0):
|
||||
self.fail_code = with_code
|
||||
with self.mxDB:
|
||||
self.chainstate_delay_event.set()
|
||||
self.delay_event.set()
|
||||
|
||||
def openLogFile(self):
|
||||
self.fp = open(os.path.join(self.data_dir, "basicswap.log"), "a")
|
||||
|
||||
def prepareLogging(self):
|
||||
logging.setLoggerClass(BSXLogger)
|
||||
self.log = logging.getLogger(self.log_name)
|
||||
self.log.propagate = False
|
||||
|
||||
self.openLogFile()
|
||||
|
||||
# Remove any existing handlers
|
||||
self.log.handlers = []
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
stream_stdout = logging.StreamHandler(sys.stdout)
|
||||
stream_stdout = logging.StreamHandler()
|
||||
if self.log_name != "BasicSwap":
|
||||
stream_stdout.setFormatter(
|
||||
logging.Formatter(
|
||||
@@ -108,7 +98,6 @@ class BaseApp(DBMethods):
|
||||
)
|
||||
else:
|
||||
stream_stdout.setFormatter(formatter)
|
||||
self.log_formatter = formatter
|
||||
stream_fp = logging.StreamHandler(self.fp)
|
||||
stream_fp.setFormatter(formatter)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import concurrent.futures
|
||||
import copy
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
@@ -33,7 +32,7 @@ from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
|
||||
from . import __version__
|
||||
from .rpc import escape_rpcauth
|
||||
from .rpc_xmr import make_xmr_rpc2_func
|
||||
from .ui.util import getCoinName
|
||||
from .ui.util import getCoinName, known_chart_coins
|
||||
from .util import (
|
||||
AutomationConstraint,
|
||||
AutomationConstraintTemporary,
|
||||
@@ -66,12 +65,6 @@ from basicswap.util.network import is_private_ip_address
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
chainparams,
|
||||
Fiat,
|
||||
ticker_map,
|
||||
)
|
||||
from .explorers import (
|
||||
default_chart_api_key,
|
||||
default_coingecko_api_key,
|
||||
)
|
||||
from .script import (
|
||||
OpCodes,
|
||||
@@ -122,16 +115,8 @@ from .explorers import (
|
||||
ExplorerBitAps,
|
||||
ExplorerChainz,
|
||||
)
|
||||
from .network.simplex import (
|
||||
initialiseSimplexNetwork,
|
||||
sendSimplexMsg,
|
||||
readSimplexMsgs,
|
||||
)
|
||||
from .network.util import (
|
||||
getMsgPubkey,
|
||||
)
|
||||
import basicswap.config as cfg
|
||||
import basicswap.network.network as bsn
|
||||
import basicswap.network as bsn
|
||||
import basicswap.protocols.atomic_swap_1 as atomic_swap_1
|
||||
import basicswap.protocols.xmr_swap_1 as xmr_swap_1
|
||||
from .basicswap_util import (
|
||||
@@ -141,8 +126,6 @@ from .basicswap_util import (
|
||||
BidStates,
|
||||
DebugTypes,
|
||||
EventLogTypes,
|
||||
fiatTicker,
|
||||
get_api_key_setting,
|
||||
KeyTypes,
|
||||
MessageTypes,
|
||||
NotificationTypes as NT,
|
||||
@@ -290,13 +273,14 @@ class BasicSwap(BaseApp):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fp,
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
log_name="BasicSwap",
|
||||
transient_instance=False,
|
||||
):
|
||||
super().__init__(data_dir, settings, chain, log_name)
|
||||
super().__init__(fp, data_dir, settings, chain, log_name)
|
||||
|
||||
v = __version__.split(".")
|
||||
self._version = struct.pack(">HHH", int(v[0]), int(v[1]), int(v[2]))
|
||||
@@ -360,13 +344,6 @@ class BasicSwap(BaseApp):
|
||||
self._expire_db_records_after = self.get_int_setting(
|
||||
"expire_db_records_after", 7 * 86400, 0, 31 * 86400
|
||||
) # Seconds
|
||||
self._max_logfile_bytes = self.settings.get(
|
||||
"max_logfile_size", 100
|
||||
) # In MB 0 to disable truncation
|
||||
if self._max_logfile_bytes > 0:
|
||||
self._max_logfile_bytes *= 1024 * 1024
|
||||
self._max_logfiles = self.get_int_setting("max_logfiles", 10, 1, 100)
|
||||
|
||||
self._notifications_cache = {}
|
||||
self._is_encrypted = None
|
||||
self._is_locked = None
|
||||
@@ -399,7 +376,7 @@ class BasicSwap(BaseApp):
|
||||
Coins.PART_BLIND,
|
||||
Coins.BCH,
|
||||
)
|
||||
self.coins_without_segwit = (Coins.PIVX, Coins.DASH)
|
||||
self.coins_without_segwit = (Coins.PIVX, Coins.DASH, Coins.NMC)
|
||||
|
||||
# TODO: Adjust ranges
|
||||
self.min_delay_event = self.get_int_setting("min_delay_event", 10, 0, 20 * 60)
|
||||
@@ -436,9 +413,6 @@ class BasicSwap(BaseApp):
|
||||
|
||||
self.swaps_in_progress = dict()
|
||||
|
||||
self.dleag_split_size_init = 16000
|
||||
self.dleag_split_size = 17000
|
||||
|
||||
self.SMSG_SECONDS_IN_HOUR = (
|
||||
60 * 60
|
||||
) # Note: Set smsgsregtestadjust=0 for regtest
|
||||
@@ -537,8 +511,6 @@ class BasicSwap(BaseApp):
|
||||
self._network = None
|
||||
|
||||
for t in self.threads:
|
||||
if hasattr(t, "stop") and callable(t.stop):
|
||||
t.stop()
|
||||
t.join()
|
||||
|
||||
if sys.version_info[1] >= 9:
|
||||
@@ -720,7 +692,7 @@ class BasicSwap(BaseApp):
|
||||
def getXMRTrustedDaemon(self, coin, node_host: str) -> bool:
|
||||
coin = Coins(coin) # Errors for invalid coin value
|
||||
chain_client_settings = self.getChainClientSettings(coin)
|
||||
trusted_daemon_setting = chain_client_settings.get("trusted_daemon", True)
|
||||
trusted_daemon_setting = chain_client_settings.get("trusted_daemon", "auto")
|
||||
self.log.debug(
|
||||
f"'trusted_daemon' setting for {getCoinName(coin)}: {trusted_daemon_setting}."
|
||||
)
|
||||
@@ -1067,9 +1039,7 @@ class BasicSwap(BaseApp):
|
||||
elif c in (Coins.XMR, Coins.WOW):
|
||||
try:
|
||||
ci.ensureWalletExists()
|
||||
except Exception as e:
|
||||
if "invalid signature" in str(e): # wallet is corrupt
|
||||
raise
|
||||
except Exception as e: # noqa: F841
|
||||
self.log.warning(
|
||||
f"Can't open {ci.coin_name()} wallet, could be locked."
|
||||
)
|
||||
@@ -1093,17 +1063,6 @@ class BasicSwap(BaseApp):
|
||||
f"network_key {self.network_key}\nnetwork_pubkey {self.network_pubkey}\nnetwork_addr {self.network_addr}"
|
||||
)
|
||||
|
||||
self.active_networks = []
|
||||
network_config_list = self.settings.get("networks", [])
|
||||
if len(network_config_list) < 1:
|
||||
network_config_list = [{"type": "smsg", "enabled": True}]
|
||||
|
||||
for network in network_config_list:
|
||||
if network["type"] == "smsg":
|
||||
self.active_networks.append({"type": "smsg"})
|
||||
elif network["type"] == "simplex":
|
||||
initialiseSimplexNetwork(self, network)
|
||||
|
||||
ro = self.callrpc("smsglocalkeys")
|
||||
found = False
|
||||
for k in ro["smsg_keys"]:
|
||||
@@ -1358,9 +1317,7 @@ class BasicSwap(BaseApp):
|
||||
legacy_root_hash = ci.getSeedHash(root_key, 20)
|
||||
self.setStringKV(key_str, legacy_root_hash.hex(), cursor)
|
||||
|
||||
def initialiseWallet(
|
||||
self, interface_type, raise_errors: bool = False, restore_time: int = -1
|
||||
) -> None:
|
||||
def initialiseWallet(self, interface_type, raise_errors: bool = False) -> None:
|
||||
if interface_type == Coins.PART:
|
||||
return
|
||||
ci = self.ci(interface_type)
|
||||
@@ -1379,7 +1336,7 @@ class BasicSwap(BaseApp):
|
||||
|
||||
root_key = self.getWalletKey(interface_type, 1)
|
||||
try:
|
||||
ci.initialiseWallet(root_key, restore_time)
|
||||
ci.initialiseWallet(root_key)
|
||||
except Exception as e:
|
||||
# < 0.21: sethdseed cannot set a new HD seed while still in Initial Block Download.
|
||||
self.log.error(f"initialiseWallet failed: {e}")
|
||||
@@ -1683,33 +1640,6 @@ class BasicSwap(BaseApp):
|
||||
bid_valid = (bid.expire_at - now) + 10 * 60 # Add 10 minute buffer
|
||||
return max(smsg_min_valid, min(smsg_max_valid, bid_valid))
|
||||
|
||||
def sendMessage(
|
||||
self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int, cursor
|
||||
) -> bytes:
|
||||
message_id: bytes = None
|
||||
# First network in list will set message_id
|
||||
for network in self.active_networks:
|
||||
net_message_id = None
|
||||
if network["type"] == "smsg":
|
||||
net_message_id = self.sendSmsg(
|
||||
addr_from, addr_to, payload_hex, msg_valid
|
||||
)
|
||||
elif network["type"] == "simplex":
|
||||
net_message_id = sendSimplexMsg(
|
||||
self,
|
||||
network,
|
||||
addr_from,
|
||||
addr_to,
|
||||
bytes.fromhex(payload_hex),
|
||||
msg_valid,
|
||||
cursor,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Unknown network: {}".format(network["type"]))
|
||||
if not message_id:
|
||||
message_id = net_message_id
|
||||
return message_id
|
||||
|
||||
def sendSmsg(
|
||||
self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int
|
||||
) -> bytes:
|
||||
@@ -2220,24 +2150,6 @@ class BasicSwap(BaseApp):
|
||||
msg_buf.fee_rate_to
|
||||
) # Unused: TODO - Set priority?
|
||||
|
||||
# Set auto-accept type
|
||||
automation_id = extra_options.get("automation_id", -1)
|
||||
if automation_id == -1 and auto_accept_bids:
|
||||
automation_id = 1 # Default strategy
|
||||
|
||||
if automation_id != -1:
|
||||
strategy = self.queryOne(
|
||||
AutomationStrategy,
|
||||
cursor,
|
||||
{"active_ind": 1, "record_id": automation_id},
|
||||
)
|
||||
if strategy:
|
||||
msg_buf.auto_accept_type = (
|
||||
2 if strategy.only_known_identities else 1
|
||||
)
|
||||
else:
|
||||
msg_buf.auto_accept_type = 0
|
||||
|
||||
# If a prefunded txn is not used, check that the wallet balance can cover the tx fee.
|
||||
if "prefunded_itx" not in extra_options:
|
||||
# TODO: Better tx size estimate, xmr_swap_b_lock_tx_vsize could be larger than xmr_swap_b_lock_spend_tx_vsize
|
||||
@@ -2255,9 +2167,7 @@ class BasicSwap(BaseApp):
|
||||
offer_bytes = msg_buf.to_bytes()
|
||||
payload_hex = str.format("{:02x}", MessageTypes.OFFER) + offer_bytes.hex()
|
||||
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
|
||||
offer_id = self.sendMessage(
|
||||
offer_addr, offer_addr_to, payload_hex, msg_valid, cursor
|
||||
)
|
||||
offer_id = self.sendSmsg(offer_addr, offer_addr_to, payload_hex, msg_valid)
|
||||
|
||||
security_token = extra_options.get("security_token", None)
|
||||
if security_token is not None and len(security_token) != 20:
|
||||
@@ -2292,7 +2202,6 @@ class BasicSwap(BaseApp):
|
||||
security_token=security_token,
|
||||
from_feerate=msg_buf.fee_rate_from,
|
||||
to_feerate=msg_buf.fee_rate_to,
|
||||
auto_accept_type=msg_buf.auto_accept_type,
|
||||
)
|
||||
offer.setState(OfferStates.OFFER_SENT)
|
||||
|
||||
@@ -2362,8 +2271,8 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
|
||||
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, offer.time_valid)
|
||||
msg_id = self.sendMessage(
|
||||
offer.addr_from, self.network_addr, payload_hex, msg_valid, cursor
|
||||
msg_id = self.sendSmsg(
|
||||
offer.addr_from, self.network_addr, payload_hex, msg_valid
|
||||
)
|
||||
self.log.debug(
|
||||
f"Revoked offer {self.log.id(offer_id)} in msg {self.log.id(msg_id)}"
|
||||
@@ -2741,8 +2650,7 @@ class BasicSwap(BaseApp):
|
||||
self.log.warning(msg)
|
||||
return False
|
||||
|
||||
seed_key: str = "main_wallet_seedid_" + ci.coin_name().lower()
|
||||
expect_seedid: str = self.getStringKV(seed_key)
|
||||
expect_seedid = self.getStringKV("main_wallet_seedid_" + ci.coin_name().lower())
|
||||
if expect_seedid is None:
|
||||
self.log.warning(
|
||||
f"Can't find expected wallet seed id for coin {ci.coin_name()}."
|
||||
@@ -2754,7 +2662,6 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
root_key = self.getWalletKey(c, 1)
|
||||
self.storeSeedIDForCoin(root_key, c)
|
||||
expect_seedid: str = self.getStringKV(seed_key)
|
||||
else:
|
||||
self.log.warning("Node is locked.")
|
||||
return False
|
||||
@@ -3209,9 +3116,7 @@ class BasicSwap(BaseApp):
|
||||
|
||||
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
|
||||
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
|
||||
bid_id = self.sendMessage(
|
||||
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
|
||||
)
|
||||
bid_id = self.sendSmsg(bid_addr, offer.addr_from, payload_hex, msg_valid)
|
||||
|
||||
bid = Bid(
|
||||
protocol_version=msg_buf.protocol_version,
|
||||
@@ -3547,8 +3452,8 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
|
||||
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
|
||||
accept_msg_id = self.sendMessage(
|
||||
offer.addr_from, bid.bid_addr, payload_hex, msg_valid, cursor
|
||||
accept_msg_id = self.sendSmsg(
|
||||
offer.addr_from, bid.bid_addr, payload_hex, msg_valid
|
||||
)
|
||||
|
||||
self.addMessageLink(
|
||||
@@ -3578,29 +3483,20 @@ class BasicSwap(BaseApp):
|
||||
dleag: bytes,
|
||||
msg_valid: int,
|
||||
bid_msg_ids,
|
||||
cursor,
|
||||
) -> None:
|
||||
msg_buf2 = XmrSplitMessage(
|
||||
msg_id=bid_id, msg_type=msg_type, sequence=1, dleag=dleag[16000:32000]
|
||||
)
|
||||
msg_bytes = msg_buf2.to_bytes()
|
||||
payload_hex = str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
|
||||
bid_msg_ids[1] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
|
||||
|
||||
sent_bytes = self.dleag_split_size_init
|
||||
|
||||
num_sent = 1
|
||||
while sent_bytes < len(dleag):
|
||||
size_to_send: int = min(self.dleag_split_size, len(dleag) - sent_bytes)
|
||||
msg_buf = XmrSplitMessage(
|
||||
msg_id=bid_id,
|
||||
msg_type=msg_type,
|
||||
sequence=num_sent,
|
||||
dleag=dleag[sent_bytes : sent_bytes + size_to_send],
|
||||
msg_buf3 = XmrSplitMessage(
|
||||
msg_id=bid_id, msg_type=msg_type, sequence=2, dleag=dleag[32000:]
|
||||
)
|
||||
msg_bytes = msg_buf.to_bytes()
|
||||
payload_hex = (
|
||||
str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
|
||||
)
|
||||
bid_msg_ids[num_sent] = self.sendMessage(
|
||||
addr_from, addr_to, payload_hex, msg_valid, cursor
|
||||
)
|
||||
num_sent += 1
|
||||
sent_bytes += size_to_send
|
||||
msg_bytes = msg_buf3.to_bytes()
|
||||
payload_hex = str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
|
||||
bid_msg_ids[2] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
|
||||
|
||||
def postXmrBid(
|
||||
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
|
||||
@@ -3676,8 +3572,8 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
|
||||
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
|
||||
xmr_swap.bid_id = self.sendMessage(
|
||||
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
|
||||
xmr_swap.bid_id = self.sendSmsg(
|
||||
bid_addr, offer.addr_from, payload_hex, msg_valid
|
||||
)
|
||||
|
||||
bid = Bid(
|
||||
@@ -3759,7 +3655,7 @@ class BasicSwap(BaseApp):
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
|
||||
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
|
||||
msg_buf.kbsf_dleag = xmr_swap.kbsf_dleag[: self.dleag_split_size_init]
|
||||
msg_buf.kbsf_dleag = xmr_swap.kbsf_dleag[:16000]
|
||||
elif ci_to.curve_type() == Curves.secp256k1:
|
||||
for i in range(10):
|
||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
|
||||
@@ -3789,8 +3685,8 @@ class BasicSwap(BaseApp):
|
||||
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
|
||||
|
||||
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
|
||||
xmr_swap.bid_id = self.sendMessage(
|
||||
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
|
||||
xmr_swap.bid_id = self.sendSmsg(
|
||||
bid_addr, offer.addr_from, payload_hex, msg_valid
|
||||
)
|
||||
|
||||
bid_msg_ids = {}
|
||||
@@ -3803,7 +3699,6 @@ class BasicSwap(BaseApp):
|
||||
xmr_swap.kbsf_dleag,
|
||||
msg_valid,
|
||||
bid_msg_ids,
|
||||
cursor,
|
||||
)
|
||||
|
||||
bid = Bid(
|
||||
@@ -4082,7 +3977,7 @@ class BasicSwap(BaseApp):
|
||||
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
xmr_swap.kbsl_dleag = ci_to.proveDLEAG(kbsl)
|
||||
msg_buf.kbsl_dleag = xmr_swap.kbsl_dleag[: self.dleag_split_size_init]
|
||||
msg_buf.kbsl_dleag = xmr_swap.kbsl_dleag[:16000]
|
||||
elif ci_to.curve_type() == Curves.secp256k1:
|
||||
for i in range(10):
|
||||
xmr_swap.kbsl_dleag = ci_to.signRecoverable(
|
||||
@@ -4117,9 +4012,7 @@ class BasicSwap(BaseApp):
|
||||
|
||||
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
|
||||
bid_msg_ids = {}
|
||||
bid_msg_ids[0] = self.sendMessage(
|
||||
addr_from, addr_to, payload_hex, msg_valid, use_cursor
|
||||
)
|
||||
bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
|
||||
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
self.sendXmrSplitMessages(
|
||||
@@ -4130,7 +4023,6 @@ class BasicSwap(BaseApp):
|
||||
xmr_swap.kbsl_dleag,
|
||||
msg_valid,
|
||||
bid_msg_ids,
|
||||
use_cursor,
|
||||
)
|
||||
|
||||
bid.setState(BidStates.BID_ACCEPTED) # ADS
|
||||
@@ -4252,8 +4144,8 @@ class BasicSwap(BaseApp):
|
||||
msg_buf.kbvf = kbvf
|
||||
msg_buf.kbsf_dleag = (
|
||||
xmr_swap.kbsf_dleag
|
||||
if len(xmr_swap.kbsf_dleag) < self.dleag_split_size_init
|
||||
else xmr_swap.kbsf_dleag[: self.dleag_split_size_init]
|
||||
if len(xmr_swap.kbsf_dleag) < 16000
|
||||
else xmr_swap.kbsf_dleag[:16000]
|
||||
)
|
||||
|
||||
bid_bytes = msg_buf.to_bytes()
|
||||
@@ -4265,9 +4157,7 @@ class BasicSwap(BaseApp):
|
||||
addr_to: str = bid.bid_addr
|
||||
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
|
||||
bid_msg_ids = {}
|
||||
bid_msg_ids[0] = self.sendMessage(
|
||||
addr_from, addr_to, payload_hex, msg_valid, use_cursor
|
||||
)
|
||||
bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
|
||||
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
self.sendXmrSplitMessages(
|
||||
@@ -4278,7 +4168,6 @@ class BasicSwap(BaseApp):
|
||||
xmr_swap.kbsf_dleag,
|
||||
msg_valid,
|
||||
bid_msg_ids,
|
||||
use_cursor,
|
||||
)
|
||||
|
||||
bid.setState(BidStates.BID_REQUEST_ACCEPTED)
|
||||
@@ -4764,7 +4653,7 @@ class BasicSwap(BaseApp):
|
||||
+ (len(txn_script)).to_bytes(1, "big")
|
||||
+ txn_script
|
||||
)
|
||||
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script).hex()
|
||||
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script)
|
||||
|
||||
if coin_type in (Coins.NAV, Coins.DCR):
|
||||
# Only checks signature
|
||||
@@ -6883,61 +6772,29 @@ class BasicSwap(BaseApp):
|
||||
now: int = self.getTime()
|
||||
ttl_xmr_split_messages = 60 * 60
|
||||
bid_cursor = None
|
||||
dleag_proof_len: int = 48893 # coincurve.dleag.dleag_proof_len()
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
bid_cursor = self.getNewDBCursor()
|
||||
q_bids = self.query(
|
||||
Bid,
|
||||
bid_cursor,
|
||||
{
|
||||
"state": (
|
||||
int(BidStates.BID_RECEIVING),
|
||||
int(BidStates.BID_RECEIVING_ACC),
|
||||
)
|
||||
},
|
||||
Bid, bid_cursor, {"state": int(BidStates.BID_RECEIVING)}
|
||||
)
|
||||
for bid in q_bids:
|
||||
q = cursor.execute(
|
||||
"SELECT LENGTH(kbsl_dleag), LENGTH(kbsf_dleag) FROM xmr_swaps WHERE bid_id = :bid_id",
|
||||
{
|
||||
"bid_id": bid.bid_id,
|
||||
},
|
||||
"SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
|
||||
{"bid_id": bid.bid_id, "msg_type": int(XmrSplitMsgTypes.BID)},
|
||||
).fetchone()
|
||||
kbsl_dleag_len: int = q[0]
|
||||
kbsf_dleag_len: int = q[1]
|
||||
|
||||
if bid.state == int(BidStates.BID_RECEIVING_ACC):
|
||||
bid_type: str = "bid accept"
|
||||
msg_type: int = int(XmrSplitMsgTypes.BID_ACCEPT)
|
||||
total_dleag_size: int = kbsl_dleag_len
|
||||
else:
|
||||
bid_type: str = "bid"
|
||||
msg_type: int = int(XmrSplitMsgTypes.BID)
|
||||
total_dleag_size: int = kbsf_dleag_len
|
||||
|
||||
q = cursor.execute(
|
||||
"SELECT COUNT(*), SUM(LENGTH(dleag)) AS total_dleag_size FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
|
||||
{"bid_id": bid.bid_id, "msg_type": msg_type},
|
||||
).fetchone()
|
||||
total_dleag_size += 0 if q[1] is None else q[1]
|
||||
|
||||
if total_dleag_size >= dleag_proof_len:
|
||||
num_segments = q[0]
|
||||
if num_segments > 1:
|
||||
try:
|
||||
if bid.state == int(BidStates.BID_RECEIVING):
|
||||
self.receiveXmrBid(bid, cursor)
|
||||
elif bid.state == int(BidStates.BID_RECEIVING_ACC):
|
||||
self.receiveXmrBidAccept(bid, cursor)
|
||||
else:
|
||||
raise ValueError("Unexpected bid state")
|
||||
except Exception as ex:
|
||||
self.log.info(
|
||||
f"Verify adaptor-sig {bid_type} {self.log.id(bid.bid_id)} failed: {ex}"
|
||||
f"Verify adaptor-sig bid {self.log.id(bid.bid_id)} failed: {ex}"
|
||||
)
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
bid.setState(
|
||||
BidStates.BID_ERROR, f"Failed {bid_type} validation: {ex}"
|
||||
BidStates.BID_ERROR, "Failed validation: " + str(ex)
|
||||
)
|
||||
self.updateDB(
|
||||
bid,
|
||||
@@ -6950,7 +6807,7 @@ class BasicSwap(BaseApp):
|
||||
continue
|
||||
if bid.created_at + ttl_xmr_split_messages < now:
|
||||
self.log.debug(
|
||||
f"Expiring partially received {bid_type}: {self.log.id(bid.bid_id)}."
|
||||
f"Expiring partially received bid: {self.log.id(bid.bid_id)}."
|
||||
)
|
||||
bid.setState(BidStates.BID_ERROR, "Timed out")
|
||||
self.updateDB(
|
||||
@@ -6960,6 +6817,53 @@ class BasicSwap(BaseApp):
|
||||
"bid_id",
|
||||
],
|
||||
)
|
||||
|
||||
q_bids = self.query(
|
||||
Bid, bid_cursor, {"state": int(BidStates.BID_RECEIVING_ACC)}
|
||||
)
|
||||
for bid in q_bids:
|
||||
q = cursor.execute(
|
||||
"SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
|
||||
{
|
||||
"bid_id": bid.bid_id,
|
||||
"msg_type": int(XmrSplitMsgTypes.BID_ACCEPT),
|
||||
},
|
||||
).fetchone()
|
||||
num_segments = q[0]
|
||||
if num_segments > 1:
|
||||
try:
|
||||
self.receiveXmrBidAccept(bid, cursor)
|
||||
except Exception as ex:
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
self.log.info(
|
||||
f"Verify adaptor-sig bid accept {self.log.id(bid.bid_id)} failed: {ex}."
|
||||
)
|
||||
bid.setState(
|
||||
BidStates.BID_ERROR, "Failed accept validation: " + str(ex)
|
||||
)
|
||||
self.updateDB(
|
||||
bid,
|
||||
cursor,
|
||||
[
|
||||
"bid_id",
|
||||
],
|
||||
)
|
||||
self.updateBidInProgress(bid)
|
||||
continue
|
||||
if bid.created_at + ttl_xmr_split_messages < now:
|
||||
self.log.debug(
|
||||
f"Expiring partially received bid accept: {self.log.id(bid.bid_id)}."
|
||||
)
|
||||
bid.setState(BidStates.BID_ERROR, "Timed out")
|
||||
self.updateDB(
|
||||
bid,
|
||||
cursor,
|
||||
[
|
||||
"bid_id",
|
||||
],
|
||||
)
|
||||
|
||||
# Expire old records
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_split_data WHERE created_at + :ttl < :now",
|
||||
@@ -7089,7 +6993,6 @@ class BasicSwap(BaseApp):
|
||||
if self.isOfferRevoked(offer_id, msg["from"]):
|
||||
raise ValueError("Offer has been revoked {}.".format(offer_id.hex()))
|
||||
|
||||
pk_from: bytes = getMsgPubkey(self, msg)
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
# Offers must be received on the public network_addr or manually created addresses
|
||||
@@ -7130,16 +7033,10 @@ class BasicSwap(BaseApp):
|
||||
rate_negotiable=offer_data.rate_negotiable,
|
||||
addr_to=msg["to"],
|
||||
addr_from=msg["from"],
|
||||
pk_from=pk_from,
|
||||
created_at=msg["sent"],
|
||||
expire_at=msg["sent"] + offer_data.time_valid,
|
||||
was_sent=False,
|
||||
bid_reversed=bid_reversed,
|
||||
auto_accept_type=(
|
||||
offer_data.auto_accept_type
|
||||
if b"\xa0\x01" in offer_bytes
|
||||
else None
|
||||
),
|
||||
)
|
||||
offer.setState(OfferStates.OFFER_RECEIVED)
|
||||
self.add(offer, cursor)
|
||||
@@ -7479,7 +7376,6 @@ class BasicSwap(BaseApp):
|
||||
|
||||
bid = self.getBid(bid_id)
|
||||
if bid is None:
|
||||
pk_from: bytes = getMsgPubkey(self, msg)
|
||||
bid = Bid(
|
||||
active_ind=1,
|
||||
bid_id=bid_id,
|
||||
@@ -7494,7 +7390,6 @@ class BasicSwap(BaseApp):
|
||||
created_at=msg["sent"],
|
||||
expire_at=msg["sent"] + bid_data.time_valid,
|
||||
bid_addr=msg["from"],
|
||||
pk_bid_addr=pk_from,
|
||||
was_received=True,
|
||||
chain_a_height_start=ci_from.getChainHeight(),
|
||||
chain_b_height_start=ci_to.getChainHeight(),
|
||||
@@ -7893,13 +7788,12 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
ensure(len(bid_data.kbsf_dleag) <= 16000, "Invalid kbsf_dleag size")
|
||||
ensure(len(bid_data.kbsf_dleag) == 16000, "Invalid kbsf_dleag size")
|
||||
|
||||
bid_id = bytes.fromhex(msg["msgid"])
|
||||
|
||||
bid, xmr_swap = self.getXmrBid(bid_id)
|
||||
if bid is None:
|
||||
pk_from: bytes = getMsgPubkey(self, msg)
|
||||
bid = Bid(
|
||||
active_ind=1,
|
||||
bid_id=bid_id,
|
||||
@@ -7911,7 +7805,6 @@ class BasicSwap(BaseApp):
|
||||
created_at=msg["sent"],
|
||||
expire_at=msg["sent"] + bid_data.time_valid,
|
||||
bid_addr=msg["from"],
|
||||
pk_bid_addr=pk_from,
|
||||
was_received=True,
|
||||
chain_a_height_start=ci_from.getChainHeight(),
|
||||
chain_b_height_start=ci_to.getChainHeight(),
|
||||
@@ -8241,8 +8134,8 @@ class BasicSwap(BaseApp):
|
||||
msg_valid: int = self.getActiveBidMsgValidTime()
|
||||
addr_send_from: str = offer.addr_from if reverse_bid else bid.bid_addr
|
||||
addr_send_to: str = bid.bid_addr if reverse_bid else offer.addr_from
|
||||
coin_a_lock_tx_sigs_l_msg_id = self.sendMessage(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
|
||||
coin_a_lock_tx_sigs_l_msg_id = self.sendSmsg(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid
|
||||
)
|
||||
self.addMessageLink(
|
||||
Concepts.BID,
|
||||
@@ -8610,8 +8503,8 @@ class BasicSwap(BaseApp):
|
||||
addr_send_from: str = bid.bid_addr if reverse_bid else offer.addr_from
|
||||
addr_send_to: str = offer.addr_from if reverse_bid else bid.bid_addr
|
||||
msg_valid: int = self.getActiveBidMsgValidTime()
|
||||
coin_a_lock_release_msg_id = self.sendMessage(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
|
||||
coin_a_lock_release_msg_id = self.sendSmsg(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid
|
||||
)
|
||||
self.addMessageLink(
|
||||
Concepts.BID,
|
||||
@@ -9030,8 +8923,8 @@ class BasicSwap(BaseApp):
|
||||
)
|
||||
|
||||
msg_valid: int = self.getActiveBidMsgValidTime()
|
||||
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendMessage(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
|
||||
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendSmsg(
|
||||
addr_send_from, addr_send_to, payload_hex, msg_valid
|
||||
)
|
||||
|
||||
bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX)
|
||||
@@ -9413,7 +9306,6 @@ class BasicSwap(BaseApp):
|
||||
|
||||
bid, xmr_swap = self.getXmrBid(bid_id)
|
||||
if bid is None:
|
||||
pk_from: bytes = getMsgPubkey(self, msg)
|
||||
bid = Bid(
|
||||
active_ind=1,
|
||||
bid_id=bid_id,
|
||||
@@ -9425,7 +9317,6 @@ class BasicSwap(BaseApp):
|
||||
created_at=msg["sent"],
|
||||
expire_at=msg["sent"] + bid_data.time_valid,
|
||||
bid_addr=msg["from"],
|
||||
pk_bid_addr=pk_from,
|
||||
was_sent=False,
|
||||
was_received=True,
|
||||
chain_a_height_start=ci_from.getChainHeight(),
|
||||
@@ -9528,7 +9419,7 @@ class BasicSwap(BaseApp):
|
||||
"Invalid destination address",
|
||||
)
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
ensure(len(msg_data.kbsf_dleag) <= 16000, "Invalid kbsf_dleag size")
|
||||
ensure(len(msg_data.kbsf_dleag) == 16000, "Invalid kbsf_dleag size")
|
||||
|
||||
xmr_swap.dest_af = msg_data.dest_af
|
||||
xmr_swap.pkaf = msg_data.pkaf
|
||||
@@ -9563,14 +9454,6 @@ class BasicSwap(BaseApp):
|
||||
|
||||
def processMsg(self, msg) -> None:
|
||||
try:
|
||||
if "hex" not in msg:
|
||||
if self.debug:
|
||||
if "error" in msg:
|
||||
self.log.debug(
|
||||
"Message error {}: {}.".format(msg["msgid"], msg["error"])
|
||||
)
|
||||
raise ValueError("Invalid msg received {}.".format(msg["msgid"]))
|
||||
return
|
||||
msg_type = int(msg["hex"][:2], 16)
|
||||
|
||||
if msg_type == MessageTypes.OFFER:
|
||||
@@ -9784,10 +9667,6 @@ class BasicSwap(BaseApp):
|
||||
self.processMsg(msg)
|
||||
|
||||
try:
|
||||
for network in self.active_networks:
|
||||
if network["type"] == "simplex":
|
||||
readSimplexMsgs(self, network)
|
||||
|
||||
# TODO: Wait for blocks / txns, would need to check multiple coins
|
||||
now: int = self.getTime()
|
||||
self.expireBidsAndOffers(now)
|
||||
@@ -9837,51 +9716,6 @@ class BasicSwap(BaseApp):
|
||||
self.checkAcceptedBids()
|
||||
self._last_checked_expired = now
|
||||
|
||||
if self._max_logfile_bytes > 0:
|
||||
logfile_size: int = self.fp.tell()
|
||||
self.log.debug(f"Log file bytes: {logfile_size}.")
|
||||
if logfile_size > self._max_logfile_bytes:
|
||||
for i, log_handler in enumerate(self.log.handlers):
|
||||
stream_name = getattr(log_handler.stream, "name", "")
|
||||
if stream_name.endswith(".log"):
|
||||
del self.log.handlers[i]
|
||||
break
|
||||
|
||||
self.fp.close()
|
||||
log_path = os.path.join(self.data_dir, "basicswap.log")
|
||||
if self._max_logfiles == 1:
|
||||
os.remove(log_path)
|
||||
else:
|
||||
last_log = os.path.join(
|
||||
self.data_dir,
|
||||
f"basicswap_{self._max_logfiles - 1:0>2}.log",
|
||||
)
|
||||
if os.path.exists(last_log):
|
||||
os.remove(last_log)
|
||||
|
||||
for i in range(self._max_logfiles - 2, 0, -1):
|
||||
path_from = os.path.join(
|
||||
self.data_dir, f"basicswap_{i:0>2}.log"
|
||||
)
|
||||
path_to = os.path.join(
|
||||
self.data_dir, f"basicswap_{i + 1:0>2}.log"
|
||||
)
|
||||
if os.path.exists(path_from):
|
||||
os.rename(path_from, path_to)
|
||||
|
||||
log_path = os.path.join(self.data_dir, "basicswap.log")
|
||||
os.rename(
|
||||
log_path,
|
||||
os.path.join(self.data_dir, "basicswap_01.log"),
|
||||
)
|
||||
|
||||
self.openLogFile()
|
||||
|
||||
stream_fp = logging.StreamHandler(self.fp)
|
||||
stream_fp.setFormatter(self.log_formatter)
|
||||
self.log.addHandler(stream_fp)
|
||||
self.log.info("Log file rotated.")
|
||||
|
||||
if now - self._last_checked_actions >= self.check_actions_seconds:
|
||||
self.checkQueuedActions()
|
||||
self._last_checked_actions = now
|
||||
@@ -10060,7 +9894,7 @@ class BasicSwap(BaseApp):
|
||||
seen_tickers = []
|
||||
for ticker in tickers:
|
||||
upcased_ticker = ticker.strip().upper()
|
||||
if upcased_ticker.lower() not in ticker_map:
|
||||
if upcased_ticker not in known_chart_coins:
|
||||
raise ValueError(f"Unknown coin: {ticker}")
|
||||
if upcased_ticker in seen_tickers:
|
||||
raise ValueError(f"Duplicate coin: {ticker}")
|
||||
@@ -11224,175 +11058,6 @@ class BasicSwap(BaseApp):
|
||||
).isWalletEncryptedLocked()
|
||||
return self._is_encrypted, self._is_locked
|
||||
|
||||
def getExchangeName(self, coin_id: int, exchange_name: str) -> str:
|
||||
if coin_id == Coins.BCH:
|
||||
return "bitcoin-cash"
|
||||
if coin_id == Coins.FIRO:
|
||||
return "zcoin"
|
||||
return chainparams[coin_id]["name"]
|
||||
|
||||
def lookupFiatRates(
|
||||
self,
|
||||
coins_list,
|
||||
currency_to: int = Fiat.USD,
|
||||
rate_source: str = "coingecko.com",
|
||||
saved_ttl: int = 300,
|
||||
):
|
||||
if self.debug:
|
||||
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
|
||||
self.log.debug(f"lookupFiatRates {coins_list_display}.")
|
||||
ensure(len(coins_list) > 0, "Must specify coin/s")
|
||||
ensure(saved_ttl >= 0, "Invalid saved time")
|
||||
|
||||
now: int = int(time.time())
|
||||
oldest_time_valid: int = now - saved_ttl
|
||||
return_rates = {}
|
||||
|
||||
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
|
||||
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
parameters = {
|
||||
"rate_source": rate_source,
|
||||
"oldest_time_valid": oldest_time_valid,
|
||||
"currency_to": currency_to,
|
||||
}
|
||||
coins_list_query = ""
|
||||
for i, coin_id in enumerate(coins_list):
|
||||
try:
|
||||
_ = Coins(coin_id)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin_id}")
|
||||
|
||||
param_name = f"coin_{i}"
|
||||
if i > 0:
|
||||
coins_list_query += ","
|
||||
coins_list_query += f":{param_name}"
|
||||
parameters[param_name] = coin_id
|
||||
|
||||
query = f"SELECT currency_from, rate FROM coinrates WHERE currency_from IN ({coins_list_query}) AND currency_to = :currency_to AND source = :rate_source AND last_updated >= :oldest_time_valid"
|
||||
rows = cursor.execute(query, parameters)
|
||||
|
||||
for row in rows:
|
||||
return_rates[int(row[0])] = float(row[1])
|
||||
|
||||
need_coins = []
|
||||
new_values = {}
|
||||
exchange_name_map = {}
|
||||
for coin_id in coins_list:
|
||||
if coin_id not in return_rates:
|
||||
need_coins.append(coin_id)
|
||||
|
||||
if len(need_coins) < 1:
|
||||
return return_rates
|
||||
|
||||
if rate_source == "coingecko.com":
|
||||
ticker_to: str = fiatTicker(currency_to).lower()
|
||||
# Update all requested coins
|
||||
coin_ids: str = ""
|
||||
for coin_id in coins_list:
|
||||
if len(coin_ids) > 0:
|
||||
coin_ids += ","
|
||||
exchange_name: str = self.getExchangeName(coin_id, rate_source)
|
||||
coin_ids += exchange_name
|
||||
exchange_name_map[exchange_name] = coin_id
|
||||
|
||||
api_key: str = get_api_key_setting(
|
||||
self.settings,
|
||||
"coingecko_api_key",
|
||||
default_coingecko_api_key,
|
||||
escape=True,
|
||||
)
|
||||
url: str = (
|
||||
f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies={ticker_to}"
|
||||
)
|
||||
if api_key != "":
|
||||
url += f"&api_key={api_key}"
|
||||
|
||||
self.log.debug(f"lookupFiatRates: {url}")
|
||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||
|
||||
for k, v in js.items():
|
||||
return_rates[int(exchange_name_map[k])] = v[ticker_to]
|
||||
new_values[exchange_name_map[k]] = v[ticker_to]
|
||||
elif rate_source == "cryptocompare.com":
|
||||
ticker_to: str = fiatTicker(currency_to).upper()
|
||||
api_key: str = get_api_key_setting(
|
||||
self.settings,
|
||||
"chart_api_key",
|
||||
default_chart_api_key,
|
||||
escape=True,
|
||||
)
|
||||
if len(need_coins) == 1:
|
||||
coin_ticker: str = chainparams[coin_id]["ticker"]
|
||||
url: str = (
|
||||
f"https://min-api.cryptocompare.com/data/price?fsym={coin_ticker}&tsyms={ticker_to}"
|
||||
)
|
||||
self.log.debug(f"lookupFiatRates: {url}")
|
||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||
return_rates[int(coin_id)] = js[ticker_to]
|
||||
new_values[coin_id] = js[ticker_to]
|
||||
else:
|
||||
coin_ids: str = ""
|
||||
for coin_id in coins_list:
|
||||
if len(coin_ids) > 0:
|
||||
coin_ids += ","
|
||||
coin_ticker: str = chainparams[coin_id]["ticker"]
|
||||
coin_ids += coin_ticker
|
||||
exchange_name_map[coin_ticker] = coin_id
|
||||
url: str = (
|
||||
f"https://min-api.cryptocompare.com/data/pricemulti?fsyms={coin_ids}&tsyms={ticker_to}"
|
||||
)
|
||||
self.log.debug(f"lookupFiatRates: {url}")
|
||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||
for k, v in js.items():
|
||||
return_rates[int(exchange_name_map[k])] = v[ticker_to]
|
||||
new_values[exchange_name_map[k]] = v[ticker_to]
|
||||
else:
|
||||
raise ValueError(f"Unknown rate source {rate_source}")
|
||||
|
||||
if len(new_values) < 1:
|
||||
return return_rates
|
||||
|
||||
# ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
|
||||
update_query = """
|
||||
UPDATE coinrates SET
|
||||
rate=:rate,
|
||||
last_updated=:last_updated
|
||||
WHERE currency_from = :currency_from AND currency_to = :currency_to AND source = :rate_source
|
||||
"""
|
||||
|
||||
insert_query = """INSERT INTO coinrates(currency_from, currency_to, rate, source, last_updated)
|
||||
VALUES(:currency_from, :currency_to, :rate, :rate_source, :last_updated)"""
|
||||
|
||||
for k, v in new_values.items():
|
||||
cursor.execute(
|
||||
update_query,
|
||||
{
|
||||
"currency_from": k,
|
||||
"currency_to": currency_to,
|
||||
"rate": v,
|
||||
"rate_source": rate_source,
|
||||
"last_updated": now,
|
||||
},
|
||||
)
|
||||
if cursor.rowcount < 1:
|
||||
cursor.execute(
|
||||
insert_query,
|
||||
{
|
||||
"currency_from": k,
|
||||
"currency_to": currency_to,
|
||||
"rate": v,
|
||||
"rate_source": rate_source,
|
||||
"last_updated": now,
|
||||
},
|
||||
)
|
||||
|
||||
self.commitDB()
|
||||
return return_rates
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
def lookupRates(self, coin_from, coin_to, output_array=False):
|
||||
self.log.debug(
|
||||
"lookupRates {}, {}.".format(
|
||||
@@ -11405,14 +11070,25 @@ class BasicSwap(BaseApp):
|
||||
ci_to = self.ci(int(coin_to))
|
||||
name_from = ci_from.chainparams()["name"]
|
||||
name_to = ci_to.chainparams()["name"]
|
||||
exchange_name_from = ci_from.getExchangeName("coingecko.com")
|
||||
exchange_name_to = ci_to.getExchangeName("coingecko.com")
|
||||
ticker_from = ci_from.chainparams()["ticker"]
|
||||
ticker_to = ci_to.chainparams()["ticker"]
|
||||
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
|
||||
rv = {}
|
||||
|
||||
if rate_sources.get("coingecko.com", True):
|
||||
try:
|
||||
js = self.lookupFiatRates([int(coin_from), int(coin_to)])
|
||||
rate = float(js[int(coin_from)]) / float(js[int(coin_to)])
|
||||
url = "https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd,btc".format(
|
||||
exchange_name_from, exchange_name_to
|
||||
)
|
||||
self.log.debug(f"lookupRates: {url}")
|
||||
start = time.time()
|
||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||
js["time_taken"] = time.time() - start
|
||||
rate = float(js[exchange_name_from]["usd"]) / float(
|
||||
js[exchange_name_to]["usd"]
|
||||
)
|
||||
js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
|
||||
rv["coingecko"] = js
|
||||
except Exception as e:
|
||||
@@ -11420,10 +11096,12 @@ class BasicSwap(BaseApp):
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
js[name_from] = {"usd": js[int(coin_from)]}
|
||||
js.pop(int(coin_from))
|
||||
js[name_to] = {"usd": js[int(coin_to)]}
|
||||
js.pop(int(coin_to))
|
||||
if exchange_name_from != name_from:
|
||||
js[name_from] = js[exchange_name_from]
|
||||
js.pop(exchange_name_from)
|
||||
if exchange_name_to != name_to:
|
||||
js[name_to] = js[exchange_name_to]
|
||||
js.pop(exchange_name_to)
|
||||
|
||||
if output_array:
|
||||
|
||||
@@ -11442,6 +11120,8 @@ class BasicSwap(BaseApp):
|
||||
ticker_to,
|
||||
format_float(float(js[name_from]["usd"])),
|
||||
format_float(float(js[name_to]["usd"])),
|
||||
format_float(float(js[name_from]["btc"])),
|
||||
format_float(float(js[name_to]["btc"])),
|
||||
format_float(float(js["rate_inferred"])),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
import struct
|
||||
import hashlib
|
||||
from enum import IntEnum, auto
|
||||
from html import escape as html_escape
|
||||
from .util.address import (
|
||||
encodeAddress,
|
||||
decodeAddress,
|
||||
)
|
||||
from .chainparams import (
|
||||
chainparams,
|
||||
Fiat,
|
||||
)
|
||||
|
||||
|
||||
@@ -522,7 +520,7 @@ def getLastBidState(packed_states):
|
||||
return BidStates.BID_STATE_UNKNOWN
|
||||
|
||||
|
||||
def strSwapType(swap_type) -> str:
|
||||
def strSwapType(swap_type):
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return "seller_first"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
@@ -530,7 +528,7 @@ def strSwapType(swap_type) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def strSwapDesc(swap_type) -> str:
|
||||
def strSwapDesc(swap_type):
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return "Secret Hash"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
@@ -538,31 +536,6 @@ def strSwapDesc(swap_type) -> str:
|
||||
return None
|
||||
|
||||
|
||||
def fiatTicker(fiat_ind: int) -> str:
|
||||
try:
|
||||
return Fiat(fiat_ind).name
|
||||
except Exception as e: # noqa: F841
|
||||
raise ValueError(f"Unknown fiat ind {fiat_ind}")
|
||||
|
||||
|
||||
def fiatFromTicker(ticker: str) -> int:
|
||||
ticker_uc = ticker.upper()
|
||||
for entry in Fiat:
|
||||
if entry.name == ticker_uc:
|
||||
return entry
|
||||
raise ValueError(f"Unknown fiat {ticker}")
|
||||
|
||||
|
||||
def get_api_key_setting(
|
||||
settings, setting_name: str, default_value: str = "", escape: bool = False
|
||||
):
|
||||
setting_name_enc: str = setting_name + "_enc"
|
||||
if setting_name_enc in settings:
|
||||
rv = bytes.fromhex(settings[setting_name_enc]).decode("utf-8")
|
||||
return html_escape(rv) if escape else rv
|
||||
return settings.get(setting_name, default_value)
|
||||
|
||||
|
||||
inactive_states = [
|
||||
BidStates.SWAP_COMPLETED,
|
||||
BidStates.BID_ERROR,
|
||||
|
||||
@@ -52,22 +52,16 @@ PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
|
||||
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
|
||||
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
|
||||
|
||||
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0")
|
||||
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
|
||||
|
||||
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4")
|
||||
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
|
||||
|
||||
DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1")
|
||||
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
|
||||
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0")
|
||||
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
|
||||
|
||||
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
|
||||
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
|
||||
|
||||
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.0")
|
||||
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.3.4")
|
||||
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
|
||||
XMR_SITE_COMMIT = (
|
||||
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version
|
||||
"3751c0d7987a9e78324a718c32c008e2ec91b339" # Lock hashes.txt to monero version
|
||||
)
|
||||
|
||||
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
|
||||
@@ -88,6 +82,9 @@ FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
|
||||
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
|
||||
NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "")
|
||||
|
||||
DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1")
|
||||
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
|
||||
|
||||
BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "28.0.1")
|
||||
BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
|
||||
|
||||
@@ -106,7 +103,7 @@ known_coins = {
|
||||
"bitcoin": (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ("laanwj",)),
|
||||
"litecoin": (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ("davidburkett38",)),
|
||||
"decred": (DCR_VERSION, DCR_VERSION_TAG, ("decred_release",)),
|
||||
"namecoin": (NMC_VERSION, NMC_VERSION_TAG, ("RoseTuring",)),
|
||||
"namecoin": ("0.18.0", "", ("JeremyRand",)),
|
||||
"monero": (MONERO_VERSION, MONERO_VERSION_TAG, ("binaryfate",)),
|
||||
"wownero": (WOWNERO_VERSION, WOWNERO_VERSION_TAG, ("wowario",)),
|
||||
"pivx": (PIVX_VERSION, PIVX_VERSION_TAG, ("fuzzbawls",)),
|
||||
@@ -119,32 +116,26 @@ known_coins = {
|
||||
|
||||
disabled_coins = [
|
||||
"navcoin",
|
||||
"namecoin", # Needs update
|
||||
]
|
||||
|
||||
expected_key_ids = {
|
||||
"tecnovert": ("8E517DC12EC1CC37F6423A8A13F13651C9CF0D6B",),
|
||||
"thrasher": ("59CAF0E96F23F53747945FD4FE3348877809386C",),
|
||||
"laanwj": ("9DEAE0DC7063249FB05474681E4AED62986CD25D",),
|
||||
"RoseTuring": ("FD8366A807A99FA27FD9CCEA9FE3BFDDA6C53495",),
|
||||
"binaryfate": ("81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92",),
|
||||
"wowario": ("AB3A2F725818FCFF2794841C793504B449C69220",),
|
||||
"davidburkett38": ("D35621D53A1CC6A3456758D03620E9D387E55666",),
|
||||
"xanimo": ("2EAA8B1021C71AD5186CA07F6E8F17C1B1BCDCBE",),
|
||||
"patricklodder": ("DC6EF4A8BF9F1B1E4DE1EE522D3A345B98D0DC1F",),
|
||||
"fuzzbawls": ("0CFBDA9F60D661BA31EB5D50C1ABA64407731FD9",),
|
||||
"pasta": (
|
||||
"29590362EC878A81FD3C202B52527BEDABE87984",
|
||||
"02B8E7D002167C8B451AF05FE2F3D7916E722D38",
|
||||
),
|
||||
"reuben": ("0186454D63E83D85EF91DE4E1290A1D0FA7EE109",),
|
||||
"nav_builder": ("1BF9B51BAED51BA0B3A174EE2782262BF6E7FADB",),
|
||||
"nicolasdorier": (
|
||||
"AB4CFA9895ACA0DBE27F6B346618763EF09186FE",
|
||||
"015B4C837B245509E4AC8995223FDA69DEBEA82D",
|
||||
"7121BDE3555D9BE06BDDC68162FE85647DEDDA2E",
|
||||
),
|
||||
"decred_release": ("F516ADB7A069852C7C28A02D6D897EDF518A031D",),
|
||||
"Calin_Culianu": ("D465135F97D0047E18E99DC321810A542031C02C",),
|
||||
"tecnovert": ("13F13651C9CF0D6B",),
|
||||
"thrasher": ("FE3348877809386C",),
|
||||
"laanwj": ("1E4AED62986CD25D",),
|
||||
"JeremyRand": ("2DBE339E29F6294C",),
|
||||
"binaryfate": ("F0AF4D462A0BDF92",),
|
||||
"wowario": ("793504B449C69220",),
|
||||
"davidburkett38": ("3620E9D387E55666",),
|
||||
"xanimo": ("6E8F17C1B1BCDCBE",),
|
||||
"patricklodder": ("2D3A345B98D0DC1F",),
|
||||
"fuzzbawls": ("C1ABA64407731FD9",),
|
||||
"pasta": ("52527BEDABE87984", "E2F3D7916E722D38"),
|
||||
"reuben": ("1290A1D0FA7EE109",),
|
||||
"nav_builder": ("2782262BF6E7FADB",),
|
||||
"nicolasdorier": ("6618763EF09186FE", "223FDA69DEBEA82D", "62FE85647DEDDA2E"),
|
||||
"decred_release": ("6D897EDF518A031D",),
|
||||
"Calin_Culianu": ("21810A542031C02C",),
|
||||
}
|
||||
|
||||
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
|
||||
@@ -182,7 +173,6 @@ BSX_UPDATE_UNMANAGED = toBool(
|
||||
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
|
||||
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
|
||||
COINS_RPCBIND_IP = os.getenv("COINS_RPCBIND_IP", "127.0.0.1")
|
||||
DEFAULT_RESTORE_TIME = int(os.getenv("DEFAULT_RESTORE_TIME", 1577833261)) # 2020
|
||||
|
||||
PART_ZMQ_PORT = int(os.getenv("PART_ZMQ_PORT", 20792))
|
||||
PART_RPC_HOST = os.getenv("PART_RPC_HOST", "127.0.0.1")
|
||||
@@ -191,36 +181,6 @@ PART_ONION_PORT = int(os.getenv("PART_ONION_PORT", 51734))
|
||||
PART_RPC_USER = os.getenv("PART_RPC_USER", "")
|
||||
PART_RPC_PWD = os.getenv("PART_RPC_PWD", "")
|
||||
|
||||
BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1")
|
||||
BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996))
|
||||
BTC_PORT = int(os.getenv("BTC_PORT", 8333))
|
||||
BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334))
|
||||
BTC_RPC_USER = os.getenv("BTC_RPC_USER", "")
|
||||
BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "")
|
||||
|
||||
LTC_RPC_HOST = os.getenv("LTC_RPC_HOST", "127.0.0.1")
|
||||
LTC_RPC_PORT = int(os.getenv("LTC_RPC_PORT", 19895))
|
||||
LTC_ONION_PORT = int(os.getenv("LTC_ONION_PORT", 9333))
|
||||
LTC_RPC_USER = os.getenv("LTC_RPC_USER", "")
|
||||
LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "")
|
||||
|
||||
DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1")
|
||||
DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109))
|
||||
DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1")
|
||||
DCR_WALLET_RPC_PORT = int(os.getenv("DCR_WALLET_RPC_PORT", 9209))
|
||||
DCR_WALLET_PWD = os.getenv(
|
||||
"DCR_WALLET_PWD", random.randbytes(random.randint(14, 18)).hex()
|
||||
)
|
||||
DCR_RPC_USER = os.getenv("DCR_RPC_USER", "user")
|
||||
DCR_RPC_PWD = os.getenv("DCR_RPC_PWD", random.randbytes(random.randint(14, 18)).hex())
|
||||
|
||||
NMC_RPC_HOST = os.getenv("NMC_RPC_HOST", "127.0.0.1")
|
||||
NMC_RPC_PORT = int(os.getenv("NMC_RPC_PORT", 19698))
|
||||
NMC_PORT = int(os.getenv("NMC_PORT", 8134))
|
||||
NMC_ONION_PORT = int(os.getenv("NMC_ONION_PORT", 9698))
|
||||
NMC_RPC_USER = os.getenv("NMC_RPC_USER", "")
|
||||
NMC_RPC_PWD = os.getenv("NMC_RPC_PWD", "")
|
||||
|
||||
XMR_RPC_HOST = os.getenv("XMR_RPC_HOST", "127.0.0.1")
|
||||
XMR_RPC_PORT = int(os.getenv("XMR_RPC_PORT", 29798))
|
||||
XMR_ZMQ_PORT = int(os.getenv("XMR_ZMQ_PORT", 30898))
|
||||
@@ -243,6 +203,32 @@ WOW_RPC_USER = os.getenv("WOW_RPC_USER", "")
|
||||
WOW_RPC_PWD = os.getenv("WOW_RPC_PWD", "")
|
||||
DEFAULT_WOW_RESTORE_HEIGHT = int(os.getenv("DEFAULT_WOW_RESTORE_HEIGHT", 450000))
|
||||
|
||||
LTC_RPC_HOST = os.getenv("LTC_RPC_HOST", "127.0.0.1")
|
||||
LTC_RPC_PORT = int(os.getenv("LTC_RPC_PORT", 19895))
|
||||
LTC_ONION_PORT = int(os.getenv("LTC_ONION_PORT", 9333))
|
||||
LTC_RPC_USER = os.getenv("LTC_RPC_USER", "")
|
||||
LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "")
|
||||
|
||||
BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1")
|
||||
BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996))
|
||||
BTC_PORT = int(os.getenv("BTC_PORT", 8333))
|
||||
BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334))
|
||||
BTC_RPC_USER = os.getenv("BTC_RPC_USER", "")
|
||||
BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "")
|
||||
|
||||
DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1")
|
||||
DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109))
|
||||
DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1")
|
||||
DCR_WALLET_RPC_PORT = int(os.getenv("DCR_WALLET_RPC_PORT", 9209))
|
||||
DCR_WALLET_PWD = os.getenv(
|
||||
"DCR_WALLET_PWD", random.randbytes(random.randint(14, 18)).hex()
|
||||
)
|
||||
DCR_RPC_USER = os.getenv("DCR_RPC_USER", "user")
|
||||
DCR_RPC_PWD = os.getenv("DCR_RPC_PWD", random.randbytes(random.randint(14, 18)).hex())
|
||||
|
||||
NMC_RPC_HOST = os.getenv("NMC_RPC_HOST", "127.0.0.1")
|
||||
NMC_RPC_PORT = int(os.getenv("NMC_RPC_PORT", 19698))
|
||||
|
||||
PIVX_RPC_HOST = os.getenv("PIVX_RPC_HOST", "127.0.0.1")
|
||||
PIVX_RPC_PORT = int(os.getenv("PIVX_RPC_PORT", 51473))
|
||||
PIVX_ONION_PORT = int(os.getenv("PIVX_ONION_PORT", 51472)) # nDefaultPort
|
||||
@@ -284,10 +270,6 @@ TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1")
|
||||
TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050))
|
||||
TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051))
|
||||
TOR_DNS_PORT = int(os.getenv("TOR_DNS_PORT", 5353))
|
||||
|
||||
|
||||
def setTorrcVars():
|
||||
global TOR_CONTROL_LISTEN_INTERFACE, TORRC_PROXY_HOST, TORRC_CONTROL_HOST, TORRC_DNS_HOST
|
||||
TOR_CONTROL_LISTEN_INTERFACE = os.getenv(
|
||||
"TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
|
||||
)
|
||||
@@ -301,7 +283,6 @@ def setTorrcVars():
|
||||
"TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
|
||||
)
|
||||
|
||||
|
||||
TEST_TOR_PROXY = toBool(
|
||||
os.getenv("TEST_TOR_PROXY", "true")
|
||||
) # Expects a known exit node
|
||||
@@ -394,12 +375,6 @@ def getWalletName(coin_params: str, default_name: str, prefix_override=None) ->
|
||||
return wallet_name
|
||||
|
||||
|
||||
def getDescriptorWalletOption(coin_params):
|
||||
ticker: str = coin_params["ticker"]
|
||||
default_option: bool = True if ticker in ("NMC",) else False
|
||||
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
|
||||
|
||||
|
||||
def getKnownVersion(coin_name: str) -> str:
|
||||
version, version_tag, _ = known_coins[coin_name]
|
||||
return version + version_tag
|
||||
@@ -518,8 +493,6 @@ def importPubkey(gpg, pubkey_filename, pubkeyurls):
|
||||
return
|
||||
except Exception as e:
|
||||
logging.warning(f"Import from file failed: {e}")
|
||||
else:
|
||||
logger.warning(f"Public key file {pubkey_filename} not found locally.")
|
||||
|
||||
for url in pubkeyurls:
|
||||
try:
|
||||
@@ -553,7 +526,7 @@ def testOnionLink():
|
||||
|
||||
def havePubkey(gpg, key_id):
|
||||
for key in gpg.list_keys():
|
||||
if key["fingerprint"] == key_id:
|
||||
if key["keyid"] == key_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -616,10 +589,8 @@ def ensureValidSignatureBy(result, signing_key_name):
|
||||
if not isValidSignature(result):
|
||||
raise ValueError("Signature verification failed.")
|
||||
|
||||
if result.fingerprint not in expected_key_ids[signing_key_name]:
|
||||
raise ValueError(
|
||||
"Signature made by unexpected key fingerprint: " + result.fingerprint
|
||||
)
|
||||
if result.key_id not in expected_key_ids[signing_key_name]:
|
||||
raise ValueError("Signature made by unexpected keyid: " + result.key_id)
|
||||
|
||||
logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).")
|
||||
|
||||
@@ -815,8 +786,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
else:
|
||||
architecture = "x86_64-apple-darwin11"
|
||||
elif USE_PLATFORM == "Windows":
|
||||
if machine == "AMD64":
|
||||
machine = "x86_64"
|
||||
architecture = machine + "-w64-mingw32"
|
||||
|
||||
release_url = "https://codeberg.org/wownero/wownero/releases/download/v{}/wownero-{}-v{}.{}".format(
|
||||
@@ -960,10 +929,16 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
% (version, assert_filename)
|
||||
)
|
||||
elif coin == "namecoin":
|
||||
release_url = f"https://www.namecoin.org/files/namecoin-core/namecoin-core-{version}/{release_filename}"
|
||||
signing_key = "Rose%20Turing"
|
||||
assert_filename = "noncodesigned.SHA256SUMS"
|
||||
assert_url = f"https://raw.githubusercontent.com/namecoin/guix.sigs/main/{version}/{signing_key}/{assert_filename}"
|
||||
release_url = "https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}".format(
|
||||
version, release_filename
|
||||
)
|
||||
assert_filename = "{}-{}-{}-build.assert".format(
|
||||
coin, os_name, version.rsplit(".", 1)[0]
|
||||
)
|
||||
assert_url = (
|
||||
"https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s"
|
||||
% (version, os_dir_name, signing_key_name, assert_filename)
|
||||
)
|
||||
elif coin == "pivx":
|
||||
release_filename = "{}-{}-{}.{}".format(coin, version, BIN_ARCH, FILE_EXT)
|
||||
release_url = (
|
||||
@@ -1052,7 +1027,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
if not os.path.exists(assert_sig_path):
|
||||
downloadFile(assert_sig_url, assert_sig_path)
|
||||
|
||||
release_hash: str = getFileHash(release_path)
|
||||
release_hash = getFileHash(release_path)
|
||||
logger.info(f"{release_filename} hash: {release_hash}")
|
||||
with (
|
||||
open(assert_path, "rb", 0) as fp,
|
||||
@@ -1150,7 +1125,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
logger.warning("Double checking Navcoin release hash.")
|
||||
with open(assert_sig_path, "rb") as fp:
|
||||
decrypted = gpg.decrypt_file(fp)
|
||||
assert release_hash in str(decrypted)
|
||||
assert release_hash.hex() in str(decrypted)
|
||||
else:
|
||||
with open(assert_sig_path, "rb") as fp:
|
||||
verified = gpg.verify_file(fp, assert_path)
|
||||
@@ -1368,8 +1343,6 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
fp.write("printtoconsole=0\n")
|
||||
fp.write("daemon=0\n")
|
||||
fp.write(f"wallet={wallet_name}\n")
|
||||
if "watch_wallet_name" in core_settings:
|
||||
fp.write("wallet={}\n".format(core_settings["watch_wallet_name"]))
|
||||
|
||||
if tor_control_password is not None:
|
||||
writeTorSettings(fp, coin, core_settings, tor_control_password)
|
||||
@@ -1426,10 +1399,6 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
)
|
||||
elif coin == "namecoin":
|
||||
fp.write("prune=2000\n")
|
||||
fp.write("deprecatedrpc=create_bdb\n")
|
||||
fp.write("addresstype=bech32\n")
|
||||
fp.write("changetype=bech32\n")
|
||||
fp.write("fallbackfee=0.001\n") # minrelaytxfee
|
||||
elif coin == "pivx":
|
||||
params_dir = os.path.join(data_dir, "pivx-params")
|
||||
downloadPIVXParams(params_dir)
|
||||
@@ -1708,11 +1677,6 @@ def printHelp():
|
||||
DEFAULT_WOW_RESTORE_HEIGHT
|
||||
)
|
||||
)
|
||||
print(
|
||||
"--walletrestoretime=n Time to restore wallets from, default:{}, -1 for now.".format(
|
||||
DEFAULT_RESTORE_TIME
|
||||
)
|
||||
)
|
||||
print(
|
||||
"--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false"
|
||||
)
|
||||
@@ -1745,8 +1709,6 @@ def printHelp():
|
||||
print(
|
||||
"--dashv20compatible Generate the same DASH wallet seed as for DASH v20 - Use only when importing an existing seed."
|
||||
)
|
||||
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.")
|
||||
|
||||
active_coins = []
|
||||
for coin_name in known_coins.keys():
|
||||
@@ -1771,8 +1733,11 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
swap_client = None
|
||||
daemons = []
|
||||
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
|
||||
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
|
||||
try:
|
||||
swap_client = BasicSwap(data_dir, settings, chain, transient_instance=True)
|
||||
swap_client = BasicSwap(
|
||||
fp, data_dir, settings, chain, transient_instance=True
|
||||
)
|
||||
if not swap_client.use_tor_proxy:
|
||||
# Cannot set -bind or -whitebind together with -listen=0
|
||||
daemon_args.append("-nolisten")
|
||||
@@ -1814,18 +1779,12 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
|
||||
def encrypt_wallet(swap_client, coin_type) -> None:
|
||||
ci = swap_client.ci(coin_type)
|
||||
ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False)
|
||||
ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD)
|
||||
ci.unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
|
||||
|
||||
def initialise_wallets(
|
||||
particl_wallet_mnemonic,
|
||||
with_coins,
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts={},
|
||||
particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy
|
||||
):
|
||||
swap_client = None
|
||||
daemons = []
|
||||
@@ -1834,8 +1793,11 @@ def initialise_wallets(
|
||||
|
||||
coins_failed_to_initialise = []
|
||||
|
||||
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
|
||||
try:
|
||||
swap_client = BasicSwap(data_dir, settings, chain, transient_instance=True)
|
||||
swap_client = BasicSwap(
|
||||
fp, data_dir, settings, chain, transient_instance=True
|
||||
)
|
||||
if not swap_client.use_tor_proxy:
|
||||
# Cannot set -bind or -whitebind together with -listen=0
|
||||
daemon_args.append("-nolisten")
|
||||
@@ -1846,7 +1808,6 @@ def initialise_wallets(
|
||||
Coins.DOGE,
|
||||
Coins.DCR,
|
||||
Coins.DASH,
|
||||
Coins.NMC,
|
||||
)
|
||||
# Always start Particl, it must be running to initialise a wallet in addcoin mode
|
||||
# Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic
|
||||
@@ -1861,7 +1822,9 @@ def initialise_wallets(
|
||||
if c == Coins.XMR:
|
||||
if coin_settings["manage_wallet_daemon"]:
|
||||
filename = (
|
||||
coin_name + "-wallet-rpc" + (".exe" if os.name == "nt" else "")
|
||||
coin_name
|
||||
+ "-wallet-rpc"
|
||||
+ (".exe" if os.name == "nt" else "")
|
||||
)
|
||||
filename: str = getWalletBinName(
|
||||
c, coin_settings, coin_name + "-wallet-rpc"
|
||||
@@ -1889,7 +1852,9 @@ def initialise_wallets(
|
||||
pass
|
||||
else:
|
||||
if coin_settings["manage_daemon"]:
|
||||
filename: str = getCoreBinName(c, coin_settings, coin_name + "d")
|
||||
filename: str = getCoreBinName(
|
||||
c, coin_settings, coin_name + "d"
|
||||
)
|
||||
coin_args = (
|
||||
["-nofindpeers", "-nostaking"] if c == Coins.PART else []
|
||||
)
|
||||
@@ -1917,13 +1882,6 @@ def initialise_wallets(
|
||||
swap_client.createCoinInterface(c)
|
||||
|
||||
if c in coins_to_create_wallets_for:
|
||||
if c == Coins.PART and "particl" not in with_coins:
|
||||
# Running addcoin with an existing particl wallet
|
||||
swap_client.waitForDaemonRPC(c, with_wallet=True)
|
||||
# Particl wallet must be unlocked to call getWalletKey
|
||||
if WALLET_ENCRYPTION_PWD != "":
|
||||
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
continue
|
||||
if c == Coins.DCR:
|
||||
if coin_settings["manage_wallet_daemon"] is False:
|
||||
continue
|
||||
@@ -1934,7 +1892,7 @@ def initialise_wallets(
|
||||
if WALLET_ENCRYPTION_PWD == ""
|
||||
else WALLET_ENCRYPTION_PWD
|
||||
)
|
||||
extra_args = [
|
||||
extra_opts = [
|
||||
'--appdata="{}"'.format(coin_settings["datadir"]),
|
||||
"--pass={}".format(dcr_password),
|
||||
]
|
||||
@@ -1943,7 +1901,7 @@ def initialise_wallets(
|
||||
args = [
|
||||
os.path.join(coin_settings["bindir"], filename),
|
||||
"--create",
|
||||
] + extra_args
|
||||
] + extra_opts
|
||||
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
|
||||
createDCRWallet(args, hex_seed, logger, threading.Event())
|
||||
continue
|
||||
@@ -1954,28 +1912,13 @@ def initialise_wallets(
|
||||
logger.info(
|
||||
f'Creating wallet "{wallet_name}" for {getCoinName(c)}.'
|
||||
)
|
||||
use_descriptors = coin_settings.get("use_descriptors", False)
|
||||
if c in (Coins.DASH,):
|
||||
# TODO: Remove when fixed
|
||||
if WALLET_ENCRYPTION_PWD != "":
|
||||
logger.warning(
|
||||
"Workaround for Dash sethdseed error if wallet is encrypted."
|
||||
) # Errors with "AddHDChainSingle failed"
|
||||
assert use_descriptors is False
|
||||
swap_client.callcoinrpc(
|
||||
c,
|
||||
"createwallet",
|
||||
[
|
||||
wallet_name,
|
||||
False,
|
||||
True,
|
||||
"",
|
||||
False,
|
||||
use_descriptors,
|
||||
],
|
||||
)
|
||||
elif c in (Coins.BTC, Coins.LTC, Coins.NMC, Coins.DOGE):
|
||||
|
||||
if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH):
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||
|
||||
use_descriptors = coin_settings.get(
|
||||
"use_descriptors", False
|
||||
)
|
||||
swap_client.callcoinrpc(
|
||||
c,
|
||||
"createwallet",
|
||||
@@ -1989,15 +1932,11 @@ def initialise_wallets(
|
||||
],
|
||||
)
|
||||
if use_descriptors:
|
||||
watch_wallet_name = coin_settings["watch_wallet_name"]
|
||||
logger.info(
|
||||
f'Creating wallet "{watch_wallet_name}" for {getCoinName(c)}.'
|
||||
)
|
||||
swap_client.callcoinrpc(
|
||||
c,
|
||||
"createwallet",
|
||||
[
|
||||
watch_wallet_name,
|
||||
coin_settings["watch_wallet_name"],
|
||||
True,
|
||||
True,
|
||||
"",
|
||||
@@ -2005,9 +1944,7 @@ def initialise_wallets(
|
||||
use_descriptors,
|
||||
],
|
||||
)
|
||||
swap_client.ci(c).unlockWallet(
|
||||
WALLET_ENCRYPTION_PWD, check_seed=False
|
||||
)
|
||||
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
else:
|
||||
swap_client.callcoinrpc(
|
||||
c,
|
||||
@@ -2038,9 +1975,11 @@ def initialise_wallets(
|
||||
swap_client.callcoinrpc(
|
||||
Coins.PART, "extkeyimportmaster", [particl_wallet_mnemonic]
|
||||
)
|
||||
# Particl wallet must be unlocked to call getWalletKey
|
||||
if WALLET_ENCRYPTION_PWD != "":
|
||||
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
|
||||
for coin_name in with_coins:
|
||||
coin_settings = settings["chainclients"][coin_name]
|
||||
c = swap_client.getCoinIdFromName(coin_name)
|
||||
if c in (Coins.PART,):
|
||||
continue
|
||||
@@ -2048,28 +1987,13 @@ def initialise_wallets(
|
||||
# initialiseWallet only sets main_wallet_seedid_
|
||||
swap_client.waitForDaemonRPC(c)
|
||||
try:
|
||||
default_restore_time = (
|
||||
-1 if generated_mnemonic else DEFAULT_RESTORE_TIME
|
||||
) # Set to -1 (now) if key is newly generated
|
||||
restore_time: int = extra_opts.get(
|
||||
"walletrestoretime", default_restore_time
|
||||
)
|
||||
|
||||
swap_client.initialiseWallet(
|
||||
c, raise_errors=True, restore_time=restore_time
|
||||
)
|
||||
if c not in (Coins.XMR, Coins.WOW):
|
||||
if restore_time == -1:
|
||||
restore_time = int(time.time())
|
||||
coin_settings["restore_time"] = restore_time
|
||||
swap_client.initialiseWallet(c, raise_errors=True)
|
||||
except Exception as e:
|
||||
coins_failed_to_initialise.append((c, e))
|
||||
if WALLET_ENCRYPTION_PWD != "" and (
|
||||
c not in coins_to_create_wallets_for or c in (Coins.DASH,)
|
||||
): # TODO: Remove DASH workaround
|
||||
if WALLET_ENCRYPTION_PWD != "" and c not in coins_to_create_wallets_for:
|
||||
try:
|
||||
swap_client.ci(c).changeWalletPassword(
|
||||
"", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False
|
||||
"", WALLET_ENCRYPTION_PWD
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
logger.warning(f"changeWalletPassword failed for {coin_name}.")
|
||||
@@ -2084,12 +2008,12 @@ def initialise_wallets(
|
||||
print("")
|
||||
for pair in coins_failed_to_initialise:
|
||||
c, e = pair
|
||||
if c in (Coins.PIVX, Coins.BCH):
|
||||
if c in (Coins.PIVX,):
|
||||
print(
|
||||
f"NOTE - Unable to initialise wallet for {getCoinName(c)}. To complete setup click 'Reseed Wallet' from the ui page once chain is synced."
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Failed to initialise wallet for {getCoinName(c)}: {e}")
|
||||
print(f"WARNING - Failed to initialise wallet for {getCoinName(c)}: {e}")
|
||||
|
||||
if "decred" in with_coins and WALLET_ENCRYPTION_PWD != "":
|
||||
print(
|
||||
@@ -2110,68 +2034,7 @@ def load_config(config_path):
|
||||
if not os.path.exists(config_path):
|
||||
exitWithError("{} does not exist".format(config_path))
|
||||
with open(config_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
BSX_ALLOW_ENV_OVERRIDE = toBool(os.getenv("BSX_ALLOW_ENV_OVERRIDE", "false"))
|
||||
|
||||
saved_env_var_settings = [
|
||||
("setup_docker_mode", "BSX_DOCKER_MODE"),
|
||||
("setup_local_tor", "BSX_LOCAL_TOR"),
|
||||
("setup_tor_control_listen_interface", "TOR_CONTROL_LISTEN_INTERFACE"),
|
||||
("setup_torrc_proxy_host", "TORRC_PROXY_HOST"),
|
||||
("setup_torrc_control_host", "TORRC_CONTROL_HOST"),
|
||||
("setup_torrc_dns_host", "TORRC_DNS_HOST"),
|
||||
("tor_proxy_host", "TOR_PROXY_HOST"),
|
||||
("tor_proxy_port", "TOR_PROXY_PORT"),
|
||||
("tor_control_port", "TOR_CONTROL_PORT"),
|
||||
]
|
||||
for setting in saved_env_var_settings:
|
||||
config_name, env_name = setting
|
||||
env_value = globals()[env_name]
|
||||
saved_config_value = settings.get(config_name, env_value)
|
||||
if saved_config_value != env_value:
|
||||
if os.getenv(env_name):
|
||||
# If the env var was manually set override the saved config if allowed else fail.
|
||||
if BSX_ALLOW_ENV_OVERRIDE:
|
||||
logger.warning(
|
||||
f"Env var {env_name} differs from saved config '{config_name}', overriding."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Env var {env_name} differs from saved config '{config_name}', set 'BSX_ALLOW_ENV_OVERRIDE' to override.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.info(f"Setting {env_name} from saved config '{config_name}'.")
|
||||
globals()[env_name] = saved_config_value
|
||||
# Recalculate env vars that depend on the changed var
|
||||
if env_name == "BSX_LOCAL_TOR":
|
||||
setTorrcVars()
|
||||
return settings
|
||||
|
||||
|
||||
def save_config(config_path, settings, add_options: bool = True) -> None:
|
||||
|
||||
if add_options is True:
|
||||
# Add to config file only if manually set
|
||||
if os.getenv("BSX_DOCKER_MODE"):
|
||||
settings["setup_docker_mode"] = BSX_DOCKER_MODE
|
||||
if os.getenv("BSX_LOCAL_TOR"):
|
||||
settings["setup_local_tor"] = BSX_LOCAL_TOR
|
||||
if os.getenv("TOR_CONTROL_LISTEN_INTERFACE"):
|
||||
settings["setup_tor_control_listen_interface"] = (
|
||||
TOR_CONTROL_LISTEN_INTERFACE
|
||||
)
|
||||
if os.getenv("TORRC_PROXY_HOST"):
|
||||
settings["setup_torrc_proxy_host"] = TORRC_PROXY_HOST
|
||||
if os.getenv("TORRC_CONTROL_HOST"):
|
||||
settings["setup_torrc_control_host"] = TORRC_CONTROL_HOST
|
||||
if os.getenv("TORRC_DNS_HOST"):
|
||||
settings["setup_torrc_dns_host"] = TORRC_DNS_HOST
|
||||
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
return json.load(fs)
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
@@ -2218,10 +2081,7 @@ def check_btc_fastsync_data(base_dir, sync_filename):
|
||||
importPubkey(gpg, pubkey_filename, pubkeyurls)
|
||||
with open(asc_file_path, "rb") as fp:
|
||||
verified = gpg.verify_file(fp)
|
||||
if (
|
||||
isValidSignature(verified)
|
||||
and verified.fingerprint in expected_key_ids["tecnovert"]
|
||||
):
|
||||
if isValidSignature(verified) and verified.key_id in expected_key_ids["tecnovert"]:
|
||||
ensureValidSignatureBy(verified, "tecnovert")
|
||||
else:
|
||||
pubkey_filename = "nicolasdorier.asc"
|
||||
@@ -2241,7 +2101,6 @@ def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None:
|
||||
|
||||
def main():
|
||||
global use_tor_proxy, with_coins_changed
|
||||
setTorrcVars()
|
||||
data_dir = None
|
||||
bin_dir = None
|
||||
port_offset = None
|
||||
@@ -2263,8 +2122,6 @@ def main():
|
||||
disable_tor = False
|
||||
initwalletsonly = False
|
||||
tor_control_password = None
|
||||
client_auth_pwd_value = None
|
||||
disable_client_auth_flag = False
|
||||
extra_opts = {}
|
||||
|
||||
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
|
||||
@@ -2391,24 +2248,13 @@ def main():
|
||||
if name == "wowrestoreheight":
|
||||
wow_restore_height = int(s[1])
|
||||
continue
|
||||
if name == "walletrestoretime":
|
||||
extra_opts["walletrestoretime"] = int(s[1])
|
||||
continue
|
||||
if name == "keysdirpath":
|
||||
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
|
||||
continue
|
||||
if name == "trustremotenode":
|
||||
extra_opts["trust_remote_node"] = toBool(s[1])
|
||||
continue
|
||||
if name == "client-auth-password":
|
||||
client_auth_pwd_value = s[1].strip('"')
|
||||
continue
|
||||
|
||||
if name == "disable-client-auth":
|
||||
disable_client_auth_flag = True
|
||||
continue
|
||||
if len(s) != 2:
|
||||
exitWithError("Unknown argument {}".format(v))
|
||||
exitWithError("Unknown argument {}".format(v))
|
||||
|
||||
if print_versions:
|
||||
@@ -2438,34 +2284,6 @@ def main():
|
||||
os.makedirs(data_dir)
|
||||
config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||
|
||||
config_exists = os.path.exists(config_path)
|
||||
if config_exists and (
|
||||
client_auth_pwd_value is not None or disable_client_auth_flag
|
||||
):
|
||||
try:
|
||||
settings = load_config(config_path)
|
||||
modified = False
|
||||
if client_auth_pwd_value is not None:
|
||||
settings["client_auth_hash"] = rfc2440_hash_password(
|
||||
client_auth_pwd_value
|
||||
)
|
||||
logger.info("Client authentication password updated.")
|
||||
modified = True
|
||||
elif disable_client_auth_flag:
|
||||
if "client_auth_hash" in settings:
|
||||
del settings["client_auth_hash"]
|
||||
logger.info("Client authentication disabled.")
|
||||
modified = True
|
||||
else:
|
||||
logger.info("Client authentication is already disabled.")
|
||||
|
||||
if modified:
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
return 0
|
||||
except Exception as e:
|
||||
exitWithError(f"Failed to update client auth settings: {e}")
|
||||
|
||||
if use_tor_proxy and extra_opts.get("no_tor_proxy", False):
|
||||
exitWithError("Can't use --usetorproxy and --notorproxy together")
|
||||
|
||||
@@ -2569,6 +2387,22 @@ def main():
|
||||
"core_version_no": getKnownVersion("bitcoin"),
|
||||
"core_version_group": 28,
|
||||
},
|
||||
"bitcoincash": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("BCH"),
|
||||
"rpchost": BCH_RPC_HOST,
|
||||
"rpcport": BCH_RPC_PORT + port_offset,
|
||||
"onionport": BCH_ONION_PORT + port_offset,
|
||||
"datadir": os.getenv("BCH_DATA_DIR", os.path.join(data_dir, "bitcoincash")),
|
||||
"bindir": os.path.join(bin_dir, "bitcoincash"),
|
||||
"port": BCH_PORT + port_offset,
|
||||
"config_filename": "bitcoin.conf",
|
||||
"use_segwit": False,
|
||||
"blocks_confirmed": 1,
|
||||
"conf_target": 2,
|
||||
"core_version_no": getKnownVersion("bitcoincash"),
|
||||
"core_version_group": 22,
|
||||
},
|
||||
"litecoin": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("LTC"),
|
||||
@@ -2584,6 +2418,22 @@ def main():
|
||||
"core_version_group": 20,
|
||||
"min_relay_fee": 0.00001,
|
||||
},
|
||||
"dogecoin": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("DOGE"),
|
||||
"rpchost": DOGE_RPC_HOST,
|
||||
"rpcport": DOGE_RPC_PORT + port_offset,
|
||||
"onionport": DOGE_ONION_PORT + port_offset,
|
||||
"datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")),
|
||||
"bindir": os.path.join(bin_dir, "dogecoin"),
|
||||
"use_segwit": False,
|
||||
"use_csv": False,
|
||||
"blocks_confirmed": 2,
|
||||
"conf_target": 2,
|
||||
"core_version_no": getKnownVersion("dogecoin"),
|
||||
"core_version_group": 23,
|
||||
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
|
||||
},
|
||||
"decred": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("DCR"),
|
||||
@@ -2611,16 +2461,14 @@ def main():
|
||||
"manage_daemon": shouldManageDaemon("NMC"),
|
||||
"rpchost": NMC_RPC_HOST,
|
||||
"rpcport": NMC_RPC_PORT + port_offset,
|
||||
"onionport": NMC_ONION_PORT + port_offset,
|
||||
"datadir": os.getenv("NMC_DATA_DIR", os.path.join(data_dir, "namecoin")),
|
||||
"bindir": os.path.join(bin_dir, "namecoin"),
|
||||
"port": NMC_PORT + port_offset,
|
||||
"use_segwit": True,
|
||||
"use_csv": True,
|
||||
"use_segwit": False,
|
||||
"use_csv": False,
|
||||
"blocks_confirmed": 1,
|
||||
"conf_target": 2,
|
||||
"core_version_no": getKnownVersion("namecoin"),
|
||||
"core_version_group": 28,
|
||||
"core_version_group": 18,
|
||||
"chain_lookups": "local",
|
||||
},
|
||||
"monero": {
|
||||
@@ -2631,7 +2479,7 @@ def main():
|
||||
"zmqport": XMR_ZMQ_PORT + port_offset,
|
||||
"walletrpcport": XMR_WALLET_RPC_PORT + port_offset,
|
||||
"rpchost": XMR_RPC_HOST,
|
||||
"trusted_daemon": extra_opts.get("trust_remote_node", True),
|
||||
"trusted_daemon": extra_opts.get("trust_remote_node", "auto"),
|
||||
"walletrpchost": XMR_WALLET_RPC_HOST,
|
||||
"walletrpcuser": XMR_WALLET_RPC_USER,
|
||||
"walletrpcpassword": XMR_WALLET_RPC_PWD,
|
||||
@@ -2646,28 +2494,6 @@ def main():
|
||||
"core_version_no": getKnownVersion("monero"),
|
||||
"core_type_group": "xmr",
|
||||
},
|
||||
"wownero": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("WOW"),
|
||||
"manage_wallet_daemon": shouldManageDaemon("WOW_WALLET"),
|
||||
"rpcport": WOW_RPC_PORT + port_offset,
|
||||
"zmqport": WOW_ZMQ_PORT + port_offset,
|
||||
"walletrpcport": WOW_WALLET_RPC_PORT + port_offset,
|
||||
"rpchost": WOW_RPC_HOST,
|
||||
"trusted_daemon": extra_opts.get("trust_remote_node", True),
|
||||
"walletrpchost": WOW_WALLET_RPC_HOST,
|
||||
"walletrpcuser": WOW_WALLET_RPC_USER,
|
||||
"walletrpcpassword": WOW_WALLET_RPC_PWD,
|
||||
"datadir": os.getenv("WOW_DATA_DIR", os.path.join(data_dir, "wownero")),
|
||||
"bindir": os.path.join(bin_dir, "wownero"),
|
||||
"restore_height": wow_restore_height,
|
||||
"blocks_confirmed": 2,
|
||||
"rpctimeout": 60,
|
||||
"walletrpctimeout": 120,
|
||||
"walletrpctimeoutlong": 300,
|
||||
"core_version_no": getKnownVersion("wownero"),
|
||||
"core_type_group": "xmr",
|
||||
},
|
||||
"pivx": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("PIVX"),
|
||||
@@ -2731,37 +2557,27 @@ def main():
|
||||
"chain_lookups": "local",
|
||||
"startup_tries": 40,
|
||||
},
|
||||
"bitcoincash": {
|
||||
"wownero": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("BCH"),
|
||||
"rpchost": BCH_RPC_HOST,
|
||||
"rpcport": BCH_RPC_PORT + port_offset,
|
||||
"onionport": BCH_ONION_PORT + port_offset,
|
||||
"datadir": os.getenv("BCH_DATA_DIR", os.path.join(data_dir, "bitcoincash")),
|
||||
"bindir": os.path.join(bin_dir, "bitcoincash"),
|
||||
"port": BCH_PORT + port_offset,
|
||||
"config_filename": "bitcoin.conf",
|
||||
"use_segwit": False,
|
||||
"blocks_confirmed": 1,
|
||||
"conf_target": 2,
|
||||
"core_version_no": getKnownVersion("bitcoincash"),
|
||||
"core_version_group": 22,
|
||||
},
|
||||
"dogecoin": {
|
||||
"connection_type": "rpc",
|
||||
"manage_daemon": shouldManageDaemon("DOGE"),
|
||||
"rpchost": DOGE_RPC_HOST,
|
||||
"rpcport": DOGE_RPC_PORT + port_offset,
|
||||
"onionport": DOGE_ONION_PORT + port_offset,
|
||||
"datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")),
|
||||
"bindir": os.path.join(bin_dir, "dogecoin"),
|
||||
"use_segwit": False,
|
||||
"use_csv": False,
|
||||
"manage_daemon": shouldManageDaemon("WOW"),
|
||||
"manage_wallet_daemon": shouldManageDaemon("WOW_WALLET"),
|
||||
"rpcport": WOW_RPC_PORT + port_offset,
|
||||
"zmqport": WOW_ZMQ_PORT + port_offset,
|
||||
"walletrpcport": WOW_WALLET_RPC_PORT + port_offset,
|
||||
"rpchost": WOW_RPC_HOST,
|
||||
"trusted_daemon": extra_opts.get("trust_remote_node", "auto"),
|
||||
"walletrpchost": WOW_WALLET_RPC_HOST,
|
||||
"walletrpcuser": WOW_WALLET_RPC_USER,
|
||||
"walletrpcpassword": WOW_WALLET_RPC_PWD,
|
||||
"datadir": os.getenv("WOW_DATA_DIR", os.path.join(data_dir, "wownero")),
|
||||
"bindir": os.path.join(bin_dir, "wownero"),
|
||||
"restore_height": wow_restore_height,
|
||||
"blocks_confirmed": 2,
|
||||
"conf_target": 2,
|
||||
"core_version_no": getKnownVersion("dogecoin"),
|
||||
"core_version_group": 23,
|
||||
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
|
||||
"rpctimeout": 60,
|
||||
"walletrpctimeout": 120,
|
||||
"walletrpctimeoutlong": 300,
|
||||
"core_version_no": getKnownVersion("wownero"),
|
||||
"core_type_group": "xmr",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2785,8 +2601,9 @@ def main():
|
||||
coin_settings["wallet_name"] = set_name
|
||||
|
||||
ticker: str = coin_params["ticker"]
|
||||
if getDescriptorWalletOption(coin_params):
|
||||
if coin_id not in (Coins.BTC, Coins.NMC):
|
||||
if toBool(os.getenv(ticker + "_USE_DESCRIPTORS", False)):
|
||||
|
||||
if coin_id not in (Coins.BTC,):
|
||||
raise ValueError(f"Descriptor wallet unavailable for {coin_name}")
|
||||
|
||||
coin_settings["use_descriptors"] = True
|
||||
@@ -2854,7 +2671,6 @@ def main():
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
@@ -2876,15 +2692,15 @@ def main():
|
||||
settings, coin, tor_control_password, enable=True, extra_opts=extra_opts
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
|
||||
if disable_tor:
|
||||
logger.info("Disabling TOR")
|
||||
settings = load_config(config_path)
|
||||
if not settings.get("use_tor", False):
|
||||
logger.info("TOR is not enabled.") # Continue anyway to clear any config
|
||||
settings["use_tor"] = False
|
||||
for coin in settings["chainclients"]:
|
||||
modify_tor_config(
|
||||
@@ -2895,7 +2711,9 @@ def main():
|
||||
extra_opts=extra_opts,
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
|
||||
@@ -2920,7 +2738,9 @@ def main():
|
||||
if "manage_wallet_daemon" in coin_settings:
|
||||
coin_settings["manage_wallet_daemon"] = False
|
||||
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
|
||||
@@ -2942,7 +2762,8 @@ def main():
|
||||
coin_settings["manage_daemon"] = True
|
||||
if "manage_wallet_daemon" in coin_settings:
|
||||
coin_settings["manage_wallet_daemon"] = True
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
exitWithError("{} is already in the settings file".format(add_coin))
|
||||
@@ -2957,6 +2778,7 @@ def main():
|
||||
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
|
||||
|
||||
settings["chainclients"][add_coin] = chainclients[add_coin]
|
||||
settings["use_tor_proxy"] = use_tor_proxy
|
||||
|
||||
if not no_cores:
|
||||
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
|
||||
@@ -2976,10 +2798,10 @@ def main():
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
logger.info(f"Done. Coin {add_coin} successfully added.")
|
||||
return 0
|
||||
@@ -2998,7 +2820,8 @@ def main():
|
||||
if c not in settings["chainclients"]:
|
||||
settings["chainclients"][c] = chainclients[c]
|
||||
elif upgrade_cores:
|
||||
settings = load_config(config_path)
|
||||
with open(config_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
with_coins_start = with_coins
|
||||
if not with_coins_changed:
|
||||
@@ -3047,8 +2870,8 @@ def main():
|
||||
# Run second loop to update, so all versions are logged together.
|
||||
# Backup settings
|
||||
old_config_path = config_path[:-5] + "_" + str(int(time.time())) + ".json"
|
||||
save_config(old_config_path, settings, add_options=False)
|
||||
|
||||
with open(old_config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
for c in with_coins:
|
||||
prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
|
||||
current_coin_settings = chainclients[c]
|
||||
@@ -3061,7 +2884,8 @@ def main():
|
||||
settings["chainclients"][c][
|
||||
"core_version_group"
|
||||
] = current_version_group
|
||||
save_config(config_path, settings)
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
@@ -3100,10 +2924,6 @@ def main():
|
||||
tor_control_password = generate_salt(24)
|
||||
addTorSettings(settings, tor_control_password)
|
||||
|
||||
if client_auth_pwd_value is not None:
|
||||
settings["client_auth_hash"] = rfc2440_hash_password(client_auth_pwd_value)
|
||||
logger.info("Client authentication password set.")
|
||||
|
||||
if not no_cores:
|
||||
for c in with_coins:
|
||||
prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
|
||||
@@ -3115,21 +2935,16 @@ def main():
|
||||
for c in with_coins:
|
||||
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts)
|
||||
|
||||
with open(config_path, "w") as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
if particl_wallet_mnemonic == "none":
|
||||
save_config(config_path, settings)
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
|
||||
initialise_wallets(
|
||||
particl_wallet_mnemonic,
|
||||
with_coins,
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy
|
||||
)
|
||||
save_config(config_path, settings)
|
||||
print("Done.")
|
||||
|
||||
|
||||
|
||||
@@ -265,19 +265,13 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
|
||||
# As BCH may use port 8334, disable it here.
|
||||
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
|
||||
if (
|
||||
prepare is False
|
||||
and use_tor_proxy is False
|
||||
and coin_id in (Coins.BTC, Coins.NMC)
|
||||
):
|
||||
if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
|
||||
port: int = coin_settings.get("port", 8333)
|
||||
extra_args.append(f"--bind=0.0.0.0:{port}")
|
||||
return extra_args
|
||||
|
||||
|
||||
def runClient(
|
||||
data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
|
||||
) -> int:
|
||||
def runClient(fp, data_dir, chain, start_only_coins):
|
||||
global swap_client, logger
|
||||
daemons = []
|
||||
pids = []
|
||||
@@ -302,7 +296,7 @@ def runClient(
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
swap_client = BasicSwap(data_dir, settings, chain, log_name=log_prefix)
|
||||
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
||||
logger = swap_client.log
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
@@ -440,7 +434,7 @@ def runClient(
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
|
||||
continue # /decred
|
||||
|
||||
@@ -486,6 +480,7 @@ def runClient(
|
||||
else cfg.DEFAULT_ALLOW_CORS
|
||||
)
|
||||
thread_http = HttpThread(
|
||||
fp,
|
||||
settings["htmlhost"],
|
||||
settings["htmlport"],
|
||||
allow_cors,
|
||||
@@ -534,7 +529,7 @@ def runClient(
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
swap_client.log.info(f"Interrupting {d.handle.pid}")
|
||||
swap_client.log.info("Interrupting {}".format(d.handle.pid))
|
||||
try:
|
||||
d.handle.send_signal(
|
||||
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||
@@ -551,9 +546,6 @@ def runClient(
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error: {e}")
|
||||
|
||||
fail_code: int = swap_client.fail_code
|
||||
del swap_client
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
lines = fd.read().split("\n")
|
||||
@@ -567,13 +559,9 @@ def runClient(
|
||||
with open(pids_path, "w") as fd:
|
||||
fd.write(still_running)
|
||||
|
||||
return fail_code
|
||||
|
||||
|
||||
def printVersion():
|
||||
logger.info(
|
||||
f"Basicswap version: {__version__}",
|
||||
)
|
||||
logger.info("Basicswap version: %s", __version__)
|
||||
|
||||
|
||||
def printHelp():
|
||||
@@ -581,7 +569,9 @@ def printHelp():
|
||||
print("\n--help, -h Print help.")
|
||||
print("--version, -v Print version.")
|
||||
print(
|
||||
f"--datadir=PATH Path to basicswap data directory, default:{cfg.BASICSWAP_DATADIR}."
|
||||
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
|
||||
cfg.BASICSWAP_DATADIR
|
||||
)
|
||||
)
|
||||
print("--mainnet Run in mainnet mode.")
|
||||
print("--testnet Run in testnet mode.")
|
||||
@@ -589,18 +579,16 @@ def printHelp():
|
||||
print(
|
||||
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
|
||||
)
|
||||
print("--logprefix Specify log prefix.")
|
||||
|
||||
|
||||
def main():
|
||||
data_dir = None
|
||||
chain = "mainnet"
|
||||
start_only_coins = set()
|
||||
log_prefix: str = "BasicSwap"
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != "-":
|
||||
logger.warning(f"Unknown argument {v}")
|
||||
logger.warning("Unknown argument %s", v)
|
||||
continue
|
||||
|
||||
s = v.split("=")
|
||||
@@ -625,9 +613,6 @@ def main():
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
continue
|
||||
if name == "logprefix":
|
||||
log_prefix = s[1]
|
||||
continue
|
||||
if name == "startonlycoin":
|
||||
for coin in [s.lower() for s in s[1].split(",")]:
|
||||
if is_known_coin(coin) is False:
|
||||
@@ -635,7 +620,7 @@ def main():
|
||||
start_only_coins.add(coin)
|
||||
continue
|
||||
|
||||
logger.warning(f"Unknown argument {v}")
|
||||
logger.warning("Unknown argument %s", v)
|
||||
|
||||
if os.name == "nt":
|
||||
logger.warning(
|
||||
@@ -644,17 +629,20 @@ def main():
|
||||
|
||||
if data_dir is None:
|
||||
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
|
||||
logger.info(f"Using datadir: {data_dir}")
|
||||
logger.info(f"Chain: {chain}")
|
||||
logger.info("Using datadir: %s", data_dir)
|
||||
logger.info("Chain: %s", chain)
|
||||
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
|
||||
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix)
|
||||
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
|
||||
logger.info(
|
||||
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
|
||||
)
|
||||
runClient(fp, data_dir, chain, start_only_coins)
|
||||
|
||||
print("Done.")
|
||||
return fail_code
|
||||
return swap_client.fail_code if swap_client is not None else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -35,12 +35,6 @@ class Coins(IntEnum):
|
||||
DOGE = 18
|
||||
|
||||
|
||||
class Fiat(IntEnum):
|
||||
USD = -1
|
||||
GBP = -2
|
||||
EUR = -3
|
||||
|
||||
|
||||
chainparams = {
|
||||
Coins.PART: {
|
||||
"name": "particl",
|
||||
@@ -58,8 +52,6 @@ chainparams = {
|
||||
"bip44": 44,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x696E82D1,
|
||||
"ext_secret_key_prefix": 0x8F1DAEB8,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 51935,
|
||||
@@ -71,8 +63,6 @@ chainparams = {
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0xE1427800,
|
||||
"ext_secret_key_prefix": 0x04889478,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 51936,
|
||||
@@ -84,8 +74,6 @@ chainparams = {
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0xE1427800,
|
||||
"ext_secret_key_prefix": 0x04889478,
|
||||
},
|
||||
},
|
||||
Coins.BTC: {
|
||||
@@ -257,38 +245,29 @@ chainparams = {
|
||||
"rpcport": 8336,
|
||||
"pubkey_address": 52,
|
||||
"script_address": 13,
|
||||
"key_prefix": 180,
|
||||
"hrp": "nc",
|
||||
"bip44": 7,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x0488B21E, # base58Prefixes[EXT_PUBLIC_KEY]
|
||||
"ext_secret_key_prefix": 0x0488ADE4,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 18336,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "tn",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "ncrt",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
},
|
||||
Coins.XMR: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2025 The Basicswap developers
|
||||
# Copyright (c) 2019-2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -40,6 +40,13 @@ DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
|
||||
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
|
||||
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix)
|
||||
|
||||
NAMECOIN_BINDIR = os.path.expanduser(
|
||||
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
|
||||
)
|
||||
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
|
||||
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
|
||||
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
|
||||
|
||||
XMR_BINDIR = os.path.expanduser(
|
||||
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,22 +5,20 @@
|
||||
"""Helpful routines for regression testing."""
|
||||
|
||||
from base64 import b64encode
|
||||
from binascii import unhexlify
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from subprocess import CalledProcessError
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
|
||||
from . import coverage
|
||||
from .authproxy import AuthServiceProxy, JSONRPCException
|
||||
from collections.abc import Callable
|
||||
from typing import Optional
|
||||
from io import BytesIO
|
||||
|
||||
logger = logging.getLogger("TestFramework.utils")
|
||||
|
||||
@@ -30,46 +28,23 @@ logger = logging.getLogger("TestFramework.utils")
|
||||
|
||||
def assert_approx(v, vexp, vspan=0.00001):
|
||||
"""Assert that `v` is within `vspan` of `vexp`"""
|
||||
if isinstance(v, Decimal) or isinstance(vexp, Decimal):
|
||||
v=Decimal(v)
|
||||
vexp=Decimal(vexp)
|
||||
vspan=Decimal(vspan)
|
||||
if v < vexp - vspan:
|
||||
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
|
||||
if v > vexp + vspan:
|
||||
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
|
||||
|
||||
|
||||
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
|
||||
"""Assert the fee is in range."""
|
||||
assert isinstance(tx_size, int)
|
||||
target_fee = get_fee(tx_size, feerate_BTC_kvB)
|
||||
def assert_fee_amount(fee, tx_size, fee_per_kB):
|
||||
"""Assert the fee was in range"""
|
||||
target_fee = round(tx_size * fee_per_kB / 1000, 8)
|
||||
if fee < target_fee:
|
||||
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
|
||||
# allow the wallet's estimation to be at most 2 bytes off
|
||||
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
|
||||
if fee > high_fee:
|
||||
if fee > (tx_size + 2) * fee_per_kB / 1000:
|
||||
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
|
||||
|
||||
|
||||
def summarise_dict_differences(thing1, thing2):
|
||||
if not isinstance(thing1, dict) or not isinstance(thing2, dict):
|
||||
return thing1, thing2
|
||||
d1, d2 = {}, {}
|
||||
for k in sorted(thing1.keys()):
|
||||
if k not in thing2:
|
||||
d1[k] = thing1[k]
|
||||
elif thing1[k] != thing2[k]:
|
||||
d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
|
||||
for k in sorted(thing2.keys()):
|
||||
if k not in thing1:
|
||||
d2[k] = thing2[k]
|
||||
return d1, d2
|
||||
|
||||
def assert_equal(thing1, thing2, *args):
|
||||
if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
|
||||
d1,d2 = summarise_dict_differences(thing1, thing2)
|
||||
raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2))
|
||||
if thing1 != thing2 or any(thing1 != arg for arg in args):
|
||||
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
|
||||
|
||||
@@ -104,7 +79,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds):
|
||||
raise AssertionError("No exception raised")
|
||||
|
||||
|
||||
def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
|
||||
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
|
||||
"""Execute a process and asserts the process return code and output.
|
||||
|
||||
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
|
||||
@@ -112,9 +87,9 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
|
||||
no CalledProcessError was raised or if the return code and output are not as expected.
|
||||
|
||||
Args:
|
||||
returncode: the process return code.
|
||||
output: [a substring of] the process output.
|
||||
fun: the function to call. This should execute a process.
|
||||
returncode (int): the process return code.
|
||||
output (string): [a substring of] the process output.
|
||||
fun (function): the function to call. This should execute a process.
|
||||
args*: positional arguments for the function.
|
||||
kwds**: named arguments for the function.
|
||||
"""
|
||||
@@ -129,7 +104,7 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
|
||||
raise AssertionError("No exception raised")
|
||||
|
||||
|
||||
def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
|
||||
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
|
||||
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
|
||||
|
||||
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
|
||||
@@ -137,11 +112,11 @@ def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Ca
|
||||
no JSONRPCException was raised or if the error code/message are not as expected.
|
||||
|
||||
Args:
|
||||
code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
|
||||
Set to None if checking the error code is not required.
|
||||
message: [a substring of] the error string returned by the RPC call.
|
||||
Set to None if checking the error string is not required.
|
||||
fun: the function to call. This should be the name of an RPC.
|
||||
code (int), optional: the error code returned by the RPC call (defined
|
||||
in src/rpc/protocol.h). Set to None if checking the error code is not required.
|
||||
message (string), optional: [a substring of] the error string returned by the
|
||||
RPC call. Set to None if checking the error string is not required.
|
||||
fun (function): the function to call. This should be the name of an RPC.
|
||||
args*: positional arguments for the function.
|
||||
kwds**: named arguments for the function.
|
||||
"""
|
||||
@@ -228,45 +203,29 @@ def check_json_precision():
|
||||
raise RuntimeError("JSON encode/decode loses precision")
|
||||
|
||||
|
||||
def EncodeDecimal(o):
|
||||
if isinstance(o, Decimal):
|
||||
return str(o)
|
||||
raise TypeError(repr(o) + " is not JSON serializable")
|
||||
|
||||
|
||||
def count_bytes(hex_string):
|
||||
return len(bytearray.fromhex(hex_string))
|
||||
|
||||
|
||||
def hex_str_to_bytes(hex_str):
|
||||
return unhexlify(hex_str.encode('ascii'))
|
||||
|
||||
|
||||
def str_to_b64str(string):
|
||||
return b64encode(string.encode('utf-8')).decode('ascii')
|
||||
|
||||
|
||||
def ceildiv(a, b):
|
||||
"""
|
||||
Divide 2 ints and round up to next int rather than round down
|
||||
Implementation requires python integers, which have a // operator that does floor division.
|
||||
Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
|
||||
"""
|
||||
assert isinstance(a, int)
|
||||
assert isinstance(b, int)
|
||||
return -(-a // b)
|
||||
|
||||
|
||||
def get_fee(tx_size, feerate_btc_kvb):
|
||||
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
|
||||
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
|
||||
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
|
||||
return target_fee_sat / Decimal(1e8) # Return result in BTC
|
||||
|
||||
|
||||
def satoshi_round(amount):
|
||||
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||
|
||||
|
||||
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
|
||||
"""Sleep until the predicate resolves to be True.
|
||||
|
||||
Warning: Note that this method is not recommended to be used in tests as it is
|
||||
not aware of the context of the test framework. Using the `wait_until()` members
|
||||
from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
|
||||
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
|
||||
`p2p.py` has a preset lock.
|
||||
"""
|
||||
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
|
||||
if attempts == float('inf') and timeout == float('inf'):
|
||||
timeout = 60
|
||||
timeout = timeout * timeout_factor
|
||||
@@ -294,16 +253,6 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa
|
||||
raise RuntimeError('Unreachable')
|
||||
|
||||
|
||||
def sha256sum_file(filename):
|
||||
h = hashlib.sha256()
|
||||
with open(filename, 'rb') as f:
|
||||
d = f.read(4096)
|
||||
while len(d) > 0:
|
||||
h.update(d)
|
||||
d = f.read(4096)
|
||||
return h.digest()
|
||||
|
||||
|
||||
# RPC/P2P connection constants and functions
|
||||
############################################
|
||||
|
||||
@@ -320,15 +269,15 @@ class PortSeed:
|
||||
n = None
|
||||
|
||||
|
||||
def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, coveragedir: Optional[str]=None) -> coverage.AuthServiceProxyWrapper:
|
||||
def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
|
||||
"""
|
||||
Args:
|
||||
url: URL of the RPC server to call
|
||||
node_number: the node number (or id) that this calls to
|
||||
url (str): URL of the RPC server to call
|
||||
node_number (int): the node number (or id) that this calls to
|
||||
|
||||
Kwargs:
|
||||
timeout: HTTP timeout in seconds
|
||||
coveragedir: Directory
|
||||
timeout (int): HTTP timeout in seconds
|
||||
coveragedir (str): Directory
|
||||
|
||||
Returns:
|
||||
AuthServiceProxy. convenience object for making RPC calls.
|
||||
@@ -339,10 +288,11 @@ def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, co
|
||||
proxy_kwargs['timeout'] = int(timeout)
|
||||
|
||||
proxy = AuthServiceProxy(url, **proxy_kwargs)
|
||||
proxy.url = url # store URL on proxy for info
|
||||
|
||||
coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None
|
||||
|
||||
return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile)
|
||||
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
|
||||
|
||||
|
||||
def p2p_port(n):
|
||||
@@ -371,76 +321,38 @@ def rpc_url(datadir, i, chain, rpchost):
|
||||
################
|
||||
|
||||
|
||||
def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
|
||||
def initialize_datadir(dirname, n, chain):
|
||||
datadir = get_datadir_path(dirname, n)
|
||||
if not os.path.isdir(datadir):
|
||||
os.makedirs(datadir)
|
||||
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
|
||||
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
|
||||
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
|
||||
return datadir
|
||||
|
||||
|
||||
def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
|
||||
# Translate chain subdirectory name to config name
|
||||
if chain == 'testnet':
|
||||
# Translate chain name to config name
|
||||
if chain == 'testnet3':
|
||||
chain_name_conf_arg = 'testnet'
|
||||
chain_name_conf_section = 'test'
|
||||
else:
|
||||
chain_name_conf_arg = chain
|
||||
chain_name_conf_section = chain
|
||||
with open(config_path, 'w', encoding='utf8') as f:
|
||||
if chain_name_conf_arg:
|
||||
with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
|
||||
f.write("{}=1\n".format(chain_name_conf_arg))
|
||||
if chain_name_conf_section:
|
||||
f.write("[{}]\n".format(chain_name_conf_section))
|
||||
f.write("port=" + str(p2p_port(n)) + "\n")
|
||||
f.write("rpcport=" + str(rpc_port(n)) + "\n")
|
||||
# Disable server-side timeouts to avoid intermittent issues
|
||||
f.write("rpcservertimeout=99000\n")
|
||||
f.write("rpcdoccheck=1\n")
|
||||
f.write("fallbackfee=0.0002\n")
|
||||
f.write("server=1\n")
|
||||
f.write("keypool=1\n")
|
||||
f.write("discover=0\n")
|
||||
f.write("dnsseed=0\n")
|
||||
f.write("fixedseeds=0\n")
|
||||
f.write("listenonion=0\n")
|
||||
# Increase peertimeout to avoid disconnects while using mocktime.
|
||||
# peertimeout is measured in mock time, so setting it large enough to
|
||||
# cover any duration in mock time is sufficient. It can be overridden
|
||||
# in tests.
|
||||
f.write("peertimeout=999999999\n")
|
||||
f.write("printtoconsole=0\n")
|
||||
f.write("upnp=0\n")
|
||||
f.write("natpmp=0\n")
|
||||
f.write("shrinkdebugfile=0\n")
|
||||
f.write("deprecatedrpc=create_bdb\n") # Required to run the tests
|
||||
# To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync
|
||||
f.write("unsafesqlitesync=1\n")
|
||||
if disable_autoconnect:
|
||||
f.write("connect=0\n")
|
||||
f.write(extra_config)
|
||||
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
|
||||
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
|
||||
return datadir
|
||||
|
||||
|
||||
def get_datadir_path(dirname, n):
|
||||
return pathlib.Path(dirname) / f"node{n}"
|
||||
|
||||
|
||||
def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]:
|
||||
"""Return os-specific environment variables that can be set to make the
|
||||
GetDefaultDataDir() function return a datadir path under the provided
|
||||
temp_dir, as well as the complete path it would return."""
|
||||
if platform.system() == "Windows":
|
||||
env = dict(APPDATA=str(temp_dir))
|
||||
datadir = temp_dir / "Particl"
|
||||
else:
|
||||
env = dict(HOME=str(temp_dir))
|
||||
if platform.system() == "Darwin":
|
||||
datadir = temp_dir / "Library/Application Support/Particl"
|
||||
else:
|
||||
datadir = temp_dir / ".particl"
|
||||
return env, datadir
|
||||
return os.path.join(dirname, "node" + str(n))
|
||||
|
||||
|
||||
def append_config(datadir, options):
|
||||
@@ -483,7 +395,7 @@ def delete_cookie_file(datadir, chain):
|
||||
|
||||
def softfork_active(node, key):
|
||||
"""Return whether a softfork is active."""
|
||||
return node.getdeploymentinfo()['deployments'][key]['active']
|
||||
return node.getblockchaininfo()['softforks'][key]['active']
|
||||
|
||||
|
||||
def set_node_times(nodes, t):
|
||||
@@ -491,51 +403,208 @@ def set_node_times(nodes, t):
|
||||
node.setmocktime(t)
|
||||
|
||||
|
||||
def check_node_connections(*, node, num_in, num_out):
|
||||
info = node.getnetworkinfo()
|
||||
assert_equal(info["connections_in"], num_in)
|
||||
assert_equal(info["connections_out"], num_out)
|
||||
def disconnect_nodes(from_connection, node_num):
|
||||
def get_peer_ids():
|
||||
result = []
|
||||
for peer in from_connection.getpeerinfo():
|
||||
if "testnode{}".format(node_num) in peer['subver']:
|
||||
result.append(peer['id'])
|
||||
return result
|
||||
|
||||
peer_ids = get_peer_ids()
|
||||
if not peer_ids:
|
||||
logger.warning("disconnect_nodes: {} and {} were not connected".format(
|
||||
from_connection.index,
|
||||
node_num,
|
||||
))
|
||||
return
|
||||
for peer_id in peer_ids:
|
||||
try:
|
||||
from_connection.disconnectnode(nodeid=peer_id)
|
||||
except JSONRPCException as e:
|
||||
# If this node is disconnected between calculating the peer id
|
||||
# and issuing the disconnect, don't worry about it.
|
||||
# This avoids a race condition if we're mass-disconnecting peers.
|
||||
if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
|
||||
raise
|
||||
|
||||
# wait to disconnect
|
||||
wait_until(lambda: not get_peer_ids(), timeout=5)
|
||||
|
||||
|
||||
def connect_nodes(from_connection, node_num):
|
||||
ip_port = "127.0.0.1:" + str(p2p_port(node_num))
|
||||
from_connection.addnode(ip_port, "onetry")
|
||||
# poll until version handshake complete to avoid race conditions
|
||||
# with transaction relaying
|
||||
# See comments in net_processing:
|
||||
# * Must have a version message before anything else
|
||||
# * Must have a verack message before anything else
|
||||
wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo()))
|
||||
wait_until(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo()))
|
||||
|
||||
|
||||
# Transaction/Block functions
|
||||
#############################
|
||||
|
||||
|
||||
def find_output(node, txid, amount, *, blockhash=None):
|
||||
"""
|
||||
Return index to output of txid with value amount
|
||||
Raises exception if there is none.
|
||||
"""
|
||||
txdata = node.getrawtransaction(txid, 1, blockhash)
|
||||
for i in range(len(txdata["vout"])):
|
||||
if txdata["vout"][i]["value"] == amount:
|
||||
return i
|
||||
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))
|
||||
|
||||
|
||||
def gather_inputs(from_node, amount_needed, confirmations_required=1):
|
||||
"""
|
||||
Return a random set of unspent txouts that are enough to pay amount_needed
|
||||
"""
|
||||
assert confirmations_required >= 0
|
||||
utxo = from_node.listunspent(confirmations_required)
|
||||
random.shuffle(utxo)
|
||||
inputs = []
|
||||
total_in = Decimal("0.00000000")
|
||||
while total_in < amount_needed and len(utxo) > 0:
|
||||
t = utxo.pop()
|
||||
total_in += t["amount"]
|
||||
inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]})
|
||||
if total_in < amount_needed:
|
||||
raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in))
|
||||
return (total_in, inputs)
|
||||
|
||||
|
||||
def make_change(from_node, amount_in, amount_out, fee):
|
||||
"""
|
||||
Create change output(s), return them
|
||||
"""
|
||||
outputs = {}
|
||||
amount = amount_out + fee
|
||||
change = amount_in - amount
|
||||
if change > amount * 2:
|
||||
# Create an extra change output to break up big inputs
|
||||
change_address = from_node.getnewaddress()
|
||||
# Split change in two, being careful of rounding:
|
||||
outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||
change = amount_in - amount - outputs[change_address]
|
||||
if change > 0:
|
||||
outputs[from_node.getnewaddress()] = change
|
||||
return outputs
|
||||
|
||||
|
||||
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||
"""
|
||||
Create a random transaction.
|
||||
Returns (txid, hex-encoded-transaction-data, fee)
|
||||
"""
|
||||
from_node = random.choice(nodes)
|
||||
to_node = random.choice(nodes)
|
||||
fee = min_fee + fee_increment * random.randint(0, fee_variants)
|
||||
|
||||
(total_in, inputs) = gather_inputs(from_node, amount + fee)
|
||||
outputs = make_change(from_node, total_in, amount, fee)
|
||||
outputs[to_node.getnewaddress()] = float(amount)
|
||||
|
||||
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||
signresult = from_node.signrawtransactionwithwallet(rawtx)
|
||||
txid = from_node.sendrawtransaction(signresult["hex"], 0)
|
||||
|
||||
return (txid, signresult["hex"], fee)
|
||||
|
||||
|
||||
# Helper to create at least "count" utxos
|
||||
# Pass in a fee that is sufficient for relay and mining new transactions.
|
||||
def create_confirmed_utxos(fee, node, count):
|
||||
to_generate = int(0.5 * count) + 101
|
||||
while to_generate > 0:
|
||||
node.generate(min(25, to_generate))
|
||||
to_generate -= 25
|
||||
utxos = node.listunspent()
|
||||
iterations = count - len(utxos)
|
||||
addr1 = node.getnewaddress()
|
||||
addr2 = node.getnewaddress()
|
||||
if iterations <= 0:
|
||||
return utxos
|
||||
for i in range(iterations):
|
||||
t = utxos.pop()
|
||||
inputs = []
|
||||
inputs.append({"txid": t["txid"], "vout": t["vout"]})
|
||||
outputs = {}
|
||||
send_value = t['amount'] - fee
|
||||
outputs[addr1] = satoshi_round(send_value / 2)
|
||||
outputs[addr2] = satoshi_round(send_value / 2)
|
||||
raw_tx = node.createrawtransaction(inputs, outputs)
|
||||
signed_tx = node.signrawtransactionwithwallet(raw_tx)["hex"]
|
||||
node.sendrawtransaction(signed_tx)
|
||||
|
||||
while (node.getmempoolinfo()['size'] > 0):
|
||||
node.generate(1)
|
||||
|
||||
utxos = node.listunspent()
|
||||
assert len(utxos) >= count
|
||||
return utxos
|
||||
|
||||
|
||||
# Create large OP_RETURN txouts that can be appended to a transaction
|
||||
# to make it large (helper for constructing large transactions). The
|
||||
# total serialized size of the txouts is about 66k vbytes.
|
||||
# to make it large (helper for constructing large transactions).
|
||||
def gen_return_txouts():
|
||||
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
|
||||
# So we have big transactions (and therefore can't fit very many into each block)
|
||||
# create one script_pubkey
|
||||
script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes
|
||||
for i in range(512):
|
||||
script_pubkey = script_pubkey + "01"
|
||||
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
|
||||
txouts = []
|
||||
from .messages import CTxOut
|
||||
from .script import CScript, OP_RETURN
|
||||
txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))]
|
||||
assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456)
|
||||
txout = CTxOut()
|
||||
txout.nValue = 0
|
||||
txout.scriptPubKey = hex_str_to_bytes(script_pubkey)
|
||||
for k in range(128):
|
||||
txouts.append(txout)
|
||||
return txouts
|
||||
|
||||
|
||||
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
|
||||
# transaction to make it large. See gen_return_txouts() above.
|
||||
def create_lots_of_big_transactions(mini_wallet, node, fee, tx_batch_size, txouts, utxos=None):
|
||||
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
|
||||
addr = node.getnewaddress()
|
||||
txids = []
|
||||
use_internal_utxos = utxos is None
|
||||
for _ in range(tx_batch_size):
|
||||
tx = mini_wallet.create_self_transfer(
|
||||
utxo_to_spend=None if use_internal_utxos else utxos.pop(),
|
||||
fee=fee,
|
||||
)["tx"]
|
||||
tx.vout.extend(txouts)
|
||||
res = node.testmempoolaccept([tx.serialize().hex()])[0]
|
||||
assert_equal(res['fees']['base'], fee)
|
||||
txids.append(node.sendrawtransaction(tx.serialize().hex()))
|
||||
from .messages import CTransaction
|
||||
for _ in range(num):
|
||||
t = utxos.pop()
|
||||
inputs = [{"txid": t["txid"], "vout": t["vout"]}]
|
||||
outputs = {}
|
||||
change = t['amount'] - fee
|
||||
outputs[addr] = satoshi_round(change)
|
||||
rawtx = node.createrawtransaction(inputs, outputs)
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(hex_str_to_bytes(rawtx)))
|
||||
for txout in txouts:
|
||||
tx.vout.append(txout)
|
||||
newtx = tx.serialize().hex()
|
||||
signresult = node.signrawtransactionwithwallet(newtx, None, "NONE")
|
||||
txid = node.sendrawtransaction(signresult["hex"], 0)
|
||||
txids.append(txid)
|
||||
return txids
|
||||
|
||||
|
||||
def mine_large_block(test_framework, mini_wallet, node):
|
||||
def mine_large_block(node, utxos=None):
|
||||
# generate a 66k transaction,
|
||||
# and 14 of them is close to the 1MB block limit
|
||||
num = 14
|
||||
txouts = gen_return_txouts()
|
||||
utxos = utxos if utxos is not None else []
|
||||
if len(utxos) < num:
|
||||
utxos.clear()
|
||||
utxos.extend(node.listunspent())
|
||||
fee = 100 * node.getnetworkinfo()["relayfee"]
|
||||
create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts)
|
||||
test_framework.generate(node, 1)
|
||||
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
|
||||
node.generate(1)
|
||||
|
||||
|
||||
def find_vout_for_address(node, txid, addr):
|
||||
@@ -545,6 +614,11 @@ def find_vout_for_address(node, txid, addr):
|
||||
"""
|
||||
tx = node.getrawtransaction(txid, True)
|
||||
for i in range(len(tx["vout"])):
|
||||
if addr == tx["vout"][i]["scriptPubKey"]["address"]:
|
||||
scriptPubKey = tx["vout"][i]["scriptPubKey"]
|
||||
if "addresses" in scriptPubKey:
|
||||
if any([addr == a for a in scriptPubKey["addresses"]]):
|
||||
return i
|
||||
elif "address" in scriptPubKey:
|
||||
if addr == scriptPubKey["address"]:
|
||||
return i
|
||||
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 28
|
||||
CURRENT_DB_DATA_VERSION = 6
|
||||
CURRENT_DB_VERSION = 25
|
||||
CURRENT_DB_DATA_VERSION = 5
|
||||
|
||||
|
||||
class Concepts(IntEnum):
|
||||
@@ -174,7 +174,6 @@ class Offer(Table):
|
||||
secret_hash = Column("blob")
|
||||
|
||||
addr_from = Column("string")
|
||||
pk_from = Column("blob")
|
||||
addr_to = Column("string")
|
||||
created_at = Column("integer")
|
||||
expire_at = Column("integer")
|
||||
@@ -184,7 +183,6 @@ class Offer(Table):
|
||||
|
||||
amount_negotiable = Column("bool")
|
||||
rate_negotiable = Column("bool")
|
||||
auto_accept_type = Column("integer")
|
||||
|
||||
# Local fields
|
||||
auto_accept_bids = Column("bool")
|
||||
@@ -217,7 +215,6 @@ class Bid(Table):
|
||||
created_at = Column("integer")
|
||||
expire_at = Column("integer")
|
||||
bid_addr = Column("string")
|
||||
pk_bid_addr = Column("blob")
|
||||
proof_address = Column("string")
|
||||
proof_utxos = Column("blob")
|
||||
# Address to spend lock tx to - address from wallet if empty TODO
|
||||
@@ -647,17 +644,6 @@ class CheckedBlock(Table):
|
||||
block_time = Column("integer")
|
||||
|
||||
|
||||
class CoinRates(Table):
|
||||
__tablename__ = "coinrates"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
currency_from = Column("integer")
|
||||
currency_to = Column("integer")
|
||||
rate = Column("string")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
def create_db(db_path: str, log) -> None:
|
||||
con = None
|
||||
try:
|
||||
@@ -929,12 +915,15 @@ class DBMethods:
|
||||
table_name: str = table_class.__tablename__
|
||||
|
||||
query: str = "SELECT "
|
||||
|
||||
columns = []
|
||||
|
||||
for mc in inspect.getmembers(table_class):
|
||||
mc_name, mc_obj = mc
|
||||
|
||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||
continue
|
||||
|
||||
if len(columns) > 0:
|
||||
query += ", "
|
||||
query += mc_name
|
||||
@@ -942,29 +931,10 @@ class DBMethods:
|
||||
|
||||
query += f" FROM {table_name} WHERE 1=1 "
|
||||
|
||||
query_data = {}
|
||||
for ck in constraints:
|
||||
if not validColumnName(ck):
|
||||
raise ValueError(f"Invalid constraint column: {ck}")
|
||||
|
||||
constraint_value = constraints[ck]
|
||||
if isinstance(constraint_value, tuple) or isinstance(
|
||||
constraint_value, list
|
||||
):
|
||||
if len(constraint_value) < 2:
|
||||
raise ValueError(f"Too few constraint values for list: {ck}")
|
||||
query += f" AND {ck} IN ("
|
||||
|
||||
for i, cv in enumerate(constraint_value):
|
||||
cv_name: str = f"{ck}_{i}"
|
||||
if i > 0:
|
||||
query += ","
|
||||
query += ":" + cv_name
|
||||
query_data[cv_name] = cv
|
||||
query += ") "
|
||||
else:
|
||||
query += f" AND {ck} = :{ck} "
|
||||
query_data[ck] = constraint_value
|
||||
|
||||
for order_col, order_dir in order_by.items():
|
||||
if validColumnName(order_col) is False:
|
||||
@@ -977,6 +947,7 @@ class DBMethods:
|
||||
if query_suffix:
|
||||
query += query_suffix
|
||||
|
||||
query_data = constraints.copy()
|
||||
query_data.update(extra_query_data)
|
||||
rows = cursor.execute(query, query_data)
|
||||
for row in rows:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -104,19 +104,17 @@ def upgradeDatabaseData(self, data_version):
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
if data_version > 0 and data_version < 6:
|
||||
if data_version > 0 and data_version < 3:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
can_accept = canAcceptBidState(state)
|
||||
cursor.execute(
|
||||
"UPDATE bidstates SET can_accept = :can_accept, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
"UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
{
|
||||
"in_error": in_error,
|
||||
"swap_failed": swap_failed,
|
||||
"swap_ended": swap_ended,
|
||||
"can_accept": can_accept,
|
||||
"state_id": int(state),
|
||||
},
|
||||
)
|
||||
@@ -412,27 +410,6 @@ def upgradeDatabase(self, db_version):
|
||||
elif current_version == 24:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
|
||||
elif current_version == 25:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE coinrates (
|
||||
record_id INTEGER NOT NULL,
|
||||
currency_from INTEGER,
|
||||
currency_to INTEGER,
|
||||
rate VARCHAR,
|
||||
source VARCHAR,
|
||||
last_updated INTEGER,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
elif current_version == 26:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN auto_accept_type INTEGER")
|
||||
elif current_version == 27:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN pk_from BLOB")
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN pk_bid_addr BLOB")
|
||||
|
||||
if current_version != db_version:
|
||||
self.db_version = db_version
|
||||
self.setIntKV("db_version", db_version, cursor)
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
|
||||
|
||||
default_chart_api_key = (
|
||||
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
|
||||
)
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
class Explorer:
|
||||
def __init__(self, swapclient, coin_type, base_url):
|
||||
self.swapclient = swapclient
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
import secrets
|
||||
import traceback
|
||||
import threading
|
||||
import http.client
|
||||
import base64
|
||||
|
||||
from urllib import parse
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from socket import error as SocketError
|
||||
from urllib import parse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from . import __version__
|
||||
from .util import (
|
||||
@@ -36,7 +30,6 @@ from .basicswap_util import (
|
||||
strTxState,
|
||||
strBidState,
|
||||
)
|
||||
from .util.rfc2440 import verify_rfc2440_password
|
||||
|
||||
from .js_server import (
|
||||
js_error,
|
||||
@@ -64,9 +57,6 @@ from .ui.page_identity import page_identity
|
||||
from .ui.page_smsgaddresses import page_smsgaddresses
|
||||
from .ui.page_debug import page_debug
|
||||
|
||||
SESSION_COOKIE_NAME = "basicswap_session_id"
|
||||
SESSION_DURATION_MINUTES = 60
|
||||
|
||||
env = Environment(loader=PackageLoader("basicswap", "templates"))
|
||||
env.filters["formatts"] = format_timestamp
|
||||
|
||||
@@ -129,57 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
def _get_session_cookie(self):
|
||||
if "Cookie" in self.headers:
|
||||
cookie = SimpleCookie(self.headers["Cookie"])
|
||||
if SESSION_COOKIE_NAME in cookie:
|
||||
return cookie[SESSION_COOKIE_NAME].value
|
||||
return None
|
||||
|
||||
def _set_session_cookie(self, session_id):
|
||||
cookie = SimpleCookie()
|
||||
cookie[SESSION_COOKIE_NAME] = session_id
|
||||
cookie[SESSION_COOKIE_NAME]["path"] = "/"
|
||||
cookie[SESSION_COOKIE_NAME]["httponly"] = True
|
||||
cookie[SESSION_COOKIE_NAME]["samesite"] = "Lax"
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
cookie[SESSION_COOKIE_NAME]["expires"] = expires.strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT"
|
||||
)
|
||||
return ("Set-Cookie", cookie.output(header="").strip())
|
||||
|
||||
def _clear_session_cookie(self):
|
||||
cookie = SimpleCookie()
|
||||
cookie[SESSION_COOKIE_NAME] = ""
|
||||
cookie[SESSION_COOKIE_NAME]["path"] = "/"
|
||||
cookie[SESSION_COOKIE_NAME]["httponly"] = True
|
||||
cookie[SESSION_COOKIE_NAME]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
return ("Set-Cookie", cookie.output(header="").strip())
|
||||
|
||||
def is_authenticated(self):
|
||||
swap_client = self.server.swap_client
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
|
||||
if not client_auth_hash:
|
||||
return True
|
||||
|
||||
session_id = self._get_session_cookie()
|
||||
if not session_id:
|
||||
return False
|
||||
|
||||
session_data = self.server.active_sessions.get(session_id)
|
||||
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
||||
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
return True
|
||||
|
||||
if session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
return False
|
||||
|
||||
def log_error(self, format, *args):
|
||||
super().log_message(format, *args)
|
||||
|
||||
@@ -202,12 +141,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return form_data
|
||||
|
||||
def render_template(
|
||||
self,
|
||||
template,
|
||||
args_dict,
|
||||
status_code=200,
|
||||
version=__version__,
|
||||
extra_headers=None,
|
||||
self, template, args_dict, status_code=200, version=__version__
|
||||
):
|
||||
swap_client = self.server.swap_client
|
||||
if swap_client.ws_server:
|
||||
@@ -218,6 +152,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["debug_ui_mode"] = True
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
# TODO: Cache value?
|
||||
try:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
@@ -243,16 +178,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.server.msg_id_counter += 1
|
||||
args_dict["err_messages"] = err_messages_with_ids
|
||||
|
||||
if self.path:
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split("/")
|
||||
if len(url_split) > 1 and url_split[1]:
|
||||
args_dict["current_page"] = url_split[1]
|
||||
else:
|
||||
args_dict["current_page"] = "index"
|
||||
else:
|
||||
args_dict["current_page"] = "index"
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
args_dict["shutdown_token"] = shutdown_token
|
||||
@@ -266,7 +191,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
args_dict["version"] = version
|
||||
|
||||
self.putHeaders(status_code, "text/html", extra_headers=extra_headers)
|
||||
self.putHeaders(status_code, "text/html")
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
@@ -278,7 +203,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
|
||||
def render_simple_template(self, template, args_dict):
|
||||
self.putHeaders(200, "text/html")
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
@@ -287,7 +211,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
def page_info(self, info_str, post_string=None, extra_headers=None):
|
||||
def page_info(self, info_str, post_string=None):
|
||||
template = env.get_template("info.html")
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
@@ -298,7 +222,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
"message_str": info_str,
|
||||
"summary": summary,
|
||||
},
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
def page_error(self, error_str, post_string=None):
|
||||
@@ -314,93 +237,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
},
|
||||
)
|
||||
|
||||
def page_login(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
template = env.get_template("login.html")
|
||||
err_messages = []
|
||||
extra_headers = []
|
||||
is_json_request = "application/json" in self.headers.get("Content-Type", "")
|
||||
security_warning = None
|
||||
if self.server.host_name not in ("127.0.0.1", "localhost"):
|
||||
security_warning = "WARNING: Server is accessible on the network. Sending password over plain HTTP is insecure. Use HTTPS (e.g., via reverse proxy) for non-local access."
|
||||
if not is_json_request:
|
||||
err_messages.append(security_warning)
|
||||
|
||||
if post_string:
|
||||
password = None
|
||||
if is_json_request:
|
||||
try:
|
||||
json_data = json.loads(post_string.decode("utf-8"))
|
||||
password = json_data.get("password")
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error parsing JSON login data: {e}")
|
||||
else:
|
||||
try:
|
||||
form_data = parse.parse_qs(post_string.decode("utf-8"))
|
||||
password = form_data.get("password", [None])[0]
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error parsing form login data: {e}")
|
||||
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
|
||||
if (
|
||||
client_auth_hash
|
||||
and password is not None
|
||||
and verify_rfc2440_password(client_auth_hash, password)
|
||||
):
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
cookie_header = self._set_session_cookie(session_id)
|
||||
|
||||
if is_json_request:
|
||||
response_data = {"success": True, "session_id": session_id}
|
||||
if security_warning:
|
||||
response_data["warning"] = security_warning
|
||||
self.putHeaders(
|
||||
200, "application/json", extra_headers=[cookie_header]
|
||||
)
|
||||
return json.dumps(response_data).encode("utf-8")
|
||||
else:
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/offers")
|
||||
self.send_header(cookie_header[0], cookie_header[1])
|
||||
self.end_headers()
|
||||
return b""
|
||||
else:
|
||||
if is_json_request:
|
||||
self.putHeaders(401, "application/json")
|
||||
return json.dumps({"error": "Invalid password"}).encode("utf-8")
|
||||
else:
|
||||
err_messages.append("Invalid password.")
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
extra_headers.append(clear_cookie_header)
|
||||
|
||||
if (
|
||||
not is_json_request
|
||||
and swap_client.settings.get("client_auth_hash")
|
||||
and self.is_authenticated()
|
||||
):
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/offers")
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"title_str": "Login",
|
||||
"err_messages": err_messages,
|
||||
"summary": {},
|
||||
"encrypted": False,
|
||||
"locked": False,
|
||||
},
|
||||
status_code=401 if post_string and not is_json_request else 200,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
def page_explorers(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -414,10 +250,14 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
form_data = self.checkForm(post_string, "explorers", err_messages)
|
||||
if form_data:
|
||||
|
||||
explorer = get_data_entry(form_data, "explorer")
|
||||
action = get_data_entry(form_data, "action")
|
||||
args = get_data_entry_or(form_data, "args", "")
|
||||
explorer = form_data[b"explorer"][0].decode("utf-8")
|
||||
action = form_data[b"action"][0].decode("utf-8")
|
||||
|
||||
args = (
|
||||
""
|
||||
if b"args" not in form_data
|
||||
else form_data[b"args"][0].decode("utf-8")
|
||||
)
|
||||
try:
|
||||
c, e = explorer.split("_")
|
||||
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
|
||||
@@ -570,6 +410,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"refresh": 30,
|
||||
"active_swaps": [
|
||||
(
|
||||
s[0].hex(),
|
||||
@@ -606,7 +447,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def page_shutdown(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
extra_headers = []
|
||||
|
||||
if len(url_split) > 2:
|
||||
token = url_split[2]
|
||||
@@ -614,15 +454,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if token != expect_token:
|
||||
return self.page_info("Unexpected token, still running.")
|
||||
|
||||
session_id = self._get_session_cookie()
|
||||
if session_id and session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
extra_headers.append(clear_cookie_header)
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
return self.page_info("Shutting down", extra_headers=extra_headers)
|
||||
return self.page_info("Shutting down")
|
||||
|
||||
def page_index(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -643,81 +477,22 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
},
|
||||
)
|
||||
|
||||
def putHeaders(self, status_code, content_type, extra_headers=None):
|
||||
def putHeaders(self, status_code, content_type):
|
||||
self.send_response(status_code)
|
||||
if self.server.allow_cors:
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Content-Type", content_type)
|
||||
if extra_headers:
|
||||
for header_tuple in extra_headers:
|
||||
self.send_header(header_tuple[0], header_tuple[1])
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string="", is_json=False):
|
||||
swap_client = self.server.swap_client
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split("/")
|
||||
page = url_split[1] if len(url_split) > 1 else ""
|
||||
|
||||
exempt_pages = ["login", "static", "error", "info"]
|
||||
auth_header = self.headers.get("Authorization")
|
||||
basic_auth_ok = False
|
||||
|
||||
if auth_header and auth_header.startswith("Basic "):
|
||||
try:
|
||||
encoded_creds = auth_header.split(" ", 1)[1]
|
||||
decoded_creds = base64.b64decode(encoded_creds).decode("utf-8")
|
||||
_, password = decoded_creds.split(":", 1)
|
||||
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
if client_auth_hash and verify_rfc2440_password(
|
||||
client_auth_hash, password
|
||||
):
|
||||
basic_auth_ok = True
|
||||
else:
|
||||
self.send_response(401)
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Invalid Basic Auth credentials"}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
return b""
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error processing Basic Auth header: {e}")
|
||||
self.send_response(401)
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Malformed Basic Auth header"}).encode("utf-8")
|
||||
)
|
||||
return b""
|
||||
|
||||
if not basic_auth_ok and page not in exempt_pages:
|
||||
if not self.is_authenticated():
|
||||
if page == "json":
|
||||
self.putHeaders(401, "application/json")
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Unauthorized"}).encode("utf-8")
|
||||
)
|
||||
return b""
|
||||
else:
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/login")
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
self.send_header(clear_cookie_header[0], clear_cookie_header[1])
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
if not post_string and len(parsed.query) > 0:
|
||||
if post_string == "" and len(parsed.query) > 0:
|
||||
post_string = parsed.query
|
||||
|
||||
if page == "json":
|
||||
if len(url_split) > 1 and url_split[1] == "json":
|
||||
try:
|
||||
self.putHeaders(status_code, "json")
|
||||
self.putHeaders(status_code, "text/plain")
|
||||
func = js_url_to_function(url_split)
|
||||
return func(self, url_split, post_string, is_json)
|
||||
except Exception as ex:
|
||||
@@ -725,20 +500,18 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
return js_error(self, str(ex))
|
||||
|
||||
if page == "static":
|
||||
if len(url_split) > 1 and url_split[1] == "static":
|
||||
try:
|
||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||
content = None
|
||||
mime_type = ""
|
||||
filepath = ""
|
||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||
filepath = os.path.join(
|
||||
static_path, "sequence_diagrams", url_split[3]
|
||||
)
|
||||
mime_type = "image/svg+xml"
|
||||
with open(
|
||||
os.path.join(static_path, "sequence_diagrams", url_split[3]),
|
||||
"rb",
|
||||
) as fp:
|
||||
self.putHeaders(status_code, "image/svg+xml")
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == "images":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
filepath = os.path.join(static_path, "images", filename)
|
||||
_, extension = os.path.splitext(filename)
|
||||
mime_type = {
|
||||
".svg": "image/svg+xml",
|
||||
@@ -747,25 +520,25 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
".gif": "image/gif",
|
||||
".ico": "image/x-icon",
|
||||
}.get(extension, "")
|
||||
if mime_type == "":
|
||||
raise ValueError("Unknown file type " + filename)
|
||||
with open(
|
||||
os.path.join(static_path, "images", filename), "rb"
|
||||
) as fp:
|
||||
self.putHeaders(status_code, mime_type)
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == "css":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
filepath = os.path.join(static_path, "css", filename)
|
||||
mime_type = "text/css; charset=utf-8"
|
||||
with open(os.path.join(static_path, "css", filename), "rb") as fp:
|
||||
self.putHeaders(status_code, "text/css; charset=utf-8")
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == "js":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
filepath = os.path.join(static_path, "js", filename)
|
||||
mime_type = "application/javascript"
|
||||
with open(os.path.join(static_path, "js", filename), "rb") as fp:
|
||||
self.putHeaders(status_code, "application/javascript")
|
||||
return fp.read()
|
||||
else:
|
||||
return self.page_404(url_split)
|
||||
|
||||
if mime_type == "" or not filepath:
|
||||
raise ValueError("Unknown file type or path")
|
||||
|
||||
with open(filepath, "rb") as fp:
|
||||
content = fp.read()
|
||||
self.putHeaders(status_code, mime_type)
|
||||
return content
|
||||
|
||||
except FileNotFoundError:
|
||||
return self.page_404(url_split)
|
||||
except Exception as ex:
|
||||
@@ -777,8 +550,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if len(url_split) > 1:
|
||||
page = url_split[1]
|
||||
|
||||
if page == "login":
|
||||
return self.page_login(url_split, post_string)
|
||||
if page == "active":
|
||||
return self.page_active(url_split, post_string)
|
||||
if page == "wallets":
|
||||
@@ -845,21 +616,14 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
response = self.handle_http(200, self.path)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(content_length)
|
||||
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
|
||||
|
||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, "text/html")
|
||||
@@ -873,10 +637,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
def __init__(self, host_name, port_no, allow_cors, swap_client):
|
||||
def __init__(self, fp, host_name, port_no, allow_cors, swap_client):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.stop_event = threading.Event()
|
||||
self.fp = fp
|
||||
self.host_name = host_name
|
||||
self.port_no = port_no
|
||||
self.allow_cors = allow_cors
|
||||
@@ -884,7 +649,6 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
self.title = "BasicSwap - " + __version__
|
||||
self.last_form_id = dict()
|
||||
self.session_tokens = dict()
|
||||
self.active_sessions = {}
|
||||
self.env = env
|
||||
self.msg_id_counter = 0
|
||||
|
||||
@@ -894,19 +658,18 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
try:
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no, timeout=0.5)
|
||||
conn.request("GET", "/shutdown_ping")
|
||||
# Send fake request
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
||||
conn.connect()
|
||||
conn.request("GET", "/none")
|
||||
response = conn.getresponse()
|
||||
_ = response.read()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def serve_forever(self):
|
||||
self.timeout = 1
|
||||
while not self.stop_event.is_set():
|
||||
self.handle_request()
|
||||
self.socket.close()
|
||||
self.swap_client.log.info("HTTP server stopped.")
|
||||
|
||||
def run(self):
|
||||
self.serve_forever()
|
||||
|
||||
@@ -106,31 +106,6 @@ class BCHInterface(BTCInterface):
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
def createWallet(self, wallet_name: str, password: str = ""):
|
||||
self.rpc("createwallet", [wallet_name, False])
|
||||
if password != "":
|
||||
self.rpc(
|
||||
"encryptwallet",
|
||||
[
|
||||
password,
|
||||
],
|
||||
override_wallet=wallet_name,
|
||||
)
|
||||
|
||||
def newKeypool(self) -> None:
|
||||
self._log.debug("Refreshing keypool.")
|
||||
|
||||
# Use up current keypool
|
||||
wi = self.rpc_wallet("getwalletinfo")
|
||||
keypool_size: int = wi["keypoolsize"]
|
||||
for i in range(keypool_size):
|
||||
_ = self.rpc_wallet("getnewaddress")
|
||||
keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"]
|
||||
for i in range(keypoolsize_hd_internal):
|
||||
_ = self.rpc_wallet("getrawchangeaddress")
|
||||
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
# returns pkh
|
||||
def decodeAddress(self, address: str) -> bytes:
|
||||
return bytes(Address.from_string(address).payload)
|
||||
|
||||
@@ -10,13 +10,8 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import mmap
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import traceback
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from basicswap.basicswap_util import (
|
||||
@@ -382,7 +377,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
last_block_header = prev_block_header
|
||||
raise ValueError(f"Block header not found at time: {time}")
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
|
||||
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||
assert len(key_bytes) == 32
|
||||
self._have_checked_seed = False
|
||||
if self._use_descriptors:
|
||||
@@ -392,7 +387,6 @@ class BTCInterface(Secp256k1Interface):
|
||||
ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
|
||||
|
||||
rv = self.rpc_wallet(
|
||||
"importdescriptors",
|
||||
[
|
||||
@@ -400,7 +394,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
{"desc": desc_external, "timestamp": "now", "active": True},
|
||||
{
|
||||
"desc": desc_internal,
|
||||
"timestamp": "now" if restore_time == -1 else restore_time,
|
||||
"timestamp": "now",
|
||||
"active": True,
|
||||
"internal": True,
|
||||
},
|
||||
@@ -417,18 +411,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
raise ValueError("Failed to import descriptors.")
|
||||
else:
|
||||
key_wif = self.encodeKey(key_bytes)
|
||||
try:
|
||||
self.rpc_wallet("sethdseed", [True, key_wif])
|
||||
except Exception as e:
|
||||
self._log.debug(f"sethdseed failed: {e}")
|
||||
"""
|
||||
# TODO: Find derived key counts
|
||||
if "Already have this key" in str(e):
|
||||
key_id: bytes = self.getSeedHash(key_bytes)
|
||||
self.setActiveKeyChain(key_id)
|
||||
else:
|
||||
"""
|
||||
raise (e)
|
||||
|
||||
def getWalletInfo(self):
|
||||
rv = self.rpc_wallet("getwalletinfo")
|
||||
@@ -472,6 +455,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
self.close_rpc(rpc_conn)
|
||||
raise ValueError(f"{self.coin_name()} wallet restore height not found.")
|
||||
|
||||
def getWalletSeedID(self) -> str:
|
||||
wi = self.rpc_wallet("getwalletinfo")
|
||||
return "Not found" if "hdseedid" not in wi else wi["hdseedid"]
|
||||
|
||||
def getActiveDescriptor(self):
|
||||
descriptors = self.rpc_wallet("listdescriptors")["descriptors"]
|
||||
for descriptor in descriptors:
|
||||
@@ -483,24 +470,21 @@ class BTCInterface(Secp256k1Interface):
|
||||
return descriptor
|
||||
return None
|
||||
|
||||
def getWalletSeedID(self) -> str:
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
if self._use_descriptors:
|
||||
descriptor = self.getActiveDescriptor()
|
||||
if descriptor is None:
|
||||
self._log.debug("Could not find active descriptor.")
|
||||
return "Not found"
|
||||
return False
|
||||
|
||||
end = descriptor["desc"].find("/")
|
||||
if end < 10:
|
||||
return "Not found"
|
||||
return False
|
||||
extkey = descriptor["desc"][5:end]
|
||||
extkey_data = b58decode(extkey)[4:-4]
|
||||
extkey_data_hash: bytes = hash160(extkey_data)
|
||||
return extkey_data_hash.hex()
|
||||
return True if extkey_data_hash.hex() == expect_seedid else False
|
||||
|
||||
wi = self.rpc_wallet("getwalletinfo")
|
||||
return "Not found" if "hdseedid" not in wi else wi["hdseedid"]
|
||||
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
wallet_seed_id = self.getWalletSeedID()
|
||||
self._expect_seedid_hex = expect_seedid
|
||||
self._have_checked_seed = True
|
||||
@@ -1878,69 +1862,19 @@ class BTCInterface(Secp256k1Interface):
|
||||
"Could not find address with enough funds for proof",
|
||||
)
|
||||
|
||||
self._log.debug(f"sign_for_addr {sign_for_addr}")
|
||||
self._log.debug("sign_for_addr %s", sign_for_addr)
|
||||
|
||||
funds_addr: str = sign_for_addr
|
||||
if (
|
||||
self.using_segwit()
|
||||
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
# 'Address does not refer to key' for non p2pkh
|
||||
pkh = self.decodeAddress(sign_for_addr)
|
||||
sign_for_addr = self.pkh_to_address(pkh)
|
||||
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
|
||||
self._log.debug("sign_for_addr converted %s", sign_for_addr)
|
||||
|
||||
if self._use_descriptors:
|
||||
# https://github.com/bitcoin/bitcoin/issues/10542
|
||||
# https://github.com/bitcoin/bitcoin/issues/26046
|
||||
priv_keys = self.rpc_wallet(
|
||||
"listdescriptors",
|
||||
[
|
||||
True,
|
||||
],
|
||||
)
|
||||
addr_info = self.rpc_wallet(
|
||||
"getaddressinfo",
|
||||
[
|
||||
funds_addr,
|
||||
],
|
||||
)
|
||||
hdkeypath = addr_info["hdkeypath"]
|
||||
|
||||
sign_for_address_key = None
|
||||
for descriptor in priv_keys["descriptors"]:
|
||||
if descriptor["active"] is False or descriptor["internal"] is True:
|
||||
continue
|
||||
desc = descriptor["desc"]
|
||||
assert desc.startswith("wpkh(")
|
||||
ext_key = desc[5:].split(")")[0].split("/", 1)[0]
|
||||
ext_key_data = decodeAddress(ext_key)[4:]
|
||||
ci_part = self._sc.ci(Coins.PART)
|
||||
ext_key_data_part = ci_part.encode_secret_extkey(ext_key_data)
|
||||
rv = ci_part.rpc_wallet(
|
||||
"extkey", ["info", ext_key_data_part, hdkeypath]
|
||||
)
|
||||
extkey_derived = rv["key_info"]["result"]
|
||||
ext_key_data = decodeAddress(extkey_derived)[4:]
|
||||
ek = ExtKeyPair()
|
||||
ek.decode(ext_key_data)
|
||||
sign_for_address_key = self.encodeKey(ek._key)
|
||||
break
|
||||
assert sign_for_address_key is not None
|
||||
signature = self.rpc(
|
||||
"signmessagewithprivkey",
|
||||
[
|
||||
sign_for_address_key,
|
||||
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
del priv_keys
|
||||
else:
|
||||
signature = self.rpc_wallet(
|
||||
"signmessage",
|
||||
[
|
||||
sign_for_addr,
|
||||
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
|
||||
],
|
||||
[sign_for_addr, sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()],
|
||||
)
|
||||
|
||||
prove_utxos = [] # TODO: Send specific utxos
|
||||
@@ -1994,237 +1928,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
locked = encrypted and wallet_info["unlocked_until"] <= 0
|
||||
return encrypted, locked
|
||||
|
||||
def createWallet(self, wallet_name: str, password: str = "") -> None:
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
[wallet_name, False, True, password, False, self._use_descriptors],
|
||||
)
|
||||
|
||||
def setActiveWallet(self, wallet_name: str) -> None:
|
||||
# For debugging
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=wallet_name
|
||||
)
|
||||
self._rpc_wallet = wallet_name
|
||||
|
||||
def newKeypool(self) -> None:
|
||||
self._log.debug("Running newkeypool.")
|
||||
self.rpc_wallet("newkeypool")
|
||||
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
orig_active_descriptors = []
|
||||
orig_hdchain_bytes = None
|
||||
walletpath = None
|
||||
max_hdchain_key_count: int = 4000000 # Arbitrary
|
||||
|
||||
chain_client_settings = self._sc.getChainClientSettings(
|
||||
self.coin_type()
|
||||
) # basicswap.json
|
||||
if (
|
||||
chain_client_settings.get("manage_daemon", False)
|
||||
and check_seed is True
|
||||
and seed_id_before != "Not found"
|
||||
):
|
||||
# Store active keys
|
||||
self.rpc("unloadwallet", [self._rpc_wallet])
|
||||
|
||||
datadir = chain_client_settings["datadir"]
|
||||
if self._network != "mainnet":
|
||||
datadir = os.path.join(datadir, self._network)
|
||||
try_wallet_path = os.path.join(datadir, self._rpc_wallet)
|
||||
if os.path.exists(try_wallet_path):
|
||||
walletpath = try_wallet_path
|
||||
else:
|
||||
try_wallet_path = os.path.join(datadir, "wallets", self._rpc_wallet)
|
||||
if os.path.exists(try_wallet_path):
|
||||
walletpath = try_wallet_path
|
||||
|
||||
walletfilepath = walletpath
|
||||
if os.path.isdir(walletpath):
|
||||
walletfilepath = os.path.join(walletpath, "wallet.dat")
|
||||
|
||||
if walletpath is None:
|
||||
self._log.warning(f"Unable to find {self.ticker()} wallet path.")
|
||||
else:
|
||||
if self._use_descriptors:
|
||||
orig_active_descriptors = []
|
||||
with sqlite3.connect(walletfilepath) as conn:
|
||||
c = conn.cursor()
|
||||
rows = c.execute(
|
||||
"SELECT * FROM main WHERE key in (:kext, :kint)",
|
||||
{
|
||||
"kext": bytes.fromhex(
|
||||
"1161637469766565787465726e616c73706b02"
|
||||
),
|
||||
"kint": bytes.fromhex(
|
||||
"11616374697665696e7465726e616c73706b02"
|
||||
),
|
||||
},
|
||||
)
|
||||
for row in rows:
|
||||
k, v = row
|
||||
orig_active_descriptors.append({"k": k, "v": v})
|
||||
else:
|
||||
seedid_bytes: bytes = bytes.fromhex(seed_id_before)[::-1]
|
||||
with open(walletfilepath, "rb") as fp:
|
||||
with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm:
|
||||
pos = mm.find(seedid_bytes)
|
||||
while pos != -1:
|
||||
mm.seek(pos - 8)
|
||||
hdchain_bytes = mm.read(12 + 20)
|
||||
version = int.from_bytes(hdchain_bytes[:4], "little")
|
||||
if version == 2:
|
||||
external_counter = int.from_bytes(
|
||||
hdchain_bytes[4:8], "little"
|
||||
)
|
||||
internal_counter = int.from_bytes(
|
||||
hdchain_bytes[-4:], "little"
|
||||
)
|
||||
if (
|
||||
external_counter > 0
|
||||
and external_counter <= max_hdchain_key_count
|
||||
and internal_counter > 0
|
||||
and internal_counter <= max_hdchain_key_count
|
||||
):
|
||||
orig_hdchain_bytes = hdchain_bytes
|
||||
self._log.debug(
|
||||
f"Found hdchain for: {seed_id_before} external_counter: {external_counter}, internal_counter: {internal_counter}."
|
||||
)
|
||||
break
|
||||
pos = mm.find(seedid_bytes, pos + 1)
|
||||
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found" or walletpath is None:
|
||||
return
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
|
||||
if chain_client_settings.get("manage_daemon", False) is False:
|
||||
self._log.warning(
|
||||
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
|
||||
)
|
||||
return
|
||||
if self._use_descriptors:
|
||||
if len(orig_active_descriptors) < 2:
|
||||
self._log.error(
|
||||
"Could not find original active descriptors for wallet."
|
||||
)
|
||||
return
|
||||
self._log.info("Attempting to revert to last descriptors.")
|
||||
else:
|
||||
if orig_hdchain_bytes is None:
|
||||
self._log.error("Could not find hdchain for wallet.")
|
||||
return
|
||||
self._log.info("Attempting to revert to last hdchain.")
|
||||
try:
|
||||
# Make a copy of the encrypted wallet before modifying it
|
||||
bkp_path = walletpath + ".bkp"
|
||||
for i in range(100):
|
||||
if not os.path.exists(bkp_path):
|
||||
break
|
||||
bkp_path = walletpath + f".bkp{i}"
|
||||
|
||||
if os.path.exists(bkp_path):
|
||||
self._log.error("Could not find backup path for wallet.")
|
||||
return
|
||||
|
||||
self.rpc("unloadwallet", [self._rpc_wallet])
|
||||
|
||||
if os.path.isfile(walletpath):
|
||||
shutil.copy(walletpath, bkp_path)
|
||||
else:
|
||||
shutil.copytree(walletpath, bkp_path)
|
||||
|
||||
hdchain_replaced: bool = False
|
||||
if self._use_descriptors:
|
||||
with sqlite3.connect(walletfilepath) as conn:
|
||||
c = conn.cursor()
|
||||
c.executemany(
|
||||
"UPDATE main SET value = :v WHERE key = :k",
|
||||
orig_active_descriptors,
|
||||
)
|
||||
conn.commit()
|
||||
else:
|
||||
seedid_after_bytes: bytes = bytes.fromhex(seed_id_after)[::-1]
|
||||
with open(walletfilepath, "r+b") as fp:
|
||||
with mmap.mmap(fp.fileno(), 0) as mm:
|
||||
pos = mm.find(seedid_after_bytes)
|
||||
while pos != -1:
|
||||
mm.seek(pos - 8)
|
||||
hdchain_bytes = mm.read(12 + 20)
|
||||
version = int.from_bytes(hdchain_bytes[:4], "little")
|
||||
if version == 2:
|
||||
external_counter = int.from_bytes(
|
||||
hdchain_bytes[4:8], "little"
|
||||
)
|
||||
internal_counter = int.from_bytes(
|
||||
hdchain_bytes[-4:], "little"
|
||||
)
|
||||
if (
|
||||
external_counter > 0
|
||||
and external_counter <= max_hdchain_key_count
|
||||
and internal_counter > 0
|
||||
and internal_counter <= max_hdchain_key_count
|
||||
):
|
||||
self._log.debug(
|
||||
f"Replacing hdchain for: {seed_id_after} external_counter: {external_counter}, internal_counter: {internal_counter}."
|
||||
)
|
||||
offset: int = pos - 8
|
||||
mm.seek(offset)
|
||||
mm.write(orig_hdchain_bytes)
|
||||
self._log.debug(
|
||||
f"hdchain replaced at offset: {offset}."
|
||||
)
|
||||
hdchain_replaced = True
|
||||
# Can appear multiple times in file, replace all.
|
||||
pos = mm.find(seedid_after_bytes, pos + 1)
|
||||
|
||||
if hdchain_replaced is False:
|
||||
self._log.error("Could not find new hdchain in wallet.")
|
||||
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
|
||||
if hdchain_replaced:
|
||||
self.unlockWallet(password, check_seed=False)
|
||||
seed_id_after_restore: str = self.getWalletSeedID()
|
||||
if seed_id_after_restore == seed_id_before:
|
||||
self.newKeypool()
|
||||
else:
|
||||
self._log.warning(
|
||||
f"Expected seed id not found: {seed_id_before}, have {seed_id_after_restore}."
|
||||
)
|
||||
|
||||
self.lockWallet()
|
||||
|
||||
except Exception as e:
|
||||
self._log.error(f"{self.ticker()} recreating wallet failed: {e}.")
|
||||
if self._sc.debug:
|
||||
self._log.error(traceback.format_exc())
|
||||
|
||||
def changeWalletPassword(
|
||||
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
def changeWalletPassword(self, old_password: str, new_password: str):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
if old_password == "":
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(new_password, check_seed=check_seed_if_encrypt)
|
||||
return self.rpc_wallet("encryptwallet", [new_password])
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
def unlockWallet(self, password: str):
|
||||
if password == "":
|
||||
return
|
||||
self._log.info(f"unlockWallet - {self.ticker()}")
|
||||
@@ -2239,20 +1951,12 @@ class BTCInterface(Secp256k1Interface):
|
||||
)
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
False,
|
||||
True,
|
||||
password,
|
||||
False,
|
||||
self._use_descriptors,
|
||||
],
|
||||
"createwallet", [self._rpc_wallet, False, True, "", False, False]
|
||||
)
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self):
|
||||
|
||||
@@ -47,7 +47,7 @@ class DASHInterface(BTCInterface):
|
||||
def entropyToMnemonic(self, key: bytes) -> None:
|
||||
return Mnemonic("english").to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
|
||||
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_v20_compatible:
|
||||
self._log.warning("Generating wallet compatible with v20 seed.")
|
||||
@@ -66,11 +66,7 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
self._expect_seedid_hex = expect_seedid
|
||||
try:
|
||||
rv = self.rpc_wallet("dumphdinfo")
|
||||
except Exception as e:
|
||||
self._log.debug(f"DASH dumphdinfo failed {e}.")
|
||||
return False
|
||||
if rv["mnemonic"] != "":
|
||||
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
@@ -115,45 +111,18 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
return None
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
super().unlockWallet(password, check_seed)
|
||||
def unlockWallet(self, password: str):
|
||||
super().unlockWallet(password)
|
||||
if self._wallet_v20_compatible:
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if not self._have_checked_seed:
|
||||
try:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
except Exception as ex:
|
||||
# dumphdinfo can fail if the wallet is not initialised
|
||||
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
|
||||
|
||||
def lockWallet(self):
|
||||
super().lockWallet()
|
||||
self._wallet_passphrase = ""
|
||||
|
||||
def encryptWallet(
|
||||
self, old_password: str, new_password: str, check_seed: bool = True
|
||||
):
|
||||
if old_password != "":
|
||||
self.unlockWallet(old_password, check_seed=False)
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [new_password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
self.unlockWallet(new_password, check_seed=False)
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
self.lockWallet()
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
|
||||
def changeWalletPassword(
|
||||
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
if old_password == "":
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
|
||||
@@ -332,14 +332,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
|
||||
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||
if with_wallet:
|
||||
self.rpc_wallet("walletislocked")
|
||||
self.rpc_wallet("getinfo")
|
||||
else:
|
||||
self.rpc("getblockchaininfo")
|
||||
|
||||
def getChainHeight(self) -> int:
|
||||
return self.rpc("getblockcount")
|
||||
|
||||
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
|
||||
def initialiseWallet(self, key: bytes) -> None:
|
||||
# Load with --create
|
||||
pass
|
||||
|
||||
@@ -354,9 +354,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
walletislocked = self.rpc_wallet("walletislocked")
|
||||
return True, walletislocked
|
||||
|
||||
def changeWalletPassword(
|
||||
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
def changeWalletPassword(self, old_password: str, new_password: str):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
if old_password == "":
|
||||
# Read initial pwd from settings
|
||||
@@ -370,14 +368,13 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Clear initial password
|
||||
self._sc.editSettings(self.coin_name().lower(), {"wallet_pwd": ""})
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
def unlockWallet(self, password: str):
|
||||
if password == "":
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self):
|
||||
|
||||
@@ -42,6 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None):
|
||||
nonlocal port, auth, host
|
||||
return callrpc(port, auth, method, params, host)
|
||||
|
||||
return rpc_func
|
||||
|
||||
@@ -51,32 +51,13 @@ class FIROInterface(BTCInterface):
|
||||
def getExchangeName(self, exchange_name: str) -> str:
|
||||
return "zcoin"
|
||||
|
||||
def initialiseWallet(self, key, restore_time: int = -1):
|
||||
def initialiseWallet(self, key):
|
||||
# load with -hdseed= parameter
|
||||
pass
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
# Firo shuts down after encryptwallet
|
||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
|
||||
def getNewAddress(self, use_segwit, label="swap_receive"):
|
||||
return self.rpc("getnewaddress", [label])
|
||||
# addr_plain = self.rpc('getnewaddress', [label])
|
||||
|
||||
@@ -146,7 +146,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
def unlockWallet(self, password: str):
|
||||
if password == "":
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
@@ -156,5 +156,5 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
else:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
if check_seed:
|
||||
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -87,7 +87,7 @@ class NAVInterface(BTCInterface):
|
||||
# p2sh-p2wsh
|
||||
return True
|
||||
|
||||
def initialiseWallet(self, key, restore_time: int = -1):
|
||||
def initialiseWallet(self, key):
|
||||
# Load with -importmnemonic= parameter
|
||||
pass
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -14,3 +13,39 @@ class NMCInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.NMC
|
||||
|
||||
def getLockTxHeight(
|
||||
self,
|
||||
txid,
|
||||
dest_address,
|
||||
bid_amount,
|
||||
rescan_from,
|
||||
find_index: bool = False,
|
||||
vout: int = -1,
|
||||
):
|
||||
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
|
||||
ro = self.rpc(
|
||||
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
|
||||
) # TODO: Use combo(address) where possible
|
||||
self._log.debug("[rm] scantxoutset end")
|
||||
return_txid = True if txid is None else False
|
||||
for o in ro["unspents"]:
|
||||
if txid and o["txid"] != txid.hex():
|
||||
continue
|
||||
# Verify amount
|
||||
if self.make_int(o["amount"]) != int(bid_amount):
|
||||
self._log.warning(
|
||||
"Found output to lock tx address of incorrect value: %s, %s",
|
||||
str(o["amount"]),
|
||||
o["txid"],
|
||||
)
|
||||
continue
|
||||
|
||||
rv = {"depth": 0, "height": o["height"]}
|
||||
if o["height"] > 0:
|
||||
rv["depth"] = ro["height"] - o["height"]
|
||||
if find_index:
|
||||
rv["index"] = o["vout"]
|
||||
if return_txid:
|
||||
rv["txid"] = o["txid"]
|
||||
return rv
|
||||
|
||||
@@ -110,7 +110,7 @@ class PARTInterface(BTCInterface):
|
||||
)
|
||||
return index_info["spentindex"]
|
||||
|
||||
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
|
||||
def initialiseWallet(self, key: bytes) -> None:
|
||||
raise ValueError("TODO")
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
|
||||
@@ -34,34 +34,8 @@ class PIVXInterface(BTCInterface):
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
|
||||
chain_client_settings = self._sc.getChainClientSettings(
|
||||
self.coin_type()
|
||||
) # basicswap.json
|
||||
|
||||
if chain_client_settings.get("manage_daemon", False) is False:
|
||||
self._log.warning(
|
||||
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
|
||||
)
|
||||
return
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def signTxWithWallet(self, tx):
|
||||
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||
|
||||
@@ -30,27 +30,3 @@ class WOWInterface(XMRInterface):
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
|
||||
# below only needed until wow is rebased to monero v0.18.4.0+
|
||||
def openWallet(self, filename):
|
||||
params = {"filename": filename}
|
||||
if self._wallet_password is not None:
|
||||
params["password"] = self._wallet_password
|
||||
|
||||
try:
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
except Exception as e:
|
||||
if "no connection to daemon" in str(e):
|
||||
self._log.debug(f"{self.coin_name()} {e}")
|
||||
return # bypass refresh error to allow startup with a busy daemon
|
||||
|
||||
try:
|
||||
# TODO Remove `store` after upstream fix to autosave on close_wallet
|
||||
self.rpc_wallet("store")
|
||||
self.rpc_wallet("close_wallet")
|
||||
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
import basicswap.ed25519_fast_util as edu
|
||||
@@ -202,57 +201,25 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
try:
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||
self.rpc_wallet("refresh")
|
||||
except Exception as e:
|
||||
if "no connection to daemon" in str(e):
|
||||
self._log.debug(f"{self.coin_name()} {e}")
|
||||
return # Bypass refresh error to allow startup with a busy daemon
|
||||
if any(
|
||||
x in str(e)
|
||||
for x in (
|
||||
"invalid signature",
|
||||
"std::bad_alloc",
|
||||
"basic_string::_M_replace_aux",
|
||||
)
|
||||
):
|
||||
self._log.error(f"{self.coin_name()} wallet is corrupt.")
|
||||
chain_client_settings = self._sc.getChainClientSettings(
|
||||
self.coin_type()
|
||||
) # basicswap.json
|
||||
if chain_client_settings.get("manage_wallet_daemon", False):
|
||||
self._log.info(f"Renaming {self.coin_name()} wallet cache file.")
|
||||
walletpath = os.path.join(
|
||||
chain_client_settings.get("datadir", "none"),
|
||||
"wallets",
|
||||
filename,
|
||||
)
|
||||
if not os.path.isfile(walletpath):
|
||||
self._log.warning(
|
||||
f"Could not find {self.coin_name()} wallet cache file."
|
||||
)
|
||||
raise
|
||||
bkp_path = walletpath + ".corrupt"
|
||||
for i in range(100):
|
||||
if not os.path.exists(bkp_path):
|
||||
break
|
||||
bkp_path = walletpath + f".corrupt{i}"
|
||||
if os.path.exists(bkp_path):
|
||||
self._log.error(
|
||||
f"Could not find backup path for {self.coin_name()} wallet."
|
||||
)
|
||||
raise
|
||||
os.rename(walletpath, bkp_path)
|
||||
# Drop through to open_wallet
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
return # bypass refresh error to allow startup with a busy daemon
|
||||
|
||||
try:
|
||||
# TODO Remove `store` after upstream fix to autosave on close_wallet
|
||||
self.rpc_wallet("store")
|
||||
self.rpc_wallet("close_wallet")
|
||||
self._log.debug(f"Closing {self.coin_name()} wallet")
|
||||
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
self._log.debug(f"Attempting to open {self.coin_name()} wallet")
|
||||
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
|
||||
|
||||
def initialiseWallet(
|
||||
self, key_view: bytes, key_spend: bytes, restore_height=None
|
||||
@@ -338,8 +305,6 @@ class XMRInterface(CoinInterface):
|
||||
raise e
|
||||
|
||||
rv = {}
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
|
||||
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
|
||||
@@ -441,8 +406,6 @@ class XMRInterface(CoinInterface):
|
||||
) -> bytes:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
Kbv = self.getPubkey(kbv)
|
||||
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
@@ -484,9 +447,6 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
"""
|
||||
# Debug
|
||||
try:
|
||||
@@ -538,8 +498,6 @@ class XMRInterface(CoinInterface):
|
||||
def findTxnByHash(self, txid):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
try:
|
||||
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
|
||||
@@ -608,8 +566,6 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(wallet_filename)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
rv = self.rpc_wallet("get_balance")
|
||||
if rv["balance"] < cb_swap_value:
|
||||
self._log.warning("Balance is too low, checking for existing spend.")
|
||||
@@ -664,8 +620,6 @@ class XMRInterface(CoinInterface):
|
||||
) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
if sweepall:
|
||||
balance = self.rpc_wallet("get_balance")
|
||||
@@ -745,9 +699,6 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
rv = self.rpc_wallet(
|
||||
"get_transfers",
|
||||
{"in": True, "out": True, "pending": True, "failed": True},
|
||||
@@ -760,15 +711,11 @@ class XMRInterface(CoinInterface):
|
||||
def getSpendableBalance(self) -> int:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
return balance_info["unlocked_balance"]
|
||||
|
||||
def changeWalletPassword(
|
||||
self, old_password, new_password, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
def changeWalletPassword(self, old_password, new_password):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
orig_password = self._wallet_password
|
||||
if old_password != "":
|
||||
@@ -783,11 +730,11 @@ class XMRInterface(CoinInterface):
|
||||
self._wallet_password = orig_password
|
||||
raise e
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
def unlockWallet(self, password: str) -> None:
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
self._wallet_password = password
|
||||
|
||||
if check_seed and not self._have_checked_seed:
|
||||
if not self._have_checked_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self) -> None:
|
||||
|
||||
@@ -14,7 +14,6 @@ from .util import (
|
||||
toBool,
|
||||
)
|
||||
from .basicswap_util import (
|
||||
fiatFromTicker,
|
||||
strBidState,
|
||||
strTxState,
|
||||
SwapTypes,
|
||||
@@ -23,9 +22,6 @@ from .basicswap_util import (
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
chainparams,
|
||||
Fiat,
|
||||
getCoinIdFromTicker,
|
||||
getCoinIdFromName,
|
||||
)
|
||||
from .ui.util import (
|
||||
PAGE_LIMIT,
|
||||
@@ -37,6 +33,7 @@ from .ui.util import (
|
||||
get_data_entry,
|
||||
get_data_entry_or,
|
||||
have_data_entry,
|
||||
tickerToCoinId,
|
||||
listOldBidStates,
|
||||
checkAddressesOwned,
|
||||
)
|
||||
@@ -127,7 +124,7 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
swap_client.checkSystemStatus()
|
||||
if len(url_split) > 3:
|
||||
ticker_str = url_split[3]
|
||||
coin_type = getCoinIdFromTicker(ticker_str)
|
||||
coin_type = tickerToCoinId(ticker_str)
|
||||
|
||||
if len(url_split) > 4:
|
||||
cmd = url_split[4]
|
||||
@@ -328,10 +325,11 @@ def formatBids(swap_client, bids, filters) -> bytes:
|
||||
offer = swap_client.getOffer(b[3])
|
||||
ci_to = swap_client.ci(offer.coin_to) if offer else None
|
||||
|
||||
bid_rate: int = 0 if b[10] is None else b[10]
|
||||
amount_to = None
|
||||
if ci_to:
|
||||
amount_to = ci_to.format_amount((b[4] * bid_rate) // ci_from.COIN())
|
||||
amount_to = ci_to.format_amount(
|
||||
(b[4] * b[10]) // ci_from.COIN()
|
||||
)
|
||||
|
||||
bid_data = {
|
||||
"bid_id": b[2].hex(),
|
||||
@@ -342,16 +340,17 @@ def formatBids(swap_client, bids, filters) -> bytes:
|
||||
"coin_to": ci_to.coin_name() if ci_to else "Unknown",
|
||||
"amount_from": ci_from.format_amount(b[4]),
|
||||
"amount_to": amount_to,
|
||||
"bid_rate": swap_client.ci(b[14]).format_amount(bid_rate),
|
||||
"bid_rate": swap_client.ci(b[14]).format_amount(b[10]),
|
||||
"bid_state": strBidState(b[5]),
|
||||
"addr_from": b[11],
|
||||
"addr_to": offer.addr_to if offer else None,
|
||||
"addr_to": offer.addr_to if offer else None
|
||||
}
|
||||
|
||||
if with_extra_info:
|
||||
bid_data.update(
|
||||
{"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])}
|
||||
)
|
||||
bid_data.update({
|
||||
"tx_state_a": strTxState(b[7]),
|
||||
"tx_state_b": strTxState(b[8])
|
||||
})
|
||||
rv.append(bid_data)
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
|
||||
@@ -824,7 +823,7 @@ def js_validateamount(self, url_split, post_string: str, is_json: bool) -> bytes
|
||||
f"Unknown rounding method, must be one of {valid_round_methods}"
|
||||
)
|
||||
|
||||
coin_type = getCoinIdFromTicker(ticker_str)
|
||||
coin_type = tickerToCoinId(ticker_str)
|
||||
ci = swap_client.ci(coin_type)
|
||||
|
||||
r = 0
|
||||
@@ -850,12 +849,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client.checkSystemStatus()
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
coin_in = get_data_entry(post_data, "coin")
|
||||
try:
|
||||
coin = getCoinIdFromName(coin_in)
|
||||
except Exception:
|
||||
coin = getCoinType(coin_in)
|
||||
|
||||
coin = getCoinType(get_data_entry(post_data, "coin"))
|
||||
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
|
||||
raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.")
|
||||
|
||||
@@ -883,17 +877,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
expect_seedid = swap_client.getStringKV(
|
||||
"main_wallet_seedid_" + ci.coin_name().lower()
|
||||
)
|
||||
try:
|
||||
wallet_seed_id = ci.getWalletSeedID()
|
||||
except Exception as e:
|
||||
wallet_seed_id = f"Error: {e}"
|
||||
|
||||
rv.update(
|
||||
{
|
||||
"seed": seed_key.hex(),
|
||||
"seed_id": seed_id.hex(),
|
||||
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
|
||||
"current_seed_id": wallet_seed_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -965,7 +954,7 @@ def js_404(self, url_split, post_string, is_json) -> bytes:
|
||||
def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||
# TODO: Add details and examples
|
||||
commands = []
|
||||
for k in endpoints:
|
||||
for k in pages:
|
||||
commands.append(k)
|
||||
return bytes(json.dumps({"commands": commands}), "UTF-8")
|
||||
|
||||
@@ -973,8 +962,7 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||
def js_readurl(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)
|
||||
if not have_data_entry(post_data, "url"):
|
||||
raise ValueError("Requires URL.")
|
||||
if have_data_entry(post_data, "url"):
|
||||
url = get_data_entry(post_data, "url")
|
||||
default_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
@@ -989,118 +977,71 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return response
|
||||
raise ValueError("Requires URL.")
|
||||
|
||||
|
||||
def js_active(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
all_bids = []
|
||||
try:
|
||||
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
|
||||
try:
|
||||
swap_data = {
|
||||
"bid_id": bid_id.hex(),
|
||||
"offer_id": offer.offer_id.hex(),
|
||||
"created_at": bid.created_at,
|
||||
"expire_at": bid.expire_at,
|
||||
"bid_state": strBidState(bid.state),
|
||||
"tx_state_a": None,
|
||||
"tx_state_b": None,
|
||||
"coin_from": swap_client.ci(offer.coin_from).coin_name(),
|
||||
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
|
||||
"amount_from": swap_client.ci(offer.coin_from).format_amount(
|
||||
bid.amount
|
||||
),
|
||||
"amount_to": swap_client.ci(offer.coin_to).format_amount(
|
||||
bid.amount_to
|
||||
),
|
||||
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
|
||||
filters = {
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc"
|
||||
}
|
||||
EXCLUDED_STATES = [
|
||||
'Completed',
|
||||
'Expired',
|
||||
'Timed-out',
|
||||
'Abandoned',
|
||||
'Failed, refunded',
|
||||
'Failed, swiped',
|
||||
'Failed',
|
||||
'Error',
|
||||
'received'
|
||||
]
|
||||
all_bids = []
|
||||
|
||||
if offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
swap_data["tx_state_a"] = (
|
||||
strTxState(bid.xmr_a_lock_tx.state)
|
||||
if bid.xmr_a_lock_tx
|
||||
else None
|
||||
)
|
||||
swap_data["tx_state_b"] = (
|
||||
strTxState(bid.xmr_b_lock_tx.state)
|
||||
if bid.xmr_b_lock_tx
|
||||
else None
|
||||
)
|
||||
else:
|
||||
swap_data["tx_state_a"] = bid.getITxState()
|
||||
swap_data["tx_state_b"] = bid.getPTxState()
|
||||
|
||||
if hasattr(bid, "rate"):
|
||||
swap_data["rate"] = bid.rate
|
||||
|
||||
try:
|
||||
received_bids = swap_client.listBids(filters=filters)
|
||||
sent_bids = swap_client.listBids(sent=True, filters=filters)
|
||||
for bid in received_bids + sent_bids:
|
||||
try:
|
||||
bid_state = strBidState(bid[5])
|
||||
tx_state_a = strTxState(bid[7])
|
||||
tx_state_b = strTxState(bid[8])
|
||||
if bid_state in EXCLUDED_STATES:
|
||||
continue
|
||||
offer = swap_client.getOffer(bid[3])
|
||||
if not offer:
|
||||
continue
|
||||
swap_data = {
|
||||
"bid_id": bid[2].hex(),
|
||||
"offer_id": bid[3].hex(),
|
||||
"created_at": bid[0],
|
||||
"bid_state": bid_state,
|
||||
"tx_state_a": tx_state_a if tx_state_a else 'None',
|
||||
"tx_state_b": tx_state_b if tx_state_b else 'None',
|
||||
"coin_from": swap_client.ci(bid[9]).coin_name(),
|
||||
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
|
||||
"amount_from": swap_client.ci(bid[9]).format_amount(bid[4]),
|
||||
"amount_to": swap_client.ci(offer.coin_to).format_amount(
|
||||
(bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN()
|
||||
),
|
||||
"addr_from": bid[11],
|
||||
"status": {
|
||||
"main": bid_state,
|
||||
"initial_tx": tx_state_a if tx_state_a else 'None',
|
||||
"payment_tx": tx_state_b if tx_state_b else 'None'
|
||||
}
|
||||
}
|
||||
all_bids.append(swap_data)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
except Exception:
|
||||
return bytes(json.dumps([]), "UTF-8")
|
||||
return bytes(json.dumps(all_bids), "UTF-8")
|
||||
|
||||
|
||||
def js_coinprices(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)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
currency_to = Fiat.USD
|
||||
if have_data_entry(post_data, "currency_to"):
|
||||
currency_to = fiatFromTicker(get_data_entry(post_data, "currency_to"))
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
coinprices = swap_client.lookupFiatRates(
|
||||
coin_ids, currency_to=currency_to, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in coinprices.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"currency": currency_to.name, "source": rate_source, "rates": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
endpoints = {
|
||||
pages = {
|
||||
"coins": js_coins,
|
||||
"wallets": js_wallets,
|
||||
"offers": js_offers,
|
||||
@@ -1126,11 +1067,10 @@ endpoints = {
|
||||
"help": js_help,
|
||||
"readurl": js_readurl,
|
||||
"active": js_active,
|
||||
"coinprices": js_coinprices,
|
||||
}
|
||||
|
||||
|
||||
def js_url_to_function(url_split):
|
||||
if len(url_split) > 2:
|
||||
return endpoints.get(url_split[2], js_404)
|
||||
return pages.get(url_split[2], js_404)
|
||||
return js_index
|
||||
|
||||
@@ -136,7 +136,6 @@ class OfferMessage(NonProtobufClass):
|
||||
17: ("amount_negotiable", 0, 2),
|
||||
18: ("rate_negotiable", 0, 2),
|
||||
19: ("proof_utxos", 2, 0),
|
||||
20: ("auto_accept_type", 0, 0),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,350 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import websocket
|
||||
|
||||
|
||||
from queue import Queue, Empty
|
||||
|
||||
from basicswap.util.smsg import (
|
||||
smsgEncrypt,
|
||||
smsgDecrypt,
|
||||
smsgGetID,
|
||||
)
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
b58decode,
|
||||
decodeWif,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
BidStates,
|
||||
)
|
||||
|
||||
|
||||
def encode_base64(data: bytes) -> str:
|
||||
return base64.b64encode(data).decode("utf-8")
|
||||
|
||||
|
||||
def decode_base64(encoded_data: str) -> bytes:
|
||||
return base64.b64decode(encoded_data)
|
||||
|
||||
|
||||
class WebSocketThread(threading.Thread):
|
||||
def __init__(self, url: str, tag: str = None, logger=None):
|
||||
super().__init__()
|
||||
self.url: str = url
|
||||
self.tag = tag
|
||||
self.logger = logger
|
||||
self.ws = None
|
||||
self.mutex = threading.Lock()
|
||||
self.corrId: int = 0
|
||||
self.connected: bool = False
|
||||
self.delay_event = threading.Event()
|
||||
|
||||
self.recv_queue = Queue()
|
||||
self.cmd_recv_queue = Queue()
|
||||
|
||||
def on_message(self, ws, message):
|
||||
if self.logger:
|
||||
self.logger.debug("Simplex received msg")
|
||||
else:
|
||||
print(f"{self.tag} - Received msg")
|
||||
|
||||
if message.startswith('{"corrId"'):
|
||||
self.cmd_recv_queue.put(message)
|
||||
else:
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def queue_get(self):
|
||||
try:
|
||||
return self.recv_queue.get(block=False)
|
||||
except Empty:
|
||||
return None
|
||||
|
||||
def cmd_queue_get(self):
|
||||
try:
|
||||
return self.cmd_recv_queue.get(block=False)
|
||||
except Empty:
|
||||
return None
|
||||
|
||||
def on_error(self, ws, error):
|
||||
if self.logger:
|
||||
self.logger.error(f"Simplex ws - {error}")
|
||||
else:
|
||||
print(f"{self.tag} - Error: {error}")
|
||||
|
||||
def on_close(self, ws, close_status_code, close_msg):
|
||||
if self.logger:
|
||||
self.logger.info(f"Simplex ws - Closed: {close_status_code}, {close_msg}")
|
||||
else:
|
||||
print(f"{self.tag} - Closed: {close_status_code}, {close_msg}")
|
||||
|
||||
def on_open(self, ws):
|
||||
if self.logger:
|
||||
self.logger.info("Simplex ws - Connection opened")
|
||||
else:
|
||||
print(f"{self.tag}: WebSocket connection opened")
|
||||
self.connected = True
|
||||
|
||||
def send_command(self, cmd_str: str):
|
||||
with self.mutex:
|
||||
self.corrId += 1
|
||||
if self.logger:
|
||||
self.logger.debug(f"Simplex sent command {self.corrId}")
|
||||
else:
|
||||
print(f"{self.tag}: sent command {self.corrId}")
|
||||
cmd = json.dumps({"corrId": str(self.corrId), "cmd": cmd_str})
|
||||
self.ws.send(cmd)
|
||||
return self.corrId
|
||||
|
||||
def run(self):
|
||||
self.ws = websocket.WebSocketApp(
|
||||
self.url,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_open=self.on_open,
|
||||
on_close=self.on_close,
|
||||
)
|
||||
while not self.delay_event.is_set():
|
||||
self.ws.run_forever()
|
||||
self.delay_event.wait(0.5)
|
||||
|
||||
def stop(self):
|
||||
self.delay_event.set()
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
|
||||
|
||||
def waitForResponse(ws_thread, sent_id, delay_event):
|
||||
sent_id = str(sent_id)
|
||||
for i in range(100):
|
||||
message = ws_thread.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
# print(f"json: {json.dumps(data, indent=4)}")
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == sent_id:
|
||||
return data
|
||||
delay_event.wait(0.5)
|
||||
raise ValueError(f"waitForResponse timed-out waiting for id: {sent_id}")
|
||||
|
||||
|
||||
def waitForConnected(ws_thread, delay_event):
|
||||
for i in range(100):
|
||||
if ws_thread.connected:
|
||||
return True
|
||||
delay_event.wait(0.5)
|
||||
raise ValueError("waitForConnected timed-out.")
|
||||
|
||||
|
||||
def getPrivkeyForAddress(self, addr) -> bytes:
|
||||
|
||||
ci_part = self.ci(Coins.PART)
|
||||
try:
|
||||
return ci_part.decodeKey(
|
||||
self.callrpc(
|
||||
"smsgdumpprivkey",
|
||||
[
|
||||
addr,
|
||||
],
|
||||
)
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
try:
|
||||
return ci_part.decodeKey(
|
||||
ci_part.rpc_wallet(
|
||||
"dumpprivkey",
|
||||
[
|
||||
addr,
|
||||
],
|
||||
)
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
raise ValueError("key not found")
|
||||
|
||||
|
||||
def sendSimplexMsg(
|
||||
self, network, addr_from: str, addr_to: str, payload: bytes, msg_valid: int, cursor
|
||||
) -> bytes:
|
||||
self.log.debug("sendSimplexMsg")
|
||||
|
||||
try:
|
||||
rv = self.callrpc(
|
||||
"smsggetpubkey",
|
||||
[
|
||||
addr_to,
|
||||
],
|
||||
)
|
||||
pubkey_to: bytes = b58decode(rv["publickey"])
|
||||
except Exception as e: # noqa: F841
|
||||
use_cursor = self.openDB(cursor)
|
||||
try:
|
||||
query: str = "SELECT pk_from FROM offers WHERE addr_from = :addr_to LIMIT 1"
|
||||
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
|
||||
if len(rows) > 0:
|
||||
pubkey_to = rows[0][0]
|
||||
else:
|
||||
query: str = (
|
||||
"SELECT pk_bid_addr FROM bids WHERE bid_addr = :addr_to LIMIT 1"
|
||||
)
|
||||
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
|
||||
if len(rows) > 0:
|
||||
pubkey_to = rows[0][0]
|
||||
else:
|
||||
raise ValueError(f"Could not get public key for address {addr_to}")
|
||||
finally:
|
||||
if cursor is None:
|
||||
self.closeDB(use_cursor, commit=False)
|
||||
|
||||
privkey_from = getPrivkeyForAddress(self, addr_from)
|
||||
|
||||
payload += bytes((0,)) # Include null byte to match smsg
|
||||
smsg_msg: bytes = smsgEncrypt(privkey_from, pubkey_to, payload)
|
||||
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
|
||||
ws_thread = network["ws_thread"]
|
||||
sent_id = ws_thread.send_command("#bsx " + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if response["resp"]["type"] != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
self.log.debug(f"Response {json_str}")
|
||||
raise ValueError("Send failed")
|
||||
|
||||
return smsg_id
|
||||
|
||||
|
||||
def decryptSimplexMsg(self, msg_data):
|
||||
ci_part = self.ci(Coins.PART)
|
||||
|
||||
# Try with the network key first
|
||||
network_key: bytes = decodeWif(self.network_key)
|
||||
try:
|
||||
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pk_from"])
|
||||
)
|
||||
decrypted["to"] = self.network_addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
return decrypted
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
# Try with all active bid/offer addresses
|
||||
query: str = """SELECT DISTINCT address FROM (
|
||||
SELECT bid_addr AS address FROM bids WHERE active_ind = 1
|
||||
AND (in_progress = 1 OR (state > :bid_received AND state < :bid_completed) OR (state IN (:bid_received, :bid_sent) AND expire_at > :now))
|
||||
UNION
|
||||
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
|
||||
)"""
|
||||
|
||||
now: int = self.getTime()
|
||||
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
addr_rows = cursor.execute(
|
||||
query,
|
||||
{
|
||||
"bid_received": int(BidStates.BID_RECEIVED),
|
||||
"bid_completed": int(BidStates.SWAP_COMPLETED),
|
||||
"bid_sent": int(BidStates.BID_SENT),
|
||||
"now": now,
|
||||
},
|
||||
).fetchall()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
decrypted = None
|
||||
for row in addr_rows:
|
||||
addr = row[0]
|
||||
try:
|
||||
vk_addr = getPrivkeyForAddress(self, addr)
|
||||
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pk_from"])
|
||||
)
|
||||
decrypted["to"] = addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
return decrypted
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
return decrypted
|
||||
|
||||
|
||||
def readSimplexMsgs(self, network):
|
||||
ws_thread = network["ws_thread"]
|
||||
|
||||
for i in range(100):
|
||||
message = ws_thread.queue_get()
|
||||
if message is None:
|
||||
break
|
||||
|
||||
data = json.loads(message)
|
||||
# self.log.debug(f"message 1: {json.dumps(data, indent=4)}")
|
||||
try:
|
||||
if data["resp"]["type"] in ("chatItemsStatusesUpdated", "newChatItems"):
|
||||
for chat_item in data["resp"]["chatItems"]:
|
||||
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
|
||||
if item_status["type"] in ("sndRcvd", "rcvNew"):
|
||||
snd_progress = item_status.get("sndProgress", None)
|
||||
if snd_progress:
|
||||
if snd_progress != "complete":
|
||||
item_id = chat_item["chatItem"]["meta"]["itemId"]
|
||||
self.log.debug(
|
||||
f"simplex chat item {item_id} {snd_progress}"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
msg_data: bytes = decode_base64(
|
||||
chat_item["chatItem"]["content"]["msgContent"]["text"]
|
||||
)
|
||||
decrypted_msg = decryptSimplexMsg(self, msg_data)
|
||||
if decrypted_msg is None:
|
||||
continue
|
||||
self.processMsg(decrypted_msg)
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.debug(f"readSimplexMsgs error: {e}")
|
||||
|
||||
self.delay_event.wait(0.05)
|
||||
|
||||
|
||||
def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
self.log.debug("initialiseSimplexNetwork")
|
||||
|
||||
client_host: str = network_config.get("client_host", "127.0.0.1")
|
||||
ws_port: str = network_config.get("ws_port")
|
||||
|
||||
ws_thread = WebSocketThread(f"ws://{client_host}:{ws_port}", logger=self.log)
|
||||
self.threads.append(ws_thread)
|
||||
ws_thread.start()
|
||||
waitForConnected(ws_thread, self.delay_event)
|
||||
|
||||
sent_id = ws_thread.send_command("/groups")
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
|
||||
if len(response["resp"]["groups"]) < 1:
|
||||
sent_id = ws_thread.send_command("/c " + network_config["group_link"])
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
assert "groupLinkId" in response["resp"]["connection"]
|
||||
|
||||
network = {
|
||||
"type": "simplex",
|
||||
"ws_thread": ws_thread,
|
||||
}
|
||||
|
||||
self.active_networks.append(network)
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from basicswap.bin.run import Daemon
|
||||
|
||||
|
||||
def initSimplexClient(args, logger, delay_event):
|
||||
logger.info("Initialising Simplex client")
|
||||
|
||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||
|
||||
if os.name == "nt":
|
||||
str_args = " ".join(args)
|
||||
p = subprocess.Popen(
|
||||
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
|
||||
)
|
||||
else:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
|
||||
def readOutput():
|
||||
buf = os.read(pipe_r, 1024).decode("utf-8")
|
||||
response = None
|
||||
# logging.debug(f"simplex-chat output: {buf}")
|
||||
if "display name:" in buf:
|
||||
logger.debug("Setting display name")
|
||||
response = b"user\n"
|
||||
else:
|
||||
logger.debug(f"Unexpected output: {buf}")
|
||||
return
|
||||
if response is not None:
|
||||
p.stdin.write(response)
|
||||
p.stdin.flush()
|
||||
|
||||
try:
|
||||
start_time: int = time.time()
|
||||
max_wait_seconds: int = 60
|
||||
while p.poll() is None:
|
||||
if time.time() > start_time + max_wait_seconds:
|
||||
raise ValueError("Timed out")
|
||||
if os.name == "nt":
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
continue
|
||||
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"initSimplexClient: {e}")
|
||||
finally:
|
||||
if p.poll() is None:
|
||||
p.terminate()
|
||||
os.close(pipe_r)
|
||||
os.close(pipe_w)
|
||||
p.stdin.close()
|
||||
|
||||
|
||||
def startSimplexClient(
|
||||
bin_path: str,
|
||||
data_path: str,
|
||||
server_address: str,
|
||||
websocket_port: int,
|
||||
logger,
|
||||
delay_event,
|
||||
) -> Daemon:
|
||||
logger.info("Starting Simplex client")
|
||||
if not os.path.exists(data_path):
|
||||
os.makedirs(data_path)
|
||||
|
||||
db_path = os.path.join(data_path, "simplex_client_data")
|
||||
|
||||
args = [bin_path, "-d", db_path, "-s", server_address, "-p", str(websocket_port)]
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
# Need to set initial profile through CLI
|
||||
# TODO: Must be a better way?
|
||||
init_args = args + ["-e", "/help"] # Run command ro exit client
|
||||
initSimplexClient(init_args, logger, delay_event)
|
||||
|
||||
args += ["-l", "debug"]
|
||||
|
||||
opened_files = []
|
||||
stdout_dest = open(
|
||||
os.path.join(data_path, "simplex_stdout.log"),
|
||||
"w",
|
||||
)
|
||||
opened_files.append(stdout_dest)
|
||||
stderr_dest = stdout_dest
|
||||
return Daemon(
|
||||
subprocess.Popen(
|
||||
args,
|
||||
shell=False,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=stdout_dest,
|
||||
stderr=stderr_dest,
|
||||
cwd=data_path,
|
||||
),
|
||||
opened_files,
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from basicswap.util.address import b58decode
|
||||
|
||||
|
||||
def getMsgPubkey(self, msg) -> bytes:
|
||||
if "pk_from" in msg:
|
||||
return bytes.fromhex(msg["pk_from"])
|
||||
rv = self.callrpc(
|
||||
"smsggetpubkey",
|
||||
[
|
||||
msg["from"],
|
||||
],
|
||||
)
|
||||
return b58decode(rv["publickey"])
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQMuBFmZ6L4RCACuqDDCIe2bzKznyKVN1aInzRQnSxdGTXuw0mcDz5HYudAhBjR8
|
||||
gY6sxCRPNxvZCJVDZDpCygXMhWZlJtWLR8KMTCXxC4HLXXOY4RxQ5KGnYWxEAcKY
|
||||
deq1ymmuOuMUp7ltRTSyWcBKbR9xTd2vW/+0W7GQIOxUW/aiT1V0x3cky+6kqaec
|
||||
BorP3+uxJcx0Q8WdlS/6N4x3pBv/lfsdrZSaDD8fU/29pQGMDUEnupKoWJVVei6r
|
||||
G+vxLHEtIFYYO8VWjZntymw3dl+aogrjyuxqWzl8mfPi9M/DgiRb4pJnH2yOGDI6
|
||||
Lvg+oo9E79Vwi98UjYSicsB1dtcptKiA96UXAQD/hDB+dil7/SX/SDTlaw/+uTdd
|
||||
Xg0No63dbN++iY4k3Qf/Xk1ZzbuDviLhe+zEhlJOw6TaMlxfwwQOtxEJXILS5uIL
|
||||
jYlGcDbBtJh3p4qUoUduDOgjumJ9m47XqIq81rQ0pqzzGMbK1Y82NQjX5Sn8yTm9
|
||||
p1hmOZ/uX9vCrUSbYBjxJXyQ1OXlerlLRLfBf5WQ0+LO+0cmgtCyX0zV4oGK7vph
|
||||
XEm7lar7AezOOXaSrWAB+CTPUdJF1E7lcJiUuMVcqMx8pphrH+rfcsqPtN6tkyUD
|
||||
TmPDpc5ViqFFelEEQnKSlmAY+3iCNZ3y/VdPPhuJ2lAsL3tm9MMh2JGV378LG45a
|
||||
6SOkQrC977Qq1dhgJA+PGJxQvL2RJWsYlJwp79+Npgf9EfFaJVNzbdjGVq1XmNie
|
||||
MZYqHRfABkyK0ooDxSyzJrq4vvuhWKInS4JhpKSabgNSsNiiaoDR+YYMHb0H8GRR
|
||||
Za6JCmfU8w97R41UTI32N7dhul4xCDs5OV6maOIoNts20oigNGb7TKH9b5N7sDJB
|
||||
zh3Of/fHCChO9Y2chbzU0bERfcn+evrWBf/9XdQGQ3ggoLbOtGpcUQuB/7ofTcBZ
|
||||
awL6K4VJ2Qlb8DPlRgju6uU9AR/KTYeAlVFC8FX7R0FGgPRcJ3GNkNHGqrbuQ72q
|
||||
AOhYOPx9nRrU5u+E2J325vOabLnLbOazze3j6LFPSFV4vfmTO9exYlwhz3g+lFAd
|
||||
CrQ2Q2FsaW4gQ3VsaWFudSAoTmlsYWNUaGVHcmltKSA8Y2FsaW4uY3VsaWFudUBn
|
||||
bWFpbC5jb20+iHoEExEIACIFAlmZ6L4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B
|
||||
AheAAAoJECGBClQgMcAsU5cBAO/ngONpHsxny2uTV4ge2f+5V2ajTjcIfN2jUZtg
|
||||
31jJAQCl1NcrwcIu98+aM2IyjB1UFXkoaMANpr8L9jBopivRGQ==
|
||||
=cf8I
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
14
basicswap/pgp/keys/namecoin_JeremyRand.pgp
Normal file
14
basicswap/pgp/keys/namecoin_JeremyRand.pgp
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEXPNfCBYJKwYBBAHaRw8BAQdAWGFiEJYnlV2TDTesLIO/eoQ3IPduzcG97GpA
|
||||
6K+Gj+K0K0BKZXJlbXlSYW5kIG9uIEdpdEh1YiA8amVyZW15QG5hbWVjb2luLm9y
|
||||
Zz6IlgQTFggAPhYhBJza8EpykDv+wJWdvi2+M54p9ilMBQJc88q7AhsDBQkB4TOA
|
||||
BQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEC2+M54p9ilMMUoA/1oZn8AtwQ7D
|
||||
wXgNKq++zqHaiEcHGgsyFeDRbQARsYRVAQDxa36p181id1YuMjeV1KhC5vaDS4nY
|
||||
GB4FHPsQ4bbqDLRESmVyZW15IFJhbmQgKE5hbWVjb2luIENvcmUgR2l0aWFuIFNp
|
||||
Z25pbmcgS2V5KSA8amVyZW15QG5hbWVjb2luLm9yZz6ImQQTFggAQQIbAwUJAeEz
|
||||
gAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBJza8EpykDv+wJWdvi2+M54p9ilM
|
||||
BQJc9WDrAhkBAAoJEC2+M54p9ilMz3IA/3mCKeFYcEJFlwP43mdIMGOV2zt/R4Fn
|
||||
z/rBJpv5hFoHAQDXAY8+mbY/9N+5Rn8Iy51tXEaTq3khdruuFFdty+bXAg==
|
||||
=EpnJ
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,52 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGdeBqoBEADuBizUBhm1m34OQ0rnqUONvkfL3tGsriWuX0n3Bq9rhf3I3kZk
|
||||
5fi+R0Jj6jmz+sbUYRULU35S6eeIY77eYiveWl81H+3JAu8kmo/S6gegINnsPM/g
|
||||
F7X2P757gQdHMAE0olME3iGfXNpLg/e0OBv3j45eimjvUejgE7eI0e4gjajq8hyf
|
||||
bizMrGT+5I2PE0g3h07NqN3OuI5xVYujuZp41EgxY99QgYm5qEoU0wMGy8+F7gXV
|
||||
0htjhvUZcSGGpixP5+kaJJXFAP1TkZ/jqya6vy7LLeEEEuU8eMWhViOmzIjqoOFW
|
||||
Mq+2rJUrzNEk43tXW5LU+DdGl90HQcXPmQP3aWL27Dx/4AcTMYPDB/0bJrU9qF9Y
|
||||
9zfJV2HcNMnkhEb9XKDwkA6m3Jx2gfYG6HoMKp6bWSWsODItEgL1taoy35OnaVSM
|
||||
NWb857DC6p6n+eQUXUNx/1ct4LWmf4lN4Uf61i4mD+hkc4cWmRLAh7vTqMGG4xmb
|
||||
8Tb3wss8mEXzJvWVP4+bE6EkNPMCVAQleD4ePItaDg3lSJH/cIueIz6NDl5ik07r
|
||||
AZOZTxhhGU1CD8NkxQKoZLZ6GgjHDEwiUbxaCoD0FAzqtG5/at+jiwyDmCsJ96aE
|
||||
f0tPLXKOOc62BbqsAUuEOIooGwX/swXrhS4Xvfh8GxBYFBlRponoWXG7XQARAQAB
|
||||
tEhSb3NlIFR1cmluZyAoUm9zZSBUdXJpbmcncyBzaWduaW5nIGtleSBmb3IgZGV2
|
||||
IHdvcmsuKSA8cm9zZXR1cmluZ0BwbS5tZT6JAk4EEwEKADgWIQT9g2aoB6mfon/Z
|
||||
zOqf47/dpsU0lQUCZ14GqgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCf
|
||||
47/dpsU0lTjfD/9WkMBWlbYhJwRU6JrdZdIPsj2jlMIDYEHXxFo+h1lNn1SLKKrE
|
||||
4c/+9+H0YGM03pL5ZTtydsxdPMTbAP5l24hBFpokySds3abOcKaPuNcct5BDWiiL
|
||||
UxsnV3SxCAsN3QcBt+0tYFYP9yIMkko9BRwsY7pSpjZOSCx26jeTKj7M4XQGdcpT
|
||||
4KMtzXe2s8ss1jLyuaDP2B5ikrFI+IZ5dHVBhohK3ug1y0SzHjfSYeskOEYSgJ/4
|
||||
uRUJCItWxrkSh16qRz+NFxwsewqIKz8Q0EmpHx4WpAii8z29IFPYKJEqdwcuPyF3
|
||||
7SiqAow4tY+CtnLAUYEbSiL52e8W/U8KSnrxqhkpMd5wZ28z+k682A5uEQn5YjOy
|
||||
7dBRjytSC2S87FJ+3zp4OtToDio8Wi0zpZWj/BD5K9raE2ct6Uiw3NG6JI8A7yaJ
|
||||
pEENfMpxMgKc8G5t8NfiZdDFDw+P+bd6sMAk3q7ZFe/o0zJcsbhtYacBFvwBpeIp
|
||||
HZnLdUQlKrZoASku7biTZyt7BBJZuNdVv6Q/K+pigJxTYCZNbbx9s/lzS6KGUKuD
|
||||
yi7n/1qYFXVFktomR+Cm045btVNeAQpnfIKiJS77FNeB5saSWEAOcCMtUkoR74lA
|
||||
9MGYdeWrPjvdeBu+Muvo/y1h57sVMwvStrXjGrJNs6KBcmvITXrek0osbrkCDQRn
|
||||
XgaqARAAu8bgP9AbeNatYshdG1xoYv20FeC0MUz0oYu+FvVuhvaAePl/VFFBlh3O
|
||||
CsCzJ+a+/hyeW22ZGZl62yblvlZcSTw1/WOv5zboFVVLD58/iiz3dCYAUUTQ2OaI
|
||||
+oMLTCmZ/+GIcuVM1ZZMEohvR9eLcyzY89CgOi8R9+agqTXxNg7Uj43tPkgY2vc0
|
||||
v66od1SrOAisduXVDAiqTbc6nax9d9aYt27zQlGfuVo5J//rnteHiGA7VphDLlCR
|
||||
+dra1ZGjbdOieSyhxiEAkBPY2js6UqO/CoRn9uHaTSv4MJqzzMOzLfPni+6y3FqH
|
||||
qaUoe3vr07Ehf85gBEL4IBiux/WL3Vi1WceqvNkS9aC0MVnnEgHbyAy2R6pWrtN5
|
||||
vlxdrkqQcnnnYHvOupG5KPsgT/CFK0jGfA23I/dBPuI372EcqFLFpAB4q14cSLQE
|
||||
ZER81pK7Q445vTv9qQIPu34oq0mg7GWlunduI4v7uGN+oSYIW0kfNLRnM4QjNhTP
|
||||
07LJZLZoCRW2MyPqTbk8cM0UQDGFOozcjlSgSZSABLdHpnudArl6fzkMi4VH8WNS
|
||||
JNXvtL2yX8cnOWXuOgK5pFuhr6zeRaHsjlMXgR5ZPSCiq0aMR4upk5n/Mn64qGVm
|
||||
EnxDEBiGfgL1sl+GGl+rYxvH8vYEEX3fjTtlsaImUzKByfLaY60AEQEAAYkCNgQY
|
||||
AQoAIBYhBP2DZqgHqZ+if9nM6p/jv92mxTSVBQJnXgaqAhsMAAoJEJ/jv92mxTSV
|
||||
+0wP/itANwrdF+9kolUUVJg8Vkx7IgIGlcdIiUTxPAu9c8JdTKpziy9q7oVVpzLf
|
||||
zo+4qgzXGUGuGtcHdM8XSFYQ8CAuuOdvPUvtKbNQiZ1DVjoS/wk4vrzIvLTS1VVd
|
||||
f4jTgOImx3Tk75/8KX3EpCk26orMMBCHk7nWWia1KF8X2K2Hu1DZ9GqsWlE/uAPN
|
||||
tS/+ONlbn6tlk1XWDvFC8DkDkRWNRPva++GP5ACylybOHy2rqWKNEtetYflDuMIc
|
||||
5tkrXZ/rdZgzASKzSrNlEjN2DEBjl15WjUppOPkSc4QPK+SVza6UZJaE7oOrIOqs
|
||||
tQRchspkyDFreCuK/WZLZC8SUwZ5rzbOsFMLUHeZtFtNkJGxwF1ZUNHbNPPCEaCN
|
||||
oqNu/nkjxFqeydJfqDM8K8An9dQE2GkUm1nACpuLNgpILXebdG7ItVbbkjosx7HI
|
||||
0i3BXHeQzT+xY1gmuFFGEVCf9bZVmYspXJaiRGFRfGVyc6mMtdow7urb/A9g5Jqb
|
||||
Dkc+p29y9hCeOAVZfTY2C/GlWu9X/E64WJ2mQ3ujhtJmSgLM4ieYJU+lxosOC6BW
|
||||
EjFrTOeLa+myW7qm+/R6Mo/545s1qXvXnDL5Z4aVkSHtUu+fiWBa4f4WaH3mxAAg
|
||||
XLVwKhulQ3wPaCehbbMPbsQ+091iAOo+hn9s2BPfehM0ltgI
|
||||
=atlH
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,29 +1,78 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFlLyYIBEADW3oJnVMDC94g+7OB1/IBUYNevCqJIOAtC0eeFS5g0XGvvqRLx
|
||||
2NLUqn5te+R7deoGElkZFJLLxFUwEnhqGCRH50Iou5aanUzvgI5fVAbK3k0fp9vc
|
||||
LKCR0fQVIidcLyqMpkLZo8BSE3+BWxFp/r2OHvh2dYtJC+BZVwblkDS3cqwKvUZx
|
||||
IocvDs47Wo3tzZfEsqUavbbiGx+Dm0fCV7TVHdVLU7q3bZsHSRiyTUZ2EAApoAmT
|
||||
ir9csVxv2IM8yf6/HwWi6/Lp7dgSG1+qBZ1lUPPTY+dFLPZyt/kul+vuOj6GLZaU
|
||||
s3D66d7TaPCHKWAOnP9RHpic/iXODXVXo1KHJfa0x8fW7I+y7/Gb+5x/m4O0Bz2T
|
||||
BivdrSAuFpXkPqwawlw4CPgI9fc801g83+ZFzD2jJ6qxkEgfnlmf+zGNn5tC4N5j
|
||||
NRTQ+GyHo1w4824SXcSN590wgz8goGJC3QPJxbifvOA8GzQIVzpxHckofOVyqIEq
|
||||
qSnkP2xn4mELqD7HcFnoojZBqFbF2cN+oWQ+niLN+v4qrUncpQI9SVWlyp66S+1T
|
||||
BhBQj2QuX+3B+K27EiDbhNV7EX6xEbGsnB1poMc2aMiz6veybW3GnoM+2ppr8Ko/
|
||||
12Ij7l+ZA44t6PWUfQQbNSbUk/0Yhd9QJ8VQVck+TaS6gtarTbURlSdmHwARAQAB
|
||||
tCl0ZWNub3ZlcnQgKGdpdGlhbikgPHRlY25vdmVydEBwYXJ0aWNsLmlvPokCVAQT
|
||||
AQgAPhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mCAhsDBQkSzAMABQsJCAcC
|
||||
BhUICQoLAgQWAgMBAh4BAheAAAoJEBPxNlHJzw1rtdYP/22iRX8O2Q++MayPHNx5
|
||||
XHAlMk9mfi5FB1qJwshtlhda7P9U/hOTi227wH+Mzh5dBje4t2DkoHzxlz/Wr4cQ
|
||||
QUJMOYd0OEZY6kpAQkvtyYobIb6zlRQK1koAfNMxewmfZZGTlr16IUVCovGSFvZ+
|
||||
hdYRDEjuHqXjpwBfrxFAy/HCnfY10qSRkJc5w1ypj1IkzlanS+xeRJSDvRTQDAEr
|
||||
zv3xKcMGjCCHaaCP+tyAaViBaUOlvmZdWwg0gwQCuPLqIh0cfDbcg0quciRIpnyp
|
||||
zINmfwngZCwXdIYfAzmCzMHw1J3iOiqfqK0EcpHMsL689VyQSPgsoEHtcOGHYjRL
|
||||
pMPGvRFHtICrnCHENK3IcwFWDGXW+i3zgOlA7g48yYWWvSup+t8I6YT+FeeFlxSO
|
||||
dj1GdeMA0O7gXZ7znLVduokL2Ef4dZjc+3NwBlFov52vwCZwQMAGsMriwEDB2rDZ
|
||||
B7YOvAxlUB/kavtx/oE8fV7mZcuwYg02lq4bozF9xlhjOFaRit6xXnLVi0TuI76c
|
||||
uz67nB9VkWczSLIzCyjNyFpWbx1BMxTYfehZX3+YNajXwG6HdEp9CAYK0u46Guz/
|
||||
Pth67bbNVYyP/cuOIrv/hqQ6xo4mOBMDDxcCEAXx3rwxfbxNM8vlwrMcpITrtNON
|
||||
r41bcxUIfMEDefPm5wnXep8W
|
||||
=szpX
|
||||
mQENBFlLyDYBCADqup3EHjFCMELf4I0smf4hDl48qDn/Hue08JLmSToMc7z9ylLk
|
||||
6Uzx6S1m7RiDO63A7yW4qyRkb54VNj+6rUSPNt2uVy1vT8OEQJAZLf2c4qpaKHAQ
|
||||
QV3utu8pYxYOJfLHh4zNEGXrbSrjDv/FTPuri+SkIABhjf70ZSocm4l49rtBanK5
|
||||
AIAp8DoXWcUdbwmAfl6qrLfzrDu75kq+bspd8p4CVy4fzdOtr6LvXW38z1t3XtLP
|
||||
+EGVMAzZQWr2WbN762rK7skH+ZfhaMjAwr8gPYymYnFGLdS1nBmhksnulQNGQOro
|
||||
WojsvQKgBJoGUnp/OrVpi3gn7UNfDo99CxMRABEBAAG0IHRlY25vdmVydCA8dGVj
|
||||
bm92ZXJ0QHBhcnRpY2wuaW8+iQFUBBMBCAA+FiEEOQGTZk5wi3vnahADcJ5tyVzr
|
||||
Ac8FAllLyDYCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQcJ5t
|
||||
yVzrAc+0LAf/SvBJFJGq1yT9pdLT+7lv7BrshfSYQBLNqPmPrxRuxzH3q/EaEk6D
|
||||
oQh/Jk4vmSXR1y+bsKtS55ekGsPZZWlUFMbXDuU0II3YkWewHXTnqxLtqzcWODoK
|
||||
6vPonjiVuhYC57d4TWw5ebzHy8wICunyVeaL/cvYQM1TfaI2fN5v0Ep+XiRpH/15
|
||||
HQzRaynKq58w7gH79mPIRA2WFz4eMIMWS3rSa+cSoJ0MhpimgnKUDlh2DebVP1eH
|
||||
keSW1JlPZHhca/XB93ghFlbO6wOrbg+gsKtB45OkpsoOzUMFIKVJLBAjK751dTcc
|
||||
Pb4xTzABaBXxk+IUxgGB1h+g3i6wzksfgLkBDQRZS8g2AQgAw7Db3G5J21jsty9S
|
||||
pMmqp93dgZFm8E4VTcsL4KVvZybhwHngNHnhG8G/DWQ53o07/BKorfRBmFD3x2Eq
|
||||
RqfOn4ytmZVw/sOjbZPi4m/tF8z+O9qR8I0CzedYip21rwz2j4UgnpDQ+BnOpyXB
|
||||
H0gDBlPFq8ih9kkm413QRTTKnkRM/U8SfyFU8vIFdH7T0Ae07m0LxePDaTyxLPg3
|
||||
x1+RvEjVkruc3/9Z4kzexoUv654wirRdxPX8GsWI1WNDQrj4GqmpF/e0WDM97+Lk
|
||||
DGzbcXy7TGMIHQx8QFlFwdSZv9x70574as9Od4jOWTk90sopSMr8t6H6wTdn+2MD
|
||||
qsZKUwARAQABiQE8BBgBCAAmFiEEOQGTZk5wi3vnahADcJ5tyVzrAc8FAllLyDYC
|
||||
GwwFCQPCZwAACgkQcJ5tyVzrAc/QFgf8CQydF/VqJtujQC/rjB1YYNQcljzoeQWA
|
||||
2F2O5cF5skTNYy+xas3PTgxfOpn5iTpixpkB+I7X8LwoPmRjZvg2MFirDVXUypcx
|
||||
HwMbQqYCuAaK1EhtVUVYbFGjM67nClmBApLdenbqEP/BhyR9kgDCBt7ZvSLe5N/6
|
||||
MKYJF1FlCgGc5OJPJrMIl0slU5QtzRy5J+l75WflkgxFUKJPotJ5Z+yduxOff//e
|
||||
qSEXqlkaebWT0ZFiAqHhExJCRJ5HBqQEdW4JHrB7j3bNh8Qdf8epiYtcXXSsE9+K
|
||||
XEP7UJRk5bFFKdn0wMONgmQLMjjspU5byMQDJ0hFNMmmrbKX2AXqRpkCDQRZS8mC
|
||||
ARAA1t6CZ1TAwveIPuzgdfyAVGDXrwqiSDgLQtHnhUuYNFxr76kS8djS1Kp+bXvk
|
||||
e3XqBhJZGRSSy8RVMBJ4ahgkR+dCKLuWmp1M74COX1QGyt5NH6fb3CygkdH0FSIn
|
||||
XC8qjKZC2aPAUhN/gVsRaf69jh74dnWLSQvgWVcG5ZA0t3KsCr1GcSKHLw7OO1qN
|
||||
7c2XxLKlGr224hsfg5tHwle01R3VS1O6t22bB0kYsk1GdhAAKaAJk4q/XLFcb9iD
|
||||
PMn+vx8Fouvy6e3YEhtfqgWdZVDz02PnRSz2crf5Lpfr7jo+hi2WlLNw+une02jw
|
||||
hylgDpz/UR6YnP4lzg11V6NShyX2tMfH1uyPsu/xm/ucf5uDtAc9kwYr3a0gLhaV
|
||||
5D6sGsJcOAj4CPX3PNNYPN/mRcw9oyeqsZBIH55Zn/sxjZ+bQuDeYzUU0Phsh6Nc
|
||||
OPNuEl3EjefdMIM/IKBiQt0DycW4n7zgPBs0CFc6cR3JKHzlcqiBKqkp5D9sZ+Jh
|
||||
C6g+x3BZ6KI2QahWxdnDfqFkPp4izfr+Kq1J3KUCPUlVpcqeukvtUwYQUI9kLl/t
|
||||
wfituxIg24TVexF+sRGxrJwdaaDHNmjIs+r3sm1txp6DPtqaa/CqP9diI+5fmQOO
|
||||
Lej1lH0EGzUm1JP9GIXfUCfFUFXJPk2kuoLWq021EZUnZh8AEQEAAbQpdGVjbm92
|
||||
ZXJ0IChnaXRpYW4pIDx0ZWNub3ZlcnRAcGFydGljbC5pbz6JAlQEEwEIAD4WIQSO
|
||||
UX3BLsHMN/ZCOooT8TZRyc8NawUCWUvJggIbAwUJEswDAAULCQgHAgYVCAkKCwIE
|
||||
FgIDAQIeAQIXgAAKCRAT8TZRyc8Na7XWD/9tokV/DtkPvjGsjxzceVxwJTJPZn4u
|
||||
RQdaicLIbZYXWuz/VP4Tk4ttu8B/jM4eXQY3uLdg5KB88Zc/1q+HEEFCTDmHdDhG
|
||||
WOpKQEJL7cmKGyG+s5UUCtZKAHzTMXsJn2WRk5a9eiFFQqLxkhb2foXWEQxI7h6l
|
||||
46cAX68RQMvxwp32NdKkkZCXOcNcqY9SJM5Wp0vsXkSUg70U0AwBK8798SnDBowg
|
||||
h2mgj/rcgGlYgWlDpb5mXVsINIMEArjy6iIdHHw23INKrnIkSKZ8qcyDZn8J4GQs
|
||||
F3SGHwM5gszB8NSd4joqn6itBHKRzLC+vPVckEj4LKBB7XDhh2I0S6TDxr0RR7SA
|
||||
q5whxDStyHMBVgxl1vot84DpQO4OPMmFlr0rqfrfCOmE/hXnhZcUjnY9RnXjANDu
|
||||
4F2e85y1XbqJC9hH+HWY3PtzcAZRaL+dr8AmcEDABrDK4sBAwdqw2Qe2DrwMZVAf
|
||||
5Gr7cf6BPH1e5mXLsGINNpauG6MxfcZYYzhWkYresV5y1YtE7iO+nLs+u5wfVZFn
|
||||
M0iyMwsozchaVm8dQTMU2H3oWV9/mDWo18Buh3RKfQgGCtLuOhrs/z7Yeu22zVWM
|
||||
j/3LjiK7/4akOsaOJjgTAw8XAhAF8d68MX28TTPL5cKzHKSE67TTja+NW3MVCHzB
|
||||
A3nz5ucJ13qfFrkCDQRZS8mCARAA7QMvR0fFA1FZKzcS6/W5Jcm0g6FQ1xHaMeEh
|
||||
LECOQpM3wSOL1A8trbpC2VgMLjRFq+h3YQRlF8Y4oIaIz2UzziqK6mGZxhtEN6y3
|
||||
IIXrVC5CTpcDXxlvJyHeHQONvMnEbmnbHfZAtxJq2wFOr7BWiLVzfioyNSND/JOP
|
||||
VlgezL6YRAocQbHU7mQKY7gCqU4jDZIxru01e2hoIHSbAFXjmEcFBFoErWXAMf5w
|
||||
HaK7dGGMpJXgNCK2weatNCBxD/krv1gA7nheT665K7HUQxu/NhUIk8XnOPD5iDoJ
|
||||
zeQXHY3SM8jrhhabRubm27c/Oads9lgk9EGZhxLhIMQ9jUu7TsX1sPZpfnoE/JAq
|
||||
ofY3WwimOXYb+p0jetg4FQaqul6FpgesSI4Nl5nHHB8/4CWUv2oV2YjUJlBpazyc
|
||||
ullt8a7GdwzQMbiw23Jgz1frrMuq/zQc4wLGUFchhnYMrva+6t0ewjxD7bCL/7N7
|
||||
3UDdNpVi+ZcBVQPVididC4iRcCLDqmr+WtTfVKw58Rnb7Qt9Z+2MqVZa1/numTG1
|
||||
DastjRg6KGkN6eYaxKcXHf7t/lYZ5ejGFVUh+wtwlb1tTpOvWKq130tuO/aDWTa2
|
||||
jViwy2UUpbyg5UbBvd0PHTJ+8TTdxEoC5wQCYHZ5Ueg9wwLhs0VQ44GI7vnXJZ8b
|
||||
aXUe/mEAEQEAAYkCPAQYAQgAJhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mC
|
||||
AhsMBQkSzAMAAAoJEBPxNlHJzw1r+3YQAM5648S/oQLnK5WO0/w3gIUI5g7BrdJO
|
||||
kRINe8SNYs6PvCFjKij/3p9YMxrc/TojTQfhxew7bNxkhDU7sudxIr6TcKW5SK9f
|
||||
g9zz2Ib5heR+orjPSX9hgSLX66t4DvJfdph+O1O3l83g0bsDUPCivTSnQ5XtdiVK
|
||||
ytOoM26/GaQHwzKbk1Qzn1nrZeLaeDAsJ30GdmteNRMof1G2H9kg/33xbcyRCMaT
|
||||
xjKS0ssa8RUmxuYsR+fjc7t5FvXwnfoXapkqUWcddFCCgAiTc0NZjzcDSXVB/++2
|
||||
KxLZ0Q86kuJwdb7KEq0SwPQAM6ikmIaoke9fJAZzhyyWX7AeSQx1ime31Xrjh0CC
|
||||
MHW+PdQMpLSNTAHEZDuybGKaShVMiHASXs7XsnJr6lOObMYzSGr0+B5fQWU7aHlM
|
||||
u+4YNHUwQldx/EqkL/DjIpocVC5ozaW+dV1zSMLBHdk24soWI+gLrL3FG0NMyNZ+
|
||||
O95X/bB/X+dqOBYpitR3xpYZes4Jl4Kechi60+mdDktFKfKfiRxyJlg2LNd7/OLB
|
||||
hpxg2zsXlHhqhSJAo9IGih2rOgcMwtCXKmHCGG5KGsNF8x3H9bPOwynAUMqUJ2cR
|
||||
7BCjzmUxUnsLcJnokUnHMbECZ+pee9YcaRNrlbVAIvED3ZHEhFJxIMaArxSLmRwE
|
||||
XHovfCfpcB/C
|
||||
=0Wkp
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGROehcBEACXWWc6dHqCos1PmKI32iHi0jP3mYM3jU57YxbjwT78QEtEwSqf
|
||||
YklpXkgTYq7jexx2JElfegM6w1sPYarq1y051RjnCgzl32da5I506SMvcJTmXumV
|
||||
Rw6erPeDxAO74PflDSlALgtGOgbKhwwWRudbWgT5hKGkl62qy0mI6GStul0rbT+3
|
||||
gq77DCGyURfe1PG1pymhO5XVz3WGtOa12NvRA+3wGIcqIji2MbtXuOhGMg//kVI5
|
||||
m2vcfHyMMuQ01xUXRu57WxRujYaJ1RB4p86JCbDX3YU2XlzTxGAhqChDLuJGqo54
|
||||
AZMUWDceftXsAoOqH8Hwmm5gFkYSpMt86ZT+umvWygmxohD5k85MuRj4AGagFj/u
|
||||
CMcQjI/SN1UU/Qozg6VL/5FO8aH9IybDzX7eE3j0V/jTweStw1CIUajYgfemWOWl
|
||||
whLPBDflRz/8EEqTN0CaSSaiYiULZUiawBO/bRIiCO2Q6QrAi3KpPUhCwiw/Yecd
|
||||
rAMLH7bytpECDdbNonQ/VMxWwtWJQ87qBtWvHFQxXBKjyuANsKL9X7v3KcYOUdd2
|
||||
fSt7eqE9GDT4DbK6sTmuTpq2TgHXET0cA39+N2zxTh5xFupI/pi2iAHJ6hgIiQnn
|
||||
662TngjGOSFvrTV/51Ua0Vx8OCMJJOcRdOVaYzuzg9DsjVcJin3aRqUh4wARAQAB
|
||||
tCBXb3dhcmlvIDx3b3dhcmlvQHByb3Rvbm1haWwuY29tPokCVAQTAQgAPhYhBKs6
|
||||
L3JYGPz/J5SEHHk1BLRJxpIgBQJkTnoXAhsDBQkHhh77BQsJCAcCBhUKCQgLAgQW
|
||||
AgMBAh4BAheAAAoJEHk1BLRJxpIgwgEP/109vw1WXRh9EaRr3Y1+sBi+/PRQ5TCx
|
||||
UEcP9ru5sQPJ0BsZK8RYw0BNIfDQX9OB1k/AoiBelL+0EoDKvjXmwz9fPUmSVk5r
|
||||
3RzfClXTnxn4HXPKkSGMt4WBUnvohTexK7CPkb9xy+K0Jtx8XF1XiQLDFg2a9lBj
|
||||
IIX2H6aHn4VjdUBv7TrTCAI2Vg0cQUpeJUwyHH+lk0r2WM3zAxzS3Iy2yDDstNT8
|
||||
audXEX4BtJhyEU1m57jwgscrbTtgwYOAsaRLcnUaAFWhbov3IiGInk7N1fkMsuW5
|
||||
HE5RcegSZRS3X4o6O/nmwdSjCEB9weydOCPrtfdbvfvuTiMg/jZBikOk/Sj7FM/D
|
||||
eZKghSHpLbT/V3S76FyIcc/xFkUmR+2fGvCNjJ1Qn2lXTS8xcbyzqR4LZPeUGppV
|
||||
hvriilLnXSjyc60wuD3kmCCo1Zw4tNL8pr09BtVmScUy6eiwca8LLzvbbivqxF1g
|
||||
Mrkkv8yQE0ZwO1kgNSn+PSzUPbwAoklcyN5Rhr5DxZh0UudiH5Jt5WWYeE8O2Uc1
|
||||
si13X575kymGkkeiUcp9WtBkh2uial+RVmTrUTDUTIR2HzT6MAR84/DHlC5dsW8a
|
||||
h4uDUhzeG2cTxuIfZC881UHKL+xT/I3PPuFdLbU5uoWJpXYpxKYulYWd7LA/k4bi
|
||||
JWBrQo7VDvvP
|
||||
=H3wS
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,13 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import json
|
||||
import traceback
|
||||
import shlex
|
||||
import urllib
|
||||
import traceback
|
||||
import subprocess
|
||||
from xmlrpc.client import (
|
||||
Fault,
|
||||
Transport,
|
||||
@@ -102,7 +104,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
@@ -118,7 +120,36 @@ def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
return Jsonrpc(url)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError(f"RPC error: {ex}")
|
||||
raise ValueError("RPC error " + str(ex))
|
||||
|
||||
|
||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None):
|
||||
cli_bin = os.path.join(bindir, cli_bin)
|
||||
|
||||
args = [
|
||||
cli_bin,
|
||||
]
|
||||
if chain != "mainnet":
|
||||
args.append("-" + chain)
|
||||
args.append("-datadir=" + datadir)
|
||||
if wallet is not None:
|
||||
args.append("-rpcwallet=" + wallet)
|
||||
args += shlex.split(cmd)
|
||||
|
||||
p = subprocess.Popen(
|
||||
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
out = p.communicate()
|
||||
|
||||
if len(out[1]) > 0:
|
||||
raise ValueError("RPC error " + str(out[1]))
|
||||
|
||||
r = out[0].decode("utf-8").strip()
|
||||
try:
|
||||
r = json.loads(r)
|
||||
except Exception:
|
||||
pass
|
||||
return r
|
||||
|
||||
|
||||
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
@@ -128,6 +159,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None, wallet_override=None):
|
||||
nonlocal port, auth, wallet, host
|
||||
return callrpc(
|
||||
port,
|
||||
auth,
|
||||
|
||||
@@ -309,6 +309,7 @@ def make_xmr_rpc2_func(
|
||||
transport.set_proxy(proxy_host, proxy_port)
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr2(
|
||||
port,
|
||||
method,
|
||||
@@ -344,6 +345,7 @@ def make_xmr_rpc_func(
|
||||
transport.set_proxy(proxy_host, proxy_port)
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr(
|
||||
port,
|
||||
method,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 743 B |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,5 +1,22 @@
|
||||
// Constants and State
|
||||
const PAGE_SIZE = 50;
|
||||
const COIN_NAME_TO_SYMBOL = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Litecoin': 'LTC',
|
||||
'Monero': 'XMR',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'PIVX': 'PIVX',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'Decred': 'DCR',
|
||||
'Wownero': 'WOW',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
'Dogecoin': 'DOGE'
|
||||
};
|
||||
|
||||
// Global state
|
||||
const state = {
|
||||
identities: new Map(),
|
||||
currentPage: 1,
|
||||
@@ -10,6 +27,7 @@ const state = {
|
||||
refreshPromise: null
|
||||
};
|
||||
|
||||
// DOM
|
||||
const elements = {
|
||||
swapsBody: document.getElementById('active-swaps-body'),
|
||||
prevPageButton: document.getElementById('prevPage'),
|
||||
@@ -22,6 +40,105 @@ const elements = {
|
||||
statusText: document.getElementById('status-text')
|
||||
};
|
||||
|
||||
// Identity Manager
|
||||
const IdentityManager = {
|
||||
cache: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
retryDelay: 2000,
|
||||
maxRetries: 3,
|
||||
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
async getIdentityData(address) {
|
||||
if (!address) {
|
||||
return { address: '' };
|
||||
}
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) {
|
||||
return { ...cachedData, address };
|
||||
}
|
||||
|
||||
if (this.pendingRequests.has(address)) {
|
||||
const pendingData = await this.pendingRequests.get(address);
|
||||
return { ...pendingData, address };
|
||||
}
|
||||
|
||||
const request = this.fetchWithRetry(address);
|
||||
this.pendingRequests.set(address, request);
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
this.cache.set(address, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
return { ...data, address };
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching identity for ${address}:`, error);
|
||||
return { address };
|
||||
} finally {
|
||||
this.pendingRequests.delete(address);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedIdentity(address) {
|
||||
const cached = this.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
if (cached) {
|
||||
this.cache.delete(address);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
...data,
|
||||
address,
|
||||
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
|
||||
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
|
||||
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
|
||||
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
|
||||
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
|
||||
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
|
||||
label: data.label || '',
|
||||
note: data.note || '',
|
||||
automation_override: safeParseInt(data.automation_override)
|
||||
};
|
||||
} catch (error) {
|
||||
if (attempt >= this.maxRetries) {
|
||||
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||
return {
|
||||
address,
|
||||
num_sent_bids_successful: 0,
|
||||
num_recv_bids_successful: 0,
|
||||
num_sent_bids_failed: 0,
|
||||
num_recv_bids_failed: 0,
|
||||
num_sent_bids_rejected: 0,
|
||||
num_recv_bids_rejected: 0,
|
||||
label: '',
|
||||
note: '',
|
||||
automation_override: 0
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
||||
return this.fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const safeParseInt = (value) => {
|
||||
const parsed = parseInt(value);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
@@ -83,6 +200,7 @@ const getTxStatusClass = (status) => {
|
||||
return 'text-blue-500';
|
||||
};
|
||||
|
||||
// Util
|
||||
const formatTimeAgo = (timestamp) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - timestamp;
|
||||
@@ -93,6 +211,7 @@ const formatTimeAgo = (timestamp) => {
|
||||
return `${Math.floor(diff / 86400)} days ago`;
|
||||
};
|
||||
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
@@ -132,6 +251,96 @@ const getTimeStrokeColor = (expireTime) => {
|
||||
return '#10B981'; // More than 30 minutes
|
||||
};
|
||||
|
||||
// WebSocket Manager
|
||||
const WebSocketManager = {
|
||||
ws: null,
|
||||
processingQueue: false,
|
||||
reconnectTimeout: null,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectAttempts: 0,
|
||||
reconnectDelay: 5000,
|
||||
|
||||
initialize() {
|
||||
this.connect();
|
||||
this.startHealthCheck();
|
||||
},
|
||||
|
||||
connect() {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
try {
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
setupEventHandlers() {
|
||||
this.ws.onopen = () => {
|
||||
state.wsConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
updateConnectionStatus('connected');
|
||||
console.log('🟢 WebSocket connection established for Swaps in Progress');
|
||||
updateSwapsTable({ resetPage: true, refreshData: true });
|
||||
};
|
||||
|
||||
this.ws.onmessage = () => {
|
||||
if (!this.processingQueue) {
|
||||
this.processingQueue = true;
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (!state.isRefreshing) {
|
||||
await updateSwapsTable({ resetPage: false, refreshData: true });
|
||||
}
|
||||
} finally {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
state.wsConnected = false;
|
||||
updateConnectionStatus('disconnected');
|
||||
this.handleReconnect();
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
updateConnectionStatus('error');
|
||||
};
|
||||
},
|
||||
|
||||
startHealthCheck() {
|
||||
setInterval(() => {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
this.handleReconnect();
|
||||
}
|
||||
}, 30000);
|
||||
},
|
||||
|
||||
handleReconnect() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
||||
} else {
|
||||
updateConnectionStatus('error');
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// UI
|
||||
const updateConnectionStatus = (status) => {
|
||||
const { statusDot, statusText } = elements;
|
||||
if (!statusDot || !statusText) return;
|
||||
@@ -293,8 +502,8 @@ const createSwapTableRow = async (swap) => {
|
||||
|
||||
const identity = await IdentityManager.getIdentityData(swap.addr_from);
|
||||
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
|
||||
const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from;
|
||||
const toSymbol = window.CoinManager.getSymbol(swap.coin_to) || swap.coin_to;
|
||||
const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from;
|
||||
const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to;
|
||||
const timeColor = getTimeStrokeColor(swap.expire_at);
|
||||
const fromAmount = parseFloat(swap.amount_from) || 0;
|
||||
const toAmount = parseFloat(swap.amount_to) || 0;
|
||||
@@ -395,10 +604,11 @@ const createSwapTableRow = async (swap) => {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Status Column -->
|
||||
<td class="py-3 px-4 text-center">
|
||||
<div data-tooltip-target="tooltip-status-${uniqueId}" class="flex justify-center">
|
||||
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center text-center rounded-full text-xs font-medium bold ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
|
||||
${swap.bid_state}
|
||||
</span>
|
||||
</div>
|
||||
@@ -501,8 +711,6 @@ const createSwapTableRow = async (swap) => {
|
||||
async function updateSwapsTable(options = {}) {
|
||||
const { resetPage = false, refreshData = true } = options;
|
||||
|
||||
//console.log('Updating swaps table:', { resetPage, refreshData });
|
||||
|
||||
if (state.refreshPromise) {
|
||||
await state.refreshPromise;
|
||||
return;
|
||||
@@ -528,19 +736,9 @@ async function updateSwapsTable(options = {}) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
//console.log('Received swap data:', data);
|
||||
|
||||
state.swapsData = Array.isArray(data)
|
||||
? data.filter(swap => {
|
||||
const isActive = isActiveSwap(swap);
|
||||
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
|
||||
return isActive;
|
||||
})
|
||||
: [];
|
||||
|
||||
//console.log('Filtered active swaps:', state.swapsData);
|
||||
state.swapsData = Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
//console.error('Error fetching swap data:', error);
|
||||
console.error('Error fetching swap data:', error);
|
||||
state.swapsData = [];
|
||||
} finally {
|
||||
state.refreshPromise = null;
|
||||
@@ -566,14 +764,13 @@ async function updateSwapsTable(options = {}) {
|
||||
const endIndex = startIndex + PAGE_SIZE;
|
||||
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
|
||||
|
||||
//console.log('Current page swaps:', currentPageSwaps);
|
||||
|
||||
if (elements.swapsBody) {
|
||||
if (currentPageSwaps.length > 0) {
|
||||
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
|
||||
const rows = await Promise.all(rowPromises);
|
||||
elements.swapsBody.innerHTML = rows.join('');
|
||||
|
||||
// Initialize tooltips
|
||||
if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
|
||||
@@ -588,7 +785,6 @@ async function updateSwapsTable(options = {}) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//console.log('No active swaps found, displaying empty state');
|
||||
elements.swapsBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
||||
@@ -598,6 +794,22 @@ async function updateSwapsTable(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.paginationControls) {
|
||||
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.currentPageSpan) {
|
||||
elements.currentPageSpan.textContent = state.currentPage;
|
||||
}
|
||||
|
||||
if (elements.prevPageButton) {
|
||||
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
if (elements.nextPageButton) {
|
||||
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating swaps table:', error);
|
||||
if (elements.swapsBody) {
|
||||
@@ -613,10 +825,7 @@ async function updateSwapsTable(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function isActiveSwap(swap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Event
|
||||
const setupEventListeners = () => {
|
||||
if (elements.refreshSwapsButton) {
|
||||
elements.refreshSwapsButton.addEventListener('click', async (e) => {
|
||||
@@ -656,11 +865,8 @@ const setupEventListeners = () => {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Init
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
WebSocketManager.initialize();
|
||||
setupEventListeners();
|
||||
await updateSwapsTable({ resetPage: true, refreshData: true });
|
||||
const autoRefreshInterval = setInterval(async () => {
|
||||
await updateSwapsTable({ resetPage: false, refreshData: true });
|
||||
}, 10000); // 30 seconds
|
||||
});
|
||||
@@ -1,5 +1,22 @@
|
||||
// Constants and State
|
||||
const PAGE_SIZE = 50;
|
||||
const COIN_NAME_TO_SYMBOL = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Litecoin': 'LTC',
|
||||
'Monero': 'XMR',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'PIVX': 'PIVX',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'Decred': 'DCR',
|
||||
'Wownero': 'WOW',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
'Dogecoin': 'DOGE'
|
||||
};
|
||||
|
||||
// Global state
|
||||
const state = {
|
||||
dentities: new Map(),
|
||||
currentPage: 1,
|
||||
@@ -10,6 +27,7 @@ const state = {
|
||||
refreshPromise: null
|
||||
};
|
||||
|
||||
// DOM
|
||||
const elements = {
|
||||
bidsBody: document.getElementById('bids-body'),
|
||||
prevPageButton: document.getElementById('prevPage'),
|
||||
@@ -22,6 +40,125 @@ const elements = {
|
||||
statusText: document.getElementById('status-text')
|
||||
};
|
||||
|
||||
// Identity Manager
|
||||
const IdentityManager = {
|
||||
cache: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
retryDelay: 2000,
|
||||
maxRetries: 3,
|
||||
cacheTimeout: 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
async getIdentityData(address) {
|
||||
if (!address) {
|
||||
return { address: '' };
|
||||
}
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) {
|
||||
return { ...cachedData, address };
|
||||
}
|
||||
|
||||
if (this.pendingRequests.has(address)) {
|
||||
const pendingData = await this.pendingRequests.get(address);
|
||||
return { ...pendingData, address };
|
||||
}
|
||||
|
||||
const request = this.fetchWithRetry(address);
|
||||
this.pendingRequests.set(address, request);
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
this.cache.set(address, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
return { ...data, address };
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching identity for ${address}:`, error);
|
||||
return { address };
|
||||
} finally {
|
||||
this.pendingRequests.delete(address);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedIdentity(address) {
|
||||
const cached = this.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
if (cached) {
|
||||
this.cache.delete(address);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
...data,
|
||||
address,
|
||||
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
|
||||
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
|
||||
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
|
||||
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
|
||||
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
|
||||
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
|
||||
label: data.label || '',
|
||||
note: data.note || '',
|
||||
automation_override: safeParseInt(data.automation_override)
|
||||
};
|
||||
} catch (error) {
|
||||
if (attempt >= this.maxRetries) {
|
||||
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||
return {
|
||||
address,
|
||||
num_sent_bids_successful: 0,
|
||||
num_recv_bids_successful: 0,
|
||||
num_sent_bids_failed: 0,
|
||||
num_recv_bids_failed: 0,
|
||||
num_sent_bids_rejected: 0,
|
||||
num_recv_bids_rejected: 0,
|
||||
label: '',
|
||||
note: '',
|
||||
automation_override: 0
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
||||
return this.fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
},
|
||||
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
this.pendingRequests.clear();
|
||||
},
|
||||
|
||||
removeFromCache(address) {
|
||||
this.cache.delete(address);
|
||||
this.pendingRequests.delete(address);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
const now = Date.now();
|
||||
for (const [address, cached] of this.cache.entries()) {
|
||||
if (now - cached.timestamp >= this.cacheTimeout) {
|
||||
this.cache.delete(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Util
|
||||
const formatTimeAgo = (timestamp) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - timestamp;
|
||||
@@ -205,6 +342,96 @@ const createIdentityTooltip = (identity) => {
|
||||
`;
|
||||
};
|
||||
|
||||
// WebSocket Manager
|
||||
const WebSocketManager = {
|
||||
ws: null,
|
||||
processingQueue: false,
|
||||
reconnectTimeout: null,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectAttempts: 0,
|
||||
reconnectDelay: 5000,
|
||||
|
||||
initialize() {
|
||||
this.connect();
|
||||
this.startHealthCheck();
|
||||
},
|
||||
|
||||
connect() {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
try {
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
setupEventHandlers() {
|
||||
this.ws.onopen = () => {
|
||||
state.wsConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
updateConnectionStatus('connected');
|
||||
console.log('🟢 WebSocket connection established for Bid Requests');
|
||||
updateBidsTable({ resetPage: true, refreshData: true });
|
||||
};
|
||||
|
||||
this.ws.onmessage = () => {
|
||||
if (!this.processingQueue) {
|
||||
this.processingQueue = true;
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (!state.isRefreshing) {
|
||||
await updateBidsTable({ resetPage: false, refreshData: true });
|
||||
}
|
||||
} finally {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
state.wsConnected = false;
|
||||
updateConnectionStatus('disconnected');
|
||||
this.handleReconnect();
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
updateConnectionStatus('error');
|
||||
};
|
||||
},
|
||||
|
||||
startHealthCheck() {
|
||||
setInterval(() => {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
this.handleReconnect();
|
||||
}
|
||||
}, 30000);
|
||||
},
|
||||
|
||||
handleReconnect() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
||||
} else {
|
||||
updateConnectionStatus('error');
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// UI
|
||||
const updateConnectionStatus = (status) => {
|
||||
const { statusDot, statusText } = elements;
|
||||
if (!statusDot || !statusText) return;
|
||||
@@ -272,8 +499,8 @@ const createBidTableRow = async (bid) => {
|
||||
const rate = toAmount > 0 ? toAmount / fromAmount : 0;
|
||||
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
|
||||
|
||||
const fromSymbol = window.CoinManager.getSymbol(bid.coin_from) || bid.coin_from;
|
||||
const toSymbol = window.CoinManager.getSymbol(bid.coin_to) || bid.coin_to;
|
||||
const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from;
|
||||
const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to;
|
||||
|
||||
const timeColor = getTimeStrokeColor(bid.expire_at);
|
||||
const uniqueId = `${bid.bid_id}_${bid.created_at}`;
|
||||
@@ -368,6 +595,7 @@ const createBidTableRow = async (bid) => {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- You Get Column -->
|
||||
<td class="py-0">
|
||||
<div class="py-3 px-4 text-right">
|
||||
@@ -624,6 +852,7 @@ async function updateBidsTable(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Event
|
||||
const setupEventListeners = () => {
|
||||
if (elements.refreshBidsButton) {
|
||||
elements.refreshBidsButton.addEventListener('click', async () => {
|
||||
@@ -663,8 +892,8 @@ if (elements.refreshBidsButton) {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Init
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
WebSocketManager.initialize();
|
||||
setupEventListeners();
|
||||
await updateBidsTable({ resetPage: true, refreshData: true });
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Constants and State
|
||||
const PAGE_SIZE = 50;
|
||||
const state = {
|
||||
currentPage: {
|
||||
@@ -166,225 +167,246 @@ const EventManager = {
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
//console.log('Starting comprehensive cleanup process for bids table');
|
||||
console.log('Starting cleanup process');
|
||||
EventManager.clearAll();
|
||||
|
||||
try {
|
||||
const exportSentButton = document.getElementById('exportSentBids');
|
||||
const exportReceivedButton = document.getElementById('exportReceivedBids');
|
||||
|
||||
if (exportSentButton) {
|
||||
exportSentButton.remove();
|
||||
}
|
||||
|
||||
if (exportReceivedButton) {
|
||||
exportReceivedButton.remove();
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
const originalCleanup = window.TooltipManager.cleanup;
|
||||
window.TooltipManager.cleanup = function() {
|
||||
originalCleanup.call(window.TooltipManager);
|
||||
|
||||
setTimeout(() => {
|
||||
forceTooltipDOMCleanup();
|
||||
|
||||
const detachedTooltips = document.querySelectorAll('[id^="tooltip-"]');
|
||||
detachedTooltips.forEach(tooltip => {
|
||||
const tooltipId = tooltip.id;
|
||||
const trigger = document.querySelector(`[data-tooltip-target="${tooltipId}"]`);
|
||||
if (!trigger || !document.body.contains(trigger)) {
|
||||
tooltip.remove();
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
};
|
||||
}
|
||||
|
||||
WebSocketManager.cleanup();
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = null;
|
||||
}
|
||||
|
||||
if (state.refreshPromise) {
|
||||
state.isRefreshing = false;
|
||||
}
|
||||
|
||||
if (window.WebSocketManager) {
|
||||
WebSocketManager.disconnect();
|
||||
}
|
||||
|
||||
cleanupTooltips();
|
||||
forceTooltipDOMCleanup();
|
||||
|
||||
if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
tooltipIdsToCleanup.clear();
|
||||
|
||||
const cleanupTableBody = (tableId) => {
|
||||
const tbody = document.getElementById(tableId);
|
||||
if (!tbody) return;
|
||||
|
||||
const rows = tbody.querySelectorAll('tr');
|
||||
rows.forEach(row => {
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.removeListenersByElement(row);
|
||||
} else {
|
||||
EventManager.removeAll(row);
|
||||
}
|
||||
Array.from(row.attributes).forEach(attr => {
|
||||
if (attr.name.startsWith('data-')) {
|
||||
row.removeAttribute(attr.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
while (tbody.firstChild) {
|
||||
tbody.removeChild(tbody.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
cleanupTableBody('sent-tbody');
|
||||
cleanupTableBody('received-tbody');
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.clearAll();
|
||||
} else {
|
||||
EventManager.clearAll();
|
||||
}
|
||||
|
||||
const clearAllAnimationFrames = () => {
|
||||
const rafList = window.requestAnimationFrameList;
|
||||
if (Array.isArray(rafList)) {
|
||||
rafList.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
window.requestAnimationFrameList = [];
|
||||
}
|
||||
};
|
||||
clearAllAnimationFrames();
|
||||
|
||||
state.data = {
|
||||
sent: [],
|
||||
received: []
|
||||
};
|
||||
|
||||
state.currentPage = {
|
||||
sent: 1,
|
||||
received: 1
|
||||
};
|
||||
|
||||
state.isLoading = false;
|
||||
state.isRefreshing = false;
|
||||
state.wsConnected = false;
|
||||
state.refreshPromise = null;
|
||||
|
||||
state.filters = {
|
||||
state: -1,
|
||||
sort_by: 'created_at',
|
||||
sort_dir: 'desc',
|
||||
with_expired: true,
|
||||
searchQuery: '',
|
||||
coin_from: 'any',
|
||||
coin_to: 'any'
|
||||
};
|
||||
|
||||
if (window.IdentityManager) {
|
||||
IdentityManager.clearCache();
|
||||
}
|
||||
|
||||
if (window.CacheManager) {
|
||||
CacheManager.cleanup(true);
|
||||
}
|
||||
|
||||
if (window.MemoryManager) {
|
||||
MemoryManager.forceCleanup();
|
||||
}
|
||||
|
||||
Object.keys(elements).forEach(key => {
|
||||
elements[key] = null;
|
||||
});
|
||||
|
||||
console.log('Comprehensive cleanup completed');
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup process:', error);
|
||||
console.log('Cleanup completed');
|
||||
}
|
||||
|
||||
document.addEventListener('beforeunload', cleanup);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
WebSocketManager.pause();
|
||||
} else {
|
||||
WebSocketManager.resume();
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket Management
|
||||
const WebSocketManager = {
|
||||
ws: null,
|
||||
processingQueue: false,
|
||||
reconnectTimeout: null,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectAttempts: 0,
|
||||
reconnectDelay: 5000,
|
||||
healthCheckInterval: null,
|
||||
isPaused: false,
|
||||
lastMessageTime: Date.now(),
|
||||
|
||||
initialize() {
|
||||
this.connect();
|
||||
this.startHealthCheck();
|
||||
},
|
||||
|
||||
isConnected() {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
},
|
||||
|
||||
connect() {
|
||||
if (this.isConnected() || this.isPaused) return;
|
||||
|
||||
if (this.ws) {
|
||||
this.cleanupConnection();
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.EventManager) EventManager.clearAll();
|
||||
if (window.CleanupManager) CleanupManager.clearAll();
|
||||
if (window.WebSocketManager) WebSocketManager.disconnect();
|
||||
|
||||
state.data = { sent: [], received: [] };
|
||||
state.isLoading = false;
|
||||
|
||||
Object.keys(elements).forEach(key => {
|
||||
elements[key] = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failsafe cleanup also failed:', e);
|
||||
}
|
||||
}
|
||||
const wsPort = window.ws_port || '11700';
|
||||
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection error:', error);
|
||||
this.handleReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
window.cleanupBidsTable = cleanup;
|
||||
setupEventHandlers() {
|
||||
if (!this.ws) return;
|
||||
|
||||
CleanupManager.addListener(document, 'visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
//console.log('Page hidden - pausing WebSocket and optimizing memory');
|
||||
|
||||
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
|
||||
WebSocketManager.pause();
|
||||
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
|
||||
WebSocketManager.disconnect();
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
// Run memory optimization
|
||||
if (window.MemoryManager) {
|
||||
MemoryManager.forceCleanup();
|
||||
}
|
||||
} else {
|
||||
|
||||
if (WebSocketManager && typeof WebSocketManager.resume === 'function') {
|
||||
WebSocketManager.resume();
|
||||
} else if (WebSocketManager && typeof WebSocketManager.connect === 'function') {
|
||||
WebSocketManager.connect();
|
||||
}
|
||||
|
||||
const lastUpdateTime = state.lastRefresh || 0;
|
||||
const now = Date.now();
|
||||
const refreshInterval = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
if (now - lastUpdateTime > refreshInterval) {
|
||||
setTimeout(() => {
|
||||
this.ws.onopen = () => {
|
||||
state.wsConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.lastMessageTime = Date.now();
|
||||
updateConnectionStatus('connected');
|
||||
console.log('🟢 WebSocket connection established for Sent Bids / Received Bids');
|
||||
updateBidsTable();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
this.ws.onmessage = () => {
|
||||
this.lastMessageTime = Date.now();
|
||||
if (this.isPaused) return;
|
||||
|
||||
if (!this.processingQueue) {
|
||||
this.processingQueue = true;
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (!state.isRefreshing) {
|
||||
await updateBidsTable();
|
||||
}
|
||||
} finally {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
});
|
||||
|
||||
CleanupManager.addListener(window, 'beforeunload', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function cleanupRow(row) {
|
||||
if (!row) return;
|
||||
|
||||
const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]');
|
||||
tooltipTriggers.forEach(trigger => {
|
||||
if (window.TooltipManager) {
|
||||
window.TooltipManager.destroy(trigger);
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.removeListenersByElement(row);
|
||||
this.ws.onclose = () => {
|
||||
state.wsConnected = false;
|
||||
updateConnectionStatus('disconnected');
|
||||
if (!this.isPaused) {
|
||||
this.handleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
updateConnectionStatus('error');
|
||||
};
|
||||
},
|
||||
|
||||
startHealthCheck() {
|
||||
this.stopHealthCheck();
|
||||
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
if (this.isPaused) return;
|
||||
|
||||
const timeSinceLastMessage = Date.now() - this.lastMessageTime;
|
||||
if (timeSinceLastMessage > 120000) {
|
||||
console.log('WebSocket connection appears stale. Reconnecting...');
|
||||
this.cleanupConnection();
|
||||
this.connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isConnected()) {
|
||||
this.handleReconnect();
|
||||
}
|
||||
}, 30000);
|
||||
},
|
||||
|
||||
stopHealthCheck() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleReconnect() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
if (this.isPaused) return;
|
||||
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
|
||||
//console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
|
||||
} else {
|
||||
EventManager.removeAll(row);
|
||||
updateConnectionStatus('error');
|
||||
//console.log('Maximum reconnection attempts reached. Will try again in 60 seconds.');
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}, 60000);
|
||||
}
|
||||
},
|
||||
|
||||
row.removeAttribute('data-offer-id');
|
||||
row.removeAttribute('data-bid-id');
|
||||
|
||||
while (row.firstChild) {
|
||||
const child = row.firstChild;
|
||||
row.removeChild(child);
|
||||
cleanupConnection() {
|
||||
if (this.ws) {
|
||||
this.ws.onopen = null;
|
||||
this.ws.onmessage = null;
|
||||
this.ws.onclose = null;
|
||||
this.ws.onerror = null;
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.close(1000, 'Cleanup');
|
||||
} catch (e) {
|
||||
console.warn('Error closing WebSocket:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function optimizeMemoryUsage() {
|
||||
const MAX_BIDS_IN_MEMORY = 500;
|
||||
|
||||
['sent', 'received'].forEach(type => {
|
||||
if (state.data[type] && state.data[type].length > MAX_BIDS_IN_MEMORY) {
|
||||
console.log(`Trimming ${type} bids data from ${state.data[type].length} to ${MAX_BIDS_IN_MEMORY}`);
|
||||
state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY);
|
||||
this.ws = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
cleanupOffscreenTooltips();
|
||||
|
||||
if (window.IdentityManager && typeof IdentityManager.limitCacheSize === 'function') {
|
||||
IdentityManager.limitCacheSize(100);
|
||||
pause() {
|
||||
this.isPaused = true;
|
||||
//console.log('WebSocket operations paused');
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
if (window.MemoryManager) {
|
||||
MemoryManager.forceCleanup();
|
||||
}
|
||||
resume() {
|
||||
if (!this.isPaused) return;
|
||||
this.isPaused = false;
|
||||
//console.log('WebSocket operations resumed');
|
||||
this.lastMessageTime = Date.now();
|
||||
if (!this.isConnected()) {
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
this.isPaused = true;
|
||||
this.stopHealthCheck();
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
this.cleanupConnection();
|
||||
}
|
||||
};
|
||||
|
||||
// Core
|
||||
const safeParseInt = (value) => {
|
||||
const parsed = parseInt(value);
|
||||
return isNaN(parsed) ? 0 : parsed;
|
||||
@@ -490,6 +512,7 @@ function coinMatches(offerCoin, filterCoin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// State
|
||||
function hasActiveFilters() {
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
const coinToSelect = document.getElementById('coin_to');
|
||||
@@ -557,58 +580,11 @@ function filterAndSortData(bids) {
|
||||
const searchStr = state.filters.searchQuery.toLowerCase();
|
||||
const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr);
|
||||
const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr);
|
||||
|
||||
let label = '';
|
||||
try {
|
||||
if (window.IdentityManager) {
|
||||
|
||||
let identity = null;
|
||||
|
||||
if (IdentityManager.cache && typeof IdentityManager.cache.get === 'function') {
|
||||
identity = IdentityManager.cache.get(bid.addr_from);
|
||||
}
|
||||
|
||||
if (identity && identity.label) {
|
||||
label = identity.label;
|
||||
} else if (identity && identity.data && identity.data.label) {
|
||||
label = identity.data.label;
|
||||
}
|
||||
|
||||
if (!label && bid.identity) {
|
||||
label = bid.identity.label || '';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing identity for search:', e);
|
||||
}
|
||||
|
||||
const identity = IdentityManager.cache.get(bid.addr_from);
|
||||
const label = identity?.data?.label || '';
|
||||
const matchesLabel = label.toLowerCase().includes(searchStr);
|
||||
|
||||
let matchesDisplayedLabel = false;
|
||||
if (!matchesLabel && document) {
|
||||
try {
|
||||
const tableId = state.currentTab === 'sent' ? 'sent' : 'received';
|
||||
const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`);
|
||||
|
||||
for (const cell of cells) {
|
||||
|
||||
const href = cell.getAttribute('href');
|
||||
const cellAddress = href ? href.split('/').pop() : '';
|
||||
|
||||
if (cellAddress === bid.addr_from) {
|
||||
const cellText = cell.textContent.trim().toLowerCase();
|
||||
if (cellText.includes(searchStr)) {
|
||||
matchesDisplayedLabel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error checking displayed labels:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(matchesBidId || matchesIdentity || matchesLabel || matchesDisplayedLabel)) {
|
||||
if (!(matchesBidId || matchesIdentity || matchesLabel)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -623,37 +599,6 @@ function filterAndSortData(bids) {
|
||||
});
|
||||
}
|
||||
|
||||
async function preloadIdentitiesForSearch(bids) {
|
||||
if (!window.IdentityManager || typeof IdentityManager.getIdentityData !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const addresses = new Set();
|
||||
bids.forEach(bid => {
|
||||
if (bid.addr_from) {
|
||||
addresses.add(bid.addr_from);
|
||||
}
|
||||
});
|
||||
|
||||
const BATCH_SIZE = 20;
|
||||
const addressArray = Array.from(addresses);
|
||||
|
||||
for (let i = 0; i < addressArray.length; i += BATCH_SIZE) {
|
||||
const batch = addressArray.slice(i, i + BATCH_SIZE);
|
||||
await Promise.all(batch.map(addr => IdentityManager.getIdentityData(addr)));
|
||||
|
||||
if (i + BATCH_SIZE < addressArray.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Preloaded ${addressArray.length} identities for search`);
|
||||
} catch (error) {
|
||||
console.error('Error preloading identities:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCoinFilterImages() {
|
||||
const coinToSelect = document.getElementById('coin_to');
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
@@ -748,6 +693,108 @@ const updateConnectionStatus = (status) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Identity
|
||||
const IdentityManager = {
|
||||
cache: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
retryDelay: 2000,
|
||||
maxRetries: 3,
|
||||
cacheTimeout: 5 * 60 * 1000,
|
||||
maxCacheSize: 500,
|
||||
|
||||
async getIdentityData(address) {
|
||||
if (!address) return { address: '' };
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) return { ...cachedData, address };
|
||||
|
||||
if (this.pendingRequests.has(address)) {
|
||||
try {
|
||||
const pendingData = await this.pendingRequests.get(address);
|
||||
return { ...pendingData, address };
|
||||
} catch (error) {
|
||||
this.pendingRequests.delete(address);
|
||||
}
|
||||
}
|
||||
|
||||
const request = this.fetchWithRetry(address);
|
||||
this.pendingRequests.set(address, request);
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
|
||||
this.trimCacheIfNeeded();
|
||||
|
||||
this.cache.set(address, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return { ...data, address };
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching identity for ${address}:`, error);
|
||||
return { address };
|
||||
} finally {
|
||||
this.pendingRequests.delete(address);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedIdentity(address) {
|
||||
const cached = this.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
|
||||
cached.timestamp = Date.now();
|
||||
return cached.data;
|
||||
}
|
||||
if (cached) {
|
||||
this.cache.delete(address);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
trimCacheIfNeeded() {
|
||||
if (this.cache.size > this.maxCacheSize) {
|
||||
|
||||
const entries = Array.from(this.cache.entries());
|
||||
const sortedByAge = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
const toRemove = Math.ceil(this.maxCacheSize * 0.2);
|
||||
for (let i = 0; i < toRemove && i < sortedByAge.length; i++) {
|
||||
this.cache.delete(sortedByAge[i][0]);
|
||||
}
|
||||
console.log(`Trimmed identity cache: removed ${toRemove} oldest entries`);
|
||||
}
|
||||
},
|
||||
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
this.pendingRequests.clear();
|
||||
},
|
||||
|
||||
async fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (attempt >= this.maxRetries) {
|
||||
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||
return { address };
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
||||
return this.fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Stats
|
||||
const processIdentityStats = (identity) => {
|
||||
if (!identity) return null;
|
||||
|
||||
@@ -847,7 +894,8 @@ const createIdentityTooltipContent = (identity) => {
|
||||
`;
|
||||
};
|
||||
|
||||
const tooltipIdsToCleanup = new Set();
|
||||
// Table
|
||||
let tooltipIdsToCleanup = new Set();
|
||||
|
||||
const cleanupTooltips = () => {
|
||||
if (window.TooltipManager) {
|
||||
@@ -869,6 +917,7 @@ const forceTooltipDOMCleanup = () => {
|
||||
foundCount += allTooltipElements.length;
|
||||
|
||||
allTooltipElements.forEach(element => {
|
||||
|
||||
const isDetached = !document.body.contains(element) ||
|
||||
element.classList.contains('hidden') ||
|
||||
element.style.display === 'none';
|
||||
@@ -898,6 +947,7 @@ const forceTooltipDOMCleanup = () => {
|
||||
|
||||
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
|
||||
foundCount += tippyRoots.length;
|
||||
|
||||
tippyRoots.forEach(element => {
|
||||
const isOrphan = !element.children.length ||
|
||||
element.children[0].classList.contains('hidden') ||
|
||||
@@ -925,10 +975,13 @@ const forceTooltipDOMCleanup = () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle legacy tooltip elements
|
||||
document.querySelectorAll('.tooltip').forEach(element => {
|
||||
const isTrulyDetached = !element.parentElement ||
|
||||
!document.body.contains(element.parentElement) ||
|
||||
element.classList.contains('hidden');
|
||||
|
||||
if (isTrulyDetached) {
|
||||
try {
|
||||
element.remove();
|
||||
@@ -939,11 +992,14 @@ const forceTooltipDOMCleanup = () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') {
|
||||
const activeTooltips = window.TooltipManager.getActiveTooltipInstances();
|
||||
activeTooltips.forEach(([element, instance]) => {
|
||||
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!document.body.contains(element)) {
|
||||
if (window.TooltipManager && window.TooltipManager.activeTooltips) {
|
||||
window.TooltipManager.activeTooltips.forEach((instance, id) => {
|
||||
const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]);
|
||||
const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`);
|
||||
|
||||
if (!tooltipElement || !triggerElement ||
|
||||
!document.body.contains(tooltipElement) ||
|
||||
!document.body.contains(triggerElement)) {
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
@@ -951,13 +1007,14 @@ const forceTooltipDOMCleanup = () => {
|
||||
console.warn('Error destroying tooltip instance:', e);
|
||||
}
|
||||
}
|
||||
window.TooltipManager.activeTooltips.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (removedCount > 0) {
|
||||
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createTableRow = async (bid) => {
|
||||
const identity = await IdentityManager.getIdentityData(bid.addr_from);
|
||||
@@ -1033,14 +1090,14 @@ const createTableRow = async (bid) => {
|
||||
<!-- Status Column -->
|
||||
<td class="py-3 px-6">
|
||||
<div class="relative flex justify-center" data-tooltip-target="tooltip-status-${uniqueId}">
|
||||
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center text-center rounded-full text-xs font-medium bold ${getStatusClass(bid.bid_state)}">
|
||||
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center rounded-full text-xs font-medium bold ${getStatusClass(bid.bid_state)}">
|
||||
${bid.bid_state}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<td class="py-3 pr-4">
|
||||
<td class="py-3 pr-4 pl-3">
|
||||
<div class="flex justify-center">
|
||||
<a href="/bid/${bid.bid_id}"
|
||||
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
|
||||
@@ -1166,7 +1223,7 @@ const initializeTooltips = () => {
|
||||
|
||||
window.TooltipManager.cleanup();
|
||||
|
||||
const selector = '#' + state.currentTab + ' [data-tooltip-target]';
|
||||
let selector = '#' + state.currentTab + ' [data-tooltip-target]';
|
||||
const tooltipTriggers = document.querySelectorAll(selector);
|
||||
const tooltipCount = tooltipTriggers.length;
|
||||
if (tooltipCount > 50) {
|
||||
@@ -1293,6 +1350,7 @@ function implementVirtualizedRows() {
|
||||
});
|
||||
}
|
||||
|
||||
// Fetching
|
||||
let activeFetchController = null;
|
||||
|
||||
const fetchBids = async () => {
|
||||
@@ -1335,7 +1393,7 @@ const fetchBids = async () => {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let data = await response.json();
|
||||
//console.log('Received raw data:', data.length, 'bids');
|
||||
|
||||
state.filters.with_expired = includeExpired;
|
||||
@@ -1367,19 +1425,20 @@ const fetchBids = async () => {
|
||||
|
||||
const updateBidsTable = async () => {
|
||||
if (state.isLoading) {
|
||||
//console.log('Already loading, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//console.log('Starting updateBidsTable for tab:', state.currentTab);
|
||||
//console.log('Current filters:', state.filters);
|
||||
|
||||
state.isLoading = true;
|
||||
updateLoadingState(true);
|
||||
|
||||
const bids = await fetchBids();
|
||||
|
||||
// Add identity preloading if we're searching
|
||||
if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
|
||||
await preloadIdentitiesForSearch(bids);
|
||||
}
|
||||
//console.log('Fetched bids:', bids.length);
|
||||
|
||||
state.data[state.currentTab] = bids;
|
||||
state.currentPage[state.currentTab] = 1;
|
||||
@@ -1437,6 +1496,7 @@ const updatePaginationControls = (type) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Filter
|
||||
let searchTimeout;
|
||||
function handleSearch(event) {
|
||||
if (searchTimeout) {
|
||||
@@ -1641,6 +1701,7 @@ const setupRefreshButtons = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Tabs
|
||||
const switchTab = (tabId) => {
|
||||
if (state.isLoading) return;
|
||||
|
||||
@@ -1857,17 +1918,10 @@ function setupMemoryMonitoring() {
|
||||
const intervalId = setInterval(() => {
|
||||
if (document.hidden) {
|
||||
console.log('Tab hidden - running memory optimization');
|
||||
|
||||
if (window.IdentityManager) {
|
||||
if (typeof IdentityManager.limitCacheSize === 'function') {
|
||||
IdentityManager.limitCacheSize(100);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
IdentityManager.trimCacheIfNeeded();
|
||||
if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
if (state.data.sent.length > 1000) {
|
||||
console.log('Trimming sent bids data');
|
||||
state.data.sent = state.data.sent.slice(0, 1000);
|
||||
@@ -1881,7 +1935,6 @@ function setupMemoryMonitoring() {
|
||||
cleanupTooltips();
|
||||
}
|
||||
}, MEMORY_CHECK_INTERVAL);
|
||||
|
||||
document.addEventListener('beforeunload', () => {
|
||||
clearInterval(intervalId);
|
||||
}, { once: true });
|
||||
@@ -1925,12 +1978,6 @@ function initialize() {
|
||||
updateBidsTable();
|
||||
}, 100);
|
||||
|
||||
setInterval(() => {
|
||||
if ((state.data.sent.length + state.data.received.length) > 1000) {
|
||||
optimizeMemoryUsage();
|
||||
}
|
||||
}, 5 * 60 * 1000); // Check every 5 minutes
|
||||
|
||||
window.cleanupBidsTable = cleanup;
|
||||
}
|
||||
|
||||
|
||||
68
basicswap/static/js/coin_icons.js
Normal file
68
basicswap/static/js/coin_icons.js
Normal file
@@ -0,0 +1,68 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const selectCache = {};
|
||||
|
||||
function updateSelectCache(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image') || '';
|
||||
const name = selectedOption.textContent.trim();
|
||||
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
|
||||
|
||||
const selectImage = select.nextElementSibling.querySelector('.select-image');
|
||||
if (selectImage) {
|
||||
selectImage.src = image;
|
||||
}
|
||||
|
||||
const selectNameElement = select.nextElementSibling.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
}
|
||||
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
|
||||
selectIcons.forEach(icon => icon.style.display = 'none');
|
||||
selectImages.forEach(image => image.style.display = 'none');
|
||||
selectNames.forEach(name => name.style.display = 'none');
|
||||
|
||||
function setupCustomSelect(select) {
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement.querySelector('.select-image');
|
||||
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
const storedValue = localStorage.getItem(select.name);
|
||||
if (storedValue && select.value == '-1') {
|
||||
select.value = storedValue;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
localStorage.setItem(select.name, select.value);
|
||||
});
|
||||
|
||||
setSelectData(select);
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(setupCustomSelect);
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
const dropdownInstances = [];
|
||||
|
||||
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
|
||||
targetEl.style.visibility = 'hidden';
|
||||
targetEl.style.display = 'block';
|
||||
@@ -60,9 +58,6 @@
|
||||
this._handleScroll = this._handleScroll.bind(this);
|
||||
this._handleResize = this._handleResize.bind(this);
|
||||
this._handleOutsideClick = this._handleOutsideClick.bind(this);
|
||||
|
||||
dropdownInstances.push(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -71,8 +66,7 @@
|
||||
this._targetEl.style.margin = '0';
|
||||
this._targetEl.style.display = 'none';
|
||||
this._targetEl.style.position = 'fixed';
|
||||
this._targetEl.style.zIndex = '40';
|
||||
this._targetEl.classList.add('dropdown-menu');
|
||||
this._targetEl.style.zIndex = '50';
|
||||
|
||||
this._setupEventListeners();
|
||||
this._initialized = true;
|
||||
@@ -129,12 +123,6 @@
|
||||
|
||||
show() {
|
||||
if (!this._visible) {
|
||||
dropdownInstances.forEach(instance => {
|
||||
if (instance !== this && instance._visible) {
|
||||
instance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this._targetEl.style.display = 'block';
|
||||
this._targetEl.style.visibility = 'hidden';
|
||||
|
||||
@@ -172,12 +160,6 @@
|
||||
document.removeEventListener('click', this._handleOutsideClick);
|
||||
window.removeEventListener('scroll', this._handleScroll, true);
|
||||
window.removeEventListener('resize', this._handleResize);
|
||||
|
||||
const index = dropdownInstances.indexOf(this);
|
||||
if (index > -1) {
|
||||
dropdownInstances.splice(index, 1);
|
||||
}
|
||||
|
||||
this._initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -202,8 +184,6 @@
|
||||
initDropdowns();
|
||||
}
|
||||
|
||||
Dropdown.instances = dropdownInstances;
|
||||
|
||||
window.Dropdown = Dropdown;
|
||||
window.initDropdowns = initDropdowns;
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const burger = document.querySelectorAll('.navbar-burger');
|
||||
const menu = document.querySelectorAll('.navbar-menu');
|
||||
|
||||
if (burger.length && menu.length) {
|
||||
for (var i = 0; i < burger.length; i++) {
|
||||
burger[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const close = document.querySelectorAll('.navbar-close');
|
||||
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
||||
|
||||
if (close.length) {
|
||||
for (var k = 0; k < close.length; k++) {
|
||||
close[k].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (backdrop.length) {
|
||||
for (var l = 0; l < backdrop.length; l++) {
|
||||
backdrop[l].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipManager = TooltipManager.initialize();
|
||||
tooltipManager.initializeTooltips();
|
||||
setupShutdownModal();
|
||||
setupDarkMode();
|
||||
toggleImages();
|
||||
});
|
||||
|
||||
function setupShutdownModal() {
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
const shutdownModal = document.getElementById('shutdownModal');
|
||||
const closeModalButton = document.getElementById('closeShutdownModal');
|
||||
const confirmShutdownButton = document.getElementById('confirmShutdown');
|
||||
const shutdownWarning = document.getElementById('shutdownWarning');
|
||||
|
||||
function updateShutdownButtons() {
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
shutdownButtons.forEach(button => {
|
||||
if (activeSwaps > 0) {
|
||||
button.classList.add('shutdown-disabled');
|
||||
button.setAttribute('data-disabled', 'true');
|
||||
button.setAttribute('title', 'Caution: Swaps in progress');
|
||||
} else {
|
||||
button.classList.remove('shutdown-disabled');
|
||||
button.removeAttribute('data-disabled');
|
||||
button.removeAttribute('title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllDropdowns() {
|
||||
|
||||
const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
|
||||
openDropdowns.forEach(dropdown => {
|
||||
if (dropdown.style.display !== 'none') {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (window.Dropdown && window.Dropdown.instances) {
|
||||
window.Dropdown.instances.forEach(instance => {
|
||||
if (instance._visible) {
|
||||
instance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showShutdownModal() {
|
||||
closeAllDropdowns();
|
||||
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
if (activeSwaps > 0) {
|
||||
shutdownWarning.classList.remove('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
|
||||
} else {
|
||||
shutdownWarning.classList.add('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down';
|
||||
}
|
||||
shutdownModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideShutdownModal() {
|
||||
shutdownModal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (shutdownButtons.length) {
|
||||
shutdownButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
showShutdownModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalButton) {
|
||||
closeModalButton.addEventListener('click', hideShutdownModal);
|
||||
}
|
||||
|
||||
if (confirmShutdownButton) {
|
||||
confirmShutdownButton.addEventListener('click', function() {
|
||||
const shutdownToken = document.querySelector('.shutdown-button')
|
||||
.getAttribute('href').split('/').pop();
|
||||
window.location.href = '/shutdown/' + shutdownToken;
|
||||
});
|
||||
}
|
||||
|
||||
if (shutdownModal) {
|
||||
shutdownModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideShutdownModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (shutdownButtons.length) {
|
||||
updateShutdownButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function setupDarkMode() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
if (localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
|
||||
if (themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImages() {
|
||||
var html = document.querySelector('html');
|
||||
var darkImages = document.querySelectorAll('.dark-image');
|
||||
var lightImages = document.querySelectorAll('.light-image');
|
||||
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImageDisplay(images, display) {
|
||||
images.forEach(function(img) {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
40
basicswap/static/js/main.js
Normal file
40
basicswap/static/js/main.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Burger menus
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// open
|
||||
const burger = document.querySelectorAll('.navbar-burger');
|
||||
const menu = document.querySelectorAll('.navbar-menu');
|
||||
|
||||
if (burger.length && menu.length) {
|
||||
for (var i = 0; i < burger.length; i++) {
|
||||
burger[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// close
|
||||
const close = document.querySelectorAll('.navbar-close');
|
||||
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
||||
|
||||
if (close.length) {
|
||||
for (var k = 0; k < close.length; k++) {
|
||||
close[k].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (backdrop.length) {
|
||||
for (var l = 0; l < backdrop.length; l++) {
|
||||
backdrop[l].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,400 +0,0 @@
|
||||
const ApiManager = (function() {
|
||||
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: 1200,
|
||||
cryptocompare: 2000
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
|
||||
canMakeRequest: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return (now - lastRequest) >= this.minRequestInterval[apiName];
|
||||
},
|
||||
|
||||
updateLastRequestTime: function(apiName) {
|
||||
this.lastRequestTime[apiName] = Date.now();
|
||||
},
|
||||
|
||||
getWaitTime: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
|
||||
},
|
||||
|
||||
queueRequest: async function(apiName, requestFn, retryCount = 0) {
|
||||
if (!this.requestQueue[apiName]) {
|
||||
this.requestQueue[apiName] = Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.requestQueue[apiName];
|
||||
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateLastRequestTime(apiName);
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
||||
retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||
apiName,
|
||||
retryCount,
|
||||
error: error.message
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
this.requestQueue[apiName] = executeRequest();
|
||||
return await this.requestQueue[apiName];
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'NetworkError') {
|
||||
const cacheKey = `coinData_${apiName}`;
|
||||
try {
|
||||
const cachedData = JSON.parse(localStorage.getItem(cacheKey));
|
||||
if (cachedData && cachedData.value) {
|
||||
return cachedData.value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing cached data:', e);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
config,
|
||||
rateLimiter,
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ApiManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options.config) {
|
||||
Object.assign(config, options.config);
|
||||
}
|
||||
|
||||
if (config.rateLimits) {
|
||||
Object.keys(config.rateLimits).forEach(api => {
|
||||
if (config.rateLimits[api].minInterval) {
|
||||
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.retryDelays) {
|
||||
rateLimiter.retryDelays = [...config.retryDelays];
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
state.isInitialized = true;
|
||||
console.log('ApiManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(config.requestTimeout)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
makePostRequest: async function(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/json/readurl', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
headers: headers
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.Error) {
|
||||
reject(new Error(data.Error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
|
||||
if (!coins) {
|
||||
throw new Error('No coins specified for price lookup');
|
||||
}
|
||||
let coinsParam;
|
||||
if (Array.isArray(coins)) {
|
||||
coinsParam = coins.filter(c => c && c.trim() !== '').join(',');
|
||||
} else if (typeof coins === 'object' && coins.coins) {
|
||||
coinsParam = coins.coins;
|
||||
} else {
|
||||
coinsParam = coins;
|
||||
}
|
||||
if (!coinsParam || coinsParam.trim() === '') {
|
||||
throw new Error('No valid coins to fetch prices for');
|
||||
}
|
||||
|
||||
return this.makeRequest('/json/coinprices', 'POST', {}, {
|
||||
coins: coinsParam,
|
||||
source: source,
|
||||
ttl: ttl
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinGeckoData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coins = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => coin.name)
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
||||
|
||||
//console.log('Fetching coin prices for:', coins);
|
||||
const response = await this.fetchCoinPrices(coins);
|
||||
|
||||
//console.log('Full API response:', response);
|
||||
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error('Invalid response type');
|
||||
}
|
||||
|
||||
if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
|
||||
throw new Error('No valid rates found in response');
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchCoinGeckoData:', {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchVolumeData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coins = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
||||
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
|
||||
|
||||
const response = await this.makePostRequest(url, {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
|
||||
const volumeData = {};
|
||||
Object.entries(response).forEach(([coinId, data]) => {
|
||||
if (data && data.usd_24h_vol) {
|
||||
volumeData[coinId] = {
|
||||
total_volume: data.usd_24h_vol,
|
||||
price_change_percentage_24h: data.usd_24h_change || 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return volumeData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching volume data:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchCryptoCompareData: function(coin) {
|
||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
||||
try {
|
||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
||||
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
return await this.makePostRequest(url, headers);
|
||||
} catch (error) {
|
||||
console.error(`CryptoCompare request failed for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
|
||||
if (!Array.isArray(coinSymbols)) {
|
||||
coinSymbols = [coinSymbols];
|
||||
}
|
||||
|
||||
const results = {};
|
||||
const fetchPromises = coinSymbols.map(async coin => {
|
||||
if (coin === 'WOW') {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
|
||||
try {
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response && response.prices) {
|
||||
results[coin] = response.prices;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CoinGecko data for WOW:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
||||
try {
|
||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
||||
let url;
|
||||
|
||||
if (resolution === 'day') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
|
||||
} else if (resolution === 'year') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
|
||||
} else {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response.Response === "Error") {
|
||||
console.error(`API Error for ${coin}:`, response.Message);
|
||||
throw new Error(response.Message);
|
||||
} else if (response.Data && response.Data.Data) {
|
||||
results[coin] = response.Data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(fetchPromises);
|
||||
return results;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
// Clear any pending requests or resources
|
||||
rateLimiter.requestQueue = {};
|
||||
rateLimiter.lastRequestTime = {};
|
||||
state.isInitialized = false;
|
||||
console.log('ApiManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
function getCoinBackendId(coinName) {
|
||||
const nameMap = {
|
||||
'bitcoin-cash': 'bitcoincash',
|
||||
'bitcoin cash': 'bitcoincash',
|
||||
'firo': 'zcoin',
|
||||
'zcoin': 'zcoin',
|
||||
'bitcoincash': 'bitcoin-cash'
|
||||
};
|
||||
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
|
||||
}
|
||||
|
||||
window.Api = ApiManager;
|
||||
window.ApiManager = ApiManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.apiManagerInitialized) {
|
||||
ApiManager.initialize();
|
||||
window.apiManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
|
||||
console.log('ApiManager initialized');
|
||||
@@ -1,536 +0,0 @@
|
||||
const CacheManager = (function() {
|
||||
const defaults = window.config?.cacheConfig?.storage || {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
defaultTTL: 5 * 60 * 1000
|
||||
};
|
||||
|
||||
const PRICES_CACHE_KEY = 'crypto_prices_unified';
|
||||
|
||||
const CACHE_KEY_PATTERNS = [
|
||||
'coinData_',
|
||||
'chartData_',
|
||||
'historical_',
|
||||
'rates_',
|
||||
'prices_',
|
||||
'offers_',
|
||||
'fallback_',
|
||||
'volumeData'
|
||||
];
|
||||
|
||||
const isCacheKey = (key) => {
|
||||
return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
|
||||
key === 'coinGeckoOneLiner' ||
|
||||
key === PRICES_CACHE_KEY;
|
||||
};
|
||||
|
||||
const isLocalStorageAvailable = () => {
|
||||
try {
|
||||
const testKey = '__storage_test__';
|
||||
localStorage.setItem(testKey, testKey);
|
||||
localStorage.removeItem(testKey);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
const memoryCache = new Map();
|
||||
|
||||
if (!storageAvailable) {
|
||||
console.warn('localStorage is not available. Using in-memory cache instead.');
|
||||
}
|
||||
|
||||
const cacheAPI = {
|
||||
getTTL: function(resourceType) {
|
||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
|
||||
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
},
|
||||
|
||||
set: function(key, value, resourceTypeOrCustomTtl = null) {
|
||||
try {
|
||||
this.cleanup();
|
||||
|
||||
if (!value) {
|
||||
console.warn('Attempted to cache null/undefined value for key:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
let ttl;
|
||||
if (typeof resourceTypeOrCustomTtl === 'string') {
|
||||
ttl = this.getTTL(resourceTypeOrCustomTtl);
|
||||
} else if (typeof resourceTypeOrCustomTtl === 'number') {
|
||||
ttl = resourceTypeOrCustomTtl;
|
||||
} else {
|
||||
ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
}
|
||||
|
||||
const item = {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + ttl
|
||||
};
|
||||
|
||||
let serializedItem;
|
||||
try {
|
||||
serializedItem = JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemSize = new Blob([serializedItem]).size;
|
||||
if (itemSize > defaults.maxSizeBytes) {
|
||||
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
this.cleanup(true);
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (retryError) {
|
||||
console.error('Storage quota exceeded even after cleanup:', retryError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to quota issues');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.error('localStorage error:', storageError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memoryCache.set(key, item);
|
||||
if (memoryCache.size > defaults.maxItems) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(k => isCacheKey(k))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.2)); // Remove oldest 20%
|
||||
|
||||
keysToDelete.forEach(k => memoryCache.delete(k));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Cache set error:', error);
|
||||
try {
|
||||
memoryCache.set(key, {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Memory cache set error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
if (itemStr) {
|
||||
let item;
|
||||
try {
|
||||
item = JSON.parse(itemStr);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse cached item:', parseError);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
|
||||
console.warn('Invalid cache item structure for key:', key);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
}
|
||||
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("localStorage access error:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryCache.has(key)) {
|
||||
const item = memoryCache.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
} else {
|
||||
|
||||
memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Cache retrieval error:", error);
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
memoryCache.delete(key);
|
||||
} catch (removeError) {
|
||||
console.error("Failed to remove invalid cache entry:", removeError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function(key) {
|
||||
return this.get(key) !== null;
|
||||
},
|
||||
|
||||
cleanup: function(aggressive = false) {
|
||||
const now = Date.now();
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
const items = [];
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
localStorage.removeItem(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
key,
|
||||
size,
|
||||
expiresAt: item.expiresAt,
|
||||
timestamp: item.timestamp
|
||||
});
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
} catch (error) {
|
||||
console.error("Error processing cache item:", error);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
|
||||
items.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
|
||||
const item = items.pop();
|
||||
try {
|
||||
localStorage.removeItem(item.key);
|
||||
totalSize -= item.size;
|
||||
itemCount--;
|
||||
} catch (error) {
|
||||
console.error("Error removing cache item:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during localStorage cleanup:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
const expiredKeys = [];
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (now >= item.expiresAt) {
|
||||
expiredKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
expiredKeys.forEach(key => memoryCache.delete(key));
|
||||
|
||||
if (aggressive && memoryCache.size > defaults.maxItems / 2) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.3)); // Remove oldest 30% during aggressive cleanup
|
||||
|
||||
keysToDelete.forEach(key => memoryCache.delete(key));
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
itemCount,
|
||||
memoryCacheSize: memoryCache.size,
|
||||
cleaned: items.length,
|
||||
storageAvailable
|
||||
};
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const keys = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (isCacheKey(key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache item:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing localStorage cache:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.forEach(key => memoryCache.delete(key));
|
||||
|
||||
console.log("Cache cleared successfully");
|
||||
return true;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
let expiredCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
expiredCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting cache stats:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting localStorage stats:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
let memoryCacheSize = 0;
|
||||
let memoryCacheItems = 0;
|
||||
let memoryCacheExpired = 0;
|
||||
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
memoryCacheItems++;
|
||||
if (now >= item.expiresAt) {
|
||||
memoryCacheExpired++;
|
||||
}
|
||||
try {
|
||||
memoryCacheSize += new Blob([JSON.stringify(item)]).size;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
||||
itemCount,
|
||||
expiredCount,
|
||||
utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
|
||||
memoryCacheItems,
|
||||
memoryCacheExpired,
|
||||
memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
|
||||
storageType: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
},
|
||||
|
||||
checkStorage: function() {
|
||||
const wasAvailable = storageAvailable;
|
||||
storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
|
||||
console.log('localStorage is now available. Migrating memory cache...');
|
||||
let migratedCount = 0;
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
memoryCache.delete(key);
|
||||
migratedCount++;
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
|
||||
}
|
||||
|
||||
return {
|
||||
available: storageAvailable,
|
||||
type: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...cacheAPI,
|
||||
|
||||
setPrices: function(priceData, customTtl = null) {
|
||||
return this.set(PRICES_CACHE_KEY, priceData,
|
||||
customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
|
||||
},
|
||||
|
||||
getPrices: function() {
|
||||
return this.get(PRICES_CACHE_KEY);
|
||||
},
|
||||
|
||||
getCoinPrice: function(symbol) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedSymbol = symbol.toLowerCase();
|
||||
return prices.value[normalizedSymbol] || null;
|
||||
},
|
||||
|
||||
getCompatiblePrices: function(format) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch(format) {
|
||||
case 'rates':
|
||||
const ratesFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const coinKey = coin.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.replace(' ', '-');
|
||||
|
||||
ratesFormat[coinKey] = {
|
||||
usd: data.price || data.usd,
|
||||
btc: data.price_btc || data.btc
|
||||
};
|
||||
});
|
||||
return {
|
||||
value: ratesFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
case 'coinGecko':
|
||||
const geckoFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const symbol = this.getSymbolFromCoinId(coin);
|
||||
if (symbol) {
|
||||
geckoFormat[symbol.toLowerCase()] = {
|
||||
current_price: data.price || data.usd,
|
||||
price_btc: data.price_btc || data.btc,
|
||||
total_volume: data.total_volume,
|
||||
price_change_percentage_24h: data.price_change_percentage_24h,
|
||||
displayName: symbol
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
value: geckoFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
default:
|
||||
return prices;
|
||||
}
|
||||
},
|
||||
|
||||
getSymbolFromCoinId: function(coinId) {
|
||||
const symbolMap = {
|
||||
'bitcoin': 'BTC',
|
||||
'litecoin': 'LTC',
|
||||
'monero': 'XMR',
|
||||
'wownero': 'WOW',
|
||||
'particl': 'PART',
|
||||
'pivx': 'PIVX',
|
||||
'firo': 'FIRO',
|
||||
'zcoin': 'FIRO',
|
||||
'dash': 'DASH',
|
||||
'decred': 'DCR',
|
||||
'namecoin': 'NMR',
|
||||
'bitcoin-cash': 'BCH',
|
||||
'dogecoin': 'DOGE'
|
||||
};
|
||||
|
||||
return symbolMap[coinId] || null;
|
||||
}
|
||||
};
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
|
||||
cm.clear();
|
||||
});
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.CacheManager = CacheManager;
|
||||
|
||||
|
||||
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
|
||||
console.log('CacheManager initialized');
|
||||
@@ -1,270 +0,0 @@
|
||||
const CleanupManager = (function() {
|
||||
|
||||
const state = {
|
||||
eventListeners: [],
|
||||
timeouts: [],
|
||||
intervals: [],
|
||||
animationFrames: [],
|
||||
resources: new Map(),
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (state.debug) {
|
||||
console.log(`[CleanupManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
addListener: function(element, type, handler, options = false) {
|
||||
if (!element) {
|
||||
log('Warning: Attempted to add listener to null/undefined element');
|
||||
return handler;
|
||||
}
|
||||
|
||||
element.addEventListener(type, handler, options);
|
||||
state.eventListeners.push({ element, type, handler, options });
|
||||
log(`Added ${type} listener to`, element);
|
||||
return handler;
|
||||
},
|
||||
|
||||
setTimeout: function(callback, delay) {
|
||||
const id = window.setTimeout(callback, delay);
|
||||
state.timeouts.push(id);
|
||||
log(`Created timeout ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
setInterval: function(callback, delay) {
|
||||
const id = window.setInterval(callback, delay);
|
||||
state.intervals.push(id);
|
||||
log(`Created interval ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
requestAnimationFrame: function(callback) {
|
||||
const id = window.requestAnimationFrame(callback);
|
||||
state.animationFrames.push(id);
|
||||
log(`Requested animation frame ${id}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
registerResource: function(type, resource, cleanupFn) {
|
||||
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
state.resources.set(id, { resource, cleanupFn });
|
||||
log(`Registered custom resource ${id} of type ${type}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
unregisterResource: function(id) {
|
||||
const resourceInfo = state.resources.get(id);
|
||||
if (resourceInfo) {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
state.resources.delete(id);
|
||||
log(`Unregistered and cleaned up resource ${id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
log(`Resource ${id} not found`);
|
||||
return false;
|
||||
},
|
||||
|
||||
clearTimeout: function(id) {
|
||||
const index = state.timeouts.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearTimeout(id);
|
||||
state.timeouts.splice(index, 1);
|
||||
log(`Cleared timeout ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
clearInterval: function(id) {
|
||||
const index = state.intervals.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearInterval(id);
|
||||
state.intervals.splice(index, 1);
|
||||
log(`Cleared interval ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
cancelAnimationFrame: function(id) {
|
||||
const index = state.animationFrames.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.cancelAnimationFrame(id);
|
||||
state.animationFrames.splice(index, 1);
|
||||
log(`Cancelled animation frame ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener: function(element, type, handler, options = false) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => !(listener.element === element &&
|
||||
listener.type === type &&
|
||||
listener.handler === handler)
|
||||
);
|
||||
},
|
||||
|
||||
removeListenersByElement: function(element) {
|
||||
if (!element) return;
|
||||
|
||||
const listenersToRemove = state.eventListeners.filter(
|
||||
listener => listener.element === element
|
||||
);
|
||||
|
||||
listenersToRemove.forEach(({ element, type, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => listener.element !== element
|
||||
);
|
||||
},
|
||||
|
||||
clearAllTimeouts: function() {
|
||||
state.timeouts.forEach(id => {
|
||||
window.clearTimeout(id);
|
||||
});
|
||||
const count = state.timeouts.length;
|
||||
state.timeouts = [];
|
||||
log(`Cleared all timeouts (${count})`);
|
||||
},
|
||||
|
||||
clearAllIntervals: function() {
|
||||
state.intervals.forEach(id => {
|
||||
window.clearInterval(id);
|
||||
});
|
||||
const count = state.intervals.length;
|
||||
state.intervals = [];
|
||||
log(`Cleared all intervals (${count})`);
|
||||
},
|
||||
|
||||
clearAllAnimationFrames: function() {
|
||||
state.animationFrames.forEach(id => {
|
||||
window.cancelAnimationFrame(id);
|
||||
});
|
||||
const count = state.animationFrames.length;
|
||||
state.animationFrames = [];
|
||||
log(`Cancelled all animation frames (${count})`);
|
||||
},
|
||||
|
||||
clearAllResources: function() {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
state.resources.forEach((resourceInfo, id) => {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
state.resources.clear();
|
||||
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
|
||||
},
|
||||
|
||||
clearAllListeners: function() {
|
||||
state.eventListeners.forEach(({ element, type, handler, options }) => {
|
||||
if (element) {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
const count = state.eventListeners.length;
|
||||
state.eventListeners = [];
|
||||
log(`Removed all event listeners (${count})`);
|
||||
},
|
||||
|
||||
clearAll: function() {
|
||||
const counts = {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size
|
||||
};
|
||||
|
||||
this.clearAllListeners();
|
||||
this.clearAllTimeouts();
|
||||
this.clearAllIntervals();
|
||||
this.clearAllAnimationFrames();
|
||||
this.clearAllResources();
|
||||
|
||||
log(`All resources cleaned up:`, counts);
|
||||
return counts;
|
||||
},
|
||||
|
||||
getResourceCounts: function() {
|
||||
return {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size,
|
||||
total: state.eventListeners.length +
|
||||
state.timeouts.length +
|
||||
state.intervals.length +
|
||||
state.animationFrames.length +
|
||||
state.resources.size
|
||||
};
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.debug = Boolean(enabled);
|
||||
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
|
||||
return state.debug;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.clearAll();
|
||||
log('CleanupManager disposed');
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
log('CleanupManager initialized');
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
|
||||
window.CleanupManager = CleanupManager;
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.cleanupManagerInitialized) {
|
||||
CleanupManager.initialize();
|
||||
window.cleanupManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
|
||||
console.log('CleanupManager initialized');
|
||||
@@ -1,230 +0,0 @@
|
||||
const CoinManager = (function() {
|
||||
const coinRegistry = [
|
||||
{
|
||||
symbol: 'BTC',
|
||||
name: 'bitcoin',
|
||||
displayName: 'Bitcoin',
|
||||
aliases: ['btc', 'bitcoin'],
|
||||
coingeckoId: 'bitcoin',
|
||||
cryptocompareId: 'BTC',
|
||||
usesCryptoCompare: false,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Bitcoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'XMR',
|
||||
name: 'monero',
|
||||
displayName: 'Monero',
|
||||
aliases: ['xmr', 'monero'],
|
||||
coingeckoId: 'monero',
|
||||
cryptocompareId: 'XMR',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Monero.png'
|
||||
},
|
||||
{
|
||||
symbol: 'PART',
|
||||
name: 'particl',
|
||||
displayName: 'Particl',
|
||||
aliases: ['part', 'particl', 'particl anon', 'particl blind'],
|
||||
variants: ['Particl', 'Particl Blind', 'Particl Anon'],
|
||||
coingeckoId: 'particl',
|
||||
cryptocompareId: 'PART',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Particl.png'
|
||||
},
|
||||
{
|
||||
symbol: 'BCH',
|
||||
name: 'bitcoin-cash',
|
||||
displayName: 'Bitcoin Cash',
|
||||
aliases: ['bch', 'bitcoincash', 'bitcoin cash'],
|
||||
coingeckoId: 'bitcoin-cash',
|
||||
cryptocompareId: 'BCH',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Bitcoin-Cash.png'
|
||||
},
|
||||
{
|
||||
symbol: 'PIVX',
|
||||
name: 'pivx',
|
||||
displayName: 'PIVX',
|
||||
aliases: ['pivx'],
|
||||
coingeckoId: 'pivx',
|
||||
cryptocompareId: 'PIVX',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'PIVX.png'
|
||||
},
|
||||
{
|
||||
symbol: 'FIRO',
|
||||
name: 'firo',
|
||||
displayName: 'Firo',
|
||||
aliases: ['firo', 'zcoin'],
|
||||
coingeckoId: 'firo',
|
||||
cryptocompareId: 'FIRO',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Firo.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DASH',
|
||||
name: 'dash',
|
||||
displayName: 'Dash',
|
||||
aliases: ['dash'],
|
||||
coingeckoId: 'dash',
|
||||
cryptocompareId: 'DASH',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Dash.png'
|
||||
},
|
||||
{
|
||||
symbol: 'LTC',
|
||||
name: 'litecoin',
|
||||
displayName: 'Litecoin',
|
||||
aliases: ['ltc', 'litecoin'],
|
||||
variants: ['Litecoin', 'Litecoin MWEB'],
|
||||
coingeckoId: 'litecoin',
|
||||
cryptocompareId: 'LTC',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Litecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DOGE',
|
||||
name: 'dogecoin',
|
||||
displayName: 'Dogecoin',
|
||||
aliases: ['doge', 'dogecoin'],
|
||||
coingeckoId: 'dogecoin',
|
||||
cryptocompareId: 'DOGE',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Dogecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DCR',
|
||||
name: 'decred',
|
||||
displayName: 'Decred',
|
||||
aliases: ['dcr', 'decred'],
|
||||
coingeckoId: 'decred',
|
||||
cryptocompareId: 'DCR',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Decred.png'
|
||||
},
|
||||
{
|
||||
symbol: 'NMC',
|
||||
name: 'namecoin',
|
||||
displayName: 'Namecoin',
|
||||
aliases: ['nmc', 'namecoin'],
|
||||
coingeckoId: 'namecoin',
|
||||
cryptocompareId: 'NMC',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Namecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'WOW',
|
||||
name: 'wownero',
|
||||
displayName: 'Wownero',
|
||||
aliases: ['wow', 'wownero'],
|
||||
coingeckoId: 'wownero',
|
||||
cryptocompareId: 'WOW',
|
||||
usesCryptoCompare: false,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Wownero.png'
|
||||
}
|
||||
];
|
||||
const symbolToInfo = {};
|
||||
const nameToInfo = {};
|
||||
const displayNameToInfo = {};
|
||||
const coinAliasesMap = {};
|
||||
|
||||
function buildLookupMaps() {
|
||||
coinRegistry.forEach(coin => {
|
||||
symbolToInfo[coin.symbol.toLowerCase()] = coin;
|
||||
nameToInfo[coin.name.toLowerCase()] = coin;
|
||||
displayNameToInfo[coin.displayName.toLowerCase()] = coin;
|
||||
if (coin.aliases && Array.isArray(coin.aliases)) {
|
||||
coin.aliases.forEach(alias => {
|
||||
coinAliasesMap[alias.toLowerCase()] = coin;
|
||||
});
|
||||
}
|
||||
coinAliasesMap[coin.symbol.toLowerCase()] = coin;
|
||||
coinAliasesMap[coin.name.toLowerCase()] = coin;
|
||||
coinAliasesMap[coin.displayName.toLowerCase()] = coin;
|
||||
if (coin.variants && Array.isArray(coin.variants)) {
|
||||
coin.variants.forEach(variant => {
|
||||
coinAliasesMap[variant.toLowerCase()] = coin;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildLookupMaps();
|
||||
|
||||
function getCoinByAnyIdentifier(identifier) {
|
||||
if (!identifier) return null;
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
const coin = coinAliasesMap[normalizedId];
|
||||
if (coin) return coin;
|
||||
if (normalizedId.includes('bitcoin') && normalizedId.includes('cash') ||
|
||||
normalizedId === 'bch') {
|
||||
return symbolToInfo['bch'];
|
||||
}
|
||||
if (normalizedId === 'zcoin' || normalizedId.includes('firo')) {
|
||||
return symbolToInfo['firo'];
|
||||
}
|
||||
if (normalizedId.includes('particl')) {
|
||||
return symbolToInfo['part'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
getAllCoins: function() {
|
||||
return [...coinRegistry];
|
||||
},
|
||||
getCoinByAnyIdentifier: getCoinByAnyIdentifier,
|
||||
getSymbol: function(identifier) {
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.symbol : null;
|
||||
},
|
||||
getDisplayName: function(identifier) {
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.displayName : null;
|
||||
},
|
||||
getCoingeckoId: function(identifier) {
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.coingeckoId : null;
|
||||
},
|
||||
coinMatches: function(coinId1, coinId2) {
|
||||
if (!coinId1 || !coinId2) return false;
|
||||
const coin1 = getCoinByAnyIdentifier(coinId1);
|
||||
const coin2 = getCoinByAnyIdentifier(coinId2);
|
||||
if (!coin1 || !coin2) return false;
|
||||
return coin1.symbol === coin2.symbol;
|
||||
},
|
||||
getPriceKey: function(coinIdentifier) {
|
||||
if (!coinIdentifier) return null;
|
||||
const coin = getCoinByAnyIdentifier(coinIdentifier);
|
||||
if (!coin) return coinIdentifier.toLowerCase();
|
||||
return coin.coingeckoId;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.CoinManager = CoinManager;
|
||||
console.log('CoinManager initialized');
|
||||
@@ -1,321 +0,0 @@
|
||||
const ConfigManager = (function() {
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
function determineWebSocketPort() {
|
||||
const wsPort =
|
||||
window.ws_port ||
|
||||
(typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
|
||||
'11700';
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
const selectedWsPort = determineWebSocketPort();
|
||||
|
||||
const defaultConfig = {
|
||||
cacheDuration: 10 * 60 * 1000,
|
||||
requestTimeout: 60000,
|
||||
wsPort: selectedWsPort,
|
||||
cacheConfig: {
|
||||
defaultTTL: 10 * 60 * 1000,
|
||||
ttlSettings: {
|
||||
prices: 5 * 60 * 1000,
|
||||
chart: 5 * 60 * 1000,
|
||||
historical: 60 * 60 * 1000,
|
||||
volume: 30 * 60 * 1000,
|
||||
offers: 2 * 60 * 1000,
|
||||
identity: 15 * 60 * 1000
|
||||
},
|
||||
storage: {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200
|
||||
},
|
||||
fallbackTTL: 24 * 60 * 60 * 1000
|
||||
},
|
||||
itemsPerPage: 50,
|
||||
apiEndpoints: {
|
||||
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
|
||||
coinGecko: 'https://api.coingecko.com/api/v3',
|
||||
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
|
||||
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
|
||||
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
|
||||
},
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
}
|
||||
},
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
get coins() {
|
||||
return window.CoinManager ? window.CoinManager.getAllCoins() : [
|
||||
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
|
||||
];
|
||||
},
|
||||
chartConfig: {
|
||||
colors: {
|
||||
default: {
|
||||
lineColor: 'rgba(77, 132, 240, 1)',
|
||||
backgroundColor: 'rgba(77, 132, 240, 0.1)'
|
||||
}
|
||||
},
|
||||
showVolume: false,
|
||||
specialCoins: [''],
|
||||
resolutions: {
|
||||
year: { days: 365, interval: 'month' },
|
||||
sixMonths: { days: 180, interval: 'daily' },
|
||||
day: { days: 1, interval: 'hourly' }
|
||||
},
|
||||
currentResolution: 'year'
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...defaultConfig,
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ConfigManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
if (options) {
|
||||
Object.assign(this, options);
|
||||
}
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
this.utils = utils;
|
||||
state.isInitialized = true;
|
||||
console.log('ConfigManager initialized');
|
||||
return this;
|
||||
},
|
||||
getAPIKeys: function() {
|
||||
if (typeof window.getAPIKeys === 'function') {
|
||||
const apiKeys = window.getAPIKeys();
|
||||
return {
|
||||
cryptoCompare: apiKeys.cryptoCompare || '',
|
||||
coinGecko: apiKeys.coinGecko || ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
cryptoCompare: '',
|
||||
coinGecko: ''
|
||||
};
|
||||
},
|
||||
getCoinBackendId: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getPriceKey(coinName);
|
||||
}
|
||||
const nameMap = {
|
||||
'bitcoin-cash': 'bitcoincash',
|
||||
'bitcoin cash': 'bitcoincash',
|
||||
'firo': 'firo',
|
||||
'zcoin': 'firo',
|
||||
'bitcoincash': 'bitcoin-cash'
|
||||
};
|
||||
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
||||
return nameMap[lowerCoinName] || lowerCoinName;
|
||||
},
|
||||
coinMatches: function(offerCoin, filterCoin) {
|
||||
if (!offerCoin || !filterCoin) return false;
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.coinMatches(offerCoin, filterCoin);
|
||||
}
|
||||
offerCoin = offerCoin.toLowerCase();
|
||||
filterCoin = filterCoin.toLowerCase();
|
||||
if (offerCoin === filterCoin) return true;
|
||||
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
||||
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
||||
return true;
|
||||
}
|
||||
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
||||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
|
||||
return true;
|
||||
}
|
||||
const particlVariants = ['particl', 'particl anon', 'particl blind'];
|
||||
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
|
||||
return true;
|
||||
}
|
||||
if (particlVariants.includes(filterCoin)) {
|
||||
return offerCoin === filterCoin;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
update: function(path, value) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]]) {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
return this;
|
||||
},
|
||||
get: function(path, defaultValue = null) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (current === undefined || current === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return current !== undefined ? current : defaultValue;
|
||||
},
|
||||
dispose: function() {
|
||||
state.isInitialized = false;
|
||||
console.log('ConfigManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
const utils = {
|
||||
formatNumber: function(number, decimals = 2) {
|
||||
if (typeof number !== 'number' || isNaN(number)) {
|
||||
console.warn('formatNumber received a non-number value:', number);
|
||||
return '0';
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
},
|
||||
formatDate: function(timestamp, resolution) {
|
||||
const date = new Date(timestamp);
|
||||
const options = {
|
||||
day: { hour: '2-digit', minute: '2-digit', hour12: true },
|
||||
week: { month: 'short', day: 'numeric' },
|
||||
month: { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
};
|
||||
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
|
||||
},
|
||||
debounce: function(func, delay) {
|
||||
let timeoutId;
|
||||
return function(...args) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
},
|
||||
formatTimeLeft: function(timestamp) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (timestamp <= now) return "Expired";
|
||||
return this.formatTime(timestamp);
|
||||
},
|
||||
formatTime: function(timestamp, addAgoSuffix = false) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = Math.abs(now - timestamp);
|
||||
let timeString;
|
||||
if (diff < 60) {
|
||||
timeString = `${diff} seconds`;
|
||||
} else if (diff < 3600) {
|
||||
timeString = `${Math.floor(diff / 60)} minutes`;
|
||||
} else if (diff < 86400) {
|
||||
timeString = `${Math.floor(diff / 3600)} hours`;
|
||||
} else if (diff < 2592000) {
|
||||
timeString = `${Math.floor(diff / 86400)} days`;
|
||||
} else if (diff < 31536000) {
|
||||
timeString = `${Math.floor(diff / 2592000)} months`;
|
||||
} else {
|
||||
timeString = `${Math.floor(diff / 31536000)} years`;
|
||||
}
|
||||
return addAgoSuffix ? `${timeString} ago` : timeString;
|
||||
},
|
||||
escapeHtml: function(unsafe) {
|
||||
if (typeof unsafe !== 'string') {
|
||||
console.warn('escapeHtml received a non-string value:', unsafe);
|
||||
return '';
|
||||
}
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
formatPrice: function(coin, price) {
|
||||
if (typeof price !== 'number' || isNaN(price)) {
|
||||
console.warn(`Invalid price for ${coin}:`, price);
|
||||
return 'N/A';
|
||||
}
|
||||
if (price < 0.000001) return price.toExponential(2);
|
||||
if (price < 0.001) return price.toFixed(8);
|
||||
if (price < 1) return price.toFixed(4);
|
||||
if (price < 10) return price.toFixed(3);
|
||||
if (price < 1000) return price.toFixed(2);
|
||||
if (price < 100000) return price.toFixed(1);
|
||||
return price.toFixed(0);
|
||||
},
|
||||
getEmptyPriceData: function() {
|
||||
return {
|
||||
'bitcoin': { usd: null, btc: null },
|
||||
'bitcoin-cash': { usd: null, btc: null },
|
||||
'dash': { usd: null, btc: null },
|
||||
'dogecoin': { usd: null, btc: null },
|
||||
'decred': { usd: null, btc: null },
|
||||
'namecoin': { usd: null, btc: null },
|
||||
'litecoin': { usd: null, btc: null },
|
||||
'particl': { usd: null, btc: null },
|
||||
'pivx': { usd: null, btc: null },
|
||||
'monero': { usd: null, btc: null },
|
||||
'zano': { usd: null, btc: null },
|
||||
'wownero': { usd: null, btc: null },
|
||||
'firo': { usd: null, btc: null }
|
||||
};
|
||||
},
|
||||
getCoinSymbol: function(fullName) {
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getSymbol(fullName) || fullName;
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
};
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.logger = {
|
||||
log: function(message) {
|
||||
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
warn: function(message) {
|
||||
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
error: function(message) {
|
||||
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.config = ConfigManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.configManagerInitialized) {
|
||||
ConfigManager.initialize();
|
||||
window.configManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = ConfigManager;
|
||||
}
|
||||
|
||||
console.log('ConfigManager initialized');
|
||||
@@ -1,192 +0,0 @@
|
||||
const IdentityManager = (function() {
|
||||
const state = {
|
||||
cache: new Map(),
|
||||
pendingRequests: new Map(),
|
||||
config: {
|
||||
retryDelay: 2000,
|
||||
maxRetries: 3,
|
||||
maxCacheSize: 100,
|
||||
cacheTimeout: window.config?.cacheConfig?.ttlSettings?.identity || 15 * 60 * 1000,
|
||||
debug: false
|
||||
}
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (state.config.debug) {
|
||||
console.log(`[IdentityManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
getIdentityData: async function(address) {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) {
|
||||
log(`Cache hit for ${address}`);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
if (state.pendingRequests.has(address)) {
|
||||
log(`Using pending request for ${address}`);
|
||||
return state.pendingRequests.get(address);
|
||||
}
|
||||
|
||||
log(`Fetching identity for ${address}`);
|
||||
const request = fetchWithRetry(address);
|
||||
state.pendingRequests.set(address, request);
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
this.setCachedIdentity(address, data);
|
||||
return data;
|
||||
} finally {
|
||||
state.pendingRequests.delete(address);
|
||||
}
|
||||
},
|
||||
|
||||
getCachedIdentity: function(address) {
|
||||
const cached = state.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
|
||||
return cached.data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
setCachedIdentity: function(address, data) {
|
||||
if (state.cache.size >= state.config.maxCacheSize) {
|
||||
const oldestEntries = [...state.cache.entries()]
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, Math.floor(state.config.maxCacheSize * 0.2));
|
||||
|
||||
oldestEntries.forEach(([key]) => {
|
||||
state.cache.delete(key);
|
||||
log(`Pruned cache entry for ${key}`);
|
||||
});
|
||||
}
|
||||
|
||||
state.cache.set(address, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
log(`Cached identity for ${address}`);
|
||||
},
|
||||
|
||||
clearCache: function() {
|
||||
log(`Clearing identity cache (${state.cache.size} entries)`);
|
||||
state.cache.clear();
|
||||
state.pendingRequests.clear();
|
||||
},
|
||||
|
||||
limitCacheSize: function(maxSize = state.config.maxCacheSize) {
|
||||
if (state.cache.size <= maxSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const entriesToRemove = [...state.cache.entries()]
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, state.cache.size - maxSize);
|
||||
|
||||
entriesToRemove.forEach(([key]) => state.cache.delete(key));
|
||||
log(`Limited cache size, removed ${entriesToRemove.length} entries`);
|
||||
|
||||
return entriesToRemove.length;
|
||||
},
|
||||
|
||||
getCacheSize: function() {
|
||||
return state.cache.size;
|
||||
},
|
||||
|
||||
configure: function(options = {}) {
|
||||
Object.assign(state.config, options);
|
||||
log(`Configuration updated:`, state.config);
|
||||
return state.config;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
const now = Date.now();
|
||||
let expiredCount = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
state.cache.forEach((value, key) => {
|
||||
if (now - value.timestamp > state.config.cacheTimeout) {
|
||||
expiredCount++;
|
||||
}
|
||||
const keySize = key.length * 2;
|
||||
const dataSize = JSON.stringify(value.data).length * 2;
|
||||
totalSize += keySize + dataSize;
|
||||
});
|
||||
|
||||
return {
|
||||
cacheEntries: state.cache.size,
|
||||
pendingRequests: state.pendingRequests.size,
|
||||
expiredEntries: expiredCount,
|
||||
estimatedSizeKB: Math.round(totalSize / 1024),
|
||||
config: { ...state.config }
|
||||
};
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.config.debug = Boolean(enabled);
|
||||
return `Debug mode ${state.config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
|
||||
if (options) {
|
||||
this.configure(options);
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('identityManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
log('IdentityManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.clearCache();
|
||||
log('IdentityManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (attempt >= state.config.maxRetries) {
|
||||
console.error(`[IdentityManager] Error:`, error.message);
|
||||
console.warn(`[IdentityManager] Failed to fetch identity for ${address} after ${attempt} attempts`);
|
||||
return null;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
|
||||
return fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.IdentityManager = IdentityManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.identityManagerInitialized) {
|
||||
IdentityManager.initialize();
|
||||
window.identityManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
|
||||
console.log('IdentityManager initialized');
|
||||
@@ -1,219 +0,0 @@
|
||||
const MemoryManager = (function() {
|
||||
|
||||
const state = {
|
||||
isMonitoringEnabled: false,
|
||||
monitorInterval: null,
|
||||
cleanupInterval: null
|
||||
};
|
||||
|
||||
const config = {
|
||||
monitorInterval: 30000,
|
||||
cleanupInterval: 60000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[MemoryManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
enableMonitoring: function(interval = config.monitorInterval) {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = true;
|
||||
config.monitorInterval = interval;
|
||||
|
||||
this.logMemoryUsage();
|
||||
|
||||
state.monitorInterval = setInterval(() => {
|
||||
this.logMemoryUsage();
|
||||
}, interval);
|
||||
|
||||
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
|
||||
return true;
|
||||
},
|
||||
|
||||
disableMonitoring: function() {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
state.monitorInterval = null;
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = false;
|
||||
console.log('Memory monitoring disabled');
|
||||
return true;
|
||||
},
|
||||
|
||||
logMemoryUsage: function() {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
console.log(`=== Memory Monitor [${timestamp}] ===`);
|
||||
|
||||
if (window.performance && window.performance.memory) {
|
||||
console.log('Memory usage:', {
|
||||
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
|
||||
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
|
||||
});
|
||||
}
|
||||
|
||||
if (navigator.deviceMemory) {
|
||||
console.log('Device memory:', navigator.deviceMemory, 'GB');
|
||||
}
|
||||
|
||||
const nodeCount = document.querySelectorAll('*').length;
|
||||
console.log('DOM node count:', nodeCount);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
const counts = CleanupManager.getResourceCounts();
|
||||
console.log('Managed resources:', counts);
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
|
||||
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
|
||||
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
|
||||
}
|
||||
|
||||
if (window.CacheManager && window.CacheManager.getStats) {
|
||||
const cacheStats = CacheManager.getStats();
|
||||
console.log('Cache stats:', cacheStats);
|
||||
}
|
||||
|
||||
if (window.IdentityManager && window.IdentityManager.getStats) {
|
||||
const identityStats = window.IdentityManager.getStats();
|
||||
console.log('Identity cache stats:', identityStats);
|
||||
}
|
||||
|
||||
console.log('==============================');
|
||||
},
|
||||
|
||||
enableAutoCleanup: function(interval = config.cleanupInterval) {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
}
|
||||
|
||||
config.cleanupInterval = interval;
|
||||
|
||||
this.forceCleanup();
|
||||
|
||||
state.cleanupInterval = setInterval(() => {
|
||||
this.forceCleanup();
|
||||
}, interval);
|
||||
|
||||
log('Auto-cleanup enabled every', interval/1000, 'seconds');
|
||||
return true;
|
||||
},
|
||||
|
||||
disableAutoCleanup: function() {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
state.cleanupInterval = null;
|
||||
}
|
||||
|
||||
console.log('Memory auto-cleanup disabled');
|
||||
return true;
|
||||
},
|
||||
|
||||
forceCleanup: function() {
|
||||
if (config.debug) {
|
||||
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
|
||||
}
|
||||
|
||||
if (window.CacheManager && CacheManager.cleanup) {
|
||||
CacheManager.cleanup(true);
|
||||
}
|
||||
|
||||
if (window.TooltipManager && TooltipManager.cleanup) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
|
||||
if (window.TooltipManager && TooltipManager.destroy) {
|
||||
window.TooltipManager.destroy(element);
|
||||
}
|
||||
});
|
||||
|
||||
if (window.chartModule && chartModule.cleanup) {
|
||||
chartModule.cleanup();
|
||||
}
|
||||
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
} else {
|
||||
const arr = new Array(1000);
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
arr[i] = new Array(10000).join('x');
|
||||
}
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
console.log('Memory cleanup completed');
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
config.debug = Boolean(enabled);
|
||||
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
getStatus: function() {
|
||||
return {
|
||||
monitoring: {
|
||||
enabled: Boolean(state.monitorInterval),
|
||||
interval: config.monitorInterval
|
||||
},
|
||||
autoCleanup: {
|
||||
enabled: Boolean(state.cleanupInterval),
|
||||
interval: config.cleanupInterval
|
||||
},
|
||||
debug: config.debug
|
||||
};
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
|
||||
if (options.enableMonitoring) {
|
||||
this.enableMonitoring(options.monitorInterval || config.monitorInterval);
|
||||
}
|
||||
|
||||
if (options.enableAutoCleanup) {
|
||||
this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
log('MemoryManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.disableMonitoring();
|
||||
this.disableAutoCleanup();
|
||||
log('MemoryManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.MemoryManager = MemoryManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.memoryManagerInitialized) {
|
||||
MemoryManager.initialize();
|
||||
window.memoryManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
|
||||
console.log('MemoryManager initialized');
|
||||
@@ -1,280 +0,0 @@
|
||||
const NetworkManager = (function() {
|
||||
const state = {
|
||||
isOnline: navigator.onLine,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
lastNetworkError: null,
|
||||
eventHandlers: {},
|
||||
connectionTestInProgress: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 5000,
|
||||
reconnectBackoff: 1.5,
|
||||
connectionTestEndpoint: '/json',
|
||||
connectionTestTimeout: 3000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[NetworkManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function generateHandlerId() {
|
||||
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
window.addEventListener('online', this.handleOnlineStatus.bind(this));
|
||||
window.addEventListener('offline', this.handleOfflineStatus.bind(this));
|
||||
|
||||
state.isOnline = navigator.onLine;
|
||||
log(`Network status initialized: ${state.isOnline ? 'online' : 'offline'}`);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('networkManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
isOnline: function() {
|
||||
return state.isOnline;
|
||||
},
|
||||
|
||||
getReconnectAttempts: function() {
|
||||
return state.reconnectAttempts;
|
||||
},
|
||||
|
||||
resetReconnectAttempts: function() {
|
||||
state.reconnectAttempts = 0;
|
||||
return this;
|
||||
},
|
||||
|
||||
handleOnlineStatus: function() {
|
||||
log('Browser reports online status');
|
||||
state.isOnline = true;
|
||||
this.notifyHandlers('online');
|
||||
|
||||
if (state.reconnectTimer) {
|
||||
this.scheduleReconnectRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
handleOfflineStatus: function() {
|
||||
log('Browser reports offline status');
|
||||
state.isOnline = false;
|
||||
this.notifyHandlers('offline');
|
||||
},
|
||||
|
||||
handleNetworkError: function(error) {
|
||||
if (error && (
|
||||
(error.name === 'TypeError' && error.message.includes('NetworkError')) ||
|
||||
(error.name === 'AbortError') ||
|
||||
(error.message && error.message.includes('network')) ||
|
||||
(error.message && error.message.includes('timeout'))
|
||||
)) {
|
||||
log('Network error detected:', error.message);
|
||||
|
||||
if (state.isOnline) {
|
||||
state.isOnline = false;
|
||||
state.lastNetworkError = error;
|
||||
this.notifyHandlers('error', error);
|
||||
}
|
||||
|
||||
if (!state.reconnectTimer) {
|
||||
this.scheduleReconnectRefresh();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
scheduleReconnectRefresh: function() {
|
||||
if (state.reconnectTimer) {
|
||||
clearTimeout(state.reconnectTimer);
|
||||
state.reconnectTimer = null;
|
||||
}
|
||||
|
||||
const delay = config.reconnectDelay * Math.pow(config.reconnectBackoff,
|
||||
Math.min(state.reconnectAttempts, 5));
|
||||
|
||||
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
|
||||
|
||||
state.reconnectTimer = setTimeout(() => {
|
||||
state.reconnectTimer = null;
|
||||
this.attemptReconnect();
|
||||
}, delay);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
attemptReconnect: function() {
|
||||
if (!navigator.onLine) {
|
||||
log('Browser still reports offline, delaying reconnection attempt');
|
||||
this.scheduleReconnectRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.connectionTestInProgress) {
|
||||
log('Connection test already in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
state.reconnectAttempts++;
|
||||
state.connectionTestInProgress = true;
|
||||
|
||||
log(`Attempting reconnect #${state.reconnectAttempts}`);
|
||||
|
||||
this.testBackendConnection()
|
||||
.then(isAvailable => {
|
||||
state.connectionTestInProgress = false;
|
||||
|
||||
if (isAvailable) {
|
||||
log('Backend connection confirmed');
|
||||
state.isOnline = true;
|
||||
state.reconnectAttempts = 0;
|
||||
state.lastNetworkError = null;
|
||||
this.notifyHandlers('reconnected');
|
||||
} else {
|
||||
log('Backend still unavailable');
|
||||
|
||||
if (state.reconnectAttempts < config.maxReconnectAttempts) {
|
||||
this.scheduleReconnectRefresh();
|
||||
} else {
|
||||
log('Maximum reconnect attempts reached');
|
||||
this.notifyHandlers('maxAttemptsReached');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
state.connectionTestInProgress = false;
|
||||
log('Error during connection test:', error);
|
||||
|
||||
if (state.reconnectAttempts < config.maxReconnectAttempts) {
|
||||
this.scheduleReconnectRefresh();
|
||||
} else {
|
||||
log('Maximum reconnect attempts reached');
|
||||
this.notifyHandlers('maxAttemptsReached');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
testBackendConnection: function() {
|
||||
return fetch(config.connectionTestEndpoint, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
},
|
||||
timeout: config.connectionTestTimeout,
|
||||
signal: AbortSignal.timeout(config.connectionTestTimeout)
|
||||
})
|
||||
.then(response => {
|
||||
return response.ok;
|
||||
})
|
||||
.catch(error => {
|
||||
log('Backend connection test failed:', error.message);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
manualReconnect: function() {
|
||||
log('Manual reconnection requested');
|
||||
|
||||
state.isOnline = navigator.onLine;
|
||||
state.reconnectAttempts = 0;
|
||||
|
||||
this.notifyHandlers('manualReconnect');
|
||||
|
||||
if (state.isOnline) {
|
||||
return this.attemptReconnect();
|
||||
} else {
|
||||
log('Cannot attempt manual reconnect while browser reports offline');
|
||||
this.notifyHandlers('offlineWarning');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addHandler: function(event, handler) {
|
||||
if (!state.eventHandlers[event]) {
|
||||
state.eventHandlers[event] = {};
|
||||
}
|
||||
|
||||
const handlerId = generateHandlerId();
|
||||
state.eventHandlers[event][handlerId] = handler;
|
||||
|
||||
return handlerId;
|
||||
},
|
||||
|
||||
removeHandler: function(event, handlerId) {
|
||||
if (state.eventHandlers[event] && state.eventHandlers[event][handlerId]) {
|
||||
delete state.eventHandlers[event][handlerId];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
notifyHandlers: function(event, data) {
|
||||
if (state.eventHandlers[event]) {
|
||||
Object.values(state.eventHandlers[event]).forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch (error) {
|
||||
log(`Error in ${event} handler:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
config.debug = Boolean(enabled);
|
||||
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return {
|
||||
isOnline: state.isOnline,
|
||||
reconnectAttempts: state.reconnectAttempts,
|
||||
hasReconnectTimer: Boolean(state.reconnectTimer),
|
||||
connectionTestInProgress: state.connectionTestInProgress
|
||||
};
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (state.reconnectTimer) {
|
||||
clearTimeout(state.reconnectTimer);
|
||||
state.reconnectTimer = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('online', this.handleOnlineStatus);
|
||||
window.removeEventListener('offline', this.handleOfflineStatus);
|
||||
|
||||
state.eventHandlers = {};
|
||||
|
||||
log('NetworkManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.NetworkManager = NetworkManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.networkManagerInitialized) {
|
||||
NetworkManager.initialize();
|
||||
window.networkManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
|
||||
console.log('NetworkManager initialized');
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
const NotificationManager = (function() {
|
||||
|
||||
const config = {
|
||||
showNewOffers: false,
|
||||
showNewBids: true,
|
||||
showBidAccepted: true
|
||||
};
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('ul_updates');
|
||||
if (!container) {
|
||||
const floating_div = document.createElement('div');
|
||||
floating_div.classList.add('floatright');
|
||||
container = document.createElement('ul');
|
||||
container.setAttribute('id', 'ul_updates');
|
||||
floating_div.appendChild(container);
|
||||
document.body.appendChild(floating_div);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
|
||||
|
||||
console.log('NotificationManager disposed');
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
createToast: function(title, type = 'success') {
|
||||
const messages = ensureToastContainer();
|
||||
const message = document.createElement('li');
|
||||
message.innerHTML = `
|
||||
<div id="hide">
|
||||
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
|
||||
bg-white rounded-lg shadow" role="alert">
|
||||
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
|
||||
bg-blue-500 rounded-lg">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
|
||||
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
|
||||
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
|
||||
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
|
||||
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
|
||||
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
|
||||
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messages.appendChild(message);
|
||||
},
|
||||
|
||||
handleWebSocketEvent: function(data) {
|
||||
if (!data || !data.event) return;
|
||||
let toastTitle;
|
||||
let shouldShowToast = false;
|
||||
|
||||
switch (data.event) {
|
||||
case 'new_offer':
|
||||
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = config.showNewOffers;
|
||||
break;
|
||||
case 'new_bid':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
|
||||
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = config.showNewBids;
|
||||
break;
|
||||
case 'bid_accepted':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
|
||||
shouldShowToast = config.showBidAccepted;
|
||||
break;
|
||||
}
|
||||
|
||||
if (toastTitle && shouldShowToast) {
|
||||
this.createToast(toastTitle);
|
||||
}
|
||||
},
|
||||
|
||||
updateConfig: function(newConfig) {
|
||||
Object.assign(config, newConfig);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
window.closeAlert = function(event) {
|
||||
let element = event.target;
|
||||
while (element.nodeName !== "BUTTON") {
|
||||
element = element.parentNode;
|
||||
}
|
||||
element.parentNode.parentNode.removeChild(element.parentNode);
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.NotificationManager = NotificationManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!window.notificationManagerInitialized) {
|
||||
window.NotificationManager.initialize(window.notificationConfig || {});
|
||||
window.notificationManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
|
||||
console.log('NotificationManager initialized');
|
||||
@@ -1,233 +0,0 @@
|
||||
const PriceManager = (function() {
|
||||
const PRICES_CACHE_KEY = 'prices_unified';
|
||||
let fetchPromise = null;
|
||||
let lastFetchTime = 0;
|
||||
const MIN_FETCH_INTERVAL = 60000;
|
||||
let isInitialized = false;
|
||||
const eventListeners = {
|
||||
'priceUpdate': [],
|
||||
'error': []
|
||||
};
|
||||
|
||||
return {
|
||||
addEventListener: function(event, callback) {
|
||||
if (eventListeners[event]) {
|
||||
eventListeners[event].push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
removeEventListener: function(event, callback) {
|
||||
if (eventListeners[event]) {
|
||||
eventListeners[event] = eventListeners[event].filter(cb => cb !== callback);
|
||||
}
|
||||
},
|
||||
|
||||
triggerEvent: function(event, data) {
|
||||
if (eventListeners[event]) {
|
||||
eventListeners[event].forEach(callback => callback(data));
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
if (isInitialized) {
|
||||
console.warn('PriceManager: Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('priceManager', this, (mgr) => {
|
||||
Object.keys(eventListeners).forEach(event => {
|
||||
eventListeners[event] = [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => this.getPrices(), 1500);
|
||||
isInitialized = true;
|
||||
return this;
|
||||
},
|
||||
|
||||
getPrices: async function(forceRefresh = false) {
|
||||
if (!forceRefresh) {
|
||||
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
return cachedData.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchPromise && Date.now() - lastFetchTime < MIN_FETCH_INTERVAL) {
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
console.log('PriceManager: Fetching latest prices.');
|
||||
lastFetchTime = Date.now();
|
||||
fetchPromise = this.fetchPrices()
|
||||
.then(prices => {
|
||||
this.triggerEvent('priceUpdate', prices);
|
||||
return prices;
|
||||
})
|
||||
.catch(error => {
|
||||
this.triggerEvent('error', error);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
fetchPromise = null;
|
||||
});
|
||||
|
||||
return fetchPromise;
|
||||
},
|
||||
|
||||
fetchPrices: async function() {
|
||||
try {
|
||||
if (!NetworkManager.isOnline()) {
|
||||
throw new Error('Network is offline');
|
||||
}
|
||||
|
||||
const coinSymbols = window.CoinManager
|
||||
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||
: (window.config.coins
|
||||
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
|
||||
|
||||
console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
|
||||
|
||||
if (!coinSymbols.length) {
|
||||
throw new Error('No valid coins configured');
|
||||
}
|
||||
|
||||
let apiResponse;
|
||||
try {
|
||||
apiResponse = await Api.fetchCoinPrices(
|
||||
coinSymbols,
|
||||
"coingecko.com",
|
||||
300
|
||||
);
|
||||
|
||||
if (!apiResponse) {
|
||||
throw new Error('Empty response received from API');
|
||||
}
|
||||
|
||||
if (apiResponse.error) {
|
||||
throw new Error(`API error: ${apiResponse.error}`);
|
||||
}
|
||||
|
||||
if (!apiResponse.rates) {
|
||||
throw new Error('No rates found in API response');
|
||||
}
|
||||
|
||||
if (typeof apiResponse.rates !== 'object' || Object.keys(apiResponse.rates).length === 0) {
|
||||
throw new Error('Empty rates object in API response');
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error('API call error:', apiError);
|
||||
throw new Error(`API error: ${apiError.message}`);
|
||||
}
|
||||
|
||||
const processedData = {};
|
||||
|
||||
Object.entries(apiResponse.rates).forEach(([coinId, price]) => {
|
||||
let normalizedCoinId;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
|
||||
if (coin) {
|
||||
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
|
||||
} else {
|
||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
||||
}
|
||||
|
||||
if (coinId.toLowerCase() === 'zcoin') {
|
||||
normalizedCoinId = 'firo';
|
||||
}
|
||||
|
||||
processedData[normalizedCoinId] = {
|
||||
usd: price,
|
||||
btc: normalizedCoinId === 'bitcoin' ? 1 : price / (apiResponse.rates.bitcoin || 1)
|
||||
};
|
||||
});
|
||||
|
||||
CacheManager.set(PRICES_CACHE_KEY, processedData, 'prices');
|
||||
|
||||
Object.entries(processedData).forEach(([coin, prices]) => {
|
||||
if (prices.usd) {
|
||||
if (window.tableRateModule) {
|
||||
window.tableRateModule.setFallbackValue(coin, prices.usd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return processedData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching prices:', error);
|
||||
NetworkManager.handleNetworkError(error);
|
||||
|
||||
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
console.log('Using cached price data');
|
||||
return cachedData.value;
|
||||
}
|
||||
|
||||
try {
|
||||
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
|
||||
if (existingCache) {
|
||||
console.log('Using localStorage cached price data');
|
||||
return JSON.parse(existingCache).value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse existing cache:', e);
|
||||
}
|
||||
|
||||
const emptyData = {};
|
||||
|
||||
const coinNames = window.CoinManager
|
||||
? window.CoinManager.getAllCoins().map(c => c.name.toLowerCase())
|
||||
: ['bitcoin', 'bitcoin-cash', 'dash', 'dogecoin', 'decred', 'namecoin', 'litecoin', 'particl', 'pivx', 'monero', 'wownero', 'firo'];
|
||||
|
||||
coinNames.forEach(coin => {
|
||||
emptyData[coin] = { usd: null, btc: null };
|
||||
});
|
||||
|
||||
return emptyData;
|
||||
}
|
||||
},
|
||||
|
||||
getCoinPrice: function(coinSymbol) {
|
||||
if (!coinSymbol) return null;
|
||||
const prices = this.getPrices();
|
||||
if (!prices) return null;
|
||||
|
||||
let normalizedSymbol;
|
||||
if (window.CoinManager) {
|
||||
normalizedSymbol = window.CoinManager.getPriceKey(coinSymbol);
|
||||
} else {
|
||||
normalizedSymbol = coinSymbol.toLowerCase();
|
||||
}
|
||||
|
||||
return prices[normalizedSymbol] || null;
|
||||
},
|
||||
|
||||
formatPrice: function(coin, price) {
|
||||
if (window.config && window.config.utils && window.config.utils.formatPrice) {
|
||||
return window.config.utils.formatPrice(coin, price);
|
||||
}
|
||||
if (typeof price !== 'number' || isNaN(price)) return 'N/A';
|
||||
if (price < 0.01) return price.toFixed(8);
|
||||
if (price < 1) return price.toFixed(4);
|
||||
if (price < 1000) return price.toFixed(2);
|
||||
return price.toFixed(0);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.PriceManager = PriceManager;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.priceManagerInitialized) {
|
||||
window.PriceManager = PriceManager.initialize();
|
||||
window.priceManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('PriceManager initialized');
|
||||
@@ -1,338 +0,0 @@
|
||||
const SummaryManager = (function() {
|
||||
const config = {
|
||||
refreshInterval: window.config?.cacheDuration || 30000,
|
||||
summaryEndpoint: '/json',
|
||||
retryDelay: 5000,
|
||||
maxRetries: 3,
|
||||
requestTimeout: 15000
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
let webSocket = null;
|
||||
let fetchRetryCount = 0;
|
||||
let lastSuccessfulData = null;
|
||||
|
||||
function updateElement(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return false;
|
||||
|
||||
const safeValue = (value !== undefined && value !== null)
|
||||
? value
|
||||
: (element.dataset.lastValue || 0);
|
||||
|
||||
element.dataset.lastValue = safeValue;
|
||||
|
||||
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
|
||||
const svg = element.querySelector('svg');
|
||||
element.textContent = safeValue;
|
||||
if (svg) {
|
||||
element.insertBefore(svg, element.firstChild);
|
||||
}
|
||||
} else {
|
||||
element.textContent = safeValue;
|
||||
}
|
||||
|
||||
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
|
||||
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
|
||||
'watched-outputs-counter'].includes(elementId)) {
|
||||
element.classList.remove('bg-blue-500', 'bg-gray-400');
|
||||
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
||||
}
|
||||
|
||||
if (elementId === 'swaps-counter') {
|
||||
const swapContainer = document.getElementById('swapContainer');
|
||||
if (swapContainer) {
|
||||
const isSwapping = safeValue > 0;
|
||||
if (isSwapping) {
|
||||
swapContainer.innerHTML = document.querySelector('#swap-in-progress-green-template').innerHTML || '';
|
||||
swapContainer.style.animation = 'spin 2s linear infinite';
|
||||
} else {
|
||||
swapContainer.innerHTML = document.querySelector('#swap-in-progress-template').innerHTML || '';
|
||||
swapContainer.style.animation = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateUIFromData(data) {
|
||||
if (!data) return;
|
||||
|
||||
updateElement('network-offers-counter', data.num_network_offers);
|
||||
updateElement('offers-counter', data.num_sent_active_offers);
|
||||
updateElement('sent-bids-counter', data.num_sent_active_bids);
|
||||
updateElement('recv-bids-counter', data.num_recv_active_bids);
|
||||
updateElement('bid-requests-counter', data.num_available_bids);
|
||||
updateElement('swaps-counter', data.num_swapping);
|
||||
updateElement('watched-outputs-counter', data.num_watched_outputs);
|
||||
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
shutdownButtons.forEach(button => {
|
||||
button.setAttribute('data-active-swaps', data.num_swapping);
|
||||
if (data.num_swapping > 0) {
|
||||
button.classList.add('shutdown-disabled');
|
||||
button.setAttribute('data-disabled', 'true');
|
||||
button.setAttribute('title', 'Caution: Swaps in progress');
|
||||
} else {
|
||||
button.classList.remove('shutdown-disabled');
|
||||
button.removeAttribute('data-disabled');
|
||||
button.removeAttribute('title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cacheSummaryData(data) {
|
||||
if (!data) return;
|
||||
|
||||
localStorage.setItem('summary_data_cache', JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
}));
|
||||
}
|
||||
|
||||
function getCachedSummaryData() {
|
||||
let cachedData = null;
|
||||
|
||||
cachedData = localStorage.getItem('summary_data_cache');
|
||||
if (!cachedData) return null;
|
||||
|
||||
const parsedCache = JSON.parse(cachedData);
|
||||
const maxAge = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (Date.now() - parsedCache.timestamp < maxAge) {
|
||||
return parsedCache.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fetchSummaryDataWithTimeout() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
|
||||
|
||||
return fetch(config.summaryEndpoint, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function setupWebSocket() {
|
||||
if (webSocket) {
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
const wsPort = window.config?.wsPort ||
|
||||
(typeof determineWebSocketPort === 'function' ? determineWebSocketPort() : '11700');
|
||||
|
||||
const wsUrl = "ws://" + window.location.hostname + ":" + wsPort;
|
||||
webSocket = new WebSocket(wsUrl);
|
||||
|
||||
webSocket.onopen = () => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
webSocket.onmessage = (event) => {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
if (window.logger && window.logger.error) {
|
||||
window.logger.error('WebSocket message processing error: ' + error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.event) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
webSocket.onclose = () => {
|
||||
setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSwapTemplates() {
|
||||
if (!document.getElementById('swap-in-progress-template')) {
|
||||
const template = document.createElement('template');
|
||||
template.id = 'swap-in-progress-template';
|
||||
template.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
||||
document.body.appendChild(template);
|
||||
}
|
||||
|
||||
if (!document.getElementById('swap-in-progress-green-template') &&
|
||||
document.querySelector('[id^="swapContainer"]')?.innerHTML) {
|
||||
const greenTemplate = document.createElement('template');
|
||||
greenTemplate.id = 'swap-in-progress-green-template';
|
||||
greenTemplate.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
||||
document.body.appendChild(greenTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
function startRefreshTimer() {
|
||||
stopRefreshTimer();
|
||||
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = setInterval(() => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}, config.refreshInterval);
|
||||
}
|
||||
|
||||
function stopRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
ensureSwapTemplates();
|
||||
|
||||
const cachedData = getCachedSummaryData();
|
||||
if (cachedData) {
|
||||
updateUIFromData(cachedData);
|
||||
}
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
const wsManager = window.WebSocketManager;
|
||||
|
||||
if (!wsManager.isConnected()) {
|
||||
wsManager.connect();
|
||||
}
|
||||
|
||||
wsManager.addMessageHandler('message', (data) => {
|
||||
if (data.event) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setupWebSocket();
|
||||
}
|
||||
|
||||
startRefreshTimer();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('summaryManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchSummaryData: function() {
|
||||
return fetchSummaryDataWithTimeout()
|
||||
.then(data => {
|
||||
lastSuccessfulData = data;
|
||||
cacheSummaryData(data);
|
||||
fetchRetryCount = 0;
|
||||
|
||||
updateUIFromData(data);
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (window.logger && window.logger.error) {
|
||||
window.logger.error('Summary data fetch error: ' + error.message);
|
||||
}
|
||||
|
||||
if (fetchRetryCount < config.maxRetries) {
|
||||
fetchRetryCount++;
|
||||
|
||||
if (window.logger && window.logger.warn) {
|
||||
window.logger.warn(`Retrying summary data fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(this.fetchSummaryData());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
} else {
|
||||
const cachedData = lastSuccessfulData || getCachedSummaryData();
|
||||
|
||||
if (cachedData) {
|
||||
if (window.logger && window.logger.warn) {
|
||||
window.logger.warn('Using cached summary data after fetch failures');
|
||||
}
|
||||
updateUIFromData(cachedData);
|
||||
}
|
||||
|
||||
fetchRetryCount = 0;
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startRefreshTimer: function() {
|
||||
startRefreshTimer();
|
||||
},
|
||||
|
||||
stopRefreshTimer: function() {
|
||||
stopRefreshTimer();
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
stopRefreshTimer();
|
||||
|
||||
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
webSocket = null;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.SummaryManager = SummaryManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.summaryManagerInitialized) {
|
||||
window.SummaryManager = SummaryManager.initialize();
|
||||
window.summaryManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
|
||||
console.log('SummaryManager initialized');
|
||||
@@ -1,588 +0,0 @@
|
||||
const TooltipManager = (function() {
|
||||
let instance = null;
|
||||
|
||||
class TooltipManagerImpl {
|
||||
constructor() {
|
||||
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
this.activeTooltips = new WeakMap();
|
||||
this.tooltipIdCounter = 0;
|
||||
this.pendingAnimationFrames = new Set();
|
||||
this.tooltipElementsMap = new Map();
|
||||
this.maxTooltips = 300;
|
||||
this.cleanupThreshold = 1.3;
|
||||
this.disconnectedCheckInterval = null;
|
||||
|
||||
this.setupStyles();
|
||||
this.setupCleanupEvents();
|
||||
this.initializeMutationObserver();
|
||||
this.startDisconnectedElementsCheck();
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
create(element, content, options = {}) {
|
||||
if (!element) return null;
|
||||
|
||||
this.destroy(element);
|
||||
|
||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
||||
const oldestEntries = Array.from(this.tooltipElementsMap.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||
.slice(0, 20);
|
||||
|
||||
oldestEntries.forEach(([el]) => {
|
||||
this.destroy(el);
|
||||
});
|
||||
}
|
||||
|
||||
const originalContent = content;
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
this.pendingAnimationFrames.delete(rafId);
|
||||
|
||||
if (!document.body.contains(element)) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
this.createTooltip(element, originalContent, options, rect);
|
||||
} else {
|
||||
let retryCount = 0;
|
||||
const retryCreate = () => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) {
|
||||
if (newRect.width > 0 && newRect.height > 0) {
|
||||
this.createTooltip(element, originalContent, options, newRect);
|
||||
}
|
||||
} else {
|
||||
retryCount++;
|
||||
const newRafId = requestAnimationFrame(retryCreate);
|
||||
this.pendingAnimationFrames.add(newRafId);
|
||||
}
|
||||
};
|
||||
const initialRetryId = requestAnimationFrame(retryCreate);
|
||||
this.pendingAnimationFrames.add(initialRetryId);
|
||||
}
|
||||
});
|
||||
|
||||
this.pendingAnimationFrames.add(rafId);
|
||||
return null;
|
||||
}
|
||||
|
||||
createTooltip(element, content, options, rect) {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
let bgClass = 'bg-gray-400';
|
||||
let arrowColor = 'rgb(156 163 175)';
|
||||
|
||||
if (targetId?.includes('tooltip-offer-') && window.jsonData) {
|
||||
try {
|
||||
const offerId = targetId.split('tooltip-offer-')[1];
|
||||
let actualOfferId = offerId;
|
||||
|
||||
if (offerId.includes('_')) {
|
||||
[actualOfferId] = offerId.split('_');
|
||||
}
|
||||
|
||||
let offer = null;
|
||||
if (Array.isArray(window.jsonData)) {
|
||||
for (let i = 0; i < window.jsonData.length; i++) {
|
||||
const o = window.jsonData[i];
|
||||
if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) {
|
||||
offer = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (offer) {
|
||||
if (offer.is_revoked) {
|
||||
bgClass = 'bg-red-500';
|
||||
arrowColor = 'rgb(239 68 68)';
|
||||
} else if (offer.is_own_offer) {
|
||||
bgClass = 'bg-gray-300';
|
||||
arrowColor = 'rgb(209 213 219)';
|
||||
} else {
|
||||
bgClass = 'bg-green-700';
|
||||
arrowColor = 'rgb(21 128 61)';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error finding offer for tooltip:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
|
||||
|
||||
try {
|
||||
if (typeof tippy !== 'function') {
|
||||
console.error('Tippy.js is not loaded. Cannot create tooltip.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const instance = tippy(element, {
|
||||
content: content,
|
||||
allowHTML: true,
|
||||
placement: options.placement || 'top',
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
duration: 0,
|
||||
delay: 0,
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
theme: '',
|
||||
moveTransition: 'none',
|
||||
offset: [0, 10],
|
||||
onShow(instance) {
|
||||
if (!document.body.contains(element)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onMount(instance) {
|
||||
if (instance.popper && instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
|
||||
}
|
||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||
if (arrow) {
|
||||
arrow.style.setProperty('color', arrowColor, 'important');
|
||||
}
|
||||
},
|
||||
popperOptions: {
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'viewport',
|
||||
padding: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: 10,
|
||||
fallbackPlacements: ['top', 'bottom', 'right', 'left']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
element.setAttribute('data-tooltip-trigger-id', tooltipId);
|
||||
this.activeTooltips.set(element, instance);
|
||||
|
||||
this.tooltipElementsMap.set(element, {
|
||||
timestamp: Date.now(),
|
||||
id: tooltipId
|
||||
});
|
||||
|
||||
return instance;
|
||||
} catch (e) {
|
||||
console.error('Error creating tooltip:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
if (!element) return;
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!id) return;
|
||||
|
||||
const instance = this.activeTooltips.get(element);
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying tooltip:', e);
|
||||
|
||||
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
|
||||
if (tippyRoot && tippyRoot.parentNode) {
|
||||
tippyRoot.parentNode.removeChild(tippyRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.activeTooltips.delete(element);
|
||||
this.tooltipElementsMap.delete(element);
|
||||
|
||||
element.removeAttribute('data-tooltip-trigger-id');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.pendingAnimationFrames.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
this.pendingAnimationFrames.clear();
|
||||
|
||||
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
|
||||
const batchSize = 20;
|
||||
|
||||
const processElementsBatch = (startIdx) => {
|
||||
const endIdx = Math.min(startIdx + batchSize, elements.length);
|
||||
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
this.destroy(elements[i]);
|
||||
}
|
||||
|
||||
if (endIdx < elements.length) {
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
this.pendingAnimationFrames.delete(rafId);
|
||||
processElementsBatch(endIdx);
|
||||
});
|
||||
this.pendingAnimationFrames.add(rafId);
|
||||
} else {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
};
|
||||
|
||||
if (elements.length > 0) {
|
||||
processElementsBatch(0);
|
||||
} else {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
|
||||
this.tooltipElementsMap.clear();
|
||||
}
|
||||
|
||||
cleanupOrphanedTippyElements() {
|
||||
const tippyElements = document.querySelectorAll('[data-tippy-root]');
|
||||
tippyElements.forEach(element => {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupStyles() {
|
||||
if (document.getElementById('tooltip-styles')) return;
|
||||
|
||||
document.head.insertAdjacentHTML('beforeend', `
|
||||
<style id="tooltip-styles">
|
||||
[data-tippy-root] {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
position: relative !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-400 {
|
||||
background-color: rgb(156 163 175);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-400) .tippy-arrow {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.tippy-box .bg-red-500 {
|
||||
background-color: rgb(239 68 68);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-red-500) .tippy-arrow {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-300 {
|
||||
background-color: rgb(209 213 219);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-300) .tippy-arrow {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
.tippy-box .bg-green-700 {
|
||||
background-color: rgb(21 128 61);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-green-700) .tippy-arrow {
|
||||
color: rgb(21 128 61);
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
|
||||
border-top-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
|
||||
border-left-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
|
||||
border-right-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow {
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
|
||||
setupCleanupEvents() {
|
||||
this.boundCleanup = this.cleanup.bind(this);
|
||||
this.handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this.cleanup();
|
||||
|
||||
if (window.MemoryManager) {
|
||||
window.MemoryManager.forceCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', this.boundCleanup);
|
||||
window.addEventListener('unload', this.boundCleanup);
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose());
|
||||
}
|
||||
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.performPeriodicCleanup();
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
startDisconnectedElementsCheck() {
|
||||
|
||||
if (this.disconnectedCheckInterval) {
|
||||
clearInterval(this.disconnectedCheckInterval);
|
||||
}
|
||||
|
||||
this.disconnectedCheckInterval = setInterval(() => {
|
||||
this.checkForDisconnectedElements();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
checkForDisconnectedElements() {
|
||||
if (this.tooltipElementsMap.size === 0) return;
|
||||
|
||||
const elementsToCheck = Array.from(this.tooltipElementsMap.keys());
|
||||
let removedCount = 0;
|
||||
|
||||
elementsToCheck.forEach(element => {
|
||||
|
||||
if (!document.body.contains(element)) {
|
||||
this.destroy(element);
|
||||
removedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (removedCount > 0) {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
}
|
||||
|
||||
performPeriodicCleanup() {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
this.checkForDisconnectedElements();
|
||||
|
||||
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
|
||||
const sortedTooltips = Array.from(this.tooltipElementsMap.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips);
|
||||
tooltipsToRemove.forEach(([element]) => {
|
||||
this.destroy(element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeCleanupEvents() {
|
||||
window.removeEventListener('beforeunload', this.boundCleanup);
|
||||
window.removeEventListener('unload', this.boundCleanup);
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
|
||||
if (this.disconnectedCheckInterval) {
|
||||
clearInterval(this.disconnectedCheckInterval);
|
||||
this.disconnectedCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
initializeMutationObserver() {
|
||||
if (this.mutationObserver) return;
|
||||
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
let needsCleanup = false;
|
||||
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.removedNodes.length) {
|
||||
Array.from(mutation.removedNodes).forEach(node => {
|
||||
if (node.nodeType === 1) {
|
||||
|
||||
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
|
||||
this.destroy(node);
|
||||
needsCleanup = true;
|
||||
}
|
||||
|
||||
if (node.querySelectorAll) {
|
||||
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
|
||||
if (tooltipTriggers.length > 0) {
|
||||
tooltipTriggers.forEach(el => {
|
||||
this.destroy(el);
|
||||
});
|
||||
needsCleanup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (needsCleanup) {
|
||||
this.cleanupOrphanedTippyElements();
|
||||
}
|
||||
});
|
||||
|
||||
this.mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
initializeTooltips(selector = '[data-tooltip-target]') {
|
||||
document.querySelectorAll(selector).forEach(element => {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
const tooltipContent = document.getElementById(targetId);
|
||||
|
||||
if (tooltipContent) {
|
||||
this.create(element, tooltipContent.innerHTML, {
|
||||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.cleanup();
|
||||
|
||||
this.pendingAnimationFrames.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
this.pendingAnimationFrames.clear();
|
||||
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
}
|
||||
|
||||
this.removeCleanupEvents();
|
||||
|
||||
const styleElement = document.getElementById('tooltip-styles');
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
|
||||
this.activeTooltips = new WeakMap();
|
||||
this.tooltipElementsMap.clear();
|
||||
|
||||
instance = null;
|
||||
}
|
||||
|
||||
initialize(options = {}) {
|
||||
|
||||
if (options.maxTooltips) {
|
||||
this.maxTooltips = options.maxTooltips;
|
||||
}
|
||||
|
||||
console.log('TooltipManager initialized');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: function(options = {}) {
|
||||
if (!instance) {
|
||||
const manager = new TooltipManagerImpl();
|
||||
manager.initialize(options);
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
getInstance: function() {
|
||||
if (!instance) {
|
||||
const manager = new TooltipManagerImpl();
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
create: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.create(...args);
|
||||
},
|
||||
|
||||
destroy: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.destroy(...args);
|
||||
},
|
||||
|
||||
cleanup: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.cleanup(...args);
|
||||
},
|
||||
|
||||
initializeTooltips: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.initializeTooltips(...args);
|
||||
},
|
||||
|
||||
dispose: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.dispose(...args);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.TooltipManager = TooltipManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.tooltipManagerInitialized) {
|
||||
TooltipManager.initialize();
|
||||
TooltipManager.initializeTooltips();
|
||||
window.tooltipManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TooltipManager;
|
||||
}
|
||||
|
||||
//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager));
|
||||
console.log('TooltipManager initialized');
|
||||
@@ -1,623 +0,0 @@
|
||||
const WalletManager = (function() {
|
||||
|
||||
const config = {
|
||||
maxRetries: 5,
|
||||
baseDelay: 500,
|
||||
cacheExpiration: 5 * 60 * 1000,
|
||||
priceUpdateInterval: 5 * 60 * 1000,
|
||||
apiTimeout: 30000,
|
||||
debounceDelay: 300,
|
||||
cacheMinInterval: 60 * 1000,
|
||||
defaultTTL: 300,
|
||||
priceSource: {
|
||||
primary: 'coingecko.com',
|
||||
fallback: 'cryptocompare.com',
|
||||
enabledSources: ['coingecko.com', 'cryptocompare.com']
|
||||
}
|
||||
};
|
||||
|
||||
const stateKeys = {
|
||||
lastUpdate: 'last-update-time',
|
||||
previousTotal: 'previous-total-usd',
|
||||
currentTotal: 'current-total-usd',
|
||||
balancesVisible: 'balancesVisible'
|
||||
};
|
||||
|
||||
const state = {
|
||||
lastFetchTime: 0,
|
||||
toggleInProgress: false,
|
||||
toggleDebounceTimer: null,
|
||||
priceUpdateInterval: null,
|
||||
lastUpdateTime: 0,
|
||||
isWalletsPage: false,
|
||||
initialized: false,
|
||||
cacheKey: 'rates_crypto_prices'
|
||||
};
|
||||
|
||||
function getShortName(fullName) {
|
||||
return window.CoinManager.getSymbol(fullName) || fullName;
|
||||
}
|
||||
|
||||
function getCoingeckoId(coinName) {
|
||||
if (!window.CoinManager) {
|
||||
console.warn('[WalletManager] CoinManager not available');
|
||||
return coinName;
|
||||
}
|
||||
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
|
||||
|
||||
if (!coin) {
|
||||
console.warn(`[WalletManager] No coin found for: ${coinName}`);
|
||||
return coinName;
|
||||
}
|
||||
|
||||
return coin.symbol;
|
||||
}
|
||||
|
||||
async function fetchPrices(forceUpdate = false) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastFetch = now - state.lastFetchTime;
|
||||
|
||||
if (!forceUpdate && timeSinceLastFetch < config.cacheMinInterval) {
|
||||
const cachedData = CacheManager.get(state.cacheKey);
|
||||
if (cachedData) {
|
||||
return cachedData.value;
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
||||
try {
|
||||
const processedData = {};
|
||||
const currentSource = config.priceSource.primary;
|
||||
|
||||
const shouldIncludeWow = currentSource === 'coingecko.com';
|
||||
|
||||
const coinsToFetch = [];
|
||||
const processedCoins = new Set();
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
|
||||
if (!coinName || processedCoins.has(coinName)) return;
|
||||
|
||||
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
|
||||
coinName.includes('Particl') ? 'Particl' :
|
||||
coinName;
|
||||
|
||||
const coinId = getCoingeckoId(adjustedName);
|
||||
|
||||
if (coinId && (shouldIncludeWow || coinId !== 'WOW')) {
|
||||
coinsToFetch.push(coinId);
|
||||
processedCoins.add(coinName);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchCoinsString = coinsToFetch.join(',');
|
||||
|
||||
const mainResponse = await fetch("/json/coinprices", {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
coins: fetchCoinsString,
|
||||
source: currentSource,
|
||||
ttl: config.defaultTTL
|
||||
})
|
||||
});
|
||||
|
||||
if (!mainResponse.ok) {
|
||||
throw new Error(`HTTP error: ${mainResponse.status}`);
|
||||
}
|
||||
|
||||
const mainData = await mainResponse.json();
|
||||
|
||||
if (mainData && mainData.rates) {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
if (!coinName) return;
|
||||
|
||||
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
|
||||
coinName.includes('Particl') ? 'Particl' :
|
||||
coinName;
|
||||
|
||||
const coinId = getCoingeckoId(adjustedName);
|
||||
const price = mainData.rates[coinId];
|
||||
|
||||
if (price) {
|
||||
const coinKey = coinName.toLowerCase().replace(' ', '-');
|
||||
processedData[coinKey] = {
|
||||
usd: price,
|
||||
btc: coinId === 'BTC' ? 1 : price / (mainData.rates.BTC || 1)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CacheManager.set(state.cacheKey, processedData, config.cacheExpiration);
|
||||
state.lastFetchTime = now;
|
||||
return processedData;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
|
||||
|
||||
if (attempt === config.maxRetries - 1 &&
|
||||
config.priceSource.fallback &&
|
||||
config.priceSource.fallback !== config.priceSource.primary) {
|
||||
const temp = config.priceSource.primary;
|
||||
config.priceSource.primary = config.priceSource.fallback;
|
||||
config.priceSource.fallback = temp;
|
||||
|
||||
console.warn(`Switching to fallback source: ${config.priceSource.primary}`);
|
||||
attempt = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attempt < config.maxRetries - 1) {
|
||||
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cachedData = CacheManager.get(state.cacheKey);
|
||||
if (cachedData) {
|
||||
console.warn('Using cached data after fetch failures');
|
||||
return cachedData.value;
|
||||
}
|
||||
|
||||
throw lastError || new Error('Failed to fetch prices');
|
||||
}
|
||||
|
||||
function storeOriginalValues() {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const value = el.textContent?.trim() || '';
|
||||
|
||||
if (coinName) {
|
||||
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinSymbol = window.CoinManager.getSymbol(coinName);
|
||||
const shortName = getShortName(coinName);
|
||||
|
||||
if (coinSymbol) {
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinSymbol.toLowerCase()}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
el.setAttribute('data-original-value', `${amount} ${shortName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const text = el.textContent?.trim() || '';
|
||||
if (text === 'Loading...') {
|
||||
el.textContent = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePrices(forceUpdate = false) {
|
||||
try {
|
||||
const prices = await fetchPrices(forceUpdate);
|
||||
let newTotal = 0;
|
||||
|
||||
const currentTime = Date.now();
|
||||
localStorage.setItem(stateKeys.lastUpdate, currentTime.toString());
|
||||
state.lastUpdateTime = currentTime;
|
||||
|
||||
if (prices) {
|
||||
Object.entries(prices).forEach(([coinId, priceData]) => {
|
||||
if (priceData?.usd) {
|
||||
localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
|
||||
|
||||
if (!coinName) return;
|
||||
|
||||
let amount = 0;
|
||||
if (amountStr) {
|
||||
const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
|
||||
if (matches && matches.length > 0) {
|
||||
amount = parseFloat(matches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
|
||||
if (!prices[coinId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
if (!price) return;
|
||||
|
||||
const usdValue = (amount * price).toFixed(2);
|
||||
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-last-value`, usdValue);
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
if (amount > 0) {
|
||||
newTotal += parseFloat(usdValue);
|
||||
}
|
||||
|
||||
let usdEl = null;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const nextFlex = flexContainer.nextElementSibling;
|
||||
if (nextFlex) {
|
||||
const usdInNextFlex = nextFlex.querySelector('.usd-value');
|
||||
if (usdInNextFlex) {
|
||||
usdEl = usdInNextFlex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const parentCell = el.closest('td');
|
||||
if (parentCell) {
|
||||
const usdInSameCell = parentCell.querySelector('.usd-value');
|
||||
if (usdInSameCell) {
|
||||
usdEl = usdInSameCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const sibling = el.nextElementSibling;
|
||||
if (sibling && sibling.classList.contains('usd-value')) {
|
||||
usdEl = sibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usdEl) {
|
||||
const parentElement = el.parentElement;
|
||||
if (parentElement) {
|
||||
const usdElNearby = parentElement.querySelector('.usd-value');
|
||||
if (usdElNearby) {
|
||||
usdEl = usdElNearby;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usdEl) {
|
||||
usdEl.textContent = `$${usdValue}`;
|
||||
usdEl.setAttribute('data-original-value', usdValue);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
const parentCell = el.closest('td');
|
||||
if (!parentCell) return;
|
||||
|
||||
const coinValueEl = parentCell.querySelector('.coinname-value');
|
||||
if (!coinValueEl) return;
|
||||
|
||||
const coinName = coinValueEl.getAttribute('data-coinname');
|
||||
if (!coinName) return;
|
||||
|
||||
const amountStr = coinValueEl.textContent?.trim() || '0';
|
||||
const amount = parseFloat(amountStr) || 0;
|
||||
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
if (!prices[coinId]) return;
|
||||
|
||||
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
if (!price) return;
|
||||
|
||||
const usdValue = (amount * price).toFixed(8);
|
||||
el.textContent = `$${usdValue}`;
|
||||
el.setAttribute('data-original-value', usdValue);
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
updateTotalValues(newTotal, prices?.bitcoin?.usd);
|
||||
}
|
||||
|
||||
localStorage.setItem(stateKeys.previousTotal, localStorage.getItem(stateKeys.currentTotal) || '0');
|
||||
localStorage.setItem(stateKeys.currentTotal, newTotal.toString());
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Price update failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTotalValues(totalUsd, btcPrice) {
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
|
||||
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
|
||||
localStorage.setItem('total-usd', totalUsd.toString());
|
||||
}
|
||||
|
||||
if (btcPrice) {
|
||||
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
|
||||
const totalBtcEl = document.getElementById('total-btc-value');
|
||||
if (totalBtcEl) {
|
||||
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
|
||||
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBalances() {
|
||||
if (state.toggleInProgress) return;
|
||||
|
||||
try {
|
||||
state.toggleInProgress = true;
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
const newVisibility = !balancesVisible;
|
||||
|
||||
localStorage.setItem('balancesVisible', newVisibility.toString());
|
||||
updateVisibility(newVisibility);
|
||||
|
||||
if (state.toggleDebounceTimer) {
|
||||
clearTimeout(state.toggleDebounceTimer);
|
||||
}
|
||||
|
||||
state.toggleDebounceTimer = window.setTimeout(async () => {
|
||||
state.toggleInProgress = false;
|
||||
if (newVisibility) {
|
||||
await updatePrices(true);
|
||||
}
|
||||
}, config.debounceDelay);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle balances:', error);
|
||||
state.toggleInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisibility(isVisible) {
|
||||
if (isVisible) {
|
||||
showBalances();
|
||||
} else {
|
||||
hideBalances();
|
||||
}
|
||||
|
||||
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
|
||||
if (eyeIcon) {
|
||||
eyeIcon.innerHTML = isVisible ?
|
||||
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
|
||||
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
|
||||
}
|
||||
}
|
||||
|
||||
function showBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'inline';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const originalValue = el.getAttribute('data-original-value');
|
||||
if (originalValue) {
|
||||
el.textContent = originalValue;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const storedValue = el.getAttribute('data-original-value');
|
||||
if (storedValue !== null && storedValue !== undefined) {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
|
||||
} else {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
|
||||
}
|
||||
} else {
|
||||
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
|
||||
el.textContent = '$0.00000000';
|
||||
} else {
|
||||
el.textContent = '$0.00';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const originalValue = el?.getAttribute('data-original-value');
|
||||
if (el && originalValue) {
|
||||
if (id === 'total-usd-value') {
|
||||
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
|
||||
el.classList.add('font-extrabold');
|
||||
} else {
|
||||
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'none';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
if (state.isWalletsPage) {
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = '****';
|
||||
}
|
||||
});
|
||||
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.classList.remove('font-extrabold');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBalanceVisibility() {
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
updateVisibility(balancesVisible);
|
||||
|
||||
if (balancesVisible) {
|
||||
await updatePrices(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
const publicAPI = {
|
||||
initialize: async function(options) {
|
||||
if (state.initialized) {
|
||||
console.warn('[WalletManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
Object.assign(config, options);
|
||||
}
|
||||
|
||||
state.lastUpdateTime = parseInt(localStorage.getItem(stateKeys.lastUpdate) || '0');
|
||||
state.isWalletsPage = document.querySelector('.wallet-list') !== null ||
|
||||
window.location.pathname.includes('/wallets');
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const text = el.textContent?.trim() || '';
|
||||
if (text === 'Loading...') {
|
||||
el.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
storeOriginalValues();
|
||||
|
||||
if (localStorage.getItem('balancesVisible') === null) {
|
||||
localStorage.setItem('balancesVisible', 'true');
|
||||
}
|
||||
|
||||
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
|
||||
if (hideBalancesToggle) {
|
||||
hideBalancesToggle.addEventListener('click', toggleBalances);
|
||||
}
|
||||
|
||||
await loadBalanceVisibility();
|
||||
|
||||
if (state.priceUpdateInterval) {
|
||||
clearInterval(state.priceUpdateInterval);
|
||||
}
|
||||
|
||||
state.priceUpdateInterval = setInterval(() => {
|
||||
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
|
||||
updatePrices(false);
|
||||
}
|
||||
}, config.priceUpdateInterval);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('walletManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
state.initialized = true;
|
||||
console.log('WalletManager initialized');
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
updatePrices: function(forceUpdate = false) {
|
||||
return updatePrices(forceUpdate);
|
||||
},
|
||||
|
||||
toggleBalances: function() {
|
||||
return toggleBalances();
|
||||
},
|
||||
|
||||
setPriceSource: function(primarySource, fallbackSource = null) {
|
||||
if (!config.priceSource.enabledSources.includes(primarySource)) {
|
||||
throw new Error(`Invalid primary source: ${primarySource}`);
|
||||
}
|
||||
|
||||
if (fallbackSource && !config.priceSource.enabledSources.includes(fallbackSource)) {
|
||||
throw new Error(`Invalid fallback source: ${fallbackSource}`);
|
||||
}
|
||||
|
||||
config.priceSource.primary = primarySource;
|
||||
if (fallbackSource) {
|
||||
config.priceSource.fallback = fallbackSource;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getConfig: function() {
|
||||
return { ...config };
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return {
|
||||
initialized: state.initialized,
|
||||
lastUpdateTime: state.lastUpdateTime,
|
||||
isWalletsPage: state.isWalletsPage,
|
||||
balancesVisible: localStorage.getItem('balancesVisible') === 'true'
|
||||
};
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
if (state.priceUpdateInterval) {
|
||||
clearInterval(state.priceUpdateInterval);
|
||||
state.priceUpdateInterval = null;
|
||||
}
|
||||
|
||||
if (state.toggleDebounceTimer) {
|
||||
clearTimeout(state.toggleDebounceTimer);
|
||||
state.toggleDebounceTimer = null;
|
||||
}
|
||||
|
||||
state.initialized = false;
|
||||
console.log('WalletManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.WalletManager = WalletManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.walletManagerInitialized) {
|
||||
WalletManager.initialize();
|
||||
window.walletManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
|
||||
console.log('WalletManager initialized');
|
||||
@@ -1,446 +0,0 @@
|
||||
const WebSocketManager = (function() {
|
||||
let ws = null;
|
||||
|
||||
const config = {
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 5000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const state = {
|
||||
isConnecting: false,
|
||||
isIntentionallyClosed: false,
|
||||
lastConnectAttempt: null,
|
||||
connectTimeout: null,
|
||||
lastHealthCheck: null,
|
||||
healthCheckInterval: null,
|
||||
isPageHidden: document.hidden,
|
||||
messageHandlers: {},
|
||||
listeners: {},
|
||||
reconnectTimeout: null
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[WebSocketManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function generateHandlerId() {
|
||||
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
function determineWebSocketPort() {
|
||||
let wsPort;
|
||||
|
||||
if (window.config && window.config.wsPort) {
|
||||
wsPort = window.config.wsPort;
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
if (window.ws_port) {
|
||||
wsPort = window.ws_port.toString();
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
wsPort = '11700';
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
setupPageVisibilityHandler();
|
||||
this.connect();
|
||||
startHealthCheck();
|
||||
|
||||
log('WebSocketManager initialized with options:', options);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('webSocketManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
if (state.isConnecting || state.isIntentionallyClosed) {
|
||||
log('Connection attempt blocked - already connecting or intentionally closed');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
state.isConnecting = true;
|
||||
state.lastConnectAttempt = Date.now();
|
||||
|
||||
try {
|
||||
const wsPort = determineWebSocketPort();
|
||||
|
||||
if (!wsPort) {
|
||||
state.isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
setupEventHandlers();
|
||||
|
||||
state.connectTimeout = setTimeout(() => {
|
||||
if (state.isConnecting) {
|
||||
log('Connection timeout, cleaning up');
|
||||
cleanup();
|
||||
handleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('Error during connection attempt:', error);
|
||||
state.isConnecting = false;
|
||||
handleReconnect();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
log('Disconnecting WebSocket');
|
||||
state.isIntentionallyClosed = true;
|
||||
cleanup();
|
||||
stopHealthCheck();
|
||||
},
|
||||
|
||||
isConnected: function() {
|
||||
return ws && ws.readyState === WebSocket.OPEN;
|
||||
},
|
||||
|
||||
sendMessage: function(message) {
|
||||
if (!this.isConnected()) {
|
||||
log('Cannot send message - not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(JSON.stringify(message));
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('Error sending message:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
addMessageHandler: function(type, handler) {
|
||||
if (!state.messageHandlers[type]) {
|
||||
state.messageHandlers[type] = {};
|
||||
}
|
||||
|
||||
const handlerId = generateHandlerId();
|
||||
state.messageHandlers[type][handlerId] = handler;
|
||||
|
||||
return handlerId;
|
||||
},
|
||||
|
||||
removeMessageHandler: function(type, handlerId) {
|
||||
if (state.messageHandlers[type] && state.messageHandlers[type][handlerId]) {
|
||||
delete state.messageHandlers[type][handlerId];
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(state.connectTimeout);
|
||||
stopHealthCheck();
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
state.isConnecting = false;
|
||||
state.messageHandlers = {};
|
||||
|
||||
|
||||
if (ws) {
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onerror = null;
|
||||
ws.onclose = null;
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'Cleanup');
|
||||
}
|
||||
|
||||
ws = null;
|
||||
window.ws = null;
|
||||
}
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
log('Disposing WebSocketManager');
|
||||
|
||||
this.disconnect();
|
||||
|
||||
if (state.listeners.visibilityChange) {
|
||||
document.removeEventListener('visibilitychange', state.listeners.visibilityChange);
|
||||
}
|
||||
|
||||
state.messageHandlers = {};
|
||||
state.listeners = {};
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
log('WebSocketManager paused');
|
||||
state.isIntentionallyClosed = true;
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'WebSocketManager paused');
|
||||
}
|
||||
|
||||
stopHealthCheck();
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
log('WebSocketManager resumed');
|
||||
state.isIntentionallyClosed = false;
|
||||
|
||||
if (!this.isConnected()) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
startHealthCheck();
|
||||
}
|
||||
};
|
||||
|
||||
function setupEventHandlers() {
|
||||
if (!ws) return;
|
||||
|
||||
ws.onopen = () => {
|
||||
state.isConnecting = false;
|
||||
config.reconnectAttempts = 0;
|
||||
clearTimeout(state.connectTimeout);
|
||||
state.lastHealthCheck = Date.now();
|
||||
window.ws = ws;
|
||||
|
||||
log('WebSocket connection established');
|
||||
|
||||
notifyHandlers('connect', { isConnected: true });
|
||||
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('connected');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
log('WebSocket message received:', message);
|
||||
notifyHandlers('message', message);
|
||||
} catch (error) {
|
||||
log('Error processing message:', error);
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log('WebSocket error:', error);
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
notifyHandlers('error', error);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
log('WebSocket closed:', event);
|
||||
state.isConnecting = false;
|
||||
window.ws = null;
|
||||
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('disconnected');
|
||||
}
|
||||
|
||||
notifyHandlers('disconnect', {
|
||||
code: event.code,
|
||||
reason: event.reason
|
||||
});
|
||||
|
||||
if (!state.isIntentionallyClosed) {
|
||||
handleReconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setupPageVisibilityHandler() {
|
||||
const visibilityChangeHandler = () => {
|
||||
if (document.hidden) {
|
||||
handlePageHidden();
|
||||
} else {
|
||||
handlePageVisible();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', visibilityChangeHandler);
|
||||
state.listeners.visibilityChange = visibilityChangeHandler;
|
||||
}
|
||||
|
||||
function handlePageHidden() {
|
||||
log('Page hidden');
|
||||
state.isPageHidden = true;
|
||||
stopHealthCheck();
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
state.isIntentionallyClosed = true;
|
||||
ws.close(1000, 'Page hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageVisible() {
|
||||
log('Page visible');
|
||||
state.isPageHidden = false;
|
||||
state.isIntentionallyClosed = false;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!publicAPI.isConnected()) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
startHealthCheck();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function startHealthCheck() {
|
||||
stopHealthCheck();
|
||||
state.healthCheckInterval = setInterval(() => {
|
||||
performHealthCheck();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopHealthCheck() {
|
||||
if (state.healthCheckInterval) {
|
||||
clearInterval(state.healthCheckInterval);
|
||||
state.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function performHealthCheck() {
|
||||
if (!publicAPI.isConnected()) {
|
||||
log('Health check failed - not connected');
|
||||
handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const lastCheck = state.lastHealthCheck;
|
||||
|
||||
if (lastCheck && (now - lastCheck) > 60000) {
|
||||
log('Health check failed - too long since last check');
|
||||
handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastHealthCheck = now;
|
||||
log('Health check passed');
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
config.reconnectAttempts++;
|
||||
if (config.reconnectAttempts <= config.maxReconnectAttempts) {
|
||||
const delay = Math.min(
|
||||
config.reconnectDelay * Math.pow(1.5, config.reconnectAttempts - 1),
|
||||
30000
|
||||
);
|
||||
|
||||
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
state.reconnectTimeout = null;
|
||||
if (!state.isIntentionallyClosed) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
}, delay);
|
||||
} else {
|
||||
log('Max reconnect attempts reached');
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
state.reconnectTimeout = null;
|
||||
config.reconnectAttempts = 0;
|
||||
publicAPI.connect();
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyHandlers(type, data) {
|
||||
if (state.messageHandlers[type]) {
|
||||
Object.values(state.messageHandlers[type]).forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch (error) {
|
||||
log(`Error in ${type} handler:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(state.connectTimeout);
|
||||
stopHealthCheck();
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
state.isConnecting = false;
|
||||
|
||||
if (ws) {
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onerror = null;
|
||||
ws.onclose = null;
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(1000, 'Cleanup');
|
||||
}
|
||||
|
||||
ws = null;
|
||||
window.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.WebSocketManager = WebSocketManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!window.webSocketManagerInitialized) {
|
||||
window.WebSocketManager.initialize();
|
||||
window.webSocketManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
|
||||
console.log('WebSocketManager initialized');
|
||||
@@ -1,548 +1,59 @@
|
||||
const DOM = {
|
||||
get: (id) => document.getElementById(id),
|
||||
getValue: (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : '';
|
||||
},
|
||||
setValue: (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = value;
|
||||
},
|
||||
addEvent: (id, event, handler) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener(event, handler);
|
||||
},
|
||||
query: (selector) => document.querySelector(selector),
|
||||
queryAll: (selector) => document.querySelectorAll(selector)
|
||||
};
|
||||
|
||||
const Storage = {
|
||||
get: (key) => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key));
|
||||
} catch(e) {
|
||||
console.warn(`Failed to retrieve item from storage: ${key}`, e);
|
||||
return null;
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const err_msgs = document.querySelectorAll('p.error_msg');
|
||||
for (let i = 0; i < err_msgs.length; i++) {
|
||||
err_msg = err_msgs[i].innerText;
|
||||
if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
|
||||
e = document.getElementById('coin_to');
|
||||
e.classList.add('error');
|
||||
}
|
||||
},
|
||||
set: (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error(`Failed to save item to storage: ${key}`, e);
|
||||
return false;
|
||||
if (err_msg.indexOf('Coin From') >= 0) {
|
||||
e = document.getElementById('coin_from');
|
||||
e.classList.add('error');
|
||||
}
|
||||
},
|
||||
setRaw: (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.error(`Failed to save raw item to storage: ${key}`, e);
|
||||
return false;
|
||||
if (err_msg.indexOf('Amount From') >= 0) {
|
||||
e = document.getElementById('amt_from');
|
||||
e.classList.add('error');
|
||||
}
|
||||
},
|
||||
getRaw: (key) => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch(e) {
|
||||
console.warn(`Failed to retrieve raw item from storage: ${key}`, e);
|
||||
return null;
|
||||
if (err_msg.indexOf('Amount To') >= 0) {
|
||||
e = document.getElementById('amt_to');
|
||||
e.classList.add('error');
|
||||
}
|
||||
if (err_msg.indexOf('Minimum Bid Amount') >= 0) {
|
||||
e = document.getElementById('amt_bid_min');
|
||||
e.classList.add('error');
|
||||
}
|
||||
};
|
||||
|
||||
const Ajax = {
|
||||
post: (url, data, onSuccess, onError) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
if (xhr.status === 200) {
|
||||
if (onSuccess) {
|
||||
try {
|
||||
const response = xhr.responseText.startsWith('{') ?
|
||||
JSON.parse(xhr.responseText) : xhr.responseText;
|
||||
onSuccess(response);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response:', e);
|
||||
if (onError) onError('Invalid response format');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Request failed:', xhr.statusText);
|
||||
if (onError) onError(xhr.statusText);
|
||||
}
|
||||
};
|
||||
xhr.open('POST', url);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(data);
|
||||
return xhr;
|
||||
}
|
||||
};
|
||||
|
||||
function handleNewOfferAddress() {
|
||||
const STORAGE_KEY = 'lastUsedAddressNewOffer';
|
||||
const selectElement = DOM.query('select[name="addr_from"]');
|
||||
const form = selectElement?.closest('form');
|
||||
|
||||
if (!selectElement || !form) return;
|
||||
|
||||
function loadInitialAddress() {
|
||||
const savedAddress = Storage.get(STORAGE_KEY);
|
||||
if (savedAddress) {
|
||||
try {
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
if (err_msg.indexOf('Select coin you send') >= 0) {
|
||||
e = document.getElementById('coin_from').parentNode;
|
||||
e.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstAddress() {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAddress(value, text) {
|
||||
Storage.set(STORAGE_KEY, { value, text });
|
||||
}
|
||||
|
||||
form.addEventListener('submit', () => {
|
||||
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
}
|
||||
|
||||
const RateManager = {
|
||||
lookupRates: () => {
|
||||
const coinFrom = DOM.getValue('coin_from');
|
||||
const coinTo = DOM.getValue('coin_to');
|
||||
const ratesDisplay = DOM.get('rates_display');
|
||||
|
||||
if (!coinFrom || !coinTo || !ratesDisplay) {
|
||||
console.log('Required elements for lookup_rates not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (coinFrom === '-1' || coinTo === '-1') {
|
||||
alert('Coins from and to must be set first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCoin = (coinFrom === '15') ? '3' : coinFrom;
|
||||
|
||||
ratesDisplay.innerHTML = '<p>Updating...</p>';
|
||||
|
||||
const priceJsonElement = DOM.query(".pricejsonhidden");
|
||||
if (priceJsonElement) {
|
||||
priceJsonElement.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo;
|
||||
|
||||
Ajax.post('/json/rates', params,
|
||||
(response) => {
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = typeof response === 'string' ?
|
||||
response : '<pre><code>' + JSON.stringify(response, null, ' ') + '</code></pre>';
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = '<p>Error loading rates: ' + error + '</p>';
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getRateInferred: (event) => {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
const coinFrom = DOM.getValue('coin_from');
|
||||
const coinTo = DOM.getValue('coin_to');
|
||||
const rateElement = DOM.get('rate');
|
||||
|
||||
if (!coinFrom || !coinTo || !rateElement) {
|
||||
console.log('Required elements for getRateInferred not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = 'coin_from=' + encodeURIComponent(coinFrom) +
|
||||
'&coin_to=' + encodeURIComponent(coinTo);
|
||||
|
||||
DOM.setValue('rate', 'Loading...');
|
||||
|
||||
Ajax.post('/json/rates', params,
|
||||
(response) => {
|
||||
if (response.coingecko && response.coingecko.rate_inferred) {
|
||||
DOM.setValue('rate', response.coingecko.rate_inferred);
|
||||
RateManager.setRate('rate');
|
||||
} else {
|
||||
DOM.setValue('rate', 'Error: No rate available');
|
||||
console.error('Rate not available in response');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
DOM.setValue('rate', 'Error: Rate lookup failed');
|
||||
console.error('Error fetching rate data:', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setRate: (valueChanged) => {
|
||||
const elements = {
|
||||
coinFrom: DOM.get('coin_from'),
|
||||
coinTo: DOM.get('coin_to'),
|
||||
amtFrom: DOM.get('amt_from'),
|
||||
amtTo: DOM.get('amt_to'),
|
||||
rate: DOM.get('rate'),
|
||||
rateLock: DOM.get('rate_lock'),
|
||||
swapType: DOM.get('swap_type')
|
||||
};
|
||||
|
||||
if (!elements.coinFrom || !elements.coinTo ||
|
||||
!elements.amtFrom || !elements.amtTo || !elements.rate) {
|
||||
console.log('Required elements for setRate not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const values = {
|
||||
coinFrom: elements.coinFrom.value,
|
||||
coinTo: elements.coinTo.value,
|
||||
amtFrom: elements.amtFrom.value,
|
||||
amtTo: elements.amtTo.value,
|
||||
rate: elements.rate.value,
|
||||
lockRate: elements.rate.value == '' ? false :
|
||||
(elements.rateLock ? elements.rateLock.checked : false)
|
||||
};
|
||||
|
||||
if (valueChanged === 'coin_from' || valueChanged === 'coin_to') {
|
||||
DOM.setValue('rate', '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (elements.swapType) {
|
||||
SwapTypeManager.setSwapTypeEnabled(
|
||||
values.coinFrom,
|
||||
values.coinTo,
|
||||
elements.swapType
|
||||
);
|
||||
}
|
||||
|
||||
if (values.coinFrom == '-1' || values.coinTo == '-1') {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo;
|
||||
|
||||
if (valueChanged == 'rate' ||
|
||||
(values.lockRate && valueChanged == 'amt_from') ||
|
||||
(values.amtTo == '' && valueChanged == 'amt_from')) {
|
||||
|
||||
if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) {
|
||||
return;
|
||||
} else if (values.amtFrom == '' && values.amtTo != '') {
|
||||
if (valueChanged == 'amt_from') {
|
||||
return;
|
||||
}
|
||||
params += '&rate=' + values.rate + '&amt_to=' + values.amtTo;
|
||||
} else {
|
||||
params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom;
|
||||
}
|
||||
} else if (values.lockRate && valueChanged == 'amt_to') {
|
||||
if (values.amtTo == '' || values.rate == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_to=' + values.amtTo + '&rate=' + values.rate;
|
||||
} else {
|
||||
if (values.amtFrom == '' || values.amtTo == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo;
|
||||
}
|
||||
|
||||
Ajax.post('/json/rate', params,
|
||||
(response) => {
|
||||
if (response.hasOwnProperty('rate')) {
|
||||
DOM.setValue('rate', response.rate);
|
||||
} else if (response.hasOwnProperty('amount_to')) {
|
||||
DOM.setValue('amt_to', response.amount_to);
|
||||
} else if (response.hasOwnProperty('amount_from')) {
|
||||
DOM.setValue('amt_from', response.amount_from);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Rate calculation failed:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function set_rate(valueChanged) {
|
||||
RateManager.setRate(valueChanged);
|
||||
}
|
||||
|
||||
function lookup_rates() {
|
||||
RateManager.lookupRates();
|
||||
}
|
||||
|
||||
function getRateInferred(event) {
|
||||
RateManager.getRateInferred(event);
|
||||
}
|
||||
|
||||
const SwapTypeManager = {
|
||||
adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'],
|
||||
secret_hash_only_coins: ['11', '12'],
|
||||
|
||||
setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => {
|
||||
if (!swapTypeElement) return;
|
||||
|
||||
let makeHidden = false;
|
||||
coinFrom = String(coinFrom);
|
||||
coinTo = String(coinTo);
|
||||
|
||||
if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) ||
|
||||
SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) {
|
||||
swapTypeElement.disabled = true;
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
makeHidden = true;
|
||||
swapTypeElement.classList.add('select-disabled');
|
||||
} else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) ||
|
||||
SwapTypeManager.secret_hash_only_coins.includes(coinTo)) {
|
||||
swapTypeElement.disabled = true;
|
||||
swapTypeElement.value = 'seller_first';
|
||||
makeHidden = true;
|
||||
swapTypeElement.classList.add('select-disabled');
|
||||
} else {
|
||||
swapTypeElement.disabled = false;
|
||||
swapTypeElement.classList.remove('select-disabled');
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
}
|
||||
|
||||
let swapTypeHidden = DOM.get('swap_type_hidden');
|
||||
if (makeHidden) {
|
||||
if (!swapTypeHidden) {
|
||||
const form = DOM.get('form');
|
||||
if (form) {
|
||||
swapTypeHidden = document.createElement('input');
|
||||
swapTypeHidden.setAttribute('id', 'swap_type_hidden');
|
||||
swapTypeHidden.setAttribute('type', 'hidden');
|
||||
swapTypeHidden.setAttribute('name', 'swap_type');
|
||||
form.appendChild(swapTypeHidden);
|
||||
}
|
||||
}
|
||||
if (swapTypeHidden) {
|
||||
swapTypeHidden.setAttribute('value', swapTypeElement.value);
|
||||
}
|
||||
} else if (swapTypeHidden) {
|
||||
swapTypeHidden.parentNode.removeChild(swapTypeHidden);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function set_swap_type_enabled(coinFrom, coinTo, swapTypeElement) {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom, coinTo, swapTypeElement);
|
||||
}
|
||||
|
||||
const UIEnhancer = {
|
||||
handleErrorHighlighting: () => {
|
||||
const errMsgs = document.querySelectorAll('p.error_msg');
|
||||
|
||||
const errorFieldMap = {
|
||||
'coin_to': ['coin_to', 'Coin To'],
|
||||
'coin_from': ['Coin From'],
|
||||
'amt_from': ['Amount From'],
|
||||
'amt_to': ['Amount To'],
|
||||
'amt_bid_min': ['Minimum Bid Amount'],
|
||||
'Select coin you send': ['coin_from', 'parentNode']
|
||||
};
|
||||
|
||||
errMsgs.forEach(errMsg => {
|
||||
const text = errMsg.innerText;
|
||||
|
||||
Object.entries(errorFieldMap).forEach(([field, keywords]) => {
|
||||
if (keywords.some(keyword => text.includes(keyword))) {
|
||||
let element = DOM.get(field);
|
||||
|
||||
if (field === 'Select coin you send' && element) {
|
||||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.classList.add('error');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('input.error, select.error').forEach(element => {
|
||||
element.addEventListener('focus', event => {
|
||||
// remove error class on input or select focus
|
||||
const inputs = document.querySelectorAll('input.error');
|
||||
const selects = document.querySelectorAll('select.error');
|
||||
const elements = [...inputs, ...selects];
|
||||
elements.forEach((element) => {
|
||||
element.addEventListener('focus', (event) => {
|
||||
event.target.classList.remove('error');
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
updateDisabledStyles: () => {
|
||||
document.querySelectorAll('select.disabled-select').forEach(select => {
|
||||
const selects = document.querySelectorAll('select.disabled-select');
|
||||
for (const select of selects) {
|
||||
if (select.disabled) {
|
||||
select.classList.add('disabled-select-enabled');
|
||||
} else {
|
||||
select.classList.remove('disabled-select-enabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => {
|
||||
|
||||
const inputs = document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input');
|
||||
for (const input of inputs) {
|
||||
if (input.readOnly) {
|
||||
input.classList.add('disabled-input-enabled');
|
||||
} else {
|
||||
input.classList.remove('disabled-input-enabled');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupCustomSelects: () => {
|
||||
const selectCache = {};
|
||||
|
||||
function updateSelectCache(select) {
|
||||
if (!select || !select.options || select.selectedIndex === undefined) return;
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (!selectedOption) return;
|
||||
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
if (!select || !select.options || select.selectedIndex === undefined) return;
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (!selectedOption) return;
|
||||
|
||||
const image = selectedOption.getAttribute('data-image') || '';
|
||||
const name = selectedOption.textContent.trim();
|
||||
|
||||
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
|
||||
|
||||
const selectImage = select.nextElementSibling?.querySelector('.select-image');
|
||||
if (selectImage) {
|
||||
selectImage.src = image;
|
||||
}
|
||||
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
}
|
||||
|
||||
function setupCustomSelect(select) {
|
||||
if (!select) return;
|
||||
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement?.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement?.querySelector('.select-image');
|
||||
|
||||
if (!options || !selectIcon || !selectImage) return;
|
||||
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
const storedValue = Storage.getRaw(select.name);
|
||||
if (storedValue && select.value == '-1') {
|
||||
select.value = storedValue;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
Storage.setRaw(select.name, select.value);
|
||||
});
|
||||
|
||||
setSelectData(select);
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
|
||||
selectIcons.forEach(icon => icon.style.display = 'none');
|
||||
selectImages.forEach(image => image.style.display = 'none');
|
||||
selectNames.forEach(name => name.style.display = 'none');
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(setupCustomSelect);
|
||||
}
|
||||
};
|
||||
|
||||
function initializeApp() {
|
||||
handleNewOfferAddress();
|
||||
|
||||
DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
|
||||
|
||||
const coinFrom = DOM.get('coin_from');
|
||||
const coinTo = DOM.get('coin_to');
|
||||
const swapType = DOM.get('swap_type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
|
||||
|
||||
coinFrom.addEventListener('change', function() {
|
||||
SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
|
||||
RateManager.setRate('coin_from');
|
||||
});
|
||||
|
||||
coinTo.addEventListener('change', function() {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
|
||||
RateManager.setRate('coin_to');
|
||||
});
|
||||
}
|
||||
|
||||
['amt_from', 'amt_to', 'rate'].forEach(id => {
|
||||
DOM.addEvent(id, 'change', function() {
|
||||
RateManager.setRate(id);
|
||||
});
|
||||
|
||||
DOM.addEvent(id, 'input', function() {
|
||||
RateManager.setRate(id);
|
||||
});
|
||||
});
|
||||
|
||||
DOM.addEvent('rate_lock', 'change', function() {
|
||||
if (DOM.getValue('rate')) {
|
||||
RateManager.setRate('rate');
|
||||
}
|
||||
});
|
||||
|
||||
DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
|
||||
|
||||
UIEnhancer.handleErrorHighlighting();
|
||||
UIEnhancer.updateDisabledStyles();
|
||||
UIEnhancer.setupCustomSelects();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
306
basicswap/static/js/tooltips.js
Normal file
306
basicswap/static/js/tooltips.js
Normal file
@@ -0,0 +1,306 @@
|
||||
class TooltipManager {
|
||||
constructor() {
|
||||
this.activeTooltips = new Map();
|
||||
this.sizeCheckIntervals = new Map();
|
||||
this.setupStyles();
|
||||
this.setupCleanupEvents();
|
||||
}
|
||||
|
||||
static initialize() {
|
||||
if (!window.TooltipManager) {
|
||||
window.TooltipManager = new TooltipManager();
|
||||
}
|
||||
return window.TooltipManager;
|
||||
}
|
||||
|
||||
create(element, content, options = {}) {
|
||||
if (!element) return null;
|
||||
|
||||
this.destroy(element);
|
||||
|
||||
const checkSize = () => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width && rect.height) {
|
||||
clearInterval(this.sizeCheckIntervals.get(element));
|
||||
this.sizeCheckIntervals.delete(element);
|
||||
this.createTooltip(element, content, options, rect);
|
||||
}
|
||||
};
|
||||
|
||||
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50));
|
||||
checkSize();
|
||||
return null;
|
||||
}
|
||||
|
||||
createTooltip(element, content, options, rect) {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
let bgClass = 'bg-gray-400';
|
||||
let arrowColor = 'rgb(156 163 175)';
|
||||
|
||||
if (targetId?.includes('tooltip-offer-')) {
|
||||
const offerId = targetId.split('tooltip-offer-')[1];
|
||||
const [actualOfferId] = offerId.split('_');
|
||||
|
||||
if (window.jsonData) {
|
||||
const offer = window.jsonData.find(o =>
|
||||
o.unique_id === offerId ||
|
||||
o.offer_id === actualOfferId
|
||||
);
|
||||
|
||||
if (offer) {
|
||||
if (offer.is_revoked) {
|
||||
bgClass = 'bg-red-500';
|
||||
arrowColor = 'rgb(239 68 68)';
|
||||
} else if (offer.is_own_offer) {
|
||||
bgClass = 'bg-gray-300';
|
||||
arrowColor = 'rgb(209 213 219)';
|
||||
} else {
|
||||
bgClass = 'bg-green-700';
|
||||
arrowColor = 'rgb(21 128 61)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = tippy(element, {
|
||||
content,
|
||||
allowHTML: true,
|
||||
placement: options.placement || 'top',
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
duration: 0,
|
||||
delay: 0,
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
theme: '',
|
||||
moveTransition: 'none',
|
||||
offset: [0, 10],
|
||||
popperOptions: {
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'viewport',
|
||||
padding: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: 10,
|
||||
fallbackPlacements: ['top', 'bottom', 'right', 'left']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
onCreate(instance) {
|
||||
instance._originalPlacement = instance.props.placement;
|
||||
},
|
||||
onShow(instance) {
|
||||
if (!document.body.contains(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
instance.setProps({
|
||||
placement: instance._originalPlacement
|
||||
});
|
||||
|
||||
if (instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onMount(instance) {
|
||||
if (instance.popper.firstElementChild) {
|
||||
instance.popper.firstElementChild.classList.add(bgClass);
|
||||
}
|
||||
const arrow = instance.popper.querySelector('.tippy-arrow');
|
||||
if (arrow) {
|
||||
arrow.style.setProperty('color', arrowColor, 'important');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id') ||
|
||||
`tooltip-${Math.random().toString(36).substring(7)}`;
|
||||
element.setAttribute('data-tooltip-trigger-id', id);
|
||||
this.activeTooltips.set(id, instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
if (!element) return;
|
||||
|
||||
if (this.sizeCheckIntervals.has(element)) {
|
||||
clearInterval(this.sizeCheckIntervals.get(element));
|
||||
this.sizeCheckIntervals.delete(element);
|
||||
}
|
||||
|
||||
const id = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!id) return;
|
||||
|
||||
const instance = this.activeTooltips.get(id);
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying tooltip:', e);
|
||||
}
|
||||
}
|
||||
this.activeTooltips.delete(id);
|
||||
element.removeAttribute('data-tooltip-trigger-id');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval));
|
||||
this.sizeCheckIntervals.clear();
|
||||
|
||||
this.activeTooltips.forEach((instance, id) => {
|
||||
if (instance?.[0]) {
|
||||
try {
|
||||
instance[0].destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error cleaning up tooltip:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.activeTooltips.clear();
|
||||
|
||||
document.querySelectorAll('[data-tippy-root]').forEach(element => {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupStyles() {
|
||||
if (document.getElementById('tooltip-styles')) return;
|
||||
|
||||
document.head.insertAdjacentHTML('beforeend', `
|
||||
<style id="tooltip-styles">
|
||||
[data-tippy-root] {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
position: relative !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-400 {
|
||||
background-color: rgb(156 163 175);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-400) .tippy-arrow {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.tippy-box .bg-red-500 {
|
||||
background-color: rgb(239 68 68);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-red-500) .tippy-arrow {
|
||||
color: rgb(239 68 68);
|
||||
}
|
||||
|
||||
.tippy-box .bg-gray-300 {
|
||||
background-color: rgb(209 213 219);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-gray-300) .tippy-arrow {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
.tippy-box .bg-green-700 {
|
||||
background-color: rgb(21 128 61);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.tippy-box:has(.bg-green-700) .tippy-arrow {
|
||||
color: rgb(21 128 61);
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
|
||||
border-top-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
|
||||
border-left-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
|
||||
border-right-color: currentColor;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='top'] > .tippy-arrow {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='left'] > .tippy-arrow {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tippy-box[data-placement^='right'] > .tippy-arrow {
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
`);
|
||||
}
|
||||
|
||||
setupCleanupEvents() {
|
||||
window.addEventListener('beforeunload', () => this.cleanup());
|
||||
window.addEventListener('unload', () => this.cleanup());
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeTooltips(selector = '[data-tooltip-target]') {
|
||||
document.querySelectorAll(selector).forEach(element => {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
const tooltipContent = document.getElementById(targetId);
|
||||
|
||||
if (tooltipContent) {
|
||||
this.create(element, tooltipContent.innerHTML, {
|
||||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TooltipManager;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
TooltipManager.initialize();
|
||||
});
|
||||
@@ -44,7 +44,7 @@
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
@@ -54,7 +54,7 @@
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
@@ -113,6 +113,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/static/js/swaps_in_progress.js"></script>
|
||||
<script src="/static/js/active.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -532,12 +532,10 @@
|
||||
<button name="show_txns" type="submit" value="Show More Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show More Info </button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if debug_ui_mode == true %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.can_abandon == true and not edit_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" 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>
|
||||
@@ -557,27 +555,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<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="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 transition-opacity duration-300 ease-out">
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
|
||||
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<button type="button" id="confirmYes"
|
||||
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">
|
||||
Confirm
|
||||
</button>
|
||||
<button type="button" id="confirmNo"
|
||||
class="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">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,74 +562,9 @@
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let confirmCallback = null;
|
||||
let triggerElement = null;
|
||||
|
||||
document.getElementById('confirmYes').addEventListener('click', function() {
|
||||
if (typeof confirmCallback === 'function') {
|
||||
confirmCallback();
|
||||
function confirmPopup(name) {
|
||||
return confirm(name + " Bid - Are you sure?");
|
||||
}
|
||||
hideConfirmDialog();
|
||||
});
|
||||
|
||||
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
|
||||
|
||||
function showConfirmDialog(title, message, callback) {
|
||||
confirmCallback = callback;
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hideConfirmDialog() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
confirmCallback = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
window.confirmPopup = function(action = 'Abandon') {
|
||||
triggerElement = document.activeElement;
|
||||
const title = `Confirm ${action} Bid`;
|
||||
const message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
|
||||
|
||||
return showConfirmDialog(title, message, function() {
|
||||
if (triggerElement) {
|
||||
const form = triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = triggerElement.name;
|
||||
hiddenInput.value = triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const overrideButtonConfirm = function(button, action) {
|
||||
if (button) {
|
||||
button.removeAttribute('onclick');
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
triggerElement = this;
|
||||
return confirmPopup(action);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const abandonBidBtn = document.querySelector('button[name="abandon_bid"]');
|
||||
overrideButtonConfirm(abandonBidBtn, 'Abandon');
|
||||
|
||||
const acceptBidBtn = document.querySelector('button[name="accept_bid"]');
|
||||
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -808,12 +808,10 @@
|
||||
<button name="show_txns" type="submit" value="Show More Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show More Info </button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if debug_ui_mode == true %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.can_abandon == true %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" 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>
|
||||
|
||||
@@ -363,6 +363,6 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/js/bids_sentreceived.js"></script>
|
||||
<script src="/static/js/bids_sentreceived_export.js"></script>
|
||||
<script src="/static/js/bids_export.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<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-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.2.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.2.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
|
||||
{{ love_svg | safe }}
|
||||
</div>
|
||||
@@ -43,3 +43,68 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
var toggleImages = function() {
|
||||
var html = document.querySelector('html');
|
||||
var darkImages = document.querySelectorAll('.dark-image');
|
||||
var lightImages = document.querySelectorAll('.light-image');
|
||||
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
};
|
||||
|
||||
var toggleImageDisplay = function(images, display) {
|
||||
images.forEach(function(img) {
|
||||
img.style.display = display;
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var themeToggle = document.getElementById('theme-toggle');
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -9,44 +9,42 @@
|
||||
swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg,
|
||||
bids_sent_svg, header_arrow_down_svg, love_svg %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
{% if refresh %}
|
||||
<meta http-equiv="refresh" content="{{ refresh }}">
|
||||
{% endif %}
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<!-- CSS Stylesheets -->>
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/libs/chart.js"></script>
|
||||
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/dropdown.js"></script>
|
||||
<script src="/static/js/libs/popper.js"></script>
|
||||
<script src="/static/js/libs/tippy.js"></script>
|
||||
<script src="/static/js/tooltips.js"></script>
|
||||
|
||||
<!-- Styles -->
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<!-- Custom styles -->
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
|
||||
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
|
||||
<!-- Initialize tooltips -->
|
||||
<script>
|
||||
function getAPIKeys() {
|
||||
return {
|
||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||
coinGecko: "{{ coingecko_api_key|safe }}"
|
||||
};
|
||||
}
|
||||
|
||||
(function() {
|
||||
Object.defineProperty(window, 'ws_port', {
|
||||
value: "{{ ws_port|safe }}",
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tooltipManager = TooltipManager.initialize();
|
||||
tooltipManager.initializeTooltips();
|
||||
});
|
||||
window.getWebSocketConfig = window.getWebSocketConfig || function() {
|
||||
return {
|
||||
port: window.ws_port || '11700',
|
||||
fallbackPort: '11700'
|
||||
};
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
(function() {
|
||||
<!-- Dark mode initialization -->
|
||||
<script>
|
||||
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
@@ -54,38 +52,78 @@
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
})();
|
||||
</script>
|
||||
<!-- Third-party Libraries -->
|
||||
<script src="/static/js/libs/chart.js"></script>
|
||||
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script src="/static/js/libs/popper.js"></script>
|
||||
<script src="/static/js/libs/tippy.js"></script>
|
||||
<!-- UI Components -->
|
||||
<script src="/static/js/ui/tabs.js"></script>
|
||||
<script src="/static/js/ui/dropdown.js"></script>
|
||||
<!-- Core functionality -->
|
||||
<script src="/static/js/modules/coin-manager.js"></script>
|
||||
<script src="/static/js/modules/config-manager.js"></script>
|
||||
<script src="/static/js/modules/cache-manager.js"></script>
|
||||
<script src="/static/js/modules/cleanup-manager.js"></script>
|
||||
<script src="/static/js/modules/websocket-manager.js"></script>
|
||||
<script src="/static/js/modules/network-manager.js"></script>
|
||||
<script src="/static/js/modules/api-manager.js"></script>
|
||||
<script src="/static/js/modules/price-manager.js"></script>
|
||||
<script src="/static/js/modules/tooltips-manager.js"></script>
|
||||
<script src="/static/js/modules/notification-manager.js"></script>
|
||||
<script src="/static/js/modules/identity-manager.js"></script>
|
||||
<script src="/static/js/modules/summary-manager.js"></script>
|
||||
{% if current_page == 'wallets' or current_page == 'wallet' %}
|
||||
<script src="/static/js/modules/wallet-manager.js"></script>
|
||||
{% endif %}
|
||||
<!-- Memory management -->
|
||||
<script src="/static/js/modules/memory-manager.js"></script>
|
||||
<!-- Main application script -->
|
||||
<script src="/static/js/global.js"></script>
|
||||
|
||||
<!-- Shutdown modal functionality -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
const shutdownModal = document.getElementById('shutdownModal');
|
||||
const closeModalButton = document.getElementById('closeShutdownModal');
|
||||
const confirmShutdownButton = document.getElementById('confirmShutdown');
|
||||
const shutdownWarning = document.getElementById('shutdownWarning');
|
||||
|
||||
function updateShutdownButtons() {
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
shutdownButtons.forEach(button => {
|
||||
if (activeSwaps > 0) {
|
||||
button.classList.add('shutdown-disabled');
|
||||
button.setAttribute('data-disabled', 'true');
|
||||
button.setAttribute('title', 'Caution: Swaps in progress');
|
||||
} else {
|
||||
button.classList.remove('shutdown-disabled');
|
||||
button.removeAttribute('data-disabled');
|
||||
button.removeAttribute('title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showShutdownModal() {
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
if (activeSwaps > 0) {
|
||||
shutdownWarning.classList.remove('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
|
||||
} else {
|
||||
shutdownWarning.classList.add('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down';
|
||||
}
|
||||
shutdownModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideShutdownModal() {
|
||||
shutdownModal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
shutdownButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
showShutdownModal();
|
||||
});
|
||||
});
|
||||
|
||||
closeModalButton.addEventListener('click', hideShutdownModal);
|
||||
|
||||
confirmShutdownButton.addEventListener('click', function() {
|
||||
const shutdownToken = document.querySelector('.shutdown-button')
|
||||
.getAttribute('href').split('/').pop();
|
||||
window.location.href = '/shutdown/' + shutdownToken;
|
||||
});
|
||||
|
||||
shutdownModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideShutdownModal();
|
||||
}
|
||||
});
|
||||
|
||||
updateShutdownButtons();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="dark:bg-gray-700">
|
||||
<!-- Shutdown Modal -->
|
||||
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"></div>
|
||||
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">
|
||||
@@ -686,3 +724,194 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- WebSocket -->
|
||||
{% if ws_port %}
|
||||
<script>
|
||||
(function() {
|
||||
window.notificationConfig = {
|
||||
showNewOffers: false,
|
||||
showNewBids: true,
|
||||
showBidAccepted: true
|
||||
};
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('ul_updates');
|
||||
if (!container) {
|
||||
const floating_div = document.createElement('div');
|
||||
floating_div.classList.add('floatright');
|
||||
container = document.createElement('ul');
|
||||
container.setAttribute('id', 'ul_updates');
|
||||
floating_div.appendChild(container);
|
||||
document.body.appendChild(floating_div);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function createToast(title, type = 'success') {
|
||||
const messages = ensureToastContainer();
|
||||
const message = document.createElement('li');
|
||||
message.innerHTML = `
|
||||
<div id="hide">
|
||||
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
|
||||
bg-white rounded-lg shadow" role="alert">
|
||||
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
|
||||
bg-blue-500 rounded-lg">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
|
||||
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
|
||||
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
|
||||
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
|
||||
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
|
||||
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
|
||||
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messages.appendChild(message);
|
||||
}
|
||||
|
||||
function updateElement(elementId, value, options = {}) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return false;
|
||||
|
||||
const safeValue = (value !== undefined && value !== null)
|
||||
? value
|
||||
: (element.dataset.lastValue || 0);
|
||||
|
||||
element.dataset.lastValue = safeValue;
|
||||
|
||||
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
|
||||
const svg = element.querySelector('svg');
|
||||
element.textContent = safeValue;
|
||||
if (svg) {
|
||||
element.insertBefore(svg, element.firstChild);
|
||||
}
|
||||
} else {
|
||||
element.textContent = safeValue;
|
||||
}
|
||||
|
||||
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
|
||||
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
|
||||
'watched-outputs-counter'].includes(elementId)) {
|
||||
element.classList.remove('bg-blue-500', 'bg-gray-400');
|
||||
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
||||
}
|
||||
|
||||
if (elementId === 'swaps-counter') {
|
||||
const swapContainer = document.getElementById('swapContainer');
|
||||
if (swapContainer) {
|
||||
const isSwapping = safeValue > 0;
|
||||
if (isSwapping) {
|
||||
swapContainer.innerHTML = `{{ swap_in_progress_green_svg | safe }}`;
|
||||
swapContainer.style.animation = 'spin 2s linear infinite';
|
||||
} else {
|
||||
swapContainer.innerHTML = `{{ swap_in_progress_svg | safe }}`;
|
||||
swapContainer.style.animation = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function fetchSummaryData() {
|
||||
fetch('/json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateElement('network-offers-counter', data.num_network_offers);
|
||||
updateElement('offers-counter', data.num_sent_active_offers);
|
||||
updateElement('sent-bids-counter', data.num_sent_active_bids);
|
||||
updateElement('recv-bids-counter', data.num_recv_active_bids);
|
||||
updateElement('bid-requests-counter', data.num_available_bids);
|
||||
updateElement('swaps-counter', data.num_swapping);
|
||||
updateElement('watched-outputs-counter', data.num_watched_outputs);
|
||||
})
|
||||
.catch(error => console.error('Summary data fetch error:', error));
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
const wsUrl = "ws://" + window.location.hostname + ":{{ ws_port }}";
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('🟢 WebSocket connection established for Dynamic Counters');
|
||||
fetchSummaryData();
|
||||
setInterval(fetchSummaryData, 30000); // Refresh every 30 seconds
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event) {
|
||||
let toastTitle;
|
||||
let shouldShowToast = false;
|
||||
|
||||
switch (data.event) {
|
||||
case 'new_offer':
|
||||
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = window.notificationConfig.showNewOffers;
|
||||
break;
|
||||
case 'new_bid':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
|
||||
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
shouldShowToast = window.notificationConfig.showNewBids;
|
||||
break;
|
||||
case 'bid_accepted':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
|
||||
shouldShowToast = window.notificationConfig.showBidAccepted;
|
||||
break;
|
||||
}
|
||||
|
||||
if (toastTitle && shouldShowToast) {
|
||||
createToast(toastTitle);
|
||||
}
|
||||
}
|
||||
fetchSummaryData();
|
||||
} catch (error) {
|
||||
console.error('WebSocket message processing error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket connection closed', event);
|
||||
setTimeout(initWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
window.closeAlert = function(event) {
|
||||
let element = event.target;
|
||||
while (element.nodeName !== "BUTTON") {
|
||||
element = element.parentNode;
|
||||
}
|
||||
element.parentNode.parentNode.removeChild(element.parentNode);
|
||||
};
|
||||
|
||||
function init() {
|
||||
initWebSocket();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
|
||||
|
||||
{% for m in messages %}
|
||||
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert">
|
||||
<div class="lg:container mx-auto">
|
||||
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
|
||||
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||
<div class="flex-1 p-2">
|
||||
@@ -27,8 +27,8 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% if err_messages %}
|
||||
<section class="py-4 px-6" id="err_messages_{{ err_messages[0][0] }}" role="alert">
|
||||
<div class="lg:container mx-auto">
|
||||
<section class="py-4" id="err_messages_{{ err_messages[0][0] }}" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
|
||||
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||
<div class="flex-1 p-2">
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{% from 'style.html' import circular_error_messages_svg %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script>
|
||||
const isDarkMode = localStorage.getItem('color-theme') === 'dark' || (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - Login - v{{ version }}</title>
|
||||
</head>
|
||||
<body class="dark:bg-gray-700">
|
||||
<section class="py-24 md:py-32">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<div class="mb-6 text-center">
|
||||
<a class="inline-block mb-6" href="#">
|
||||
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image" style="display: none;">
|
||||
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image" style="display: block;">
|
||||
</a>
|
||||
<h3 class="mb-4 text-2xl md:text-3xl font-bold dark:text-white">Login Required</h3>
|
||||
<p class="text-lg text-coolGray-500 font-medium dark:text-gray-300">Please enter the password to access BasicSwap.</p>
|
||||
</div>
|
||||
|
||||
{% for m in err_messages %}
|
||||
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-4 text-red-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-600 dark:text-red-300 rounded-md">
|
||||
<div class="flex flex-wrap items-center -m-1">
|
||||
<div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
|
||||
<p class="ml-2 font-medium text-sm">{{ m[1] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<form method="post" action="/login" autocomplete="off">
|
||||
<div class="mb-6">
|
||||
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="password">Password</label>
|
||||
<input class="appearance-none block w-full p-3 leading-5 text-coolGray-900 border border-coolGray-200 rounded-lg shadow-md placeholder-coolGray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
|
||||
type="password" name="password" id="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<button class="inline-block py-3 px-7 mb-6 w-full text-base text-blue-50 font-medium text-center leading-6 bg-blue-500 hover:bg-blue-600 rounded-md shadow-sm"
|
||||
type="submit">Login</button>
|
||||
<p class="text-center">
|
||||
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
function toggleImages() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const darkImages = document.querySelectorAll('.dark-image');
|
||||
const lightImages = document.querySelectorAll('.light-image');
|
||||
darkImages.forEach(img => img.style.display = isDark ? 'block' : 'none');
|
||||
lightImages.forEach(img => img.style.display = isDark ? 'none' : 'block');
|
||||
}
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -219,17 +219,16 @@
|
||||
<td class="py-3 px-6 bold">Revoked</td>
|
||||
<td class="py-3 px-6">{{ data.was_revoked }}</td>
|
||||
</tr>
|
||||
{% if data.sent %}
|
||||
<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">Auto Accept Type</td>
|
||||
<td class="py-3 px-6 bold">Auto Accept Strategy</td>
|
||||
<td class="py-3 px-6">
|
||||
{% if data.auto_accept_type is none %} Unknown
|
||||
{% elif data.auto_accept_type == 0 %} Bids are accepted manually
|
||||
{% elif data.auto_accept_type == 1 %} Bids are accepted automatically
|
||||
{% elif data.auto_accept_type == 2 %} Bids are accepted automatically from known identities
|
||||
{% else %} Unknown ({{ data.auto_accept_type }})
|
||||
{% if data.automation_strat_id == -1 %} None {% else %}
|
||||
<a href="/automationstrategy/{{ data.automation_strat_id }}">{{ data.automation_strat_label }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if data.xmr_type == true %}
|
||||
<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">Chain A offer fee rate</td>
|
||||
@@ -483,7 +482,7 @@ if (document.readyState === 'loading') {
|
||||
name="bid_amount_send"
|
||||
value=""
|
||||
max="{{ data.amt_to }}"
|
||||
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');">
|
||||
onchange="validateMaxAmount(this, {{ data.amt_to }}); updateBidParams('sending');">
|
||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
|
||||
max {{ data.amt_to }} ({{ data.tla_to }})
|
||||
</div>
|
||||
@@ -506,7 +505,7 @@ if (document.readyState === 'loading') {
|
||||
name="bid_amount"
|
||||
value=""
|
||||
max="{{ data.amt_from }}"
|
||||
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_from }}')); updateBidParams('receiving');">
|
||||
onchange="validateMaxAmount(this, {{ data.amt_from }}); updateBidParams('receiving');">
|
||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
|
||||
max {{ data.amt_from }} ({{ data.tla_from }})
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
|
||||
<script src="static/js/coin_icons.js"></script>
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg %}
|
||||
<script src="static/js/coin_icons.js"></script>
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
@@ -69,7 +70,7 @@
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="flex flex-wrap items-center justify-between -mx-4 pb-6 border-gray-400 border-opacity-20"> </div>
|
||||
<form method="post" autocomplete="off" id="form">
|
||||
<form method="post" autocomplete="off" id='form'>
|
||||
<div class="py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
@@ -116,6 +117,63 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function handleNewOfferAddress() {
|
||||
const selectElement = document.querySelector('select[name="addr_from"]');
|
||||
const STORAGE_KEY = 'lastUsedAddressNewOffer';
|
||||
const form = selectElement?.closest('form');
|
||||
|
||||
if (!selectElement || !form) return;
|
||||
|
||||
function loadInitialAddress() {
|
||||
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedAddressJSON) {
|
||||
try {
|
||||
const savedAddress = JSON.parse(savedAddressJSON);
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstAddress() {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAddress(value, text) {
|
||||
const addressData = {
|
||||
value: value,
|
||||
text: text
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', handleNewOfferAddress);
|
||||
} else {
|
||||
handleNewOfferAddress();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
@@ -236,7 +294,7 @@
|
||||
<button type="button" id="get_rate_inferred_button" class="px-4 py-2.5 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-md shadow-sm focus:outline-none">Get Rate Inferred</button>
|
||||
</div>
|
||||
<div class="flex form-check form-check-inline mt-5">
|
||||
<div class="flex items-center h-5"> <input class="form-check-input hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked="checked"> </div>
|
||||
<div class="flex items-center h-5"> <input class="form-check-input hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked=checked> </div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox1">Lock Rate</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Automatically adjusts the <b>You Get</b> value based on the rate you’ve entered. Without it, the rate value is automatically adjusted based on the number of coins you put in <b>You Get.</b></p>
|
||||
@@ -248,6 +306,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if debug_mode == true %}
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
@@ -257,11 +316,7 @@
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="flex form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
{% if debug_ui_mode == true %}
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av">
|
||||
{% else %}
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
|
||||
@@ -270,11 +325,7 @@
|
||||
</div>
|
||||
<div class="flex mt-2 form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
{% if debug_ui_mode == true %}
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv">
|
||||
{% else %}
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
|
||||
@@ -285,6 +336,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="flex form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2" style="opacity: 0.40;">Amount Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different amount to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3" style="opacity: 0.40;">Rate Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different rate to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pricejsonhidden hidden py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
@@ -331,6 +413,225 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
const xhr_rates = new XMLHttpRequest();
|
||||
xhr_rates.onload = () => {
|
||||
if (xhr_rates.status == 200) {
|
||||
const obj = JSON.parse(xhr_rates.response);
|
||||
inner_html = '<pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
|
||||
document.getElementById('rates_display').innerHTML = inner_html;
|
||||
}
|
||||
};
|
||||
|
||||
const xhr_rate = new XMLHttpRequest();
|
||||
xhr_rate.onload = () => {
|
||||
if (xhr_rate.status == 200) {
|
||||
const obj = JSON.parse(xhr_rate.response);
|
||||
if (obj.hasOwnProperty('rate')) {
|
||||
document.getElementById('rate').value = obj['rate'];
|
||||
} else if (obj.hasOwnProperty('amount_to')) {
|
||||
document.getElementById('amt_to').value = obj['amount_to'];
|
||||
} else if (obj.hasOwnProperty('amount_from')) {
|
||||
document.getElementById('amt_from').value = obj['amount_from'];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function lookup_rates() {
|
||||
const coin_from = document.getElementById('coin_from').value;
|
||||
const coin_to = document.getElementById('coin_to').value;
|
||||
|
||||
if (coin_from === '-1' || coin_to === '-1') {
|
||||
alert('Coins from and to must be set first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCoin = (coin_from === '15') ? '3' : coin_from;
|
||||
|
||||
inner_html = '<p>Updating...</p>';
|
||||
document.getElementById('rates_display').innerHTML = inner_html;
|
||||
document.querySelector(".pricejsonhidden").classList.remove("hidden");
|
||||
|
||||
const xhr_rates = new XMLHttpRequest();
|
||||
xhr_rates.onreadystatechange = function() {
|
||||
if (xhr_rates.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr_rates.status === 200) {
|
||||
document.getElementById('rates_display').innerHTML = xhr_rates.responseText;
|
||||
} else {
|
||||
console.error('Error fetching data:', xhr_rates.statusText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr_rates.open('POST', '/json/rates');
|
||||
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr_rates.send('coin_from=' + selectedCoin + '&coin_to=' + coin_to);
|
||||
}
|
||||
|
||||
function getRateInferred(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const coin_from = document.getElementById('coin_from').value;
|
||||
const coin_to = document.getElementById('coin_to').value;
|
||||
const params = 'coin_from=' + encodeURIComponent(coin_from) + '&coin_to=' + encodeURIComponent(coin_to);
|
||||
|
||||
const xhr_rates = new XMLHttpRequest();
|
||||
xhr_rates.onreadystatechange = function() {
|
||||
if (xhr_rates.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr_rates.status === 200) {
|
||||
try {
|
||||
const responseData = JSON.parse(xhr_rates.responseText);
|
||||
if (responseData.coingecko && responseData.coingecko.rate_inferred) {
|
||||
const rateInferred = responseData.coingecko.rate_inferred;
|
||||
document.getElementById('rate').value = rateInferred;
|
||||
set_rate('rate');
|
||||
} else {
|
||||
document.getElementById('rate').value = 'Error: Rate limit';
|
||||
console.error('Rate limit reached or invalid response format');
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('rate').value = 'Error: Rate limit';
|
||||
console.error('Error parsing response:', error);
|
||||
}
|
||||
} else {
|
||||
document.getElementById('rate').value = 'Error: Rate limit';
|
||||
console.error('Error fetching data:', xhr_rates.statusText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr_rates.open('POST', '/json/rates');
|
||||
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr_rates.send(params);
|
||||
}
|
||||
|
||||
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
|
||||
|
||||
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
|
||||
const adaptor_sig_only_coins = [
|
||||
'6', /* XMR */
|
||||
'9', /* WOW */
|
||||
'8', /* PART_ANON */
|
||||
'7', /* PART_BLIND */
|
||||
'13', /* FIRO */
|
||||
'18', /* DOGE */
|
||||
'17' /* BCH */
|
||||
];
|
||||
const secret_hash_only_coins = [
|
||||
'11', /* PIVX */
|
||||
'12' /* DASH */
|
||||
];
|
||||
|
||||
let make_hidden = false;
|
||||
|
||||
coin_from = String(coin_from);
|
||||
coin_to = String(coin_to);
|
||||
|
||||
if (adaptor_sig_only_coins.indexOf(coin_from) !== -1 || adaptor_sig_only_coins.indexOf(coin_to) !== -1) {
|
||||
swap_type.disabled = true;
|
||||
swap_type.value = 'xmr_swap';
|
||||
make_hidden = true;
|
||||
swap_type.classList.add('select-disabled');
|
||||
} else if (secret_hash_only_coins.indexOf(coin_from) !== -1 || secret_hash_only_coins.indexOf(coin_to) !== -1) {
|
||||
swap_type.disabled = true;
|
||||
swap_type.value = 'seller_first';
|
||||
make_hidden = true;
|
||||
swap_type.classList.add('select-disabled');
|
||||
} else {
|
||||
swap_type.disabled = false;
|
||||
swap_type.classList.remove('select-disabled');
|
||||
swap_type.value = 'xmr_swap';
|
||||
}
|
||||
|
||||
let swap_type_hidden = document.getElementById('swap_type_hidden');
|
||||
if (make_hidden) {
|
||||
if (!swap_type_hidden) {
|
||||
swap_type_hidden = document.createElement('input');
|
||||
swap_type_hidden.setAttribute('id', 'swap_type_hidden');
|
||||
swap_type_hidden.setAttribute('type', 'hidden');
|
||||
swap_type_hidden.setAttribute('name', 'swap_type');
|
||||
document.getElementById('form').appendChild(swap_type_hidden);
|
||||
}
|
||||
swap_type_hidden.setAttribute('value', swap_type.value);
|
||||
} else if (swap_type_hidden) {
|
||||
swap_type_hidden.parentNode.removeChild(swap_type_hidden);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const coin_from = document.getElementById('coin_from');
|
||||
const coin_to = document.getElementById('coin_to');
|
||||
|
||||
if (coin_from && coin_to) {
|
||||
coin_from.addEventListener('change', function() {
|
||||
const swap_type = document.getElementById('swap_type');
|
||||
set_swap_type_enabled(this.value, coin_to.value, swap_type);
|
||||
});
|
||||
|
||||
coin_to.addEventListener('change', function() {
|
||||
const swap_type = document.getElementById('swap_type');
|
||||
set_swap_type_enabled(coin_from.value, this.value, swap_type);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function set_rate(value_changed) {
|
||||
const coin_from = document.getElementById('coin_from').value;
|
||||
const coin_to = document.getElementById('coin_to').value;
|
||||
const amt_from = document.getElementById('amt_from').value;
|
||||
const amt_to = document.getElementById('amt_to').value;
|
||||
const rate = document.getElementById('rate').value;
|
||||
const lock_rate = rate == '' ? false : document.getElementById('rate_lock').checked;
|
||||
|
||||
if (value_changed === 'coin_from' || value_changed === 'coin_to') {
|
||||
document.getElementById('rate').value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const swap_type = document.getElementById('swap_type');
|
||||
set_swap_type_enabled(coin_from, coin_to, swap_type);
|
||||
|
||||
if (coin_from == '-1' || coin_to == '-1') {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = 'coin_from=' + coin_from + '&coin_to=' + coin_to;
|
||||
|
||||
if (value_changed == 'rate' || (lock_rate && value_changed == 'amt_from') || (amt_to == '' && value_changed == 'amt_from')) {
|
||||
if (rate == '' || (amt_from == '' && amt_to == '')) {
|
||||
return;
|
||||
} else if (amt_from == '' && amt_to != '') {
|
||||
if (value_changed == 'amt_from') {
|
||||
return;
|
||||
}
|
||||
params += '&rate=' + rate + '&amt_to=' + amt_to;
|
||||
} else {
|
||||
params += '&rate=' + rate + '&amt_from=' + amt_from;
|
||||
}
|
||||
} else if (lock_rate && value_changed == 'amt_to') {
|
||||
if (amt_to == '' || rate == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_to=' + amt_to + '&rate=' + rate;
|
||||
} else {
|
||||
if (amt_from == '' || amt_to == '') {
|
||||
return;
|
||||
}
|
||||
params += '&amt_from=' + amt_from + '&amt_to=' + amt_to;
|
||||
}
|
||||
|
||||
xhr_rate.open('POST', '/json/rate');
|
||||
xhr_rate.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr_rate.send(params);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const coin_from = document.getElementById('coin_from').value;
|
||||
const coin_to = document.getElementById('coin_to').value;
|
||||
const swap_type = document.getElementById('swap_type');
|
||||
set_swap_type_enabled(coin_from, coin_to, swap_type);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<script src="static/js/new_offer.js"></script>
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
|
||||
<script src="static/js/coin_icons.js"></script>
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function getAPIKeys() {
|
||||
return {
|
||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||
coinGecko: "{{ coingecko_api_key|safe }}"
|
||||
};
|
||||
}
|
||||
|
||||
function getWebSocketConfig() {
|
||||
return {
|
||||
port: "{{ ws_port|safe }}",
|
||||
fallbackPort: "11700"
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="py-3 px-4 mt-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
|
||||
@@ -136,12 +152,11 @@
|
||||
'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png', 'show': false},
|
||||
'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Dogecoin.png', 'show': true},
|
||||
'DCR': {'name': 'Decred', 'symbol': 'DCR', 'image': 'Decred.png', 'show': true},
|
||||
'NMC': {'name': 'Namecoin', 'symbol': 'NMC', 'image': 'Namecoin.png', 'show': true},
|
||||
'ZANO': {'name': 'Zano', 'symbol': 'ZANO', 'image': 'Zano.png', 'show': false},
|
||||
'WOW': {'name': 'Wownero', 'symbol': 'WOW', 'image': 'Wownero.png', 'show': true}
|
||||
}
|
||||
%}
|
||||
{% set custom_order = ['BTC', 'ETH', 'XMR', 'PART', 'LTC', 'BCH', 'FIRO', 'PIVX', 'DASH', 'DOGE', 'DCR', 'NMC', 'ZANO', 'WOW'] %}
|
||||
{% set custom_order = ['BTC', 'ETH', 'XMR', 'PART', 'LTC', 'BCH', 'FIRO', 'PIVX', 'DASH', 'DOGE', 'DCR', 'ZANO', 'WOW'] %}
|
||||
|
||||
{% if enabled_chart_coins is string %}
|
||||
{% if enabled_chart_coins == "" %}
|
||||
@@ -194,9 +209,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script src="/static/js/pricechart.js"></script>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
<script src="/static/js/pricechart.js"></script>
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 mt-5 h-full overflow-hidden">
|
||||
@@ -343,16 +358,16 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0" data-sortable="true" data-column-index="6">
|
||||
<th class="p-0" data-sortable="true" data-column-index="5">
|
||||
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right flex items-center justify-end">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Rate</span>
|
||||
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6">↓</span>
|
||||
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-5">↓</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0" data-sortable="true" data-column-index="7">
|
||||
<th class="p-0" data-sortable="true" data-column-index="6">
|
||||
<div class="py-3 bg-coolGray-200 dark:bg-gray-600 text-center flex items-center justify-center">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span>
|
||||
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-7">↓</span>
|
||||
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6">↓</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
@@ -402,5 +417,4 @@
|
||||
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
<script src="/static/js/offers.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Enabled Coins</td>
|
||||
<td class="py-3 px-6">
|
||||
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated<br/> list of coin tickers to show
|
||||
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated list of coin tickers to show
|
||||
<br />
|
||||
</label>
|
||||
<input name="enabledchartcoins" type="text" class="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-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" value="{{chart_settings.enabled_chart_coins}}">
|
||||
|
||||
@@ -6,87 +6,22 @@
|
||||
{% if refresh %}
|
||||
<meta http-equiv="refresh" content="{{ refresh }}">
|
||||
{% endif %}
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<!-- CSS Stylesheets -->>
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<!-- Custom styles -->
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script>
|
||||
function getAPIKeys() {
|
||||
return {
|
||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||
coinGecko: "{{ coingecko_api_key|safe }}"
|
||||
};
|
||||
}
|
||||
|
||||
(function() {
|
||||
Object.defineProperty(window, 'ws_port', {
|
||||
value: "{{ ws_port|safe }}",
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
window.getWebSocketConfig = window.getWebSocketConfig || function() {
|
||||
return {
|
||||
port: window.ws_port || '11700',
|
||||
fallbackPort: '11700'
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
(function() {
|
||||
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
})();
|
||||
</script>
|
||||
<!-- Third-party Libraries -->
|
||||
<script src="/static/js/libs/chart.js"></script>
|
||||
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script src="/static/js/libs/popper.js"></script>
|
||||
<script src="/static/js/libs/tippy.js"></script>
|
||||
<!-- UI Components -->
|
||||
<script src="/static/js/ui/tabs.js"></script>
|
||||
<script src="/static/js/ui/dropdown.js"></script>
|
||||
<!-- Core functionality -->
|
||||
<script src="/static/js/modules/coin-manager.js"></script>
|
||||
<script src="/static/js/modules/config-manager.js"></script>
|
||||
<script src="/static/js/modules/cache-manager.js"></script>
|
||||
<script src="/static/js/modules/cleanup-manager.js"></script>
|
||||
<script src="/static/js/modules/websocket-manager.js"></script>
|
||||
<script src="/static/js/modules/network-manager.js"></script>
|
||||
<script src="/static/js/modules/api-manager.js"></script>
|
||||
<script src="/static/js/modules/price-manager.js"></script>
|
||||
<script src="/static/js/modules/tooltips-manager.js"></script>
|
||||
<script src="/static/js/modules/notification-manager.js"></script>
|
||||
<script src="/static/js/modules/identity-manager.js"></script>
|
||||
<script src="/static/js/modules/summary-manager.js"></script>
|
||||
{% if current_page == 'wallets' or current_page == 'wallet' %}
|
||||
<script src="/static/js/modules/wallet-manager.js"></script>
|
||||
{% endif %}
|
||||
<!-- Memory management -->
|
||||
<script src="/static/js/modules/memory-manager.js"></script>
|
||||
<!-- Main application script -->
|
||||
<script src="/static/js/global.js"></script>
|
||||
</head>
|
||||
<script>
|
||||
(function() {
|
||||
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
</head>
|
||||
@@ -172,6 +107,7 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Password toggle functionality
|
||||
const passwordToggle = document.querySelector('.js-password-toggle');
|
||||
if (passwordToggle) {
|
||||
passwordToggle.addEventListener('change', function() {
|
||||
@@ -190,6 +126,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Image toggling function
|
||||
function toggleImages() {
|
||||
const html = document.querySelector('html');
|
||||
const darkImages = document.querySelectorAll('.dark-image');
|
||||
@@ -210,6 +147,42 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle functionality
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
// Call toggleImages on load
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg %}
|
||||
<script src="/static/js/libs//qrcode.js"></script>
|
||||
|
||||
<section class="py-3 px-4 mt-6">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="container mx-auto">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
||||
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">Home</a></li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallet/{{ w.ticker }}">{{ w.ticker }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="py-4 px-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
|
||||
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
|
||||
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
|
||||
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden"> <img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red"> <img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2">
|
||||
<h2 class="text-3xl font-bold text-white">
|
||||
<span class="inline-block align-middle">
|
||||
<img class="mr-2 h-16" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"></span>({{ w.ticker }}) {{ w.name }} Wallet </h2>
|
||||
<h2 class="text-3xl font-bold text-white"> <span class="inline-block align-middle"><img class="mr-2 h-16" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"></span>({{ w.ticker }}) {{ w.name }} Wallet </h2>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"> <a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallet/{{ w.ticker }}"> {{ circular_arrows_svg | safe }}<span>Refresh</span> </a> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
{% if w.updating %}
|
||||
<section class="py-4 px-6" id="messages_updating" role="alert">
|
||||
<div class="lg:container mx-auto">
|
||||
@@ -47,7 +55,6 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if w.havedata %}
|
||||
{% if w.error %}
|
||||
<section class="py-4 px-6" id="messages_error" role="alert">
|
||||
@@ -76,16 +83,17 @@
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
|
||||
{% if w.cid == '18' %} {# DOGE #}
|
||||
<section class="py-4 px-6" id="messages_notice">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="p-6 rounded-lg bg-coolGray-100 dark:bg-gray-500 shadow-sm">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-blue-500 mt-1 mr-3 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"></path></svg>
|
||||
<div class="flex flex-wrap -m-1">
|
||||
<ul class="ml-4">
|
||||
<li class="font-semibold text-lg dark:text-white mb-2">NOTICE:</li>
|
||||
<li class="font-medium text-gray-600 dark:text-white leading-relaxed">
|
||||
NOTICE: This version of DOGE Core is experimental and has been custom-built for compatibility with BasicSwap. As a result, it may not always be fully aligned with upstream changes, features unrelated to BasicSwap might not work as expected, and its code may differ from the official release.
|
||||
This version of DOGE Core is experimental and has been custom-built for compatibility with BasicSwap. As a result, it may not always be fully aligned with upstream changes, features unrelated to BasicSwap might not work as expected, and its code may differ from the official release.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -94,10 +102,9 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="px-6 py-0 mt-5 h-full overflow-hidden">
|
||||
<div class="pb-6 border-coolGray-100">
|
||||
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||
<div class="w-full pt-2">
|
||||
@@ -118,9 +125,7 @@
|
||||
</thead>
|
||||
<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 }}"> </span>Balance: </td>
|
||||
<td class="py-3 px-6 bold">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }} (<span class="usd-value"></span>)
|
||||
{% 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>
|
||||
{% endif %}
|
||||
@@ -129,9 +134,7 @@
|
||||
{% if w.cid == '1' %} {# PART #}
|
||||
<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 }} Blind"> </span>Blind Balance: </td>
|
||||
<td class="py-3 px-6 bold">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
|
||||
{% if w.blind_unconfirmed %}
|
||||
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
|
||||
{% endif %}
|
||||
@@ -139,21 +142,18 @@
|
||||
</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"> <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 }} Anon"> </span>Anon Balance: </td>
|
||||
<td class="py-3 px-6 bold">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
|
||||
{% if w.anon_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.anon_pending }} {{ w.ticker }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="usd-value"></td>
|
||||
</tr>
|
||||
{# / PART #}
|
||||
{% elif w.cid == '3' %} {# LTC #}
|
||||
<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="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
|
||||
{% 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>
|
||||
{% endif %}
|
||||
@@ -223,7 +223,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if block_unknown_seeds and w.expected_seed != true %} {# Only show addresses if wallet seed is correct #}
|
||||
<section class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="pb-6 border-coolGray-100">
|
||||
@@ -247,7 +246,6 @@
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
<section class="p-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="flex items-center">
|
||||
@@ -255,7 +253,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
<section>
|
||||
<div class="px-6 py-0 overflow-hidden">
|
||||
@@ -279,8 +276,9 @@
|
||||
</div>
|
||||
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Main Address: </div>
|
||||
<div class="relative flex justify-center items-center">
|
||||
<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 focus:ring-0" id="monero_main_address">{{ w.main_address }}</div>
|
||||
<div data-tooltip-target="tooltip-copy-monero-main" 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 focus:ring-0" id="monero_main_address">{{ w.main_address }}</div>
|
||||
</div>
|
||||
<div id="tooltip-copy-monero-main" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
{% else %}
|
||||
<div id="qrcode-deposit" class="qrcode"> </div>
|
||||
</div>
|
||||
@@ -294,7 +292,11 @@
|
||||
<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="newaddr_{{ w.cid }}" value="New Deposit Address"> {{ circular_arrows_svg }} New {{ w.name }} Deposit Address </button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tooltip-copy-default" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
{% endif %}
|
||||
<p>Copy to clipboard</p>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,13 +316,14 @@
|
||||
</div>
|
||||
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Subaddress: </div>
|
||||
<div class="relative flex justify-center items-center">
|
||||
<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 focus:ring-0" id="monero_sub_address">{{ w.deposit_address }}</div>
|
||||
<div data-tooltip-target="tooltip-copy-monero-sub" 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 focus:ring-0" id="monero_sub_address">{{ w.deposit_address }}</div>
|
||||
</div>
|
||||
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
|
||||
<div class="py-3 px-6 bold mt-5">
|
||||
<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="newaddr_{{ w.cid }}" value="New Subaddress"> {{ circular_arrows_svg }} New {{ w.name }} Deposit Address</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tooltip-copy-monero-sub" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
{% elif w.cid == '1' %}
|
||||
{# PART #}
|
||||
<div id="qrcode-stealth" class="qrcode"> </div>
|
||||
@@ -328,8 +331,9 @@
|
||||
</div>
|
||||
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Stealth Address: </div>
|
||||
<div class="relative flex justify-center items-center">
|
||||
<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-10 focus:ring-0" id="stealth_address"> {{ w.stealth_address }}
|
||||
<div data-tooltip-target="tooltip-copy-particl-stealth" 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-10 focus:ring-0" id="stealth_address"> {{ w.stealth_address }}
|
||||
</div>
|
||||
<div id="tooltip-copy-particl-stealth" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
{# / PART #}
|
||||
{% elif w.cid == '3' %}
|
||||
{# LTC #}
|
||||
@@ -338,7 +342,7 @@
|
||||
</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="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 data-tooltip-target="tooltip-copy-litecoin-mweb" 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>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
|
||||
</div>
|
||||
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
|
||||
@@ -346,8 +350,11 @@
|
||||
<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 id="tooltip-copy-litecoin-mweb" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
{# / LTC #}
|
||||
{% endif %}
|
||||
<p>Copy to clipboard</p>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,7 +368,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if w.cid == '1' %}
|
||||
{# PART #}
|
||||
<script>
|
||||
@@ -377,7 +383,6 @@
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
});
|
||||
</script>
|
||||
|
||||
{% elif w.cid == '3' %}
|
||||
{# LTC #}
|
||||
<script>
|
||||
@@ -394,7 +399,6 @@
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if w.cid in '6, 9' %}
|
||||
{# XMR | WOW #}
|
||||
<script>
|
||||
@@ -410,7 +414,6 @@
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Monero Main
|
||||
var moneroMainAddress = "{{ w.main_address }}";
|
||||
@@ -424,7 +427,6 @@
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
});
|
||||
</script>
|
||||
|
||||
{% else %}
|
||||
<script>
|
||||
// Default
|
||||
@@ -440,148 +442,64 @@
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupAddressCopy();
|
||||
});
|
||||
|
||||
function setupAddressCopy() {
|
||||
const copyableElements = [
|
||||
'main_deposit_address',
|
||||
'monero_main_address',
|
||||
'monero_sub_address',
|
||||
'stealth_address'
|
||||
];
|
||||
|
||||
copyableElements.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
|
||||
element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors');
|
||||
|
||||
if (!element.querySelector('.copy-icon')) {
|
||||
const copyIcon = document.createElement('span');
|
||||
copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300';
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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" />
|
||||
</svg>`;
|
||||
|
||||
element.style.position = 'relative';
|
||||
element.style.paddingRight = '2.5rem';
|
||||
element.appendChild(copyIcon);
|
||||
}
|
||||
|
||||
element.addEventListener('click', function(e) {
|
||||
const textToCopy = this.innerText.trim();
|
||||
|
||||
copyToClipboard(textToCopy);
|
||||
|
||||
this.classList.add('bg-blue-50', 'dark:bg-blue-900');
|
||||
|
||||
showCopyFeedback(this);
|
||||
|
||||
setTimeout(() => {
|
||||
this.classList.remove('bg-blue-50', 'dark:bg-blue-900');
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let activeTooltip = null;
|
||||
|
||||
function showCopyFeedback(element) {
|
||||
if (activeTooltip && activeTooltip.parentNode) {
|
||||
activeTooltip.parentNode.removeChild(activeTooltip);
|
||||
}
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
|
||||
popup.innerText = 'Copied!';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
activeTooltip = popup;
|
||||
|
||||
updateTooltipPosition(popup, element);
|
||||
|
||||
const scrollHandler = () => {
|
||||
if (popup.parentNode) {
|
||||
updateTooltipPosition(popup, element);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scrollHandler, { passive: true });
|
||||
|
||||
popup.style.opacity = '0';
|
||||
popup.style.transition = 'opacity 0.2s ease-in-out';
|
||||
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
popup.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
if (activeTooltip === popup) {
|
||||
activeTooltip = null;
|
||||
}
|
||||
}, 200);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function updateTooltipPosition(tooltip, element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
let top = rect.top - tooltip.offsetHeight - 8;
|
||||
const left = rect.left + rect.width / 2;
|
||||
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.style.left = `${left}px`;
|
||||
tooltip.style.transform = 'translateX(-50%)';
|
||||
}
|
||||
let clickTimeout = null;
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
copyToClipboardFallback(text);
|
||||
});
|
||||
} else {
|
||||
copyToClipboardFallback(text);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboardFallback(text) {
|
||||
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 {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
function copyAndShowMessage(elementId) {
|
||||
const addressElement = document.getElementById(elementId);
|
||||
if (!addressElement) return;
|
||||
const addressText = addressElement.innerText.trim();
|
||||
|
||||
if (addressText === 'Copied to clipboard') return;
|
||||
|
||||
copyToClipboard(addressText);
|
||||
addressElement.innerText = 'Copied to clipboard';
|
||||
const originalWidth = addressElement.offsetWidth;
|
||||
addressElement.classList.add('copying');
|
||||
addressElement.parentElement.style.width = `${originalWidth}px`;
|
||||
setTimeout(function () {
|
||||
addressElement.innerText = addressText;
|
||||
addressElement.classList.remove('copying');
|
||||
addressElement.parentElement.style.width = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const stealthAddressElement = document.getElementById('stealth_address');
|
||||
if (stealthAddressElement) {
|
||||
stealthAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('stealth_address');
|
||||
});
|
||||
}
|
||||
const mainDepositAddressElement = document.getElementById('main_deposit_address');
|
||||
if (mainDepositAddressElement) {
|
||||
mainDepositAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('main_deposit_address');
|
||||
});
|
||||
}
|
||||
const moneroMainAddressElement = document.getElementById('monero_main_address');
|
||||
if (moneroMainAddressElement) {
|
||||
moneroMainAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('monero_main_address');
|
||||
});
|
||||
}
|
||||
const moneroSubAddressElement = document.getElementById('monero_sub_address');
|
||||
if (moneroSubAddressElement) {
|
||||
moneroSubAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('monero_sub_address');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="p-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="flex items-center">
|
||||
@@ -589,7 +507,6 @@ function copyToClipboardFallback(text) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="border-coolGray-100">
|
||||
@@ -612,35 +529,23 @@ function copyToClipboardFallback(text) {
|
||||
</thead>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-4 pl-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 }}"> </span>Balance: </td>
|
||||
<td class="py-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
</td>
|
||||
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }} </td>
|
||||
</tr>
|
||||
{% if w.cid == '3' %}
|
||||
{# LTC #}
|
||||
<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-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
</td>
|
||||
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} </td>
|
||||
</tr>
|
||||
{% elif w.cid == '1' %}
|
||||
{# PART #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-4 pl-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 }}"> </span>Blind Balance: </td>
|
||||
<td class="py-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
</td>
|
||||
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} </td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-4 pl-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 }}"> </span>Anon Balance: </td>
|
||||
<td class="py-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
</td>
|
||||
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }} </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
@@ -652,13 +557,11 @@ function copyToClipboardFallback(text) {
|
||||
<td class="py-3 px-6">
|
||||
<div class="flex"> <input placeholder="{{ w.ticker }} Amount" class="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" type="text" id="amount" name="amt_{{ w.cid }}" value="{{ w.wd_value }}">
|
||||
<div class="ml-2 flex">
|
||||
|
||||
{% if w.cid == '1' %}
|
||||
{# PART #}
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button>
|
||||
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
|
||||
|
||||
<script>
|
||||
function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
@@ -667,75 +570,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
console.log('SetAmount Called with:', {
|
||||
percent: percent,
|
||||
balance: balance,
|
||||
cid: cid,
|
||||
blindBalance: blindBalance,
|
||||
anonBalance: anonBalance,
|
||||
selectedType: selectedType,
|
||||
blindBalanceType: typeof blindBalance,
|
||||
blindBalanceNumeric: Number(blindBalance)
|
||||
});
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const numValue = Number(value);
|
||||
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
return numValue;
|
||||
}
|
||||
|
||||
console.warn('Invalid balance value:', value);
|
||||
return 0;
|
||||
};
|
||||
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = safeParseFloat(balance);
|
||||
break;
|
||||
case 'blind':
|
||||
floatBalance = safeParseFloat(blindBalance);
|
||||
break;
|
||||
case 'anon':
|
||||
floatBalance = safeParseFloat(anonBalance);
|
||||
break;
|
||||
default:
|
||||
floatBalance = safeParseFloat(balance);
|
||||
break;
|
||||
}
|
||||
|
||||
calculatedAmount = Math.max(0, Math.floor(floatBalance * percent * 100000000) / 100000000);
|
||||
|
||||
console.log('Calculated Amount:', {
|
||||
floatBalance: floatBalance,
|
||||
calculatedAmount: calculatedAmount,
|
||||
percent: percent
|
||||
});
|
||||
|
||||
if (percent === 1) {
|
||||
calculatedAmount = floatBalance;
|
||||
}
|
||||
|
||||
if (calculatedAmount < 0.00000001) {
|
||||
console.warn('Calculated amount too small, setting to zero');
|
||||
calculatedAmount = 0;
|
||||
}
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
|
||||
console.log('Final Amount Set:', amountInput.value);
|
||||
}function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
@@ -750,31 +584,21 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
}
|
||||
|
||||
calculatedAmount = Math.floor(floatBalance * percent * 100000000) / 100000000;
|
||||
|
||||
if (percent === 1) {
|
||||
calculatedAmount = floatBalance;
|
||||
}
|
||||
|
||||
calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
{# / PART #}
|
||||
|
||||
{% elif w.cid == '3' %}
|
||||
{# LTC #}
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">25%</button>
|
||||
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
|
||||
|
||||
<script>
|
||||
function setAmount(percent, balance, cid, mwebBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
@@ -803,13 +627,11 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{# / LTC #}
|
||||
{% else %}
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
|
||||
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }})">100%</button>
|
||||
|
||||
<script>
|
||||
function setAmount(percent, balance, cid) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
@@ -854,6 +676,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -925,10 +748,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Fee Estimate:</td>
|
||||
<td class="py-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.est_fee }}</span>
|
||||
(<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
|
||||
</td>
|
||||
<td class="py-3 px-6"> {{ w.est_fee }} </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
@@ -941,7 +761,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="pb-6 ">
|
||||
@@ -969,7 +788,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if w.show_utxo_groups %}
|
||||
<section class="p-6">
|
||||
<div class="lg:container mx-auto">
|
||||
@@ -978,7 +796,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="border-coolGray-100">
|
||||
@@ -1017,7 +834,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 h-full overflow-hidden ">
|
||||
<div class="pb-6 ">
|
||||
@@ -1035,154 +851,165 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
|
||||
<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="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 transition-opacity duration-300 ease-out">
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
|
||||
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<button type="button" id="confirmYes"
|
||||
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">
|
||||
Confirm
|
||||
</button>
|
||||
<button type="button" id="confirmNo"
|
||||
class="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">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let confirmCallback = null;
|
||||
let triggerElement = null;
|
||||
let currentCoinId = '';
|
||||
const coinNameToSymbol = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Dogecoin': 'DOGE',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Zano': 'ZANO',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
};
|
||||
|
||||
function showConfirmDialog(title, message, callback) {
|
||||
confirmCallback = callback;
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
const getUsdValue = (cryptoValue, coinSymbol) => {
|
||||
if (coinSymbol === 'WOW') {
|
||||
return fetch(`https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.wownero.usd;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return cryptoValue * exchangeRate;
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.USD;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return cryptoValue * exchangeRate;
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateUsdValue = async (cryptoCell, coinFullName, usdValueSpan) => {
|
||||
const coinSymbol = coinNameToSymbol[coinFullName] || '';
|
||||
if (!coinSymbol) {
|
||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
function hideConfirmDialog() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
const cryptoValue = parseFloat(cryptoCell.textContent);
|
||||
|
||||
if (!isNaN(cryptoValue) && cryptoValue !== 0) {
|
||||
try {
|
||||
const usdValue = await getUsdValue(cryptoValue, coinSymbol);
|
||||
if (usdValueSpan) {
|
||||
usdValueSpan.textContent = `$${usdValue.toFixed(2)}`;
|
||||
}
|
||||
confirmCallback = null;
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error in updateUsdValue:', error);
|
||||
if (usdValueSpan) {
|
||||
usdValueSpan.textContent = 'Error retrieving exchange rate';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (usdValueSpan) {
|
||||
usdValueSpan.textContent = `$0.00`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTotalUsdValue = async () => {
|
||||
const coinNameValues = document.querySelectorAll('.coinname-value');
|
||||
let totalUsdValue = 0;
|
||||
|
||||
for (const coinNameValue of coinNameValues) {
|
||||
const coinFullName = coinNameValue.getAttribute('data-coinname');
|
||||
const cryptoValue = parseFloat(coinNameValue.textContent);
|
||||
const coinSymbol = coinNameToSymbol[coinFullName];
|
||||
|
||||
if (coinSymbol) {
|
||||
const usdValueSpan = coinNameValue.querySelector('.usd-value');
|
||||
|
||||
if (!isNaN(cryptoValue) && cryptoValue !== 0) {
|
||||
try {
|
||||
const usdValue = await getUsdValue(cryptoValue, coinSymbol);
|
||||
totalUsdValue += usdValue;
|
||||
if (usdValueSpan) {
|
||||
usdValueSpan.textContent = `$${usdValue.toFixed(2)}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving exchange rate for ${coinFullName}`);
|
||||
}
|
||||
} else {
|
||||
if (usdValueSpan) {
|
||||
usdValueSpan.textContent = `$0.00`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const totalUsdValueElement = document.getElementById('total-usd-value');
|
||||
if (totalUsdValueElement) {
|
||||
totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const coinNameValues = document.querySelectorAll('.coinname-value');
|
||||
|
||||
for (const coinNameValue of coinNameValues) {
|
||||
const coinFullName = coinNameValue.getAttribute('data-coinname');
|
||||
const usdValueSpan = coinNameValue.querySelector('.usd-value');
|
||||
updateUsdValue(coinNameValue, coinFullName, usdValueSpan);
|
||||
}
|
||||
|
||||
calculateTotalUsdValue();
|
||||
|
||||
function set_sweep_all(element) {
|
||||
let input = document.getElementById('amount');
|
||||
if (element.checked) {
|
||||
input.disabled = true;
|
||||
} else {
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
let cb_sweepall = document.getElementById('sweepall');
|
||||
if (cb_sweepall) {
|
||||
set_sweep_all(cb_sweepall);
|
||||
cb_sweepall.addEventListener('change', (event) => {
|
||||
set_sweep_all(event.currentTarget);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function confirmReseed() {
|
||||
triggerElement = document.activeElement;
|
||||
return showConfirmDialog(
|
||||
"Confirm Reseed Wallet",
|
||||
"Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.",
|
||||
function() {
|
||||
if (triggerElement) {
|
||||
const form = triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = triggerElement.name;
|
||||
hiddenInput.value = triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
return confirm("Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.");
|
||||
}
|
||||
|
||||
function confirmWithdrawal() {
|
||||
triggerElement = document.activeElement;
|
||||
return showConfirmDialog(
|
||||
"Confirm Withdrawal",
|
||||
"Are you sure you want to proceed with this withdrawal?",
|
||||
function() {
|
||||
if (triggerElement) {
|
||||
const form = triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = triggerElement.name;
|
||||
hiddenInput.value = triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
return confirm("Are you sure?");
|
||||
}
|
||||
|
||||
function confirmUTXOResize() {
|
||||
triggerElement = document.activeElement;
|
||||
return showConfirmDialog(
|
||||
"Confirm UTXO Resize",
|
||||
"Are you sure you want to resize UTXOs?",
|
||||
function() {
|
||||
if (triggerElement) {
|
||||
const form = triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = triggerElement.name;
|
||||
hiddenInput.value = triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
return confirm("Are you sure?");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('confirmYes').addEventListener('click', function() {
|
||||
if (typeof confirmCallback === 'function') {
|
||||
confirmCallback();
|
||||
}
|
||||
hideConfirmDialog();
|
||||
});
|
||||
|
||||
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
|
||||
|
||||
document.querySelectorAll('input[type="submit"][name^="reseed_"]').forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
currentCoinId = button.name.split('_')[1];
|
||||
return confirmReseed();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('button[name^="withdraw_"]').forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
currentCoinId = button.name.split('_')[1];
|
||||
return confirmWithdrawal();
|
||||
});
|
||||
});
|
||||
|
||||
const utxoButton = document.getElementById('create_utxo');
|
||||
if (utxoButton) {
|
||||
utxoButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
return confirmUTXOResize();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
|
||||
|
||||
<section class="py-3 px-4 mt-6">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
||||
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-3">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
|
||||
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
|
||||
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3 h-48">
|
||||
<h2 class="text-4xl font-bold text-white tracking-tighter">Wallets</h2>
|
||||
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Wallets</h2>
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
|
||||
<button id="hide-usd-amount-toggle" class="flex items-center justify-center p-1 focus:ring-0 focus:outline-none">{{ eye_show_svg | safe }}</button>
|
||||
@@ -32,7 +46,7 @@
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
<section class="py-4">
|
||||
<div class="container mx-auto">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="flex flex-wrap -m-4">
|
||||
{% for w in wallets %}
|
||||
{% if w.havedata %}
|
||||
@@ -59,7 +73,7 @@
|
||||
<div 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.balance }} {{ w.ticker }}</div>
|
||||
</div>
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
|
||||
<h4 class="text-xs font-medium dark:text-white usd-text">{{ w.ticker }} USD value:</h4>
|
||||
<div 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 usd-value" data-coinname="{{ w.name }}"></div>
|
||||
</div>
|
||||
{% if w.pending %}
|
||||
@@ -188,7 +202,448 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
<script>
|
||||
const CONFIG = {
|
||||
MAX_RETRIES: 3,
|
||||
BASE_DELAY: 1000,
|
||||
CACHE_EXPIRATION: 5 * 60 * 1000,
|
||||
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
|
||||
API_TIMEOUT: 30000,
|
||||
DEBOUNCE_DELAY: 500,
|
||||
CACHE_MIN_INTERVAL: 60 * 1000
|
||||
};
|
||||
|
||||
const STATE_KEYS = {
|
||||
LAST_UPDATE: 'last-update-time',
|
||||
PREVIOUS_TOTAL: 'previous-total-usd',
|
||||
CURRENT_TOTAL: 'current-total-usd',
|
||||
BALANCES_VISIBLE: 'balancesVisible'
|
||||
};
|
||||
|
||||
const COIN_SYMBOLS = {
|
||||
'Bitcoin': 'bitcoin',
|
||||
'Particl': 'particl',
|
||||
'Monero': 'monero',
|
||||
'Wownero': 'wownero',
|
||||
'Litecoin': 'litecoin',
|
||||
'Dogecoin': 'dogecoin',
|
||||
'Firo': 'zcoin',
|
||||
'Dash': 'dash',
|
||||
'PIVX': 'pivx',
|
||||
'Decred': 'decred',
|
||||
'Zano': 'zano',
|
||||
'Bitcoin Cash': 'bitcoin-cash'
|
||||
};
|
||||
|
||||
const SHORT_NAMES = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Litecoin MWEB': 'LTC MWEB',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Zano': 'ZANO',
|
||||
'Bitcoin Cash': 'BCH'
|
||||
};
|
||||
|
||||
class Cache {
|
||||
constructor(expirationTime) {
|
||||
this.data = null;
|
||||
this.timestamp = null;
|
||||
this.expirationTime = expirationTime;
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return Boolean(
|
||||
this.data &&
|
||||
this.timestamp &&
|
||||
(Date.now() - this.timestamp < this.expirationTime)
|
||||
);
|
||||
}
|
||||
|
||||
set(data) {
|
||||
this.data = data;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
|
||||
get() {
|
||||
if (this.isValid()) {
|
||||
return this.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.data = null;
|
||||
this.timestamp = null;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
|
||||
this.lastFetchTime = 0;
|
||||
}
|
||||
|
||||
makeRequest(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/json/readurl');
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.timeout = CONFIG.API_TIMEOUT;
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
reject(new Error('Request timed out'));
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.Error) {
|
||||
reject(new Error(response.Error));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid JSON response: ${error.message}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
reject(new Error('Network error occurred'));
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ url, headers }));
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPrices(forceUpdate = false) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastFetch = now - this.lastFetchTime;
|
||||
|
||||
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
|
||||
const cachedData = this.cache.get();
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const prices = await this.makeRequest(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'
|
||||
);
|
||||
this.cache.set(prices);
|
||||
this.lastFetchTime = now;
|
||||
return prices;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt < CONFIG.MAX_RETRIES - 1) {
|
||||
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cachedData = this.cache.get();
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
throw lastError || new Error('Failed to fetch prices');
|
||||
}
|
||||
}
|
||||
|
||||
class UiManager {
|
||||
constructor() {
|
||||
this.api = new ApiClient();
|
||||
this.toggleInProgress = false;
|
||||
this.toggleDebounceTimer = null;
|
||||
this.priceUpdateInterval = null;
|
||||
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
|
||||
}
|
||||
|
||||
getShortName(fullName) {
|
||||
return SHORT_NAMES[fullName] || fullName;
|
||||
}
|
||||
|
||||
storeOriginalValues() {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const value = el.textContent?.trim() || '';
|
||||
|
||||
if (coinName) {
|
||||
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinId = COIN_SYMBOLS[coinName];
|
||||
const shortName = this.getShortName(coinName);
|
||||
|
||||
if (coinId) {
|
||||
if (coinId === 'particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinId === 'litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
el.setAttribute('data-original-value', `${amount} ${shortName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async updatePrices(forceUpdate = false) {
|
||||
try {
|
||||
const prices = await this.api.fetchPrices(forceUpdate);
|
||||
let newTotal = 0;
|
||||
|
||||
const currentTime = Date.now();
|
||||
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
|
||||
this.lastUpdateTime = currentTime;
|
||||
|
||||
if (prices) {
|
||||
Object.entries(COIN_SYMBOLS).forEach(([coinName, coinId]) => {
|
||||
if (prices[coinId]?.usd) {
|
||||
localStorage.setItem(`${coinId}-price`, prices[coinId].usd.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
|
||||
|
||||
if (!coinName) return;
|
||||
|
||||
const amount = amountStr ? parseFloat(amountStr.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinId = COIN_SYMBOLS[coinName];
|
||||
if (!coinId) return;
|
||||
|
||||
const price = prices?.[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
const usdValue = (amount * price).toFixed(2);
|
||||
|
||||
if (coinId === 'particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinId === 'litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-last-value`, usdValue);
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
newTotal += parseFloat(usdValue);
|
||||
|
||||
const usdEl = el.closest('.flex')?.nextElementSibling?.querySelector('.usd-value');
|
||||
if (usdEl) {
|
||||
usdEl.textContent = `$${usdValue} USD`;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
|
||||
|
||||
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
|
||||
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Price update failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
updateTotalValues(totalUsd, btcPrice) {
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
|
||||
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
|
||||
localStorage.setItem('total-usd', totalUsd.toString());
|
||||
}
|
||||
|
||||
if (btcPrice) {
|
||||
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
|
||||
const totalBtcEl = document.getElementById('total-btc-value');
|
||||
if (totalBtcEl) {
|
||||
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
|
||||
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleBalances() {
|
||||
if (this.toggleInProgress) return;
|
||||
|
||||
try {
|
||||
this.toggleInProgress = true;
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
const newVisibility = !balancesVisible;
|
||||
|
||||
localStorage.setItem('balancesVisible', newVisibility.toString());
|
||||
this.updateVisibility(newVisibility);
|
||||
|
||||
if (this.toggleDebounceTimer) {
|
||||
clearTimeout(this.toggleDebounceTimer);
|
||||
}
|
||||
|
||||
this.toggleDebounceTimer = window.setTimeout(async () => {
|
||||
this.toggleInProgress = false;
|
||||
if (newVisibility) {
|
||||
await this.updatePrices(true);
|
||||
}
|
||||
}, CONFIG.DEBOUNCE_DELAY);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle balances:', error);
|
||||
this.toggleInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibility(isVisible) {
|
||||
if (isVisible) {
|
||||
this.showBalances();
|
||||
} else {
|
||||
this.hideBalances();
|
||||
}
|
||||
|
||||
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
|
||||
if (eyeIcon) {
|
||||
eyeIcon.innerHTML = isVisible ?
|
||||
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
|
||||
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
|
||||
}
|
||||
}
|
||||
|
||||
showBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'inline';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const originalValue = el.getAttribute('data-original-value');
|
||||
if (originalValue) {
|
||||
el.textContent = originalValue;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const storedValue = el.getAttribute('data-original-value');
|
||||
if (storedValue) {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(2)} USD`;
|
||||
el.style.color = 'white';
|
||||
}
|
||||
});
|
||||
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const originalValue = el?.getAttribute('data-original-value');
|
||||
if (el && originalValue) {
|
||||
if (id === 'total-usd-value') {
|
||||
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
|
||||
el.classList.add('font-extrabold');
|
||||
} else {
|
||||
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'none';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value, .usd-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = '****';
|
||||
}
|
||||
});
|
||||
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.classList.remove('font-extrabold');
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.storeOriginalValues();
|
||||
|
||||
if (localStorage.getItem('balancesVisible') === null) {
|
||||
localStorage.setItem('balancesVisible', 'true');
|
||||
}
|
||||
|
||||
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
|
||||
if (hideBalancesToggle) {
|
||||
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
|
||||
}
|
||||
|
||||
await this.loadBalanceVisibility();
|
||||
|
||||
if (this.priceUpdateInterval) {
|
||||
clearInterval(this.priceUpdateInterval);
|
||||
}
|
||||
|
||||
this.priceUpdateInterval = setInterval(() => {
|
||||
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
|
||||
this.updatePrices(false);
|
||||
}
|
||||
}, CONFIG.PRICE_UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
async loadBalanceVisibility() {
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
this.updateVisibility(balancesVisible);
|
||||
|
||||
if (balancesVisible) {
|
||||
await this.updatePrices(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const uiManager = window.uiManager;
|
||||
if (uiManager?.priceUpdateInterval) {
|
||||
clearInterval(uiManager.priceUpdateInterval);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const uiManager = new UiManager();
|
||||
window.uiManager = uiManager;
|
||||
uiManager.initialize().catch(error => {
|
||||
console.error('Failed to initialize application:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -151,9 +151,7 @@ def page_bid(self, url_split, post_string):
|
||||
)
|
||||
|
||||
|
||||
def page_bids(
|
||||
self, url_split, post_string, sent=False, available=False, received=False
|
||||
):
|
||||
def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -223,6 +221,8 @@ def page_bids(
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"page_type_available": "Bids Available",
|
||||
"page_type_available_description": "Bids available for you to accept.",
|
||||
"messages": messages,
|
||||
"filters": filters,
|
||||
"data": page_data,
|
||||
|
||||
@@ -15,6 +15,7 @@ from .util import (
|
||||
get_data_entry_or,
|
||||
have_data_entry,
|
||||
inputAmount,
|
||||
known_chart_coins,
|
||||
listAvailableCoins,
|
||||
PAGE_LIMIT,
|
||||
setCoinFilter,
|
||||
@@ -31,7 +32,6 @@ from basicswap.basicswap_util import (
|
||||
SwapTypes,
|
||||
DebugTypes,
|
||||
getLockName,
|
||||
get_api_key_setting,
|
||||
strBidState,
|
||||
strSwapDesc,
|
||||
strSwapType,
|
||||
@@ -40,12 +40,12 @@ from basicswap.basicswap_util import (
|
||||
)
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
ticker_map,
|
||||
)
|
||||
from basicswap.explorers import (
|
||||
default_chart_api_key,
|
||||
default_coingecko_api_key,
|
||||
|
||||
default_chart_api_key = (
|
||||
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
|
||||
)
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
def value_or_none(v):
|
||||
@@ -733,9 +733,6 @@ def page_offer(self, url_split, post_string):
|
||||
"swap_type": strSwapDesc(offer.swap_type),
|
||||
"reverse": reverse_bid,
|
||||
"form_id": get_data_entry_or(form_data, "formid", "") if form_data else "",
|
||||
"auto_accept_type": (
|
||||
offer.auto_accept_type if hasattr(offer, "auto_accept_type") else 0
|
||||
),
|
||||
}
|
||||
data.update(extend_data)
|
||||
|
||||
@@ -976,11 +973,22 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
|
||||
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
|
||||
|
||||
chart_api_key = get_api_key_setting(
|
||||
swap_client.settings, "chart_api_key", default_chart_api_key
|
||||
chart_api_key = swap_client.settings.get("chart_api_key", "")
|
||||
if chart_api_key == "":
|
||||
chart_api_key_enc = swap_client.settings.get("chart_api_key_enc", "")
|
||||
chart_api_key = (
|
||||
default_chart_api_key
|
||||
if chart_api_key_enc == ""
|
||||
else bytes.fromhex(chart_api_key_enc).decode("utf-8")
|
||||
)
|
||||
coingecko_api_key = get_api_key_setting(
|
||||
swap_client.settings, "coingecko_api_key", default_coingecko_api_key
|
||||
|
||||
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
|
||||
if coingecko_api_key == "":
|
||||
coingecko_api_key_enc = swap_client.settings.get("coingecko_api_key_enc", "")
|
||||
coingecko_api_key = (
|
||||
default_coingecko_api_key
|
||||
if coingecko_api_key_enc == ""
|
||||
else bytes.fromhex(coingecko_api_key_enc).decode("utf-8")
|
||||
)
|
||||
|
||||
offers_count = len(formatted_offers)
|
||||
@@ -988,8 +996,7 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
enabled_chart_coins = []
|
||||
enabled_chart_coins_setting = swap_client.settings.get("enabled_chart_coins", "")
|
||||
if enabled_chart_coins_setting.lower() == "all":
|
||||
for coin_ticker in ticker_map:
|
||||
enabled_chart_coins.append(coin_ticker.upper())
|
||||
enabled_chart_coins = known_chart_coins
|
||||
elif enabled_chart_coins_setting.strip() == "":
|
||||
for coin_id in swap_client.coin_clients:
|
||||
if not swap_client.isCoinActive(coin_id):
|
||||
@@ -1000,7 +1007,7 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
continue
|
||||
if (
|
||||
enabled_ticker not in enabled_chart_coins
|
||||
and enabled_ticker.lower() in ticker_map
|
||||
and enabled_ticker in known_chart_coins
|
||||
):
|
||||
enabled_chart_coins.append(enabled_ticker)
|
||||
else:
|
||||
@@ -1009,7 +1016,7 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
|
||||
if (
|
||||
upcased_ticker not in enabled_chart_coins
|
||||
and upcased_ticker.lower() in ticker_map
|
||||
and upcased_ticker in known_chart_coins
|
||||
):
|
||||
enabled_chart_coins.append(upcased_ticker)
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@ from basicswap.util import (
|
||||
toBool,
|
||||
InactiveCoin,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
get_api_key_setting,
|
||||
)
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
@@ -171,13 +168,23 @@ def page_settings(self, url_split, post_string):
|
||||
"debug_ui": swap_client.debug_ui,
|
||||
"expire_db_records": swap_client._expire_db_records,
|
||||
}
|
||||
if "chart_api_key_enc" in swap_client.settings:
|
||||
chart_api_key = html.escape(
|
||||
bytes.fromhex(swap_client.settings.get("chart_api_key_enc", "")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
else:
|
||||
chart_api_key = swap_client.settings.get("chart_api_key", "")
|
||||
|
||||
chart_api_key = get_api_key_setting(
|
||||
swap_client.settings, "chart_api_key", escape=True
|
||||
if "coingecko_api_key_enc" in swap_client.settings:
|
||||
coingecko_api_key = html.escape(
|
||||
bytes.fromhex(swap_client.settings.get("coingecko_api_key_enc", "")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
coingecko_api_key = get_api_key_setting(
|
||||
swap_client.settings, "coingecko_api_key", escape=True
|
||||
)
|
||||
else:
|
||||
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
|
||||
|
||||
chart_settings = {
|
||||
"show_chart": swap_client.settings.get("show_chart", True),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -15,7 +15,6 @@ from basicswap.util import (
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
chainparams,
|
||||
getCoinIdFromTicker,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
ActionTypes,
|
||||
@@ -35,6 +34,30 @@ from basicswap.basicswap_util import (
|
||||
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
|
||||
|
||||
PAGE_LIMIT = 1000
|
||||
invalid_coins_from = []
|
||||
known_chart_coins = [
|
||||
"BTC",
|
||||
"PART",
|
||||
"XMR",
|
||||
"LTC",
|
||||
"FIRO",
|
||||
"DASH",
|
||||
"PIVX",
|
||||
"DOGE",
|
||||
"ETH",
|
||||
"DCR",
|
||||
"ZANO",
|
||||
"WOW",
|
||||
"BCH",
|
||||
]
|
||||
|
||||
|
||||
def tickerToCoinId(ticker):
|
||||
search_str = ticker.upper()
|
||||
for c in Coins:
|
||||
if c.name == search_str:
|
||||
return c.value
|
||||
raise ValueError("Unknown coin")
|
||||
|
||||
|
||||
def getCoinType(coin_type_ind):
|
||||
@@ -42,7 +65,7 @@ def getCoinType(coin_type_ind):
|
||||
try:
|
||||
return int(coin_type_ind)
|
||||
except Exception:
|
||||
return getCoinIdFromTicker(coin_type_ind)
|
||||
return tickerToCoinId(coin_type_ind)
|
||||
|
||||
|
||||
def validateAmountString(amount, ci):
|
||||
@@ -644,12 +667,12 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
coins.append((int(k), getCoinName(k)))
|
||||
if split_from:
|
||||
if split_from and k not in invalid_coins_from:
|
||||
coins_from.append(coins[-1])
|
||||
if with_variants and k == Coins.PART:
|
||||
for v in (Coins.PART_ANON, Coins.PART_BLIND):
|
||||
coins.append((int(v), getCoinName(v)))
|
||||
if split_from:
|
||||
if split_from and v not in invalid_coins_from:
|
||||
coins_from.append(coins[-1])
|
||||
if with_variants and k == Coins.LTC:
|
||||
for v in (Coins.LTC_MWEB,):
|
||||
|
||||
@@ -38,7 +38,8 @@ def make_reporthook(read_start: int, logger):
|
||||
logger.info(f"Attempting to resume from byte {read_start}")
|
||||
|
||||
def reporthook(blocknum, blocksize, totalsize):
|
||||
nonlocal read, last_percent_str, time_last, read_last, display_last, abo
|
||||
nonlocal read, last_percent_str, time_last, read_last, display_last, read_start
|
||||
nonlocal average_buffer, abo, logger
|
||||
read += blocksize
|
||||
|
||||
# totalsize excludes read_start
|
||||
|
||||
@@ -29,51 +29,3 @@ def rfc2440_hash_password(password, salt=None):
|
||||
break
|
||||
rv = "16:" + salt.hex() + "60" + h.hexdigest()
|
||||
return rv.upper()
|
||||
|
||||
|
||||
def verify_rfc2440_password(stored_hash, provided_password):
|
||||
"""
|
||||
Verifies a password against a hash generated by rfc2440_hash_password.
|
||||
|
||||
Args:
|
||||
stored_hash (str): The hash string stored (e.g., "16:<salt>60<hash>").
|
||||
provided_password (str): The password attempt to verify.
|
||||
|
||||
Returns:
|
||||
bool: True if the password matches the hash, False otherwise.
|
||||
"""
|
||||
try:
|
||||
parts = stored_hash.upper().split(":")
|
||||
if len(parts) != 2 or parts[0] != "16":
|
||||
return False
|
||||
|
||||
salt_hex_plus_hash_hex = parts[1]
|
||||
separator_index = salt_hex_plus_hash_hex.find("60")
|
||||
if separator_index != 16:
|
||||
return False
|
||||
|
||||
salt_hex = salt_hex_plus_hash_hex[:separator_index]
|
||||
expected_hash_hex = salt_hex_plus_hash_hex[separator_index + 2 :]
|
||||
|
||||
salt = bytes.fromhex(salt_hex)
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
|
||||
EXPBIAS = 6
|
||||
c = 96
|
||||
count = (16 + (c & 15)) << ((c >> 4) + EXPBIAS)
|
||||
|
||||
hashbytes = salt + provided_password.encode("utf-8")
|
||||
len_hashbytes = len(hashbytes)
|
||||
h = hashlib.sha1()
|
||||
|
||||
while count > 0:
|
||||
if count >= len_hashbytes:
|
||||
h.update(hashbytes)
|
||||
count -= len_hashbytes
|
||||
continue
|
||||
h.update(hashbytes[:count])
|
||||
break
|
||||
|
||||
calculated_hash_hex = h.hexdigest().upper()
|
||||
return secrets.compare_digest(calculated_hash_hex, expected_hash_hex)
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
|
||||
from typing import Union, Dict
|
||||
from coincurve.keys import (
|
||||
PublicKey,
|
||||
PrivateKey,
|
||||
)
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
from basicswap.util.crypto import hash160, sha256, ripemd160
|
||||
from basicswap.util.ecc import getSecretInt
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
uint256_from_compact,
|
||||
uint256_from_str,
|
||||
)
|
||||
|
||||
|
||||
AES_BLOCK_SIZE = 16
|
||||
|
||||
|
||||
def aes_pad(s: bytes):
|
||||
c = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE
|
||||
return s + (bytes((c,)) * c)
|
||||
|
||||
|
||||
def aes_unpad(s: bytes):
|
||||
return s[: -(s[len(s) - 1])]
|
||||
|
||||
|
||||
def aes_encrypt(raw: bytes, pass_data: bytes, iv: bytes):
|
||||
assert len(pass_data) == 32
|
||||
assert len(iv) == 16
|
||||
raw = aes_pad(raw)
|
||||
cipher = AES.new(pass_data, AES.MODE_CBC, iv)
|
||||
return cipher.encrypt(raw)
|
||||
|
||||
|
||||
def aes_decrypt(enc, pass_data: bytes, iv: bytes):
|
||||
assert len(pass_data) == 32
|
||||
assert len(iv) == 16
|
||||
cipher = AES.new(pass_data, AES.MODE_CBC, iv)
|
||||
return aes_unpad(cipher.decrypt(enc))
|
||||
|
||||
|
||||
SMSG_MIN_TTL = 60 * 60
|
||||
SMSG_BUCKET_LEN = 60 * 60
|
||||
SMSG_HDR_LEN = (
|
||||
108 # Length of unencrypted header, 4 + 4 + 2 + 1 + 8 + 4 + 16 + 33 + 32 + 4
|
||||
)
|
||||
SMSG_PL_HDR_LEN = 1 + 20 + 65 + 4 # Length of encrypted header in payload
|
||||
|
||||
|
||||
def smsgGetTimestamp(smsg_message: bytes) -> int:
|
||||
assert len(smsg_message) > SMSG_HDR_LEN
|
||||
return int.from_bytes(smsg_message[11 : 11 + 8], byteorder="little")
|
||||
|
||||
|
||||
def smsgGetPOWHash(smsg_message: bytes) -> bytes:
|
||||
assert len(smsg_message) > SMSG_HDR_LEN
|
||||
ofs: int = 4
|
||||
nonce: bytes = smsg_message[ofs : ofs + 4]
|
||||
iv: bytes = nonce * 8
|
||||
|
||||
m = hmac.new(iv, digestmod="SHA256")
|
||||
m.update(smsg_message[4:])
|
||||
return m.digest()
|
||||
|
||||
|
||||
def smsgGetID(smsg_message: bytes) -> bytes:
|
||||
assert len(smsg_message) > SMSG_HDR_LEN
|
||||
smsg_timestamp = int.from_bytes(smsg_message[11 : 11 + 8], byteorder="little")
|
||||
return smsg_timestamp.to_bytes(8, byteorder="big") + ripemd160(smsg_message[8:])
|
||||
|
||||
|
||||
def smsgEncrypt(privkey_from: bytes, pubkey_to: bytes, payload: bytes) -> bytes:
|
||||
# assert len(payload) < 128 # Requires lz4 if payload > 128 bytes
|
||||
# TODO: Add lz4 to match core smsg
|
||||
smsg_timestamp = int(time.time())
|
||||
r = getSecretInt().to_bytes(32, byteorder="big")
|
||||
R = PublicKey.from_secret(r).format()
|
||||
p = PrivateKey(r).ecdh(pubkey_to)
|
||||
H = hashlib.sha512(p).digest()
|
||||
key_e: bytes = H[:32]
|
||||
key_m: bytes = H[32:]
|
||||
|
||||
smsg_iv: bytes = secrets.token_bytes(16)
|
||||
|
||||
payload_hash: bytes = sha256(sha256(payload))
|
||||
signature: bytes = PrivateKey(privkey_from).sign_recoverable(
|
||||
payload_hash, hasher=None
|
||||
)
|
||||
|
||||
# Convert format to BTC, add 4 to mark as compressed key
|
||||
recid = signature[64]
|
||||
signature = bytes((27 + recid + 4,)) + signature[:64]
|
||||
|
||||
pubkey_from: bytes = PublicKey.from_secret(privkey_from).format()
|
||||
pkh_from: bytes = hash160(pubkey_from)
|
||||
|
||||
len_payload = len(payload)
|
||||
address_version = 0
|
||||
plaintext_data: bytes = (
|
||||
bytes((address_version,))
|
||||
+ pkh_from
|
||||
+ signature
|
||||
+ len_payload.to_bytes(4, byteorder="little")
|
||||
+ payload
|
||||
)
|
||||
|
||||
ciphertext: bytes = aes_encrypt(plaintext_data, key_e, smsg_iv)
|
||||
|
||||
m = hmac.new(key_m, digestmod="SHA256")
|
||||
m.update(smsg_timestamp.to_bytes(8, byteorder="little"))
|
||||
m.update(smsg_iv)
|
||||
m.update(ciphertext)
|
||||
mac: bytes = m.digest()
|
||||
|
||||
smsg_hash = bytes((0,)) * 4
|
||||
smsg_nonce = bytes((0,)) * 4
|
||||
smsg_version = bytes((2, 1))
|
||||
smsg_flags = bytes((0,))
|
||||
|
||||
smsg_ttl = SMSG_MIN_TTL
|
||||
|
||||
assert len(R) == 33
|
||||
assert len(mac) == 32
|
||||
|
||||
smsg_message: bytes = (
|
||||
smsg_hash
|
||||
+ smsg_nonce
|
||||
+ smsg_version
|
||||
+ smsg_flags
|
||||
+ smsg_timestamp.to_bytes(8, byteorder="little")
|
||||
+ smsg_ttl.to_bytes(4, byteorder="little")
|
||||
+ smsg_iv
|
||||
+ R
|
||||
+ mac
|
||||
+ len(ciphertext).to_bytes(4, byteorder="little")
|
||||
+ ciphertext
|
||||
)
|
||||
|
||||
target: int = uint256_from_compact(0x1EFFFFFF)
|
||||
|
||||
for i in range(1000000):
|
||||
pow_hash = smsgGetPOWHash(smsg_message)
|
||||
if uint256_from_str(pow_hash) > target:
|
||||
smsg_nonce = (int.from_bytes(smsg_nonce, byteorder="little") + 1).to_bytes(
|
||||
4, byteorder="little"
|
||||
)
|
||||
smsg_message = pow_hash[:4] + smsg_nonce + smsg_message[8:]
|
||||
continue
|
||||
smsg_message = pow_hash[:4] + smsg_message[4:]
|
||||
return smsg_message
|
||||
raise ValueError("Failed to set POW hash.")
|
||||
|
||||
|
||||
def smsgDecrypt(
|
||||
privkey_to: bytes, encrypted_message: bytes, output_dict: bool = False
|
||||
) -> Union[bytes, Dict]:
|
||||
# Without lz4
|
||||
|
||||
assert len(encrypted_message) > SMSG_HDR_LEN
|
||||
smsg_timestamp = int.from_bytes(encrypted_message[11 : 11 + 8], byteorder="little")
|
||||
ofs: int = 23
|
||||
smsg_iv = encrypted_message[ofs : ofs + 16]
|
||||
|
||||
ofs += 16
|
||||
R = encrypted_message[ofs : ofs + 33]
|
||||
ofs += 33
|
||||
mac = encrypted_message[ofs : ofs + 32]
|
||||
ofs += 32
|
||||
ciphertextlen = int.from_bytes(encrypted_message[ofs : ofs + 4], byteorder="little")
|
||||
ofs += 4
|
||||
ciphertext = encrypted_message[ofs:]
|
||||
assert len(ciphertext) == ciphertextlen
|
||||
|
||||
p = PrivateKey(privkey_to).ecdh(R)
|
||||
H = hashlib.sha512(p).digest()
|
||||
key_e: bytes = H[:32]
|
||||
key_m: bytes = H[32:]
|
||||
|
||||
m = hmac.new(key_m, digestmod="SHA256")
|
||||
m.update(smsg_timestamp.to_bytes(8, byteorder="little"))
|
||||
m.update(smsg_iv)
|
||||
m.update(ciphertext)
|
||||
mac_calculated: bytes = m.digest()
|
||||
|
||||
assert mac == mac_calculated
|
||||
|
||||
plaintext = aes_decrypt(ciphertext, key_e, smsg_iv)
|
||||
|
||||
ofs = 1
|
||||
pkh_from = plaintext[ofs : ofs + 20]
|
||||
ofs += 20
|
||||
signature = plaintext[ofs : ofs + 65]
|
||||
ofs += 65
|
||||
ofs += 4
|
||||
payload = plaintext[ofs:]
|
||||
payload_hash: bytes = sha256(sha256(payload))
|
||||
|
||||
# Convert format from BTC
|
||||
recid = (signature[0] - 27) & 3
|
||||
signature = signature[1:] + bytes((recid,))
|
||||
|
||||
pubkey_signer = PublicKey.from_signature_and_message(
|
||||
signature, payload_hash, hasher=None
|
||||
).format()
|
||||
pkh_from_recovered: bytes = hash160(pubkey_signer)
|
||||
assert pkh_from == pkh_from_recovered
|
||||
|
||||
if output_dict:
|
||||
return {
|
||||
"msgid": smsgGetID(encrypted_message).hex(),
|
||||
"sent": smsg_timestamp,
|
||||
"hex": payload.hex(),
|
||||
"pk_from": pubkey_signer.hex(),
|
||||
}
|
||||
return payload
|
||||
@@ -66,10 +66,6 @@ Adjust `--withcoins` and `--withoutcoins` as desired, eg: `--withcoins=monero,bi
|
||||
Append `--usebtcfastsync` to the below command to optionally initialise the Bitcoin datadir with a chain snapshot from btcpayserver FastSync.<br>
|
||||
[FastSync README.md](https://github.com/btcpayserver/btcpayserver-docker/blob/master/contrib/FastSync/README.md)
|
||||
|
||||
##### FastSync
|
||||
|
||||
Append `--client-auth-password=<YOUR_PASSWORD>` to the below command to optionally enable client authentication to protect your web UI from unauthorized access.<br>
|
||||
|
||||
|
||||
Setup with a local Monero daemon (recommended):
|
||||
|
||||
@@ -86,16 +82,6 @@ To instead use Monero public nodes and not run a local Monero daemon<br>(it can
|
||||
**Mnemonics should be stored encrypted and/or air-gapped.**
|
||||
And the output of `echo $CURRENT_XMR_HEIGHT` for use if you need to later restore your wallet.
|
||||
|
||||
|
||||
##### Restore
|
||||
|
||||
To restore an existing install use --particl_mnemonic to input the existing mnemonic:
|
||||
|
||||
docker-compose run --rm swapclient basicswap-prepare --datadir=/coindata --htmlhost="0.0.0.0" --wshost="0.0.0.0" \
|
||||
--withcoins=monero --xmrrestoreheight=$ORIGINAL_XMR_HEIGHT \
|
||||
--particl_mnemonic="existing mnemonic here"
|
||||
|
||||
|
||||
#### Set the timezone (optional)
|
||||
|
||||
Edit the `.env` file in the docker directory, set TZ to your local timezone.
|
||||
@@ -204,7 +190,7 @@ Prepare the datadir:
|
||||
OR using a remote/public XMR daemon (not recommended):
|
||||
XMR_RPC_HOST="node.xmr.to" XMR_RPC_PORT=18081 basicswap-prepare --datadir=$SWAP_DATADIR --withcoins=monero --xmrrestoreheight=$CURRENT_XMR_HEIGHT
|
||||
|
||||
Append `--client-auth-password=<YOUR_PASSWORD>` to the above command to optionally enable client authentication to protect your web UI from unauthorized access.<br>
|
||||
|
||||
Record the mnemonic from the output of the above command.
|
||||
|
||||
Start Basicswap:
|
||||
|
||||
@@ -27,11 +27,11 @@ If the dependencies have changed the container must be built with `--no-cache`:
|
||||
|
||||
After updating the code and rebuilding the container run:
|
||||
|
||||
|
||||
basicswap/docker]$ docker-compose run --rm swapclient \
|
||||
basicswap-prepare --datadir=/coindata --upgradecores
|
||||
basicswap-prepare --datadir=/coindata --preparebinonly --withcoins=monero,bitcoin
|
||||
|
||||
|
||||
Specify all required coins after `--withcoins=`, separated by commas.
|
||||
If updating from versions below 0.21, you may need to add `wallet=wallet.dat` to the core config files.
|
||||
|
||||
|
||||
@@ -46,4 +46,4 @@ If updating from versions below 0.21, you may need to add `wallet=wallet.dat` to
|
||||
|
||||
#### Update core versions
|
||||
|
||||
basicswap-prepare --datadir=$SWAP_DATADIR --upgradecores
|
||||
basicswap-prepare --datadir=$SWAP_DATADIR -preparebinonly --withcoins=monero,bitcoin
|
||||
|
||||
10
guix.scm
10
guix.scm
@@ -21,7 +21,6 @@
|
||||
#:use-module (gnu packages python-check)
|
||||
#:use-module (gnu packages python-crypto)
|
||||
#:use-module (gnu packages python-science)
|
||||
#:use-module (gnu packages python-web)
|
||||
#:use-module (gnu packages python-xyz)
|
||||
#:use-module (gnu packages libffi)
|
||||
#:use-module (gnu packages license))
|
||||
@@ -115,15 +114,15 @@
|
||||
(define-public basicswap
|
||||
(package
|
||||
(name "basicswap")
|
||||
(version "0.14.4")
|
||||
(version "0.14.3")
|
||||
(source (origin
|
||||
(method git-fetch)
|
||||
(uri (git-reference
|
||||
(url "https://github.com/basicswap/basicswap")
|
||||
(commit "3c18a3ed26222bac22a9c15795bd8c6fae0b01ba")))
|
||||
(commit "3b60472c04a58f26e33665f0eb0e88a558050c74")))
|
||||
(sha256
|
||||
(base32
|
||||
"02mwyklcw9320crcm8laiw4ba24xrazbg48whvdxnbmarcbipkd3"))
|
||||
"0xrli8mzigm0ryn28y28xvy4gc0358ck2036ncx5f1sj5s8dwfkh"))
|
||||
(file-name (git-file-name name version))))
|
||||
(build-system pyproject-build-system)
|
||||
|
||||
@@ -146,8 +145,7 @@
|
||||
python-pyzmq
|
||||
python-gnupg
|
||||
python-jinja2
|
||||
python-pysocks
|
||||
python-websocket-client))
|
||||
python-pysocks))
|
||||
(native-inputs
|
||||
(list
|
||||
python-hatchling
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user