mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53b06859fc | ||
|
|
7755b4c505 | ||
|
|
95da26211b | ||
|
|
15b2030b92 | ||
|
|
336e92fff6 | ||
|
|
fd4fa37b9d | ||
|
|
005abee85d | ||
|
|
c6d5f47cea | ||
|
|
d16dc9e124 | ||
|
|
a9953c5ffe | ||
|
|
19fd15b9dc | ||
|
|
3794b58021 | ||
|
|
2d1ff4f8bf | ||
|
|
6a8c90a04b | ||
|
|
e9704510f9 | ||
|
|
14a1b0dd7d | ||
|
|
de501f4bb5 | ||
|
|
4c1c5cd1a6 | ||
|
|
1a9c153306 | ||
|
|
0a3afd4a5a | ||
|
|
3dbc5f329c | ||
|
|
eb46a4fcc5 | ||
|
|
73d486d6f0 | ||
|
|
cc6fbb9685 | ||
|
|
4ad8a3f07c | ||
|
|
2f7e425da9 | ||
|
|
a6c2251146 | ||
|
|
071675d359 | ||
|
|
9cc731d313 | ||
|
|
4e152d5a2b | ||
|
|
26392eafb4 | ||
|
|
c27ea87e9f | ||
|
|
b35f74c659 | ||
|
|
93e5ce0ab9 | ||
|
|
292a3713c0 | ||
|
|
add3a1d83e | ||
|
|
a4cc20022e | ||
|
|
390fb71aa7 | ||
|
|
91dbe6bf0e | ||
|
|
fda2d1f578 | ||
|
|
7e53af3616 | ||
|
|
6172785e2e | ||
|
|
ad472cf16f | ||
|
|
9d6e566c3b | ||
|
|
911ca189bc | ||
|
|
f309256a7f | ||
|
|
4ebb6d6441 | ||
|
|
42c40244a1 | ||
|
|
918bf60200 | ||
|
|
19b8e89836 | ||
|
|
d117938bb0 | ||
|
|
ab827833a6 | ||
|
|
a5a727a9ac | ||
|
|
c160ba5114 | ||
|
|
30226c37af | ||
|
|
43f9ae8acf | ||
|
|
4c9aa7b777 | ||
|
|
84b6850a0b | ||
|
|
ba8168938f | ||
|
|
ed69a36d5d | ||
|
|
672747cc7d | ||
|
|
a2239c0a5b | ||
|
|
667851c24a | ||
|
|
bae6aac12a | ||
|
|
6fce77f34a | ||
|
|
e3f51a7ac3 | ||
|
|
7ee1931176 | ||
|
|
a171bbb48a | ||
|
|
72481337e1 | ||
|
|
cd147da7dd | ||
|
|
aa26111665 | ||
|
|
235a8f6830 | ||
|
|
9cc4734bda | ||
|
|
11bbc9b128 | ||
|
|
4fa61e8e49 | ||
|
|
dd2e8d1b59 | ||
|
|
4b010cfee0 | ||
|
|
0174715dd2 | ||
|
|
1ea8b80bdc | ||
|
|
6b218773dc | ||
|
|
fafbd0defe | ||
|
|
e68fc6509b | ||
|
|
55bad836a9 | ||
|
|
4ba2b877dd | ||
|
|
f932a41b1a | ||
|
|
fea7130835 | ||
|
|
6d4200f871 | ||
|
|
53fc673e71 | ||
|
|
6e614ff76d | ||
|
|
355da5ee90 | ||
|
|
d0ebed93d8 | ||
|
|
10d6b13930 | ||
|
|
e73e084a6d | ||
|
|
1e0a7c7395 | ||
|
|
b6e9118797 | ||
|
|
02ceb89d14 | ||
|
|
d92fa0c61d | ||
|
|
dc692209ca | ||
|
|
56ec500797 | ||
|
|
faf76e3269 | ||
|
|
e19a99b113 | ||
|
|
27220d5d36 | ||
|
|
ba1678ad26 | ||
|
|
11f1454627 | ||
|
|
90a162f0ea | ||
|
|
96faa26c5b | ||
|
|
a5cc83157d | ||
|
|
bf5396dd17 | ||
|
|
d6ef4f2edb | ||
|
|
221a06ba44 | ||
|
|
5cecef676d | ||
|
|
d45e0bcd85 | ||
|
|
3e3b8c1cfe | ||
|
|
f2c73f6238 | ||
|
|
94b972502e | ||
|
|
543a820a12 | ||
|
|
266bbd1807 | ||
|
|
8c06508e7c | ||
|
|
6489b80666 | ||
|
|
bc71ec8246 | ||
|
|
2b945f3e3a | ||
|
|
6e5b8fb0ad | ||
|
|
f031d41a38 | ||
|
|
1797ab055b | ||
|
|
bd4ecc5306 | ||
|
|
b3dfae4289 | ||
|
|
7bfd79812f | ||
|
|
94d02ff1cc | ||
|
|
0e19f4139c | ||
|
|
dd53c8e76d | ||
|
|
6ad9cb24fe | ||
|
|
1c11767d1e | ||
|
|
b19edd6771 | ||
|
|
740924632e | ||
|
|
0e6f37a479 | ||
|
|
d1fb11e92a | ||
|
|
ff149e988c | ||
|
|
45b4ac8ca0 | ||
|
|
125fbb43db | ||
|
|
b3c946d056 | ||
|
|
4055b7d6c8 | ||
|
|
aa9b1c0eb9 | ||
|
|
0c40f14855 | ||
|
|
1a42e5e123 | ||
|
|
bc20fecc82 | ||
|
|
7f6077815a | ||
|
|
69acf00e0d | ||
|
|
f918652b6c | ||
|
|
fea19c00f2 | ||
|
|
f269881990 | ||
|
|
c6f8e5e2ba | ||
|
|
4f47267598 | ||
|
|
3faf947588 | ||
|
|
f3adc17bb8 | ||
|
|
b57ff3497a | ||
|
|
df4a6af6a0 | ||
|
|
7ba2daf671 | ||
|
|
d08e09061f | ||
|
|
f7a4798014 | ||
|
|
13847e129b | ||
|
|
f6914d7c30 | ||
|
|
2a8ac051fc | ||
|
|
3ea7a219d1 | ||
|
|
80915d9865 | ||
|
|
38302d2d79 | ||
|
|
e7b47486f5 | ||
|
|
b3c0ad7e9c | ||
|
|
ece9d7fb4b | ||
|
|
868b2475c1 | ||
|
|
27c3b93ff9 | ||
|
|
7df2f1b290 | ||
|
|
d57a148ff4 | ||
|
|
aa898a9601 | ||
|
|
ec5ea4ca3c | ||
|
|
ed18b36da6 | ||
|
|
058270ec7a | ||
|
|
2818afc933 | ||
|
|
48bfdb7462 | ||
|
|
e14b9b7e6e | ||
|
|
a87180f2ef | ||
|
|
66d763e8ea | ||
|
|
061a09f3fb | ||
|
|
e7af4f9005 | ||
|
|
a22274b06d | ||
|
|
3b2b666c75 | ||
|
|
ec092eaa6e | ||
|
|
b605bd4bc3 | ||
|
|
934aab9d8a | ||
|
|
21c0a534f2 | ||
|
|
b293b5daee |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -38,6 +38,7 @@ jobs:
|
||||
sudo apt-get install -y firefox
|
||||
fi
|
||||
python -m pip install --upgrade pip
|
||||
pip install python-gnupg
|
||||
pip install -e .[dev]
|
||||
pip install -r requirements.txt --require-hashes
|
||||
- name: Install
|
||||
@@ -99,13 +100,21 @@ jobs:
|
||||
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"
|
||||
i=0
|
||||
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
|
||||
do
|
||||
tail -n 1 /tmp/log.txt
|
||||
sleep 2
|
||||
((++i))
|
||||
if [ $i -ge 60 ]; then
|
||||
echo "Timed out waiting for test_xmr_persistent, PID $TEST_NETWORK_PID"
|
||||
kill $TEST_NETWORK_PID
|
||||
(exit 1) # Fail test
|
||||
break
|
||||
fi
|
||||
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
|
||||
kill $TEST_NETWORK_PID
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
old/
|
||||
build/
|
||||
venv/
|
||||
*.pyc
|
||||
__pycache__
|
||||
/dist/
|
||||
@@ -10,6 +11,7 @@ __pycache__
|
||||
.eggs
|
||||
.ruff_cache
|
||||
.pytest_cache
|
||||
.vectorcode
|
||||
*~
|
||||
|
||||
# geckodriver.log
|
||||
|
||||
40
.pre-commit-config.yaml
Normal file
40
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
repos:
|
||||
# Common hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
args: ["--assume-in-merge"]
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
args: ["--markdown-linebreak-ext=md"]
|
||||
|
||||
# Black - Python formatter
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: (basicswap/contrib|basicswap/interface/contrib)/
|
||||
|
||||
# Flake8 - Lint Python
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--ignore=E203,E501,W503", "--exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py"]
|
||||
|
||||
# ESLint - Lint Javascript and fix issues where possible
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v9.30.1
|
||||
hooks:
|
||||
- id: eslint
|
||||
#args: ["--fix"]
|
||||
|
||||
# djLint - Lint HTML
|
||||
#- repo: https://github.com/djlint/djlint
|
||||
# rev: v1.36.4
|
||||
# hooks:
|
||||
# - id: djlint
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.14.4"
|
||||
__version__ = "0.15.0"
|
||||
|
||||
@@ -31,6 +31,7 @@ from .util import (
|
||||
)
|
||||
from .util.logging import (
|
||||
BSXLogger,
|
||||
LogCategories as LC,
|
||||
)
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
@@ -43,7 +44,7 @@ def getaddrinfo_tor(*args):
|
||||
|
||||
|
||||
class BaseApp(DBMethods):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap", **kwargs):
|
||||
self.fp = None
|
||||
self.log_name = log_name
|
||||
self.fail_code = 0
|
||||
@@ -61,7 +62,7 @@ class BaseApp(DBMethods):
|
||||
|
||||
self._network = None
|
||||
self.prepareLogging()
|
||||
self.log.info("Network: {}".format(self.chain))
|
||||
self.log.info(f"Network: {self.chain}")
|
||||
|
||||
self.use_tor_proxy = self.settings.get("use_tor", False)
|
||||
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
|
||||
@@ -71,6 +72,32 @@ class BaseApp(DBMethods):
|
||||
self.default_socket = socket.socket
|
||||
self.default_socket_timeout = socket.getdefaulttimeout()
|
||||
self.default_socket_getaddrinfo = socket.getaddrinfo
|
||||
self._force_db_upgrade = False
|
||||
|
||||
self._enabled_log_categories = set()
|
||||
for category in self.settings.get("enabled_log_categories", []):
|
||||
category = category.lower()
|
||||
if category == "net":
|
||||
self._enabled_log_categories.add(LC.NET)
|
||||
else:
|
||||
self.log.warning(
|
||||
f'Unknown entry "{category}" in "enabled_log_categories"'
|
||||
)
|
||||
|
||||
if len(self._enabled_log_categories) > 0:
|
||||
self.log.info(
|
||||
"Enabled logging categories: {}".format(
|
||||
",".join(sorted([c.name for c in self._enabled_log_categories]))
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
data_dir=data_dir,
|
||||
settings=settings,
|
||||
chain=chain,
|
||||
log_name=log_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
if self.fp:
|
||||
@@ -78,7 +105,14 @@ class BaseApp(DBMethods):
|
||||
|
||||
def stopRunning(self, with_code=0):
|
||||
self.fail_code = with_code
|
||||
with self.mxDB:
|
||||
|
||||
# Wait for lock to shutdown gracefully.
|
||||
if self.mxDB.acquire(timeout=5):
|
||||
self.chainstate_delay_event.set()
|
||||
self.delay_event.set()
|
||||
self.mxDB.release()
|
||||
else:
|
||||
# Waiting for lock timed out, stop anyway
|
||||
self.chainstate_delay_event.set()
|
||||
self.delay_event.set()
|
||||
|
||||
@@ -143,7 +177,7 @@ class BaseApp(DBMethods):
|
||||
for c, params in chainparams.items():
|
||||
if coin_name.lower() == params["name"].lower():
|
||||
return c
|
||||
raise ValueError("Unknown coin: {}".format(coin_name))
|
||||
raise ValueError(f"Unknown coin: {coin_name}")
|
||||
|
||||
def callrpc(self, method, params=[], wallet=None):
|
||||
cc = self.coin_clients[Coins.PART]
|
||||
@@ -228,11 +262,16 @@ class BaseApp(DBMethods):
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
return opener.open(request, timeout=timeout).read()
|
||||
|
||||
def logException(self, message) -> None:
|
||||
def logException(self, message: str) -> None:
|
||||
self.log.error(message)
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
def logD(self, log_category: int, message: str) -> None:
|
||||
if log_category not in self._enabled_log_categories:
|
||||
return
|
||||
self.log.debug("(" + LC(log_category).name + ") " + message)
|
||||
|
||||
def torControl(self, query):
|
||||
try:
|
||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,16 @@ class KeyTypes(IntEnum):
|
||||
KAF = 6
|
||||
|
||||
|
||||
class MessageNetworks(IntEnum):
|
||||
SMSG = auto()
|
||||
SIMPLEX = auto()
|
||||
|
||||
|
||||
class MessageNetworkLinkTypes(IntEnum):
|
||||
RECEIVED_ON = auto()
|
||||
SENT_ON = auto()
|
||||
|
||||
|
||||
class MessageTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
@@ -53,12 +63,18 @@ class MessageTypes(IntEnum):
|
||||
ADS_BID_LF = auto()
|
||||
ADS_BID_ACCEPT_FL = auto()
|
||||
|
||||
CONNECT_REQ = auto()
|
||||
PORTAL_OFFER = auto()
|
||||
PORTAL_SEND = auto()
|
||||
|
||||
|
||||
class AddressTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
RECV_OFFER = auto()
|
||||
SEND_OFFER = auto()
|
||||
PORTAL_LOCAL = auto()
|
||||
PORTAL = auto()
|
||||
|
||||
|
||||
class SwapTypes(IntEnum):
|
||||
@@ -111,6 +127,7 @@ class BidStates(IntEnum):
|
||||
BID_EXPIRED = 31
|
||||
BID_AACCEPT_DELAY = 32
|
||||
BID_AACCEPT_FAIL = 33
|
||||
CONNECT_REQ_SENT = 34
|
||||
|
||||
|
||||
class TxStates(IntEnum):
|
||||
@@ -193,6 +210,8 @@ class EventLogTypes(IntEnum):
|
||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||
BCH_MERCY_TX_PUBLISHED = auto()
|
||||
BCH_MERCY_TX_FOUND = auto()
|
||||
LOCK_TX_A_IN_MEMPOOL = auto()
|
||||
LOCK_TX_A_CONFLICTS = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -226,6 +245,12 @@ class NotificationTypes(IntEnum):
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
SWAP_COMPLETED = auto()
|
||||
UPDATE_AVAILABLE = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
BID = 1
|
||||
|
||||
|
||||
class AutomationOverrideOptions(IntEnum):
|
||||
@@ -339,6 +364,8 @@ def strBidState(state):
|
||||
return "Auto accept delay"
|
||||
if state == BidStates.BID_AACCEPT_FAIL:
|
||||
return "Auto accept failed"
|
||||
if state == BidStates.CONNECT_REQ_SENT:
|
||||
return "Connect request sent"
|
||||
return "Unknown" + " " + str(state)
|
||||
|
||||
|
||||
@@ -381,15 +408,14 @@ def strTxType(tx_type):
|
||||
|
||||
|
||||
def strAddressType(addr_type):
|
||||
if addr_type == AddressTypes.OFFER:
|
||||
return "Offer"
|
||||
if addr_type == AddressTypes.BID:
|
||||
return "Bid"
|
||||
if addr_type == AddressTypes.RECV_OFFER:
|
||||
return "Offer recv"
|
||||
if addr_type == AddressTypes.SEND_OFFER:
|
||||
return "Offer send"
|
||||
return "Unknown"
|
||||
return {
|
||||
AddressTypes.OFFER: "Offer",
|
||||
AddressTypes.BID: "Bid",
|
||||
AddressTypes.RECV_OFFER: "Offer recv",
|
||||
AddressTypes.SEND_OFFER: "Offer send",
|
||||
AddressTypes.PORTAL_LOCAL: "Portal (local)",
|
||||
AddressTypes.PORTAL: "Portal",
|
||||
}.get(addr_type, "Unknown")
|
||||
|
||||
|
||||
def getLockName(lock_type):
|
||||
@@ -412,6 +438,10 @@ def describeEventEntry(event_type, event_msg):
|
||||
return "Lock tx B published"
|
||||
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
||||
return "Failed to publish lock tx B spend: " + event_msg
|
||||
if event_type == EventLogTypes.LOCK_TX_A_IN_MEMPOOL:
|
||||
return "Lock tx A seen in mempool"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFLICTS:
|
||||
return "Lock tx A conflicting txn/s"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
||||
return "Lock tx A seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
||||
@@ -581,6 +611,26 @@ def canAcceptBidState(state):
|
||||
)
|
||||
|
||||
|
||||
def canExpireBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_SENT,
|
||||
BidStates.BID_RECEIVING,
|
||||
BidStates.BID_RECEIVED,
|
||||
BidStates.BID_AACCEPT_DELAY,
|
||||
BidStates.BID_AACCEPT_FAIL,
|
||||
BidStates.BID_REQUEST_SENT,
|
||||
)
|
||||
|
||||
|
||||
def canTimeoutBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_ACCEPTED,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||
)
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
||||
return True
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import gnupg
|
||||
import hashlib
|
||||
@@ -26,6 +27,7 @@ import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
import zmq
|
||||
|
||||
from urllib.request import urlopen
|
||||
|
||||
@@ -47,8 +49,9 @@ from basicswap.bin.run import (
|
||||
getWalletBinName,
|
||||
)
|
||||
|
||||
|
||||
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
|
||||
# Coin clients
|
||||
PARTICL_REPO = os.getenv("PARTICL_REPO", "tecnovert")
|
||||
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.2.0")
|
||||
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
|
||||
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
|
||||
|
||||
@@ -64,10 +67,10 @@ DCR_VERSION_TAG = os.getenv("DCR_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.4.3")
|
||||
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
|
||||
XMR_SITE_COMMIT = (
|
||||
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version
|
||||
"df28b670cb3a174d7763dd6d22fb4ef20597d0ac" # Lock hashes.txt to monero version
|
||||
)
|
||||
|
||||
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
|
||||
@@ -79,10 +82,10 @@ WOW_SITE_COMMIT = (
|
||||
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
|
||||
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
|
||||
|
||||
DASH_VERSION = os.getenv("DASH_VERSION", "22.0.0")
|
||||
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3")
|
||||
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
|
||||
|
||||
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.0")
|
||||
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.3")
|
||||
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
|
||||
|
||||
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
|
||||
@@ -94,12 +97,6 @@ BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
|
||||
DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1")
|
||||
DOGECOIN_VERSION_TAG = os.getenv("DOGECOIN_VERSION_TAG", "")
|
||||
|
||||
GUIX_SSL_CERT_DIR = None
|
||||
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", "false"))
|
||||
|
||||
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
|
||||
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false"))
|
||||
|
||||
|
||||
known_coins = {
|
||||
"particl": (PARTICL_VERSION, PARTICL_VERSION_TAG, ("tecnovert",)),
|
||||
@@ -121,6 +118,21 @@ disabled_coins = [
|
||||
"navcoin",
|
||||
]
|
||||
|
||||
# Network clients
|
||||
SIMPLEX_CHAT_VERSION = os.getenv("SIMPLEX_CHAT_VERSION", "6.3.5")
|
||||
SIMPLEX_WS_PORT = int(os.getenv("SIMPLEX_WS_PORT", 5225))
|
||||
SIMPLEX_SERVER_ADDRESS = os.getenv(
|
||||
"SIMPLEX_CHAT_VERSION",
|
||||
"smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im",
|
||||
)
|
||||
SIMPLEX_SERVER_SOCKS_PROXY = os.getenv("SIMPLEX_SERVER_SOCKS_PROXY", "127.0.0.1:9150")
|
||||
SIMPLEX_GROUP_LINK = os.getenv("SIMPLEX_GROUP_LINK", None)
|
||||
|
||||
|
||||
known_networks = ["smsg", "simplex"]
|
||||
disabled_networks = []
|
||||
|
||||
|
||||
expected_key_ids = {
|
||||
"tecnovert": ("8E517DC12EC1CC37F6423A8A13F13651C9CF0D6B",),
|
||||
"thrasher": ("59CAF0E96F23F53747945FD4FE3348877809386C",),
|
||||
@@ -145,8 +157,15 @@ expected_key_ids = {
|
||||
),
|
||||
"decred_release": ("F516ADB7A069852C7C28A02D6D897EDF518A031D",),
|
||||
"Calin_Culianu": ("D465135F97D0047E18E99DC321810A542031C02C",),
|
||||
"SimpleX_Chat": ("FB44AF81A45BDE327319797C85107E357D4A17FC",),
|
||||
}
|
||||
|
||||
GUIX_SSL_CERT_DIR = None
|
||||
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", False))
|
||||
|
||||
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
|
||||
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", False))
|
||||
|
||||
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
|
||||
if USE_PLATFORM == "Darwin":
|
||||
BIN_ARCH = "osx64"
|
||||
@@ -173,11 +192,11 @@ if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
logging.getLogger("gnupg").setLevel(logging.INFO)
|
||||
|
||||
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", "false"))
|
||||
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", "false"))
|
||||
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", "false"))
|
||||
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
|
||||
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", False))
|
||||
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", False))
|
||||
BSX_UPDATE_UNMANAGED = toBool(
|
||||
os.getenv("BSX_UPDATE_UNMANAGED", "true")
|
||||
os.getenv("BSX_UPDATE_UNMANAGED", True)
|
||||
) # Disable updating unmanaged coin cores.
|
||||
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
|
||||
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
|
||||
@@ -302,10 +321,8 @@ def setTorrcVars():
|
||||
)
|
||||
|
||||
|
||||
TEST_TOR_PROXY = toBool(
|
||||
os.getenv("TEST_TOR_PROXY", "true")
|
||||
) # Expects a known exit node
|
||||
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", "false"))
|
||||
TEST_TOR_PROXY = toBool(os.getenv("TEST_TOR_PROXY", True)) # Expects a known exit node
|
||||
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", False))
|
||||
|
||||
BITCOIN_FASTSYNC_URL = os.getenv(
|
||||
"BITCOIN_FASTSYNC_URL",
|
||||
@@ -322,6 +339,8 @@ BITCOIN_FASTSYNC_SIG_URL = os.getenv(
|
||||
# Encrypt new wallets with this password, must match the Particl wallet password when adding coins
|
||||
WALLET_ENCRYPTION_PWD = os.getenv("WALLET_ENCRYPTION_PWD", "")
|
||||
|
||||
CHECK_FOR_BSX_UPDATES = toBool(os.getenv("CHECK_FOR_BSX_UPDATES", True))
|
||||
|
||||
use_tor_proxy: bool = False
|
||||
with_coins_changed: bool = False
|
||||
|
||||
@@ -340,21 +359,6 @@ monero_wallet_rpc_proxy_config = [
|
||||
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
|
||||
]
|
||||
|
||||
wownerod_proxy_config = [
|
||||
f"proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}",
|
||||
"proxy-allow-dns-leaks=0",
|
||||
"no-igd=1", # Disable UPnP port mapping
|
||||
"hide-my-port=1", # Don't share the p2p port
|
||||
"p2p-bind-ip=127.0.0.1", # Don't broadcast ip
|
||||
"in-peers=0", # Changes "error" in log to "incoming connections disabled"
|
||||
"out-peers=24",
|
||||
f"tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16", # Outgoing tx relay to onion
|
||||
]
|
||||
|
||||
wownero_wallet_rpc_proxy_config = [
|
||||
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
|
||||
]
|
||||
|
||||
default_socket = socket.socket
|
||||
default_socket_timeout = socket.getdefaulttimeout()
|
||||
default_socket_getaddrinfo = socket.getaddrinfo
|
||||
@@ -400,6 +404,12 @@ def getDescriptorWalletOption(coin_params):
|
||||
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
|
||||
|
||||
|
||||
def getLegacyKeyPathOption(coin_params):
|
||||
ticker: str = coin_params["ticker"]
|
||||
default_option: bool = False
|
||||
return toBool(os.getenv(ticker + "_USE_LEGACY_KEY_PATHS", default_option))
|
||||
|
||||
|
||||
def getKnownVersion(coin_name: str) -> str:
|
||||
version, version_tag, _ = known_coins[coin_name]
|
||||
return version + version_tag
|
||||
@@ -597,7 +607,7 @@ def downloadPIVXParams(output_dir):
|
||||
downloadFile(url, path)
|
||||
|
||||
file_hash = getFileHash(path)
|
||||
logger.info("%s hash: %s", k, file_hash)
|
||||
logger.info(f"{k} hash: {file_hash}")
|
||||
assert file_hash == v
|
||||
finally:
|
||||
popConnectionParameters()
|
||||
@@ -624,9 +634,21 @@ def ensureValidSignatureBy(result, signing_key_name):
|
||||
logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).")
|
||||
|
||||
|
||||
def ensureFileHashInFile(release_hash, assert_path):
|
||||
with (
|
||||
open(assert_path, "rb", 0) as fp,
|
||||
mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s,
|
||||
):
|
||||
if s.find(bytes(release_hash, "utf-8")) == -1:
|
||||
raise ValueError(
|
||||
f"Error: Release hash {release_hash} not found in assert file."
|
||||
)
|
||||
logger.info("Found release hash in assert file.")
|
||||
|
||||
|
||||
def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts={}):
|
||||
version, version_tag, signers = version_data
|
||||
logger.info("extractCore %s v%s%s", coin, version, version_tag)
|
||||
logger.info(f"Extracting core {coin} v{version}{version_tag}")
|
||||
extract_core_overwrite = extra_opts.get("extract_core_overwrite", True)
|
||||
|
||||
if coin in ("monero", "firo", "wownero"):
|
||||
@@ -655,9 +677,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
"Unable to set file permissions: %s, for %s",
|
||||
str(e),
|
||||
out_path,
|
||||
f"Unable to set file permissions: {e}, for {out_path}"
|
||||
)
|
||||
break
|
||||
return
|
||||
@@ -686,9 +706,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
|
||||
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
"Unable to set file permissions: %s, for %s",
|
||||
str(e),
|
||||
out_path,
|
||||
f"Unable to set file permissions: {e}, for {out_path}"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -731,9 +749,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
|
||||
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
"Unable to set file permissions: %s, for %s",
|
||||
str(e),
|
||||
out_path,
|
||||
f"Unable to set file permissions: {e}, for {out_path}"
|
||||
)
|
||||
else:
|
||||
with tarfile.open(release_path) as ft:
|
||||
@@ -749,15 +765,13 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
|
||||
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
||||
except Exception as e:
|
||||
logging.warning(
|
||||
"Unable to set file permissions: %s, for %s",
|
||||
str(e),
|
||||
out_path,
|
||||
f"Unable to set file permissions: {e}, for {out_path}"
|
||||
)
|
||||
|
||||
|
||||
def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
version, version_tag, signers = version_data
|
||||
logger.info("prepareCore %s v%s%s", coin, version, version_tag)
|
||||
logger.info(f"Prepare core {coin} v{version}{version_tag}")
|
||||
|
||||
bin_dir = os.path.expanduser(settings["chainclients"][coin]["bindir"])
|
||||
if not os.path.exists(bin_dir):
|
||||
@@ -795,11 +809,9 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
release_path = os.path.join(bin_dir, release_filename)
|
||||
downloadRelease(release_url, release_path, extra_opts)
|
||||
|
||||
assert_filename = "monero-{}-hashes.txt".format(version)
|
||||
# assert_url = 'https://www.getmonero.org/downloads/hashes.txt'
|
||||
assert_url = "https://raw.githubusercontent.com/monero-project/monero-site/{}/downloads/hashes.txt".format(
|
||||
XMR_SITE_COMMIT
|
||||
)
|
||||
assert_filename = f"monero-{version}-hashes.txt"
|
||||
# Get the hashes file as of XMR_SITE_COMMIT
|
||||
assert_url = f"https://raw.githubusercontent.com/monero-project/monero-site/{XMR_SITE_COMMIT}/downloads/hashes.txt"
|
||||
assert_path = os.path.join(bin_dir, assert_filename)
|
||||
if not os.path.exists(assert_path):
|
||||
downloadFile(assert_url, assert_path)
|
||||
@@ -876,7 +888,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
downloadFile(assert_sig_url, assert_sig_path)
|
||||
else:
|
||||
major_version = int(version.split(".")[0])
|
||||
|
||||
use_guix: bool = coin in ("dash",) or major_version >= 22
|
||||
arch_name = BIN_ARCH
|
||||
if os_name == "osx" and use_guix:
|
||||
@@ -897,21 +908,21 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
coin, version + version_tag, arch_name, filename_extra, FILE_EXT
|
||||
)
|
||||
if coin == "particl":
|
||||
release_url = "https://github.com/particl/particl-core/releases/download/v{}/{}".format(
|
||||
version + version_tag, release_filename
|
||||
release_url = (
|
||||
"https://github.com/{}/particl-core/releases/download/v{}/{}".format(
|
||||
PARTICL_REPO, version + version_tag, release_filename
|
||||
)
|
||||
)
|
||||
assert_filename = "{}-{}-{}-build.assert".format(coin, os_name, version)
|
||||
if use_guix:
|
||||
assert_url = f"https://raw.githubusercontent.com/particl/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
|
||||
assert_url = f"https://raw.githubusercontent.com/{PARTICL_REPO}/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
|
||||
else:
|
||||
assert_url = (
|
||||
"https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s"
|
||||
% (
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name,
|
||||
assert_filename,
|
||||
)
|
||||
assert_url = "https://raw.githubusercontent.com/{}/gitian.sigs/master/{}-{}/{}/{}".format(
|
||||
PARTICL_REPO,
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name,
|
||||
assert_filename,
|
||||
)
|
||||
elif coin == "litecoin":
|
||||
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
|
||||
@@ -920,9 +931,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
assert_filename = "{}-core-{}-{}-build.assert".format(
|
||||
coin, os_name, ".".join(version.split(".")[:2])
|
||||
)
|
||||
assert_url = (
|
||||
"https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s"
|
||||
% (version, os_dir_name, signing_key_name, assert_filename)
|
||||
assert_url = "https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/{}-{}/{}/{}".format(
|
||||
version, os_dir_name, signing_key_name, assert_filename
|
||||
)
|
||||
elif coin == "dogecoin":
|
||||
release_url = (
|
||||
@@ -955,10 +965,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
version, release_filename
|
||||
)
|
||||
assert_filename = "SHA256SUMS.{}.asc.Calin_Culianu".format(version)
|
||||
assert_url = (
|
||||
"https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/%s/%s"
|
||||
% (version, assert_filename)
|
||||
)
|
||||
assert_url = f"https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/{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"
|
||||
@@ -974,14 +981,11 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
assert_filename = "{}-{}-{}-build.assert".format(
|
||||
coin, os_name, version.rsplit(".", 1)[0]
|
||||
)
|
||||
assert_url = (
|
||||
"https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/%s-%s/%s/%s"
|
||||
% (
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name.capitalize(),
|
||||
assert_filename,
|
||||
)
|
||||
assert_url = "https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/{}-{}/{}/{}".format(
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name.capitalize(),
|
||||
assert_filename,
|
||||
)
|
||||
elif coin == "dash":
|
||||
release_filename = "{}-{}-{}.{}".format(
|
||||
@@ -1011,9 +1015,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
version + version_tag, release_filename
|
||||
)
|
||||
)
|
||||
assert_url = (
|
||||
"https://github.com/firoorg/firo/releases/download/v%s/SHA256SUMS"
|
||||
% (version + version_tag)
|
||||
assert_url = "https://github.com/firoorg/firo/releases/download/v{}/SHA256SUMS".format(
|
||||
version + version_tag
|
||||
)
|
||||
elif coin == "navcoin":
|
||||
release_filename = "{}-{}-{}.{}".format(coin, version, BIN_ARCH, FILE_EXT)
|
||||
@@ -1054,15 +1057,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
|
||||
release_hash: str = getFileHash(release_path)
|
||||
logger.info(f"{release_filename} hash: {release_hash}")
|
||||
with (
|
||||
open(assert_path, "rb", 0) as fp,
|
||||
mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s,
|
||||
):
|
||||
if s.find(bytes(release_hash, "utf-8")) == -1:
|
||||
raise ValueError(
|
||||
f"Error: Release hash {release_hash} not found in assert file."
|
||||
)
|
||||
logger.info("Found release hash in assert file.")
|
||||
ensureFileHashInFile(release_hash, assert_path)
|
||||
|
||||
if SKIP_GPG_VALIDATION:
|
||||
logger.warning(
|
||||
@@ -1221,16 +1216,12 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
if coin == "monero":
|
||||
if XMR_RPC_USER != "":
|
||||
fp.write(f"rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
if coin == "wownero":
|
||||
if WOW_RPC_USER != "":
|
||||
fp.write(f"rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in wownerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
wallets_dir = core_settings.get("walletsdir", data_dir)
|
||||
if not os.path.exists(wallets_dir):
|
||||
@@ -1313,7 +1304,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
)
|
||||
wallet_conf_path = os.path.join(data_dir, wallet_conf_filename)
|
||||
if os.path.exists(wallet_conf_path):
|
||||
exitWithError("{} exists".format(wallet_conf_path))
|
||||
exitWithError(f"{wallet_conf_path} exists")
|
||||
with open(wallet_conf_path, "w") as fp:
|
||||
if chain != "mainnet":
|
||||
fp.write(chainname + "=1\n")
|
||||
@@ -1340,7 +1331,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
core_conf_name: str = core_settings.get("config_filename", coin + ".conf")
|
||||
core_conf_path = os.path.join(data_dir, core_conf_name)
|
||||
if os.path.exists(core_conf_path):
|
||||
exitWithError("{} exists".format(core_conf_path))
|
||||
exitWithError(f"{core_conf_path} exists")
|
||||
with open(core_conf_path, "w") as fp:
|
||||
if chain != "mainnet":
|
||||
if coin in ("navcoin",):
|
||||
@@ -1354,7 +1345,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
elif chain == "regtest":
|
||||
fp.write("[regtest]\n\n")
|
||||
else:
|
||||
logger.warning("Unknown chain %s", chain)
|
||||
logger.warning(f"Unknown chain {chain}")
|
||||
|
||||
if COINS_RPCBIND_IP != "127.0.0.1":
|
||||
fp.write("rpcallowip=127.0.0.1\n")
|
||||
@@ -1376,10 +1367,25 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
|
||||
salt = generate_salt(16)
|
||||
if coin == "particl":
|
||||
fp.write("deprecatedrpc=create_bdb\n")
|
||||
fp.write("debugexclude=libevent\n")
|
||||
if chain == "mainnet":
|
||||
fp.write("rpcdoccheck=0\n")
|
||||
fp.write(
|
||||
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
|
||||
)
|
||||
fp.write(
|
||||
"zmqpubhashwtx=tcp://{}:{}\n".format(
|
||||
COINS_RPCBIND_IP, settings["zmqport"]
|
||||
)
|
||||
)
|
||||
zmqsecret = extra_opts.get("zmqsecret", None)
|
||||
if zmqsecret:
|
||||
try:
|
||||
_ = base64.b64decode(zmqsecret)
|
||||
except Exception as e: # noqa: F841
|
||||
raise ValueError("zmqsecret must be base64 encoded")
|
||||
fp.write(f"serverkeyzmq={zmqsecret}\n")
|
||||
fp.write("spentindex=1\n")
|
||||
fp.write("txindex=1\n")
|
||||
fp.write("staking=0\n")
|
||||
@@ -1391,6 +1397,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
)
|
||||
elif coin == "litecoin":
|
||||
fp.write("prune=4000\n")
|
||||
fp.write("changetype=bech32\n")
|
||||
if LTC_RPC_USER != "":
|
||||
fp.write(
|
||||
"rpcauth={}:{}${}\n".format(
|
||||
@@ -1408,6 +1415,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
elif coin == "bitcoin":
|
||||
fp.write("deprecatedrpc=create_bdb\n")
|
||||
fp.write("prune=2000\n")
|
||||
fp.write("changetype=bech32\n")
|
||||
fp.write("fallbackfee=0.0002\n")
|
||||
if BTC_RPC_USER != "":
|
||||
fp.write(
|
||||
@@ -1478,10 +1486,10 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warning("Unknown coin %s", coin)
|
||||
logger.warning(f"Unknown coin {coin}")
|
||||
|
||||
if coin == "bitcoin" and extra_opts.get("use_btc_fastsync", False) is True:
|
||||
logger.info("Initialising BTC chain with fastsync %s", BITCOIN_FASTSYNC_FILE)
|
||||
logger.info(f"Initialising BTC chain with fastsync {BITCOIN_FASTSYNC_FILE}")
|
||||
base_dir = extra_opts["data_dir"]
|
||||
|
||||
for dirname in ("blocks", "chainstate"):
|
||||
@@ -1558,27 +1566,18 @@ def modify_tor_config(
|
||||
# Disable tor first
|
||||
for line in fp_in:
|
||||
skip_line: bool = False
|
||||
if coin == "monero":
|
||||
if coin in ("wownero", "monero"):
|
||||
for opt_line in monerod_proxy_config:
|
||||
setting: str = opt_line[0 : opt_line.find("=") + 1]
|
||||
if line.startswith(setting):
|
||||
skip_line = True
|
||||
break
|
||||
if coin == "wownero":
|
||||
for opt_line in wownerod_proxy_config:
|
||||
setting: str = opt_line[0 : opt_line.find("=") + 1]
|
||||
if line.startswith(setting):
|
||||
skip_line = True
|
||||
break
|
||||
if not skip_line:
|
||||
fp.write(line)
|
||||
if enable:
|
||||
if coin == "monero":
|
||||
if coin in ("wownero", "monero"):
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
if coin == "wownero":
|
||||
for opt_line in wownerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
with open(wallet_conf_path, "w") as fp:
|
||||
with open(wallet_conf_path + ".last") as fp_in:
|
||||
@@ -1690,6 +1689,8 @@ def printHelp():
|
||||
)
|
||||
print("--preparebinonly Don't prepare settings or datadirs.")
|
||||
print("--nocores Don't download and extract any coin clients.")
|
||||
print("--addnetwork Add network.")
|
||||
print("--disablenetwork Remove network.")
|
||||
print("--usecontainers Expect each core to run in a unique container.")
|
||||
print("--portoffset=n Raise all ports by n.")
|
||||
print(
|
||||
@@ -1767,7 +1768,7 @@ def finalise_daemon(d):
|
||||
fp.close()
|
||||
|
||||
|
||||
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
|
||||
swap_client = None
|
||||
daemons = []
|
||||
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
|
||||
@@ -1780,7 +1781,7 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
coin_name = "particl"
|
||||
coin_settings = settings["chainclients"][coin_name]
|
||||
daemon_args += getCoreBinArgs(c, coin_settings, prepare=True)
|
||||
extra_config = {"stdout_to_file": True}
|
||||
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
|
||||
if coin_settings["manage_daemon"]:
|
||||
filename: str = getCoreBinName(c, coin_settings, coin_name + "d")
|
||||
daemons.append(
|
||||
@@ -1804,12 +1805,14 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
"Must set WALLET_ENCRYPTION_PWD to add coin when Particl wallet is encrypted"
|
||||
)
|
||||
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
extra_opts["particl_daemon"] = daemons[-1]
|
||||
finally:
|
||||
if swap_client:
|
||||
swap_client.finalise()
|
||||
del swap_client
|
||||
for d in daemons:
|
||||
finalise_daemon(d)
|
||||
if "particl_daemon" not in extra_opts:
|
||||
for d in daemons:
|
||||
finalise_daemon(d)
|
||||
|
||||
|
||||
def encrypt_wallet(swap_client, coin_type) -> None:
|
||||
@@ -1902,16 +1905,21 @@ def initialise_wallets(
|
||||
)
|
||||
]
|
||||
|
||||
extra_config = {"stdout_to_file": True}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
coin_settings["datadir"],
|
||||
coin_settings["bindir"],
|
||||
filename,
|
||||
daemon_args + coin_args,
|
||||
extra_config=extra_config,
|
||||
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
|
||||
|
||||
if c == Coins.PART and "particl_daemon" in extra_opts:
|
||||
daemons.append(extra_opts["particl_daemon"])
|
||||
del extra_opts["particl_daemon"]
|
||||
else:
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
coin_settings["datadir"],
|
||||
coin_settings["bindir"],
|
||||
filename,
|
||||
daemon_args + coin_args,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
)
|
||||
swap_client.setDaemonPID(c, daemons[-1].handle.pid)
|
||||
swap_client.setCoinRunParams(c)
|
||||
swap_client.createCoinInterface(c)
|
||||
@@ -2152,7 +2160,6 @@ def load_config(config_path):
|
||||
|
||||
|
||||
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"):
|
||||
@@ -2232,11 +2239,18 @@ def check_btc_fastsync_data(base_dir, sync_filename):
|
||||
ensureValidSignatureBy(verified, "nicolasdorier")
|
||||
|
||||
|
||||
def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None:
|
||||
if coin not in known_coins:
|
||||
exitWithError(f"Unknown coin {coin.capitalize()}")
|
||||
if test_disabled and not OVERRIDE_DISABLED_COINS and coin in disabled_coins:
|
||||
exitWithError(f"{coin.capitalize()} is disabled")
|
||||
def ensure_coin_valid(coin_name: str, test_disabled: bool = True) -> None:
|
||||
if coin_name not in known_coins:
|
||||
exitWithError(f"Unknown coin {coin_name.capitalize()}")
|
||||
if test_disabled and not OVERRIDE_DISABLED_COINS and coin_name in disabled_coins:
|
||||
exitWithError(f"{coin_name.capitalize()} is disabled")
|
||||
|
||||
|
||||
def ensure_network_valid(network_name: str, test_disabled: bool = True) -> None:
|
||||
if network_name not in known_networks:
|
||||
exitWithError(f"Unknown network {network_name.capitalize()}")
|
||||
if test_disabled and network_name in disabled_networks:
|
||||
exitWithError(f"{network_name.capitalize()} is disabled")
|
||||
|
||||
|
||||
def main():
|
||||
@@ -2345,10 +2359,10 @@ def main():
|
||||
continue
|
||||
if len(s) == 2:
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1].strip('"'))
|
||||
data_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
|
||||
continue
|
||||
if name == "bindir":
|
||||
bin_dir = os.path.expanduser(s[1].strip('"'))
|
||||
bin_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
|
||||
continue
|
||||
if name == "portoffset":
|
||||
port_offset = int(s[1])
|
||||
@@ -2379,6 +2393,16 @@ def main():
|
||||
disable_coin = s[1].strip().lower()
|
||||
ensure_coin_valid(disable_coin, test_disabled=False)
|
||||
continue
|
||||
if name == "addnetwork":
|
||||
network_name = s[1].strip().lower()
|
||||
ensure_network_valid(network_name)
|
||||
extra_opts["addnetwork"] = network_name
|
||||
continue
|
||||
if name == "disablenetwork":
|
||||
network_name = s[1].strip().lower()
|
||||
ensure_network_valid(network_name, test_disabled=False)
|
||||
extra_opts["disablenetwork"] = network_name
|
||||
continue
|
||||
if name == "htmlhost":
|
||||
htmlhost = s[1].strip('"')
|
||||
continue
|
||||
@@ -2395,7 +2419,9 @@ def main():
|
||||
extra_opts["walletrestoretime"] = int(s[1])
|
||||
continue
|
||||
if name == "keysdirpath":
|
||||
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
|
||||
extra_opts["keysdirpath"] = os.path.abspath(
|
||||
os.path.expanduser(s[1].strip('"'))
|
||||
)
|
||||
continue
|
||||
if name == "trustremotenode":
|
||||
extra_opts["trust_remote_node"] = toBool(s[1])
|
||||
@@ -2793,6 +2819,8 @@ def main():
|
||||
coin_settings["watch_wallet_name"] = getWalletName(
|
||||
coin_params, "bsx_watch", prefix_override=f"{ticker}_WATCH"
|
||||
)
|
||||
if getLegacyKeyPathOption(coin_params) is True:
|
||||
coin_settings["use_legacy_key_paths"] = True
|
||||
|
||||
if PART_RPC_USER != "":
|
||||
chainclients["particl"]["rpcuser"] = PART_RPC_USER
|
||||
@@ -2843,10 +2871,10 @@ def main():
|
||||
settings = load_config(config_path)
|
||||
|
||||
init_coins = settings["chainclients"].keys()
|
||||
logger.info("Active coins: %s", ", ".join(init_coins))
|
||||
logger.info("Active coins: {}".format(", ".join(init_coins)))
|
||||
if with_coins_changed:
|
||||
init_coins = with_coins
|
||||
logger.info("Initialising coins: %s", ", ".join(init_coins))
|
||||
logger.info("Initialising coins: {}".format(", ".join(init_coins)))
|
||||
initialise_wallets(
|
||||
particl_wallet_mnemonic,
|
||||
init_coins,
|
||||
@@ -2903,7 +2931,7 @@ def main():
|
||||
if "particl" in disable_coin:
|
||||
exitWithError("Cannot disable Particl (required for operation)")
|
||||
|
||||
logger.info("Disabling coin: %s", disable_coin)
|
||||
logger.info(f"Disabling coin: {disable_coin}")
|
||||
settings = load_config(config_path)
|
||||
|
||||
if disable_coin not in settings["chainclients"]:
|
||||
@@ -2928,7 +2956,7 @@ def main():
|
||||
extra_opts["tor_control_password"] = tor_control_password
|
||||
|
||||
if add_coin != "":
|
||||
logger.info("Adding coin: %s", add_coin)
|
||||
logger.info(f"Adding coin: {add_coin}")
|
||||
settings = load_config(config_path)
|
||||
|
||||
if add_coin in settings["chainclients"]:
|
||||
@@ -2937,7 +2965,7 @@ def main():
|
||||
coin_settings["connection_type"] == "none"
|
||||
and coin_settings["manage_daemon"] is False
|
||||
):
|
||||
logger.info("Enabling coin: %s", add_coin)
|
||||
logger.info(f"Enabling coin: {add_coin}")
|
||||
coin_settings["connection_type"] = "rpc"
|
||||
coin_settings["manage_daemon"] = True
|
||||
if "manage_wallet_daemon" in coin_settings:
|
||||
@@ -2945,45 +2973,185 @@ def main():
|
||||
save_config(config_path, settings)
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
exitWithError("{} is already in the settings file".format(add_coin))
|
||||
exitWithError(f"{add_coin} is already in the settings file")
|
||||
|
||||
if tor_control_password is None and settings.get("use_tor", False):
|
||||
extra_opts["tor_control_password"] = settings.get(
|
||||
"tor_control_password", None
|
||||
)
|
||||
|
||||
if particl_wallet_mnemonic != "none":
|
||||
# Ensure Particl wallet is unencrypted or correct password is supplied
|
||||
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
|
||||
|
||||
settings["chainclients"][add_coin] = chainclients[add_coin]
|
||||
|
||||
if not no_cores:
|
||||
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
|
||||
|
||||
if not (prepare_bin_only or upgrade_cores):
|
||||
prepareDataDir(
|
||||
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
|
||||
)
|
||||
|
||||
try:
|
||||
if particl_wallet_mnemonic != "none":
|
||||
initialise_wallets(
|
||||
None,
|
||||
{
|
||||
add_coin,
|
||||
},
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
# Ensure Particl wallet is unencrypted or correct password is supplied
|
||||
# Keep daemon running to use in initialise_wallets
|
||||
test_particl_encryption(
|
||||
data_dir, settings, chain, use_tor_proxy, extra_opts
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
settings["chainclients"][add_coin] = chainclients[add_coin]
|
||||
|
||||
if not no_cores:
|
||||
prepareCore(
|
||||
add_coin, known_coins[add_coin], settings, data_dir, extra_opts
|
||||
)
|
||||
|
||||
if not (prepare_bin_only or upgrade_cores):
|
||||
prepareDataDir(
|
||||
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
|
||||
)
|
||||
|
||||
if particl_wallet_mnemonic != "none":
|
||||
initialise_wallets(
|
||||
None,
|
||||
{
|
||||
add_coin,
|
||||
},
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
finally:
|
||||
if "particl_daemon" in extra_opts:
|
||||
finalise_daemon(extra_opts["particl_daemon"])
|
||||
del extra_opts["particl_daemon"]
|
||||
|
||||
logger.info(f"Done. Coin {add_coin} successfully added.")
|
||||
return 0
|
||||
|
||||
if "addnetwork" in extra_opts:
|
||||
network_name = extra_opts["addnetwork"]
|
||||
logger.info(f"Adding network: {network_name}")
|
||||
settings = load_config(config_path)
|
||||
network_config_list = settings.get("networks", [])
|
||||
if len(network_config_list) < 1:
|
||||
network_config_list = [{"type": "smsg", "enabled": True}]
|
||||
|
||||
network_enabled: bool = False
|
||||
if network_name == "simplex":
|
||||
if SIMPLEX_GROUP_LINK is None:
|
||||
raise ValueError("SIMPLEX_GROUP_LINK must be set.")
|
||||
|
||||
simplex_chat_bin_dir = os.path.join(bin_dir, "simplex")
|
||||
simplex_chat_client_path = os.path.join(
|
||||
simplex_chat_bin_dir, "simplex-chat"
|
||||
)
|
||||
simplex_chat_release_dir = os.path.join(
|
||||
simplex_chat_bin_dir, SIMPLEX_CHAT_VERSION
|
||||
)
|
||||
if not os.path.exists(simplex_chat_release_dir):
|
||||
os.makedirs(simplex_chat_release_dir)
|
||||
|
||||
if USE_PLATFORM == "Linux":
|
||||
simplex_chat_release_file = "simplex-chat-ubuntu-24_04-x86-64"
|
||||
elif USE_PLATFORM == "Darwin":
|
||||
simplex_chat_release_file = "simplex-chat-macos-x86-64"
|
||||
elif USE_PLATFORM == "Windows":
|
||||
simplex_chat_release_file = "simplex-chat-windows-x86-64"
|
||||
else:
|
||||
raise ValueError(f"Unknown platform {USE_PLATFORM}")
|
||||
|
||||
simplex_chat_release_url = f"https://github.com/simplex-chat/simplex-chat/releases/download/v{SIMPLEX_CHAT_VERSION}/{simplex_chat_release_file}"
|
||||
simplex_chat_release_path = os.path.join(
|
||||
simplex_chat_release_dir, simplex_chat_release_file
|
||||
)
|
||||
downloadRelease(
|
||||
simplex_chat_release_url, simplex_chat_release_path, extra_opts
|
||||
)
|
||||
|
||||
assert_filename = "_sha256sums"
|
||||
assert_path = os.path.join(simplex_chat_release_dir, assert_filename)
|
||||
assert_url = f"https://github.com/simplex-chat/simplex-chat/releases/download/v{SIMPLEX_CHAT_VERSION}/_sha256sums"
|
||||
if not os.path.exists(assert_path):
|
||||
downloadFile(assert_url, assert_path)
|
||||
|
||||
release_hash: str = getFileHash(simplex_chat_release_path)
|
||||
logger.info(f"{simplex_chat_release_file} hash: {release_hash}")
|
||||
ensureFileHashInFile(release_hash, assert_path)
|
||||
|
||||
assert_sig_filename = assert_filename + ".asc"
|
||||
assert_sig_url = assert_url + ".asc"
|
||||
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
|
||||
if not os.path.exists(assert_sig_path):
|
||||
downloadFile(assert_sig_url, assert_sig_path)
|
||||
|
||||
gpg = gnupg.GPG()
|
||||
pubkey_filename = "SimpleX_Chat.pgp"
|
||||
pubkeyurls = []
|
||||
if not havePubkey(gpg, expected_key_ids["SimpleX_Chat"][0]):
|
||||
importPubkey(gpg, pubkey_filename, pubkeyurls)
|
||||
with open(assert_sig_path, "rb") as fp:
|
||||
verified = gpg.verify_file(fp, assert_path)
|
||||
ensureValidSignatureBy(verified, "SimpleX_Chat")
|
||||
|
||||
shutil.copyfile(simplex_chat_release_path, simplex_chat_client_path)
|
||||
|
||||
simplex_settings = {
|
||||
"type": "simplex",
|
||||
"server_address": SIMPLEX_SERVER_ADDRESS,
|
||||
"client_path": simplex_chat_client_path,
|
||||
"ws_port": SIMPLEX_WS_PORT,
|
||||
"group_link": SIMPLEX_GROUP_LINK,
|
||||
"enabled": True,
|
||||
}
|
||||
if SIMPLEX_SERVER_SOCKS_PROXY is not None:
|
||||
simplex_settings["socks_proxy_override"] = SIMPLEX_SERVER_SOCKS_PROXY
|
||||
|
||||
found_network: bool = False
|
||||
for network in network_config_list:
|
||||
network_type: str = network.get("type", "unknown")
|
||||
if network_type == "simplex":
|
||||
found_network = True
|
||||
if network.get("enabled", False) is True:
|
||||
logger.warning(f"Network {network_type} is already active.")
|
||||
network = simplex_settings
|
||||
else:
|
||||
# TODO: Allow multiple active networks
|
||||
network["enabled"] = False
|
||||
logger.info(f"Disabling network {network_type}.")
|
||||
if found_network is False:
|
||||
network_config_list.append(simplex_settings)
|
||||
elif network_name == "smsg":
|
||||
found_network: bool = False
|
||||
for network in network_config_list:
|
||||
network_type: str = network.get("type", "unknown")
|
||||
if network_type == "smsg":
|
||||
found_network = True
|
||||
if network.get("enabled", False) is True:
|
||||
logger.warning(f"Network {network_type} is already active.")
|
||||
else:
|
||||
network["enabled"] = True
|
||||
else:
|
||||
# TODO: Allow multiple active networks
|
||||
network["enabled"] = False
|
||||
logger.info(f"Disabling network {network_type}.")
|
||||
if found_network is False:
|
||||
network_config_list.append({"type": "smsg", "enabled": True})
|
||||
else:
|
||||
raise ValueError(f"Unknown network {network_name}")
|
||||
|
||||
settings["networks"] = network_config_list
|
||||
save_config(config_path, settings)
|
||||
if network_enabled:
|
||||
logger.info(f"Done. Network {network_name} successfully added.")
|
||||
else:
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
|
||||
if "disablenetwork" in extra_opts:
|
||||
network_name = extra_opts["disablenetwork"]
|
||||
logger.info(f"Disable network: {network_name}")
|
||||
settings = load_config(config_path)
|
||||
network_config_list = settings.get("networks", [])
|
||||
if len(network_config_list) < 1:
|
||||
network_config_list = [{"type": "smsg", "enabled": True}]
|
||||
|
||||
logger.info(f"Done. Network {network_name} successfully disabled.")
|
||||
return 0
|
||||
|
||||
logger.info(
|
||||
"With coins: "
|
||||
+ (", ".join(with_coins))
|
||||
@@ -3074,6 +3242,10 @@ def main():
|
||||
for c in with_coins:
|
||||
withchainclients[c] = chainclients[c]
|
||||
|
||||
zmq_server_pubkey, zmq_server_key = zmq.curve_keypair()
|
||||
zmq_client_pubkey, zmq_client_key = zmq.curve_keypair()
|
||||
extra_opts["zmqsecret"] = base64.b64encode(zmq_server_key).decode("utf-8")
|
||||
|
||||
settings = {
|
||||
"debug": True,
|
||||
"zmqhost": f"tcp://{PART_RPC_HOST}",
|
||||
@@ -3089,6 +3261,12 @@ def main():
|
||||
"check_watched_seconds": 60,
|
||||
"check_expired_seconds": 60,
|
||||
"wallet_update_timeout": 10, # Seconds to wait for wallet page update
|
||||
"zmq_client_key": base64.b64encode(zmq_client_key).decode("utf-8"),
|
||||
"zmq_client_pubkey": base64.b64encode(zmq_client_pubkey).decode("utf-8"),
|
||||
"zmq_server_pubkey": base64.b64encode(zmq_server_pubkey).decode("utf-8"),
|
||||
"enabled_log_categories": [
|
||||
"net",
|
||||
],
|
||||
}
|
||||
|
||||
wshost: str = extra_opts.get("wshost", htmlhost)
|
||||
@@ -3096,6 +3274,11 @@ def main():
|
||||
settings["wshost"] = wshost
|
||||
settings["wsport"] = UI_WS_PORT + port_offset
|
||||
|
||||
if "CHECK_FOR_BSX_UPDATES" in os.environ:
|
||||
settings["check_updates"] = CHECK_FOR_BSX_UPDATES
|
||||
elif BSX_TEST_MODE is True:
|
||||
settings["check_updates"] = False
|
||||
|
||||
if use_tor_proxy:
|
||||
tor_control_password = generate_salt(24)
|
||||
addTorSettings(settings, tor_control_password)
|
||||
|
||||
@@ -17,12 +17,11 @@ import traceback
|
||||
|
||||
import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.chainparams import chainparams, Coins
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
|
||||
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
|
||||
from basicswap.network.simplex_chat import startSimplexClient
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.util.daemon import Daemon
|
||||
|
||||
initial_logger = logging.getLogger()
|
||||
initial_logger.level = logging.DEBUG
|
||||
@@ -33,41 +32,81 @@ logger = initial_logger
|
||||
swap_client = None
|
||||
|
||||
|
||||
class Daemon:
|
||||
__slots__ = ("handle", "files")
|
||||
|
||||
def __init__(self, handle, files):
|
||||
self.handle = handle
|
||||
self.files = files
|
||||
|
||||
|
||||
def is_known_coin(coin_name: str) -> bool:
|
||||
for k, v in chainparams.items():
|
||||
if coin_name == v["name"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
os.write(
|
||||
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
||||
)
|
||||
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
logger.info("Signal handler stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
logger.info(f"AMM signal shutdown: {msg}")
|
||||
else:
|
||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping AMM in signal handler: {e}")
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||
try:
|
||||
datadir = part_settings.get("datadir")
|
||||
if not datadir:
|
||||
return
|
||||
|
||||
config_path = os.path.join(datadir, "particl.conf")
|
||||
if not os.path.exists(config_path):
|
||||
return
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
|
||||
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
|
||||
zmq_port = swap_settings.get("zmqport", 14792)
|
||||
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
|
||||
|
||||
if "zmqpubhashwtx=" not in config_content:
|
||||
with open(config_path, "a") as f:
|
||||
f.write(f"{expected_line}\n")
|
||||
elif expected_line not in config_content:
|
||||
lines = config_content.split("\n")
|
||||
updated_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("zmqpubhashwtx="):
|
||||
updated_lines.append(expected_line)
|
||||
else:
|
||||
updated_lines.append(line)
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("\n".join(updated_lines))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking PART ZMQ config: {e}")
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
coin_name = extra_config.get("coin_name", "")
|
||||
|
||||
# Rewrite litecoin.conf
|
||||
# TODO: Remove
|
||||
needs_rewrite: bool = False
|
||||
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
|
||||
if os.path.exists(ltc_conf_path):
|
||||
needs_rewrite: bool = False
|
||||
add_changetype: bool = True
|
||||
with open(ltc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("changetype="):
|
||||
add_changetype = False
|
||||
break
|
||||
if line.endswith("=onion"):
|
||||
needs_rewrite = True
|
||||
break
|
||||
@@ -83,6 +122,29 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
fp_to.write(line.strip()[:-6] + "\n")
|
||||
else:
|
||||
fp_to.write(line)
|
||||
if add_changetype:
|
||||
fp_to.write("changetype=bech32\n")
|
||||
add_changetype = False
|
||||
if add_changetype:
|
||||
logger.info("Adding changetype to litecoin.conf")
|
||||
with open(ltc_conf_path, "a") as fp:
|
||||
fp.write("changetype=bech32\n")
|
||||
|
||||
# Rewrite bitcoin.conf
|
||||
# TODO: Remove
|
||||
btc_conf_path = os.path.join(datadir_path, "bitcoin.conf")
|
||||
if coin_name == "bitcoin" and os.path.exists(btc_conf_path):
|
||||
add_changetype: bool = True
|
||||
with open(btc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("changetype="):
|
||||
add_changetype = False
|
||||
break
|
||||
if add_changetype:
|
||||
logger.info("Adding changetype to bitcoin.conf")
|
||||
with open(btc_conf_path, "a") as fp:
|
||||
fp.write("changetype=bech32\n")
|
||||
|
||||
args = [
|
||||
daemon_bin,
|
||||
@@ -91,7 +153,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
if add_datadir:
|
||||
args.append("-datadir=" + datadir_path)
|
||||
args += opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.info(f"Starting node {daemon_bin}")
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
opened_files = []
|
||||
@@ -122,6 +184,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
opened_files,
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +200,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
"--non-interactive",
|
||||
"--config-file=" + os.path.join(datadir_path, config_filename),
|
||||
] + opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.info(f"Starting node {daemon_bin}")
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -152,6 +215,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
[file_stdout, file_stderr],
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +264,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
):
|
||||
fp_to.write(line)
|
||||
|
||||
logger.info("Starting wallet daemon {}".format(wallet_bin))
|
||||
logger.info(f"Starting wallet daemon {wallet_bin}")
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
|
||||
@@ -215,28 +279,10 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
cwd=data_dir,
|
||||
),
|
||||
[wallet_stdout, wallet_stderr],
|
||||
os.path.basename(wallet_bin),
|
||||
)
|
||||
|
||||
|
||||
def ws_new_client(client, server):
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_new_client {client["id"]}')
|
||||
|
||||
|
||||
def ws_client_left(client, server):
|
||||
if client is None:
|
||||
return
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_client_left {client["id"]}')
|
||||
|
||||
|
||||
def ws_message_received(client, server, message):
|
||||
if len(message) > 200:
|
||||
message = message[:200] + ".."
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
|
||||
|
||||
|
||||
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
|
||||
return coin_settings.get(
|
||||
"core_binname", chainparams[coin_id].get("core_binname", default_name)
|
||||
@@ -275,13 +321,36 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
|
||||
return extra_args
|
||||
|
||||
|
||||
def mainLoop(daemons, update: bool = True):
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
if update:
|
||||
swap_client.update()
|
||||
else:
|
||||
pass
|
||||
|
||||
for daemon in daemons:
|
||||
if daemon.running is False:
|
||||
continue
|
||||
poll = daemon.handle.poll()
|
||||
if poll is None:
|
||||
pass # Process is running
|
||||
else:
|
||||
daemon.running = False
|
||||
swap_client.log.error(
|
||||
f"Process {daemon.handle.pid} for {daemon.name} terminated unexpectedly returning {poll}."
|
||||
)
|
||||
|
||||
|
||||
def runClient(
|
||||
data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
|
||||
data_dir: str,
|
||||
chain: str,
|
||||
start_only_coins: bool,
|
||||
log_prefix: str = "BasicSwap",
|
||||
extra_opts=dict(),
|
||||
) -> int:
|
||||
global swap_client, logger
|
||||
daemons = []
|
||||
pids = []
|
||||
threads = []
|
||||
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||
pids_path = os.path.join(data_dir, ".pids")
|
||||
|
||||
@@ -302,30 +371,76 @@ 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(
|
||||
data_dir, settings, chain, log_name=log_prefix, extra_opts=extra_opts
|
||||
)
|
||||
logger = swap_client.log
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
for ln in fd:
|
||||
# TODO: try close
|
||||
logger.warning("Found pid for daemon {} ".format(ln.strip()))
|
||||
logger.warning("Found pid for daemon {}".format(ln.strip()))
|
||||
|
||||
# Ensure daemons are stopped
|
||||
swap_client.stopDaemons()
|
||||
|
||||
# Settings may have been modified
|
||||
settings = swap_client.settings
|
||||
|
||||
try:
|
||||
# Try start daemons
|
||||
for network in settings.get("networks", []):
|
||||
if network.get("enabled", True) is False:
|
||||
continue
|
||||
network_type: str = network.get("type", "unknown")
|
||||
if network_type == "simplex":
|
||||
simplex_dir = os.path.join(data_dir, "simplex")
|
||||
|
||||
log_level = "debug" if swap_client.debug else "info"
|
||||
|
||||
socks_proxy = None
|
||||
if "socks_proxy_override" in network:
|
||||
socks_proxy = network["socks_proxy_override"]
|
||||
elif swap_client.use_tor_proxy:
|
||||
socks_proxy = (
|
||||
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
|
||||
)
|
||||
|
||||
daemons.append(
|
||||
startSimplexClient(
|
||||
network["client_path"],
|
||||
simplex_dir,
|
||||
network["server_address"],
|
||||
network["ws_port"],
|
||||
logger,
|
||||
swap_client.delay_event,
|
||||
socks_proxy=socks_proxy,
|
||||
log_level=log_level,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info(f"Started Simplex client {pid}")
|
||||
|
||||
for c, v in settings["chainclients"].items():
|
||||
if len(start_only_coins) > 0 and c not in start_only_coins:
|
||||
continue
|
||||
if (
|
||||
len(swap_client.with_coins_override) > 0
|
||||
and c not in swap_client.with_coins_override
|
||||
) or c in swap_client.without_coins_override:
|
||||
if v.get("manage_daemon", False) or v.get(
|
||||
"manage_wallet_daemon", False
|
||||
):
|
||||
logger.warning(
|
||||
f"Not starting coin {c.capitalize()}, disabled by arguments."
|
||||
)
|
||||
continue
|
||||
try:
|
||||
coin_id = swap_client.getCoinIdFromName(c)
|
||||
display_name = getCoinName(coin_id)
|
||||
except Exception as e: # noqa: F841
|
||||
logger.warning("Not starting unknown coin: {}".format(c))
|
||||
logger.warning(f"Not starting unknown coin: {c}")
|
||||
continue
|
||||
if c in ("monero", "wownero"):
|
||||
if v["manage_daemon"] is True:
|
||||
@@ -334,7 +449,7 @@ def runClient(
|
||||
|
||||
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
@@ -382,7 +497,7 @@ def runClient(
|
||||
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
continue # /monero
|
||||
|
||||
@@ -401,6 +516,7 @@ def runClient(
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrd_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -412,7 +528,7 @@ def runClient(
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
@@ -429,6 +545,7 @@ def runClient(
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrwallet_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -445,19 +562,29 @@ def runClient(
|
||||
continue # /decred
|
||||
|
||||
if v["manage_daemon"] is True:
|
||||
if c == "particl" and swap_client._zmq_queue_enabled:
|
||||
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
|
||||
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
|
||||
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||
extra_opts = getCoreBinArgs(
|
||||
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
|
||||
)
|
||||
extra_config = {"coin_name": c}
|
||||
daemons.append(
|
||||
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
|
||||
startDaemon(
|
||||
v["datadir"],
|
||||
v["bindir"],
|
||||
filename,
|
||||
opts=extra_opts,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
pids.append((c, pid))
|
||||
swap_client.setDaemonPID(c, pid)
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
if len(pids) > 0:
|
||||
with open(pids_path, "w") as fd:
|
||||
for p in pids:
|
||||
@@ -471,47 +598,12 @@ def runClient(
|
||||
logger.info(
|
||||
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
|
||||
)
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
pass
|
||||
mainLoop(daemons, update=False)
|
||||
else:
|
||||
swap_client.start()
|
||||
if "htmlhost" in settings:
|
||||
swap_client.log.info(
|
||||
"Starting http server at http://%s:%d."
|
||||
% (settings["htmlhost"], settings["htmlport"])
|
||||
)
|
||||
allow_cors = (
|
||||
settings["allowcors"]
|
||||
if "allowcors" in settings
|
||||
else cfg.DEFAULT_ALLOW_CORS
|
||||
)
|
||||
thread_http = HttpThread(
|
||||
settings["htmlhost"],
|
||||
settings["htmlport"],
|
||||
allow_cors,
|
||||
swap_client,
|
||||
)
|
||||
threads.append(thread_http)
|
||||
thread_http.start()
|
||||
|
||||
if "wshost" in settings:
|
||||
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
|
||||
swap_client.log.info(f"Starting ws server at {ws_url}.")
|
||||
|
||||
swap_client.ws_server = WebsocketServer(
|
||||
host=settings["wshost"], port=settings["wsport"]
|
||||
)
|
||||
swap_client.ws_server.client_port = settings.get(
|
||||
"wsclientport", settings["wsport"]
|
||||
)
|
||||
swap_client.ws_server.set_fn_new_client(ws_new_client)
|
||||
swap_client.ws_server.set_fn_client_left(ws_client_left)
|
||||
swap_client.ws_server.set_fn_message_received(ws_message_received)
|
||||
swap_client.ws_server.run_forever(threaded=True)
|
||||
|
||||
logger.info("Exit with Ctrl + c.")
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
swap_client.update()
|
||||
mainLoop(daemons)
|
||||
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
@@ -524,23 +616,16 @@ def runClient(
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.finalise()
|
||||
swap_client.log.info("Stopping HTTP threads.")
|
||||
for t in threads:
|
||||
try:
|
||||
t.stop()
|
||||
t.join()
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
swap_client.log.info(f"Interrupting {d.handle.pid}")
|
||||
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}")
|
||||
try:
|
||||
d.handle.send_signal(
|
||||
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||
)
|
||||
except Exception as e:
|
||||
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
|
||||
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
@@ -576,6 +661,11 @@ def printVersion():
|
||||
)
|
||||
|
||||
|
||||
def ensure_coin_valid(coin: str) -> bool:
|
||||
if isKnownCoinName(coin) is False:
|
||||
raise ValueError(f"Unknown coin: {coin}")
|
||||
|
||||
|
||||
def printHelp():
|
||||
print("Usage: basicswap-run ")
|
||||
print("\n--help, -h Print help.")
|
||||
@@ -586,10 +676,15 @@ def printHelp():
|
||||
print("--mainnet Run in mainnet mode.")
|
||||
print("--testnet Run in testnet mode.")
|
||||
print("--regtest Run in regtest mode.")
|
||||
print("--withcoin= Run only with coin/s.")
|
||||
print("--withoutcoin= Run without coin/s.")
|
||||
print(
|
||||
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
|
||||
)
|
||||
print("--logprefix Specify log prefix.")
|
||||
print(
|
||||
"--forcedbupgrade Recheck database against schema regardless of version."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -597,6 +692,9 @@ def main():
|
||||
chain = "mainnet"
|
||||
start_only_coins = set()
|
||||
log_prefix: str = "BasicSwap"
|
||||
options = dict()
|
||||
with_coins = set()
|
||||
without_coins = set()
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != "-":
|
||||
@@ -620,18 +718,31 @@ def main():
|
||||
if name in ("mainnet", "testnet", "regtest"):
|
||||
chain = name
|
||||
continue
|
||||
|
||||
if name in ("withcoin", "withcoins"):
|
||||
for coin in [s.strip().lower() for s in s[1].split(",")]:
|
||||
ensure_coin_valid(coin)
|
||||
with_coins.add(coin)
|
||||
continue
|
||||
if name in ("withoutcoin", "withoutcoins"):
|
||||
for coin in [s.strip().lower() for s in s[1].split(",")]:
|
||||
if coin == "particl":
|
||||
raise ValueError("Particl is required.")
|
||||
ensure_coin_valid(coin)
|
||||
without_coins.add(coin)
|
||||
continue
|
||||
if name == "forcedbupgrade":
|
||||
options["force_db_upgrade"] = True
|
||||
continue
|
||||
if len(s) == 2:
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
data_dir = os.path.abspath(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:
|
||||
raise ValueError(f"Unknown coin: {coin}")
|
||||
ensure_coin_valid(coin)
|
||||
start_only_coins.add(coin)
|
||||
continue
|
||||
|
||||
@@ -650,8 +761,14 @@ def main():
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
if len(with_coins) > 0:
|
||||
with_coins.add("particl")
|
||||
options["with_coins"] = with_coins
|
||||
if len(without_coins) > 0:
|
||||
options["without_coins"] = without_coins
|
||||
|
||||
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
|
||||
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix)
|
||||
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix, options)
|
||||
|
||||
print("Done.")
|
||||
return fail_code
|
||||
|
||||
@@ -571,3 +571,7 @@ def getCoinIdFromName(name: str) -> str:
|
||||
return name_map[name.lower()]
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin {name}")
|
||||
|
||||
|
||||
def isKnownCoinName(name: str) -> bool:
|
||||
return params["name"].lower() in name_map
|
||||
|
||||
@@ -9,6 +9,8 @@ import os
|
||||
CONFIG_FILENAME = "basicswap.json"
|
||||
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
||||
DEFAULT_ALLOW_CORS = False
|
||||
DEFAULT_RPC_POOL_ENABLED = True
|
||||
DEFAULT_RPC_POOL_MAX_CONNECTIONS = 5
|
||||
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
|
||||
DEFAULT_TEST_BINDIR = os.path.expanduser(
|
||||
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
# ed25519.py - Optimized version of the reference implementation of Ed25519
|
||||
#
|
||||
# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to>
|
||||
# 2013 by Donald Stufft <donald@stufft.io>
|
||||
# 2013 by Alex Gaynor <alex.gaynor@gmail.com>
|
||||
# 2013 by Greg Price <price@mit.edu>
|
||||
#
|
||||
# To the extent possible under law, the author(s) have dedicated all copyright
|
||||
# and related and neighboring rights to this software to the public domain
|
||||
# worldwide. This software is distributed without any warranty.
|
||||
#
|
||||
# You should have received a copy of the CC0 Public Domain Dedication along
|
||||
# with this software. If not, see
|
||||
# <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
"""
|
||||
NB: This code is not safe for use with secret keys or secret data.
|
||||
The only safe use of this code is for verifying signatures on public messages.
|
||||
|
||||
Functions for computing the public key of a secret key and for signing
|
||||
a message are included, namely publickey_unsafe and signature_unsafe,
|
||||
for testing purposes only.
|
||||
|
||||
The root of the problem is that Python's long-integer arithmetic is
|
||||
not designed for use in cryptography. Specifically, it may take more
|
||||
or less time to execute an operation depending on the values of the
|
||||
inputs, and its memory access patterns may also depend on the inputs.
|
||||
This opens it to timing and cache side-channel attacks which can
|
||||
disclose data to an attacker. We rely on Python's long-integer
|
||||
arithmetic, so we cannot handle secrets without risking their disclosure.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import operator
|
||||
import sys
|
||||
|
||||
|
||||
__version__ = "1.0.dev0"
|
||||
|
||||
|
||||
# Useful for very coarse version differentiation.
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
indexbytes = operator.getitem
|
||||
intlist2bytes = bytes
|
||||
int2byte = operator.methodcaller("to_bytes", 1, "big")
|
||||
else:
|
||||
int2byte = chr
|
||||
range = xrange
|
||||
|
||||
def indexbytes(buf, i):
|
||||
return ord(buf[i])
|
||||
|
||||
def intlist2bytes(l):
|
||||
return b"".join(chr(c) for c in l)
|
||||
|
||||
|
||||
b = 256
|
||||
q = 2 ** 255 - 19
|
||||
l = 2 ** 252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
def H(m):
|
||||
return hashlib.sha512(m).digest()
|
||||
|
||||
|
||||
def pow2(x, p):
|
||||
"""== pow(x, 2**p, q)"""
|
||||
while p > 0:
|
||||
x = x * x % q
|
||||
p -= 1
|
||||
return x
|
||||
|
||||
|
||||
def inv(z):
|
||||
"""$= z^{-1} \mod q$, for z != 0"""
|
||||
# Adapted from curve25519_athlon.c in djb's Curve25519.
|
||||
z2 = z * z % q # 2
|
||||
z9 = pow2(z2, 2) * z % q # 9
|
||||
z11 = z9 * z2 % q # 11
|
||||
z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0
|
||||
z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0
|
||||
z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ...
|
||||
z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
|
||||
z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
|
||||
z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
|
||||
z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
|
||||
z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0
|
||||
return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2
|
||||
|
||||
|
||||
d = -121665 * inv(121666) % q
|
||||
I = pow(2, (q - 1) // 4, q)
|
||||
|
||||
|
||||
def xrecover(y, sign=0):
|
||||
xx = (y * y - 1) * inv(d * y * y + 1)
|
||||
x = pow(xx, (q + 3) // 8, q)
|
||||
|
||||
if (x * x - xx) % q != 0:
|
||||
x = (x * I) % q
|
||||
|
||||
if x % 2 != sign:
|
||||
x = q-x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
By = 4 * inv(5)
|
||||
Bx = xrecover(By)
|
||||
B = (Bx % q, By % q, 1, (Bx * By) % q)
|
||||
ident = (0, 1, 1, 0)
|
||||
|
||||
|
||||
def edwards_add(P, Q):
|
||||
# This is formula sequence 'addition-add-2008-hwcd-3' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
(x2, y2, z2, t2) = Q
|
||||
|
||||
a = (y1-x1)*(y2-x2) % q
|
||||
b = (y1+x1)*(y2+x2) % q
|
||||
c = t1*2*d*t2 % q
|
||||
dd = z1*2*z2 % q
|
||||
e = b - a
|
||||
f = dd - c
|
||||
g = dd + c
|
||||
h = b + a
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def edwards_sub(P, Q):
|
||||
# This is formula sequence 'addition-add-2008-hwcd-3' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
(x2, y2, z2, t2) = Q
|
||||
|
||||
# https://eprint.iacr.org/2008/522.pdf
|
||||
# The negative of (X:Y:Z)is (−X:Y:Z)
|
||||
#x2 = q-x2
|
||||
"""
|
||||
doesn't work
|
||||
x2 = q-x2
|
||||
t2 = (x2*y2) % q
|
||||
"""
|
||||
|
||||
zi = inv(z2)
|
||||
x2 = q-((x2 * zi) % q)
|
||||
y2 = (y2 * zi) % q
|
||||
z2 = 1
|
||||
t2 = (x2*y2) % q
|
||||
|
||||
|
||||
a = (y1-x1)*(y2-x2) % q
|
||||
b = (y1+x1)*(y2+x2) % q
|
||||
c = t1*2*d*t2 % q
|
||||
dd = z1*2*z2 % q
|
||||
e = b - a
|
||||
f = dd - c
|
||||
g = dd + c
|
||||
h = b + a
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def edwards_double(P):
|
||||
# This is formula sequence 'dbl-2008-hwcd' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
|
||||
a = x1*x1 % q
|
||||
b = y1*y1 % q
|
||||
c = 2*z1*z1 % q
|
||||
# dd = -a
|
||||
e = ((x1+y1)*(x1+y1) - a - b) % q
|
||||
g = -a + b # dd + b
|
||||
f = g - c
|
||||
h = -a - b # dd - b
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def scalarmult(P, e):
|
||||
if e == 0:
|
||||
return ident
|
||||
Q = scalarmult(P, e // 2)
|
||||
Q = edwards_double(Q)
|
||||
if e & 1:
|
||||
Q = edwards_add(Q, P)
|
||||
return Q
|
||||
|
||||
|
||||
# Bpow[i] == scalarmult(B, 2**i)
|
||||
Bpow = []
|
||||
|
||||
|
||||
def make_Bpow():
|
||||
P = B
|
||||
for i in range(253):
|
||||
Bpow.append(P)
|
||||
P = edwards_double(P)
|
||||
make_Bpow()
|
||||
|
||||
|
||||
def scalarmult_B(e):
|
||||
"""
|
||||
Implements scalarmult(B, e) more efficiently.
|
||||
"""
|
||||
# scalarmult(B, l) is the identity
|
||||
e = e % l
|
||||
P = ident
|
||||
for i in range(253):
|
||||
if e & 1:
|
||||
P = edwards_add(P, Bpow[i])
|
||||
e = e // 2
|
||||
assert e == 0, e
|
||||
return P
|
||||
|
||||
|
||||
def encodeint(y):
|
||||
bits = [(y >> i) & 1 for i in range(b)]
|
||||
return b''.join([
|
||||
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
|
||||
for i in range(b//8)
|
||||
])
|
||||
|
||||
|
||||
def encodepoint(P):
|
||||
(x, y, z, t) = P
|
||||
zi = inv(z)
|
||||
x = (x * zi) % q
|
||||
y = (y * zi) % q
|
||||
bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
|
||||
return b''.join([
|
||||
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
|
||||
for i in range(b // 8)
|
||||
])
|
||||
|
||||
|
||||
def bit(h, i):
|
||||
return (indexbytes(h, i // 8) >> (i % 8)) & 1
|
||||
|
||||
|
||||
def publickey_unsafe(sk):
|
||||
"""
|
||||
Not safe to use with secret keys or secret data.
|
||||
|
||||
See module docstring. This function should be used for testing only.
|
||||
"""
|
||||
h = H(sk)
|
||||
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
|
||||
A = scalarmult_B(a)
|
||||
return encodepoint(A)
|
||||
|
||||
|
||||
def Hint(m):
|
||||
h = H(m)
|
||||
return sum(2 ** i * bit(h, i) for i in range(2 * b))
|
||||
|
||||
|
||||
def signature_unsafe(m, sk, pk):
|
||||
"""
|
||||
Not safe to use with secret keys or secret data.
|
||||
|
||||
See module docstring. This function should be used for testing only.
|
||||
"""
|
||||
h = H(sk)
|
||||
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
|
||||
r = Hint(
|
||||
intlist2bytes([indexbytes(h, j) for j in range(b // 8, b // 4)]) + m
|
||||
)
|
||||
R = scalarmult_B(r)
|
||||
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
|
||||
return encodepoint(R) + encodeint(S)
|
||||
|
||||
|
||||
def isoncurve(P):
|
||||
(x, y, z, t) = P
|
||||
return (z % q != 0 and
|
||||
x*y % q == z*t % q and
|
||||
(y*y - x*x - z*z - d*t*t) % q == 0)
|
||||
|
||||
|
||||
def decodeint(s):
|
||||
return sum(2 ** i * bit(s, i) for i in range(0, b))
|
||||
|
||||
|
||||
def decodepoint(s):
|
||||
y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
|
||||
x = xrecover(y)
|
||||
if x & 1 != bit(s, b-1):
|
||||
x = q - x
|
||||
P = (x, y, 1, (x*y) % q)
|
||||
if not isoncurve(P):
|
||||
raise ValueError("decoding point that is not on curve")
|
||||
return P
|
||||
|
||||
|
||||
class SignatureMismatch(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def checkvalid(s, m, pk):
|
||||
"""
|
||||
Not safe to use when any argument is secret.
|
||||
|
||||
See module docstring. This function should be used only for
|
||||
verifying public signatures of public messages.
|
||||
"""
|
||||
if len(s) != b // 4:
|
||||
raise ValueError("signature length is wrong")
|
||||
|
||||
if len(pk) != b // 8:
|
||||
raise ValueError("public-key length is wrong")
|
||||
|
||||
R = decodepoint(s[:b // 8])
|
||||
A = decodepoint(pk)
|
||||
S = decodeint(s[b // 8:b // 4])
|
||||
h = Hint(encodepoint(R) + pk + m)
|
||||
|
||||
(x1, y1, z1, t1) = P = scalarmult_B(S)
|
||||
(x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h))
|
||||
|
||||
if (not isoncurve(P) or not isoncurve(Q) or
|
||||
(x1*z2 - x2*z1) % q != 0 or (y1*z2 - y2*z1) % q != 0):
|
||||
raise SignatureMismatch("signature does not pass verification")
|
||||
|
||||
|
||||
def is_identity(P):
|
||||
return True if P[0] == 0 else False
|
||||
|
||||
|
||||
def edwards_negated(P):
|
||||
(x, y, z, t) = P
|
||||
|
||||
zi = inv(z)
|
||||
x = q - ((x * zi) % q)
|
||||
y = (y * zi) % q
|
||||
z = 1
|
||||
t = (x * y) % q
|
||||
|
||||
return (x, y, z, t)
|
||||
@@ -1,486 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Implementation of elliptic curves, for cryptographic applications.
|
||||
#
|
||||
# This module doesn't provide any way to choose a random elliptic
|
||||
# curve, nor to verify that an elliptic curve was chosen randomly,
|
||||
# because one can simply use NIST's standard curves.
|
||||
#
|
||||
# Notes from X9.62-1998 (draft):
|
||||
# Nomenclature:
|
||||
# - Q is a public key.
|
||||
# The "Elliptic Curve Domain Parameters" include:
|
||||
# - q is the "field size", which in our case equals p.
|
||||
# - p is a big prime.
|
||||
# - G is a point of prime order (5.1.1.1).
|
||||
# - n is the order of G (5.1.1.1).
|
||||
# Public-key validation (5.2.2):
|
||||
# - Verify that Q is not the point at infinity.
|
||||
# - Verify that X_Q and Y_Q are in [0,p-1].
|
||||
# - Verify that Q is on the curve.
|
||||
# - Verify that nQ is the point at infinity.
|
||||
# Signature generation (5.3):
|
||||
# - Pick random k from [1,n-1].
|
||||
# Signature checking (5.4.2):
|
||||
# - Verify that r and s are in [1,n-1].
|
||||
#
|
||||
# Version of 2008.11.25.
|
||||
#
|
||||
# Revision history:
|
||||
# 2005.12.31 - Initial version.
|
||||
# 2008.11.25 - Change CurveFp.is_on to contains_point.
|
||||
#
|
||||
# Written in 2005 by Peter Pearson and placed in the public domain.
|
||||
|
||||
def inverse_mod(a, m):
|
||||
"""Inverse of a mod m."""
|
||||
|
||||
if a < 0 or m <= a:
|
||||
a = a % m
|
||||
|
||||
# From Ferguson and Schneier, roughly:
|
||||
|
||||
c, d = a, m
|
||||
uc, vc, ud, vd = 1, 0, 0, 1
|
||||
while c != 0:
|
||||
q, c, d = divmod(d, c) + (c,)
|
||||
uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc
|
||||
|
||||
# At this point, d is the GCD, and ud*a+vd*m = d.
|
||||
# If d == 1, this means that ud is a inverse.
|
||||
|
||||
assert d == 1
|
||||
if ud > 0:
|
||||
return ud
|
||||
else:
|
||||
return ud + m
|
||||
|
||||
|
||||
def modular_sqrt(a, p):
|
||||
# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
|
||||
""" Find a quadratic residue (mod p) of 'a'. p
|
||||
must be an odd prime.
|
||||
|
||||
Solve the congruence of the form:
|
||||
x^2 = a (mod p)
|
||||
And returns x. Note that p - x is also a root.
|
||||
|
||||
0 is returned is no square root exists for
|
||||
these a and p.
|
||||
|
||||
The Tonelli-Shanks algorithm is used (except
|
||||
for some simple cases in which the solution
|
||||
is known from an identity). This algorithm
|
||||
runs in polynomial time (unless the
|
||||
generalized Riemann hypothesis is false).
|
||||
"""
|
||||
# Simple cases
|
||||
#
|
||||
if legendre_symbol(a, p) != 1:
|
||||
return 0
|
||||
elif a == 0:
|
||||
return 0
|
||||
elif p == 2:
|
||||
return p
|
||||
elif p % 4 == 3:
|
||||
return pow(a, (p + 1) // 4, p)
|
||||
|
||||
# Partition p-1 to s * 2^e for an odd s (i.e.
|
||||
# reduce all the powers of 2 from p-1)
|
||||
#
|
||||
s = p - 1
|
||||
e = 0
|
||||
while s % 2 == 0:
|
||||
s /= 2
|
||||
e += 1
|
||||
|
||||
# Find some 'n' with a legendre symbol n|p = -1.
|
||||
# Shouldn't take long.
|
||||
#
|
||||
n = 2
|
||||
while legendre_symbol(n, p) != -1:
|
||||
n += 1
|
||||
|
||||
# Here be dragons!
|
||||
# Read the paper "Square roots from 1; 24, 51,
|
||||
# 10 to Dan Shanks" by Ezra Brown for more
|
||||
# information
|
||||
#
|
||||
|
||||
# x is a guess of the square root that gets better
|
||||
# with each iteration.
|
||||
# b is the "fudge factor" - by how much we're off
|
||||
# with the guess. The invariant x^2 = ab (mod p)
|
||||
# is maintained throughout the loop.
|
||||
# g is used for successive powers of n to update
|
||||
# both a and b
|
||||
# r is the exponent - decreases with each update
|
||||
#
|
||||
x = pow(a, (s + 1) // 2, p)
|
||||
b = pow(a, s, p)
|
||||
g = pow(n, s, p)
|
||||
r = e
|
||||
|
||||
while True:
|
||||
t = b
|
||||
m = 0
|
||||
for m in range(r):
|
||||
if t == 1:
|
||||
break
|
||||
t = pow(t, 2, p)
|
||||
|
||||
if m == 0:
|
||||
return x
|
||||
|
||||
gs = pow(g, 2 ** (r - m - 1), p)
|
||||
g = (gs * gs) % p
|
||||
x = (x * gs) % p
|
||||
b = (b * g) % p
|
||||
r = m
|
||||
|
||||
|
||||
def legendre_symbol(a, p):
|
||||
""" Compute the Legendre symbol a|p using
|
||||
Euler's criterion. p is a prime, a is
|
||||
relatively prime to p (if p divides
|
||||
a, then a|p = 0)
|
||||
|
||||
Returns 1 if a has a square root modulo
|
||||
p, -1 otherwise.
|
||||
"""
|
||||
ls = pow(a, (p - 1) // 2, p)
|
||||
return -1 if ls == p - 1 else ls
|
||||
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
|
||||
class CurveFp(object):
|
||||
"""Elliptic Curve over the field of integers modulo a prime."""
|
||||
def __init__(self, p, a, b):
|
||||
"""The curve of points satisfying y^2 = x^3 + a*x + b (mod p)."""
|
||||
self.__p = p
|
||||
self.__a = a
|
||||
self.__b = b
|
||||
|
||||
def p(self):
|
||||
return self.__p
|
||||
|
||||
def a(self):
|
||||
return self.__a
|
||||
|
||||
def b(self):
|
||||
return self.__b
|
||||
|
||||
def contains_point(self, x, y):
|
||||
"""Is the point (x,y) on this curve?"""
|
||||
return (y * y - (x * x * x + self.__a * x + self.__b)) % self.__p == 0
|
||||
|
||||
|
||||
class Point(object):
|
||||
""" A point on an elliptic curve. Altering x and y is forbidding,
|
||||
but they can be read by the x() and y() methods."""
|
||||
def __init__(self, curve, x, y, order=None):
|
||||
"""curve, x, y, order; order (optional) is the order of this point."""
|
||||
self.__curve = curve
|
||||
self.__x = x
|
||||
self.__y = y
|
||||
self.__order = order
|
||||
# self.curve is allowed to be None only for INFINITY:
|
||||
if self.__curve:
|
||||
assert self.__curve.contains_point(x, y)
|
||||
if order:
|
||||
assert self * order == INFINITY
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Return 1 if the points are identical, 0 otherwise."""
|
||||
if self.__curve == other.__curve \
|
||||
and self.__x == other.__x \
|
||||
and self.__y == other.__y:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __add__(self, other):
|
||||
"""Add one point to another point."""
|
||||
|
||||
# X9.62 B.3:
|
||||
if other == INFINITY:
|
||||
return self
|
||||
if self == INFINITY:
|
||||
return other
|
||||
assert self.__curve == other.__curve
|
||||
if self.__x == other.__x:
|
||||
if (self.__y + other.__y) % self.__curve.p() == 0:
|
||||
return INFINITY
|
||||
else:
|
||||
return self.double()
|
||||
|
||||
p = self.__curve.p()
|
||||
|
||||
l = ((other.__y - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
|
||||
|
||||
x3 = (l * l - self.__x - other.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def __sub__(self, other):
|
||||
#The inverse of a point P=(xP,yP) is its reflexion across the x-axis : P′=(xP,−yP).
|
||||
#If you want to compute Q−P, just replace yP by −yP in the usual formula for point addition.
|
||||
|
||||
# X9.62 B.3:
|
||||
if other == INFINITY:
|
||||
return self
|
||||
if self == INFINITY:
|
||||
return other
|
||||
assert self.__curve == other.__curve
|
||||
|
||||
p = self.__curve.p()
|
||||
#opi = inverse_mod(other.__y, p)
|
||||
opi = -other.__y % p
|
||||
#print(opi)
|
||||
#print(-other.__y % p)
|
||||
|
||||
if self.__x == other.__x:
|
||||
if (self.__y + opi) % self.__curve.p() == 0:
|
||||
return INFINITY
|
||||
else:
|
||||
return self.double
|
||||
|
||||
l = ((opi - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
|
||||
|
||||
x3 = (l * l - self.__x - other.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def __mul__(self, e):
|
||||
if self.__order:
|
||||
e %= self.__order
|
||||
if e == 0 or self == INFINITY:
|
||||
return INFINITY
|
||||
result, q = INFINITY, self
|
||||
while e:
|
||||
if e & 1:
|
||||
result += q
|
||||
e, q = e >> 1, q.double()
|
||||
return result
|
||||
|
||||
"""
|
||||
def __mul__(self, other):
|
||||
#Multiply a point by an integer.
|
||||
|
||||
def leftmost_bit( x ):
|
||||
assert x > 0
|
||||
result = 1
|
||||
while result <= x: result = 2 * result
|
||||
return result // 2
|
||||
|
||||
e = other
|
||||
if self.__order: e = e % self.__order
|
||||
if e == 0: return INFINITY
|
||||
if self == INFINITY: return INFINITY
|
||||
assert e > 0
|
||||
|
||||
# From X9.62 D.3.2:
|
||||
|
||||
e3 = 3 * e
|
||||
negative_self = Point( self.__curve, self.__x, -self.__y, self.__order )
|
||||
i = leftmost_bit( e3 ) // 2
|
||||
result = self
|
||||
# print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 )
|
||||
while i > 1:
|
||||
result = result.double()
|
||||
if ( e3 & i ) != 0 and ( e & i ) == 0: result = result + self
|
||||
if ( e3 & i ) == 0 and ( e & i ) != 0: result = result + negative_self
|
||||
# print ". . . i = %d, result = %s" % ( i, result )
|
||||
i = i // 2
|
||||
|
||||
return result
|
||||
"""
|
||||
|
||||
def __rmul__(self, other):
|
||||
"""Multiply a point by an integer."""
|
||||
|
||||
return self * other
|
||||
|
||||
def __str__(self):
|
||||
if self == INFINITY:
|
||||
return "infinity"
|
||||
return "(%d, %d)" % (self.__x, self.__y)
|
||||
|
||||
def inverse(self):
|
||||
return Point(self.__curve, self.__x, -self.__y % self.__curve.p())
|
||||
|
||||
def double(self):
|
||||
"""Return a new point that is twice the old."""
|
||||
|
||||
if self == INFINITY:
|
||||
return INFINITY
|
||||
|
||||
# X9.62 B.3:
|
||||
|
||||
p = self.__curve.p()
|
||||
a = self.__curve.a()
|
||||
|
||||
l = ((3 * self.__x * self.__x + a) * inverse_mod(2 * self.__y, p)) % p
|
||||
|
||||
x3 = (l * l - 2 * self.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def x(self):
|
||||
return self.__x
|
||||
|
||||
def y(self):
|
||||
return self.__y
|
||||
|
||||
def pair(self):
|
||||
return (self.__x, self.__y)
|
||||
|
||||
def curve(self):
|
||||
return self.__curve
|
||||
|
||||
def order(self):
|
||||
return self.__order
|
||||
|
||||
|
||||
# This one point is the Point At Infinity for all purposes:
|
||||
INFINITY = Point(None, None, None)
|
||||
|
||||
|
||||
def __main__():
|
||||
|
||||
class FailedTest(Exception):
|
||||
pass
|
||||
|
||||
def test_add(c, x1, y1, x2, y2, x3, y3):
|
||||
"""We expect that on curve c, (x1,y1) + (x2, y2 ) = (x3, y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p2 = Point(c, x2, y2)
|
||||
p3 = p1 + p2
|
||||
print("%s + %s = %s" % (p1, p2, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_double(c, x1, y1, x3, y3):
|
||||
"""We expect that on curve c, 2*(x1,y1) = (x3, y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p3 = p1.double()
|
||||
print("%s doubled = %s" % (p1, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_double_infinity(c):
|
||||
"""We expect that on curve c, 2*INFINITY = INFINITY."""
|
||||
p1 = INFINITY
|
||||
p3 = p1.double()
|
||||
print("%s doubled = %s" % (p1, p3))
|
||||
if p3.x() != INFINITY.x() or p3.y() != INFINITY.y():
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (INFINITY.x(), INFINITY.y()))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_multiply(c, x1, y1, m, x3, y3):
|
||||
"""We expect that on curve c, m*(x1,y1) = (x3,y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p3 = p1 * m
|
||||
print("%s * %d = %s" % (p1, m, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
# A few tests from X9.62 B.3:
|
||||
|
||||
c = CurveFp(23, 1, 1)
|
||||
test_add(c, 3, 10, 9, 7, 17, 20)
|
||||
test_double(c, 3, 10, 7, 12)
|
||||
test_add(c, 3, 10, 3, 10, 7, 12) # (Should just invoke double.)
|
||||
test_multiply(c, 3, 10, 2, 7, 12)
|
||||
|
||||
test_double_infinity(c)
|
||||
|
||||
# From X9.62 I.1 (p. 96):
|
||||
|
||||
g = Point(c, 13, 7, 7)
|
||||
|
||||
check = INFINITY
|
||||
for i in range(7 + 1):
|
||||
p = (i % 7) * g
|
||||
print("%s * %d = %s, expected %s . . ." % (g, i, p, check))
|
||||
if p == check:
|
||||
print(" Good.")
|
||||
else:
|
||||
raise FailedTest("Bad.")
|
||||
check = check + g
|
||||
|
||||
# NIST Curve P-192:
|
||||
p = 6277101735386680763835789423207666416083908700390324961279
|
||||
r = 6277101735386680763835789423176059013767194773182842284081
|
||||
#s = 0x3045ae6fc8422f64ed579528d38120eae12196d5L
|
||||
c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65
|
||||
b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
|
||||
Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
|
||||
Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
|
||||
|
||||
c192 = CurveFp(p, -3, b)
|
||||
p192 = Point(c192, Gx, Gy, r)
|
||||
|
||||
# Checking against some sample computations presented
|
||||
# in X9.62:
|
||||
|
||||
d = 651056770906015076056810763456358567190100156695615665659
|
||||
Q = d * p192
|
||||
if Q.x() != 0x62B12D60690CDCF330BABAB6E69763B471F994DD702D16A5:
|
||||
raise FailedTest("p192 * d came out wrong.")
|
||||
else:
|
||||
print("p192 * d came out right.")
|
||||
|
||||
k = 6140507067065001063065065565667405560006161556565665656654
|
||||
R = k * p192
|
||||
if R.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
|
||||
or R.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
|
||||
raise FailedTest("k * p192 came out wrong.")
|
||||
else:
|
||||
print("k * p192 came out right.")
|
||||
|
||||
u1 = 2563697409189434185194736134579731015366492496392189760599
|
||||
u2 = 6266643813348617967186477710235785849136406323338782220568
|
||||
temp = u1 * p192 + u2 * Q
|
||||
if temp.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
|
||||
or temp.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
|
||||
raise FailedTest("u1 * p192 + u2 * Q came out wrong.")
|
||||
else:
|
||||
print("u1 * p192 + u2 * Q came out right.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
__main__()
|
||||
@@ -1,386 +0,0 @@
|
||||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
||||
WARNING: This code is slow, uses bad randomness, does not properly protect
|
||||
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
||||
anything but tests."""
|
||||
import random
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
|
||||
"""
|
||||
t1, t2 = 0, 1
|
||||
r1, r2 = n, a
|
||||
while r2 != 0:
|
||||
q = r1 // r2
|
||||
t1, t2 = t2, t1 - q * t2
|
||||
r1, r2 = r2, r1 - q * r2
|
||||
if r1 > 1:
|
||||
return None
|
||||
if t1 < 0:
|
||||
t1 += n
|
||||
return t1
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
def modsqrt(a, p):
|
||||
"""Compute the square root of a modulo p when p % 4 = 3.
|
||||
|
||||
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
|
||||
|
||||
Limiting this function to only work for p % 4 = 3 means we don't need to
|
||||
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
|
||||
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
|
||||
|
||||
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
|
||||
"""
|
||||
if p % 4 != 3:
|
||||
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
|
||||
sqrt = pow(a, (p + 1)//4, p)
|
||||
if pow(sqrt, 2, p) == a % p:
|
||||
return sqrt
|
||||
return None
|
||||
|
||||
class EllipticCurve:
|
||||
def __init__(self, p, a, b):
|
||||
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
|
||||
self.p = p
|
||||
self.a = a % p
|
||||
self.b = b % p
|
||||
|
||||
def affine(self, p1):
|
||||
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
|
||||
|
||||
An affine point is represented as the Jacobian (x, y, 1)"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return None
|
||||
inv = modinv(z1, self.p)
|
||||
inv_2 = (inv**2) % self.p
|
||||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
return (x1, (self.p - y1) % self.p, z1)
|
||||
|
||||
def on_curve(self, p1):
|
||||
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
|
||||
x1, y1, z1 = p1
|
||||
z2 = pow(z1, 2, self.p)
|
||||
z4 = pow(z2, 2, self.p)
|
||||
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
|
||||
|
||||
def is_x_coord(self, x):
|
||||
"""Test whether x is a valid X coordinate on the curve."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return (0, 1, 0)
|
||||
y1_2 = (y1**2) % self.p
|
||||
y1_4 = (y1_2**2) % self.p
|
||||
x1_2 = (x1**2) % self.p
|
||||
s = (4*x1*y1_2) % self.p
|
||||
m = 3*x1_2
|
||||
if self.a:
|
||||
m += self.a * pow(z1, 4, self.p)
|
||||
m = m % self.p
|
||||
x2 = (m**2 - 2*s) % self.p
|
||||
y2 = (m*(s - x2) - 8*y1_4) % self.p
|
||||
z2 = (2*y1*z1) % self.p
|
||||
return (x2, y2, z2)
|
||||
|
||||
def add_mixed(self, p1, p2):
|
||||
"""Add a Jacobian tuple p1 and an affine tuple p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
assert(z2 == 1)
|
||||
# Adding to the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if x1 == u2:
|
||||
if (y1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - x1
|
||||
r = s2 - y1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (x1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
|
||||
z3 = (h*z1) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def add(self, p1, p2):
|
||||
"""Add two Jacobian tuples p1 and p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
# Adding the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
if z2 == 0:
|
||||
return p1
|
||||
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
|
||||
if z1 == 1:
|
||||
return self.add_mixed(p2, p1)
|
||||
if z2 == 1:
|
||||
return self.add_mixed(p1, p2)
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
z2_2 = (z2**2) % self.p
|
||||
z2_3 = (z2_2 * z2) % self.p
|
||||
u1 = (x1 * z2_2) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s1 = (y1 * z2_3) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if u1 == u2:
|
||||
if (s1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (u1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
|
||||
z3 = (h*z1*z2) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def mul(self, ps):
|
||||
"""Compute a (multi) point multiplication
|
||||
|
||||
ps is a list of (Jacobian tuple, scalar) pairs.
|
||||
"""
|
||||
r = (0, 1, 0)
|
||||
for i in range(255, -1, -1):
|
||||
r = self.double(r)
|
||||
for (p, n) in ps:
|
||||
if ((n >> i) & 1):
|
||||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
||||
class ECPubKey():
|
||||
"""A secp256k1 public key"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct an uninitialized public key"""
|
||||
self.valid = False
|
||||
|
||||
def set(self, data):
|
||||
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
||||
if (len(data) == 65 and data[0] == 0x04):
|
||||
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
|
||||
x = int.from_bytes(data[1:33], 'big')
|
||||
if SECP256K1.is_x_coord(x):
|
||||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
self.compressed = True
|
||||
else:
|
||||
self.valid = False
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
def get_bytes(self):
|
||||
assert(self.valid)
|
||||
p = SECP256K1.affine(self.p)
|
||||
if p is None:
|
||||
return None
|
||||
if self.compressed:
|
||||
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
|
||||
else:
|
||||
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
|
||||
|
||||
def verify_ecdsa(self, sig, msg, low_s=True):
|
||||
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA verifier algorithm"""
|
||||
assert(self.valid)
|
||||
|
||||
# Extract r and s from the DER formatted signature. Return false for
|
||||
# any DER encoding errors.
|
||||
if (sig[1] + 2 != len(sig)):
|
||||
return False
|
||||
if (len(sig) < 4):
|
||||
return False
|
||||
if (sig[0] != 0x30):
|
||||
return False
|
||||
if (sig[2] != 0x02):
|
||||
return False
|
||||
rlen = sig[3]
|
||||
if (len(sig) < 6 + rlen):
|
||||
return False
|
||||
if rlen < 1 or rlen > 33:
|
||||
return False
|
||||
if sig[4] >= 0x80:
|
||||
return False
|
||||
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
||||
return False
|
||||
r = int.from_bytes(sig[4:4+rlen], 'big')
|
||||
if (sig[4+rlen] != 0x02):
|
||||
return False
|
||||
slen = sig[5+rlen]
|
||||
if slen < 1 or slen > 33:
|
||||
return False
|
||||
if (len(sig) != 6 + rlen + slen):
|
||||
return False
|
||||
if sig[6+rlen] >= 0x80:
|
||||
return False
|
||||
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
||||
return False
|
||||
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
||||
|
||||
# Verify that r and s are within the group order
|
||||
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
|
||||
return False
|
||||
if low_s and s >= SECP256K1_ORDER_HALF:
|
||||
return False
|
||||
z = int.from_bytes(msg, 'big')
|
||||
|
||||
# Run verifier algorithm on r, s
|
||||
w = modinv(s, SECP256K1_ORDER)
|
||||
u1 = z*w % SECP256K1_ORDER
|
||||
u2 = r*w % SECP256K1_ORDER
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
|
||||
if R is None or R[0] != r:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid = False
|
||||
|
||||
def set(self, secret, compressed):
|
||||
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
||||
assert(len(secret) == 32)
|
||||
secret = int.from_bytes(secret, 'big')
|
||||
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
|
||||
if self.valid:
|
||||
self.secret = secret
|
||||
self.compressed = compressed
|
||||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
assert(self.valid)
|
||||
return self.secret.to_bytes(32, 'big')
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
def get_pubkey(self):
|
||||
"""Compute an ECPubKey object for this secret key."""
|
||||
assert(self.valid)
|
||||
ret = ECPubKey()
|
||||
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
|
||||
ret.p = p
|
||||
ret.valid = True
|
||||
ret.compressed = self.compressed
|
||||
return ret
|
||||
|
||||
def sign_ecdsa(self, msg, low_s=True):
|
||||
"""Construct a DER-encoded ECDSA signature with this key.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA signer algorithm."""
|
||||
assert(self.valid)
|
||||
z = int.from_bytes(msg, 'big')
|
||||
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
||||
k = random.randrange(1, SECP256K1_ORDER)
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
|
||||
r = R[0] % SECP256K1_ORDER
|
||||
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
|
||||
if low_s and s > SECP256K1_ORDER_HALF:
|
||||
s = SECP256K1_ORDER - s
|
||||
# Represent in DER format. The byte representations of r and s have
|
||||
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
||||
# bytes).
|
||||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
||||
@@ -1,393 +0,0 @@
|
||||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
||||
WARNING: This code is slow, uses bad randomness, does not properly protect
|
||||
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
||||
anything but tests."""
|
||||
import random
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
|
||||
"""
|
||||
t1, t2 = 0, 1
|
||||
r1, r2 = n, a
|
||||
while r2 != 0:
|
||||
q = r1 // r2
|
||||
t1, t2 = t2, t1 - q * t2
|
||||
r1, r2 = r2, r1 - q * r2
|
||||
if r1 > 1:
|
||||
return None
|
||||
if t1 < 0:
|
||||
t1 += n
|
||||
return t1
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
def modsqrt(a, p):
|
||||
"""Compute the square root of a modulo p when p % 4 = 3.
|
||||
|
||||
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
|
||||
|
||||
Limiting this function to only work for p % 4 = 3 means we don't need to
|
||||
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
|
||||
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
|
||||
|
||||
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
|
||||
"""
|
||||
if p % 4 != 3:
|
||||
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
|
||||
sqrt = pow(a, (p + 1)//4, p)
|
||||
if pow(sqrt, 2, p) == a % p:
|
||||
return sqrt
|
||||
return None
|
||||
|
||||
class EllipticCurve:
|
||||
def __init__(self, p, a, b):
|
||||
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
|
||||
self.p = p
|
||||
self.a = a % p
|
||||
self.b = b % p
|
||||
|
||||
def affine(self, p1):
|
||||
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
|
||||
|
||||
An affine point is represented as the Jacobian (x, y, 1)"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return None
|
||||
inv = modinv(z1, self.p)
|
||||
inv_2 = (inv**2) % self.p
|
||||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
return (x1, (self.p - y1) % self.p, z1)
|
||||
|
||||
def on_curve(self, p1):
|
||||
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
|
||||
x1, y1, z1 = p1
|
||||
z2 = pow(z1, 2, self.p)
|
||||
z4 = pow(z2, 2, self.p)
|
||||
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
|
||||
|
||||
def is_x_coord(self, x):
|
||||
"""Test whether x is a valid X coordinate on the curve."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return (0, 1, 0)
|
||||
y1_2 = (y1**2) % self.p
|
||||
y1_4 = (y1_2**2) % self.p
|
||||
x1_2 = (x1**2) % self.p
|
||||
s = (4*x1*y1_2) % self.p
|
||||
m = 3*x1_2
|
||||
if self.a:
|
||||
m += self.a * pow(z1, 4, self.p)
|
||||
m = m % self.p
|
||||
x2 = (m**2 - 2*s) % self.p
|
||||
y2 = (m*(s - x2) - 8*y1_4) % self.p
|
||||
z2 = (2*y1*z1) % self.p
|
||||
return (x2, y2, z2)
|
||||
|
||||
def add_mixed(self, p1, p2):
|
||||
"""Add a Jacobian tuple p1 and an affine tuple p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
assert(z2 == 1)
|
||||
# Adding to the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if x1 == u2:
|
||||
if (y1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - x1
|
||||
r = s2 - y1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (x1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
|
||||
z3 = (h*z1) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def add(self, p1, p2):
|
||||
"""Add two Jacobian tuples p1 and p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
# Adding the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
if z2 == 0:
|
||||
return p1
|
||||
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
|
||||
if z1 == 1:
|
||||
return self.add_mixed(p2, p1)
|
||||
if z2 == 1:
|
||||
return self.add_mixed(p1, p2)
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
z2_2 = (z2**2) % self.p
|
||||
z2_3 = (z2_2 * z2) % self.p
|
||||
u1 = (x1 * z2_2) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s1 = (y1 * z2_3) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if u1 == u2:
|
||||
if (s1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (u1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
|
||||
z3 = (h*z1*z2) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def mul(self, ps):
|
||||
"""Compute a (multi) point multiplication
|
||||
|
||||
ps is a list of (Jacobian tuple, scalar) pairs.
|
||||
"""
|
||||
r = (0, 1, 0)
|
||||
for i in range(255, -1, -1):
|
||||
r = self.double(r)
|
||||
for (p, n) in ps:
|
||||
if ((n >> i) & 1):
|
||||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
||||
class ECPubKey():
|
||||
"""A secp256k1 public key"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct an uninitialized public key"""
|
||||
self.valid = False
|
||||
|
||||
def set_int(self, x, y):
|
||||
p = (x, y, 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
|
||||
def set(self, data):
|
||||
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
||||
if (len(data) == 65 and data[0] == 0x04):
|
||||
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
|
||||
x = int.from_bytes(data[1:33], 'big')
|
||||
if SECP256K1.is_x_coord(x):
|
||||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
self.compressed = True
|
||||
else:
|
||||
self.valid = False
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
def get_bytes(self):
|
||||
assert(self.valid)
|
||||
p = SECP256K1.affine(self.p)
|
||||
if p is None:
|
||||
return None
|
||||
if self.compressed:
|
||||
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
|
||||
else:
|
||||
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
|
||||
|
||||
def verify_ecdsa(self, sig, msg, low_s=True):
|
||||
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA verifier algorithm"""
|
||||
assert(self.valid)
|
||||
|
||||
# Extract r and s from the DER formatted signature. Return false for
|
||||
# any DER encoding errors.
|
||||
if (sig[1] + 2 != len(sig)):
|
||||
return False
|
||||
if (len(sig) < 4):
|
||||
return False
|
||||
if (sig[0] != 0x30):
|
||||
return False
|
||||
if (sig[2] != 0x02):
|
||||
return False
|
||||
rlen = sig[3]
|
||||
if (len(sig) < 6 + rlen):
|
||||
return False
|
||||
if rlen < 1 or rlen > 33:
|
||||
return False
|
||||
if sig[4] >= 0x80:
|
||||
return False
|
||||
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
||||
return False
|
||||
r = int.from_bytes(sig[4:4+rlen], 'big')
|
||||
if (sig[4+rlen] != 0x02):
|
||||
return False
|
||||
slen = sig[5+rlen]
|
||||
if slen < 1 or slen > 33:
|
||||
return False
|
||||
if (len(sig) != 6 + rlen + slen):
|
||||
return False
|
||||
if sig[6+rlen] >= 0x80:
|
||||
return False
|
||||
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
||||
return False
|
||||
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
||||
|
||||
# Verify that r and s are within the group order
|
||||
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
|
||||
return False
|
||||
if low_s and s >= SECP256K1_ORDER_HALF:
|
||||
return False
|
||||
z = int.from_bytes(msg, 'big')
|
||||
|
||||
# Run verifier algorithm on r, s
|
||||
w = modinv(s, SECP256K1_ORDER)
|
||||
u1 = z*w % SECP256K1_ORDER
|
||||
u2 = r*w % SECP256K1_ORDER
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
|
||||
if R is None or R[0] != r:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid = False
|
||||
|
||||
def set(self, secret, compressed):
|
||||
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
||||
assert(len(secret) == 32)
|
||||
secret = int.from_bytes(secret, 'big')
|
||||
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
|
||||
if self.valid:
|
||||
self.secret = secret
|
||||
self.compressed = compressed
|
||||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
assert(self.valid)
|
||||
return self.secret.to_bytes(32, 'big')
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
def get_pubkey(self):
|
||||
"""Compute an ECPubKey object for this secret key."""
|
||||
assert(self.valid)
|
||||
ret = ECPubKey()
|
||||
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
|
||||
ret.p = p
|
||||
ret.valid = True
|
||||
ret.compressed = self.compressed
|
||||
return ret
|
||||
|
||||
def sign_ecdsa(self, msg, low_s=True):
|
||||
"""Construct a DER-encoded ECDSA signature with this key.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA signer algorithm."""
|
||||
assert(self.valid)
|
||||
z = int.from_bytes(msg, 'big')
|
||||
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
||||
k = random.randrange(1, SECP256K1_ORDER)
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
|
||||
r = R[0] % SECP256K1_ORDER
|
||||
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
|
||||
if low_s and s > SECP256K1_ORDER_HALF:
|
||||
s = SECP256K1_ORDER - s
|
||||
# Represent in DER format. The byte representations of r and s have
|
||||
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
||||
# bytes).
|
||||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
||||
@@ -640,7 +640,7 @@ class CTransaction:
|
||||
self.hash = tx.hash
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = int.from_bytes(f.read(1), "little")
|
||||
if self.nVersion == PARTICL_TX_VERSION:
|
||||
self.nVersion |= int.from_bytes(f.read(1), "little") << 8
|
||||
@@ -668,7 +668,7 @@ class CTransaction:
|
||||
# self.nVersion = int.from_bytes(f.read(4), "little")
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
flags = 0
|
||||
if len(self.vin) == 0:
|
||||
if len(self.vin) == 0 and allow_witness:
|
||||
flags = int.from_bytes(f.read(1), "little")
|
||||
# Not sure why flags can't be zero, but this
|
||||
# matches the implementation in bitcoind
|
||||
|
||||
@@ -166,6 +166,9 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API):
|
||||
def _message_received_(self, handler, msg):
|
||||
self.message_received(self.handler_to_client(handler), self, msg)
|
||||
|
||||
def _binary_message_received_(self, handler, msg):
|
||||
self.binary_message_received(self.handler_to_client(handler), self, msg)
|
||||
|
||||
def _ping_received_(self, handler, msg):
|
||||
handler.send_pong(msg)
|
||||
|
||||
@@ -309,6 +312,7 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
opcode = b1 & OPCODE
|
||||
masked = b2 & MASKED
|
||||
payload_length = b2 & PAYLOAD_LEN
|
||||
is_binary: bool = False
|
||||
|
||||
if opcode == OPCODE_CLOSE_CONN:
|
||||
logger.info("Client asked to close connection.")
|
||||
@@ -322,8 +326,8 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
logger.warning("Continuation frames are not supported.")
|
||||
return
|
||||
elif opcode == OPCODE_BINARY:
|
||||
logger.warning("Binary frames are not supported.")
|
||||
return
|
||||
is_binary = True
|
||||
opcode_handler = self.server._binary_message_received_
|
||||
elif opcode == OPCODE_TEXT:
|
||||
opcode_handler = self.server._message_received_
|
||||
elif opcode == OPCODE_PING:
|
||||
@@ -345,7 +349,8 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
for message_byte in self.read_bytes(payload_length):
|
||||
message_byte ^= masks[len(message_bytes) % 4]
|
||||
message_bytes.append(message_byte)
|
||||
opcode_handler(self, message_bytes.decode('utf8'))
|
||||
|
||||
opcode_handler(self, message_bytes if is_binary else message_bytes.decode('utf8'))
|
||||
|
||||
def send_message(self, message):
|
||||
self.send_text(message)
|
||||
@@ -375,6 +380,35 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def send_bytes(self, message, opcode=OPCODE_BINARY):
|
||||
header = bytearray()
|
||||
payload = message
|
||||
payload_length = len(payload)
|
||||
|
||||
# Normal payload
|
||||
if payload_length <= 125:
|
||||
header.append(FIN | opcode)
|
||||
header.append(payload_length)
|
||||
|
||||
# Extended payload
|
||||
elif payload_length >= 126 and payload_length <= 65535:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT16)
|
||||
header.extend(struct.pack(">H", payload_length))
|
||||
|
||||
# Huge extended payload
|
||||
elif payload_length < 18446744073709551616:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT64)
|
||||
header.extend(struct.pack(">Q", payload_length))
|
||||
|
||||
else:
|
||||
raise Exception("Message is too big. Consider breaking it into chunks.")
|
||||
return
|
||||
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def send_text(self, message, opcode=OPCODE_TEXT):
|
||||
"""
|
||||
Important: Fragmented(=continuation) messages are not supported since
|
||||
|
||||
347
basicswap/db.py
347
basicswap/db.py
@@ -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 = 32
|
||||
CURRENT_DB_DATA_VERSION = 7
|
||||
|
||||
|
||||
class Concepts(IntEnum):
|
||||
@@ -185,6 +185,7 @@ class Offer(Table):
|
||||
amount_negotiable = Column("bool")
|
||||
rate_negotiable = Column("bool")
|
||||
auto_accept_type = Column("integer")
|
||||
message_nets = Column("string")
|
||||
|
||||
# Local fields
|
||||
auto_accept_bids = Column("bool")
|
||||
@@ -194,6 +195,7 @@ class Offer(Table):
|
||||
) # Address to spend lock tx to - address from wallet if empty TODO
|
||||
security_token = Column("blob")
|
||||
bid_reversed = Column("bool")
|
||||
smsg_payload_version = Column("integer")
|
||||
|
||||
state = Column("integer")
|
||||
states = Column("blob") # Packed states and times
|
||||
@@ -219,6 +221,7 @@ class Bid(Table):
|
||||
bid_addr = Column("string")
|
||||
pk_bid_addr = Column("blob")
|
||||
proof_address = Column("string")
|
||||
proof_signature = Column("blob")
|
||||
proof_utxos = Column("blob")
|
||||
# Address to spend lock tx to - address from wallet if empty TODO
|
||||
withdraw_to_addr = Column("string")
|
||||
@@ -232,6 +235,7 @@ class Bid(Table):
|
||||
rate = Column("integer")
|
||||
|
||||
pkhash_seller = Column("blob")
|
||||
message_nets = Column("string")
|
||||
|
||||
initiate_txn_redeem = Column("blob")
|
||||
initiate_txn_refund = Column("blob")
|
||||
@@ -380,6 +384,8 @@ class SmsgAddress(Table):
|
||||
use_type = Column("integer")
|
||||
note = Column("string")
|
||||
|
||||
index = Index("smsgaddresses_address_index", "addr")
|
||||
|
||||
|
||||
class Action(Table):
|
||||
__tablename__ = "actions"
|
||||
@@ -485,6 +491,14 @@ class XmrSwap(Table):
|
||||
|
||||
b_lock_tx_id = Column("blob")
|
||||
|
||||
msg_split_info = Column("string")
|
||||
|
||||
def getMsgSplitInfo(self):
|
||||
if self.msg_split_info is None:
|
||||
return 16000, 17000
|
||||
msg_split_info = self.msg_split_info.split(":")
|
||||
return int(msg_split_info[0]), int(msg_split_info[1])
|
||||
|
||||
|
||||
class XmrSplitData(Table):
|
||||
__tablename__ = "xmr_split_data"
|
||||
@@ -605,6 +619,8 @@ class BidState(Table):
|
||||
swap_failed = Column("integer")
|
||||
swap_ended = Column("integer")
|
||||
can_accept = Column("integer")
|
||||
can_expire = Column("integer")
|
||||
can_timeout = Column("integer")
|
||||
|
||||
note = Column("string")
|
||||
created_at = Column("integer")
|
||||
@@ -658,82 +674,257 @@ class CoinRates(Table):
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class CoinVolume(Table):
|
||||
__tablename__ = "coinvolume"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_id = Column("integer")
|
||||
volume_24h = Column("string")
|
||||
price_change_24h = Column("string")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class CoinHistory(Table):
|
||||
__tablename__ = "coinhistory"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_id = Column("integer")
|
||||
days = Column("integer")
|
||||
price_data = Column("blob")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class MessageNetworks(Table):
|
||||
__tablename__ = "message_networks"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
name = Column("string")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class MessageNetworkLink(Table):
|
||||
__tablename__ = "message_network_links"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
|
||||
network_id = Column("string")
|
||||
link_type = Column("integer") # MessageNetworkLinkTypes
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRoute(Table):
|
||||
__tablename__ = "direct_message_routes"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
network_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
smsg_addr_local = Column("string")
|
||||
smsg_addr_remote = Column("string")
|
||||
# smsg_addr_id_local = Column("integer") # SmsgAddress
|
||||
# smsg_addr_id_remote = Column("integer") # KnownIdentity
|
||||
route_data = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRouteLink(Table):
|
||||
__tablename__ = "direct_message_route_links"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
direct_message_route_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class NetworkPortal(Table):
|
||||
__tablename__ = "network_portals"
|
||||
|
||||
def set(
|
||||
self, time_start, time_valid, network_from, network_to, address_from, address_to
|
||||
):
|
||||
super().__init__()
|
||||
self.active_ind = 1
|
||||
self.time_start = time_start
|
||||
self.time_valid = time_valid
|
||||
self.network_from = network_from
|
||||
self.network_to = network_to
|
||||
self.address_from = address_from
|
||||
self.address_to = address_to
|
||||
|
||||
self.smsg_difficulty = 0x1EFFFFFF
|
||||
|
||||
self.num_refreshes = 0
|
||||
self.messages_sent = 0
|
||||
self.responses_seen = 0
|
||||
self.time_last_used = 0
|
||||
self.num_issues = 0
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
own_portal = Column("integer")
|
||||
|
||||
address_from = Column("string", unique=True)
|
||||
address_to = Column("string")
|
||||
|
||||
network_from = Column("integer")
|
||||
network_to = Column("integer")
|
||||
|
||||
time_start = Column("integer")
|
||||
time_valid = Column("integer")
|
||||
smsg_difficulty = Column("integer")
|
||||
num_refreshes = Column("integer")
|
||||
|
||||
messages_sent = Column("integer")
|
||||
responses_seen = Column("integer")
|
||||
time_last_used = Column("integer")
|
||||
num_issues = Column("integer")
|
||||
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
def extract_schema() -> dict:
|
||||
g = globals().copy()
|
||||
tables = {}
|
||||
for name, obj in g.items():
|
||||
if not inspect.isclass(obj):
|
||||
continue
|
||||
if not hasattr(obj, "__sqlite3_table__"):
|
||||
continue
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
continue
|
||||
|
||||
table_name: str = obj.__tablename__
|
||||
table = {}
|
||||
columns = {}
|
||||
primary_key = None
|
||||
constraints = []
|
||||
indices = []
|
||||
for m in inspect.getmembers(obj):
|
||||
m_name, m_obj = m
|
||||
if hasattr(m_obj, "__sqlite3_primary_key__"):
|
||||
primary_key = m_obj
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_unique__"):
|
||||
constraints.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_index__"):
|
||||
indices.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_column__"):
|
||||
col_type: str = m_obj.column_type.upper()
|
||||
if col_type == "BOOL":
|
||||
col_type = "INTEGER"
|
||||
columns[m_name] = {
|
||||
"type": col_type,
|
||||
"primary_key": m_obj.primary_key,
|
||||
"unique": m_obj.unique,
|
||||
}
|
||||
table["columns"] = columns
|
||||
if primary_key is not None:
|
||||
table["primary_key"] = {"column_1": primary_key.column_1}
|
||||
if primary_key.column_2:
|
||||
table["primary_key"]["column_2"] = primary_key.column_2
|
||||
if primary_key.column_3:
|
||||
table["primary_key"]["column_3"] = primary_key.column_3
|
||||
|
||||
for constraint in constraints:
|
||||
if "constraints" not in table:
|
||||
table["constraints"] = []
|
||||
table_constraint = {"column_1": constraint.column_1}
|
||||
if constraint.column_2:
|
||||
table_constraint["column_2"] = constraint.column_2
|
||||
if constraint.column_3:
|
||||
table_constraint["column_3"] = constraint.column_3
|
||||
table["constraints"].append(table_constraint)
|
||||
|
||||
for i in indices:
|
||||
if "indices" not in table:
|
||||
table["indices"] = []
|
||||
table_index = {"index_name": i.name, "column_1": i.column_1}
|
||||
if i.column_2 is not None:
|
||||
table_index["column_2"] = i.column_2
|
||||
if i.column_3 is not None:
|
||||
table_index["column_3"] = i.column_3
|
||||
table["indices"].append(table_index)
|
||||
|
||||
tables[table_name] = table
|
||||
return tables
|
||||
|
||||
|
||||
def create_table(c, table_name, table) -> None:
|
||||
query: str = f"CREATE TABLE {table_name} ("
|
||||
|
||||
for i, (colname, column) in enumerate(table["columns"].items()):
|
||||
col_type = column["type"]
|
||||
query += ("," if i > 0 else "") + f" {colname} {col_type} "
|
||||
if column["primary_key"]:
|
||||
query += "PRIMARY KEY ASC "
|
||||
if column["unique"]:
|
||||
query += "UNIQUE "
|
||||
|
||||
if "primary_key" in table:
|
||||
column_1 = table["primary_key"]["column_1"]
|
||||
column_2 = table["primary_key"].get("column_2", None)
|
||||
column_3 = table["primary_key"].get("column_3", None)
|
||||
query += f", PRIMARY KEY ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ") "
|
||||
|
||||
constraints = table.get("constraints", [])
|
||||
for constraint in constraints:
|
||||
column_1 = constraint["column_1"]
|
||||
column_2 = constraint.get("column_2", None)
|
||||
column_3 = constraint.get("column_3", None)
|
||||
query += f", UNIQUE ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ") "
|
||||
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
indices = table.get("indices", [])
|
||||
for index in indices:
|
||||
index_name = index["index_name"]
|
||||
column_1 = index["column_1"]
|
||||
column_2 = index.get("column_2", None)
|
||||
column_3 = index.get("column_3", None)
|
||||
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
|
||||
def create_db_(con, log) -> None:
|
||||
db_schema = extract_schema()
|
||||
c = con.cursor()
|
||||
for table_name, table in db_schema.items():
|
||||
create_table(c, table_name, table)
|
||||
|
||||
|
||||
def create_db(db_path: str, log) -> None:
|
||||
con = None
|
||||
try:
|
||||
con = sqlite3.connect(db_path)
|
||||
c = con.cursor()
|
||||
|
||||
g = globals().copy()
|
||||
for name, obj in g.items():
|
||||
if not inspect.isclass(obj):
|
||||
continue
|
||||
if not hasattr(obj, "__sqlite3_table__"):
|
||||
continue
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
continue
|
||||
|
||||
table_name: str = obj.__tablename__
|
||||
query: str = f"CREATE TABLE {table_name} ("
|
||||
|
||||
primary_key = None
|
||||
constraints = []
|
||||
indices = []
|
||||
num_columns: int = 0
|
||||
for m in inspect.getmembers(obj):
|
||||
m_name, m_obj = m
|
||||
|
||||
if hasattr(m_obj, "__sqlite3_primary_key__"):
|
||||
primary_key = m_obj
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_unique__"):
|
||||
constraints.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_index__"):
|
||||
indices.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_column__"):
|
||||
if num_columns > 0:
|
||||
query += ","
|
||||
|
||||
col_type: str = m_obj.column_type.upper()
|
||||
if col_type == "BOOL":
|
||||
col_type = "INTEGER"
|
||||
query += f" {m_name} {col_type} "
|
||||
|
||||
if m_obj.primary_key:
|
||||
query += "PRIMARY KEY ASC "
|
||||
if m_obj.unique:
|
||||
query += "UNIQUE "
|
||||
num_columns += 1
|
||||
|
||||
if primary_key is not None:
|
||||
query += f", PRIMARY KEY ({primary_key.column_1}"
|
||||
if primary_key.column_2:
|
||||
query += f", {primary_key.column_2}"
|
||||
if primary_key.column_3:
|
||||
query += f", {primary_key.column_3}"
|
||||
query += ") "
|
||||
|
||||
for constraint in constraints:
|
||||
query += f", UNIQUE ({constraint.column_1}"
|
||||
if constraint.column_2:
|
||||
query += f", {constraint.column_2}"
|
||||
if constraint.column_3:
|
||||
query += f", {constraint.column_3}"
|
||||
query += ") "
|
||||
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
for i in indices:
|
||||
query: str = f"CREATE INDEX {i.name} ON {table_name} ({i.column_1}"
|
||||
if i.column_2 is not None:
|
||||
query += f", {i.column_2}"
|
||||
if i.column_3 is not None:
|
||||
query += f", {i.column_3}"
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
create_db_(con, log)
|
||||
con.commit()
|
||||
finally:
|
||||
if con:
|
||||
@@ -912,6 +1103,7 @@ class DBMethods:
|
||||
query += f"{key}=:{key}"
|
||||
|
||||
cursor.execute(query, values)
|
||||
return cursor.lastrowid
|
||||
|
||||
def query(
|
||||
self,
|
||||
@@ -963,8 +1155,11 @@ class DBMethods:
|
||||
query_data[cv_name] = cv
|
||||
query += ") "
|
||||
else:
|
||||
query += f" AND {ck} = :{ck} "
|
||||
query_data[ck] = constraint_value
|
||||
if constraint_value is None:
|
||||
query += f" AND {ck} IS NULL "
|
||||
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:
|
||||
|
||||
@@ -12,13 +12,17 @@ from .db import (
|
||||
AutomationStrategy,
|
||||
BidState,
|
||||
Concepts,
|
||||
create_table,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
CURRENT_DB_VERSION,
|
||||
extract_schema,
|
||||
)
|
||||
|
||||
from .basicswap_util import (
|
||||
BidStates,
|
||||
canAcceptBidState,
|
||||
canExpireBidState,
|
||||
canTimeoutBidState,
|
||||
isActiveBidState,
|
||||
isErrorBidState,
|
||||
isFailingBidState,
|
||||
@@ -37,6 +41,8 @@ def addBidState(self, state, now, cursor):
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
can_accept=canAcceptBidState(state),
|
||||
can_expire=canExpireBidState(state),
|
||||
can_timeout=canTimeoutBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now,
|
||||
),
|
||||
@@ -49,10 +55,9 @@ def upgradeDatabaseData(self, data_version):
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
"Upgrading database records from version %d to %d.",
|
||||
data_version,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
f"Upgrading database records from version {data_version} to {CURRENT_DB_DATA_VERSION}."
|
||||
)
|
||||
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
now = int(time.time())
|
||||
@@ -64,7 +69,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept All",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=False,
|
||||
created_at=now,
|
||||
@@ -77,7 +82,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept Known",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=True,
|
||||
note="Accept bids from identities with previously successful swaps only",
|
||||
@@ -104,19 +109,23 @@ def upgradeDatabaseData(self, data_version):
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
if data_version > 0 and data_version < 6:
|
||||
if data_version > 0 and data_version < 7:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
can_accept = canAcceptBidState(state)
|
||||
can_expire = canExpireBidState(state)
|
||||
can_timeout = canTimeoutBidState(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 can_accept = :can_accept, can_expire = :can_expire, can_timeout = :can_timeout, 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,
|
||||
"can_expire": can_expire,
|
||||
"can_timeout": can_timeout,
|
||||
"state_id": int(state),
|
||||
},
|
||||
)
|
||||
@@ -138,313 +147,137 @@ def upgradeDatabaseData(self, data_version):
|
||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||
self.setIntKV("db_data_version", self.db_data_version, cursor)
|
||||
self.commitDB()
|
||||
self.log.info(
|
||||
"Upgraded database records to version {}".format(self.db_data_version)
|
||||
)
|
||||
self.log.info(f"Upgraded database records to version {self.db_data_version}")
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
|
||||
def upgradeDatabase(self, db_version):
|
||||
if db_version >= CURRENT_DB_VERSION:
|
||||
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
# db_version, tablename, oldcolumnname, newcolumnname
|
||||
rename_columns = [
|
||||
(13, "actions", "event_id", "action_id"),
|
||||
(13, "actions", "event_type", "action_type"),
|
||||
(13, "actions", "event_data", "action_data"),
|
||||
(
|
||||
14,
|
||||
"xmr_swaps",
|
||||
"coin_a_lock_refund_spend_tx_msg_id",
|
||||
"coin_a_lock_spend_tx_msg_id",
|
||||
),
|
||||
]
|
||||
|
||||
current_version = db_version
|
||||
if current_version == 6:
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN security_token BLOB")
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN security_token BLOB")
|
||||
db_version += 1
|
||||
elif current_version == 7:
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN block_hash BLOB")
|
||||
expect_schema = extract_schema()
|
||||
have_tables = {}
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
|
||||
for rename_column in rename_columns:
|
||||
dbv, table_name, colname_from, colname_to = rename_column
|
||||
if db_version < dbv:
|
||||
cursor.execute(
|
||||
"ALTER TABLE transactions ADD COLUMN block_height INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN block_time INTEGER")
|
||||
db_version += 1
|
||||
elif current_version == 8:
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE wallets (
|
||||
record_id INTEGER NOT NULL,
|
||||
coin_id INTEGER,
|
||||
wallet_name VARCHAR,
|
||||
wallet_data VARCHAR,
|
||||
balance_type INTEGER,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
db_version += 1
|
||||
elif current_version == 9:
|
||||
cursor.execute("ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR")
|
||||
db_version += 1
|
||||
elif current_version == 10:
|
||||
cursor.execute(
|
||||
"ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR")
|
||||
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR")
|
||||
cursor.execute(
|
||||
"UPDATE smsgaddresses SET active_ind = 1, created_at = 1"
|
||||
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
|
||||
)
|
||||
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN addr_to VARCHAR")
|
||||
cursor.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
|
||||
db_version += 1
|
||||
elif current_version == 11:
|
||||
cursor.execute(
|
||||
"ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN protocol_version INTEGER")
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN protocol_version INTEGER")
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN tx_data BLOB")
|
||||
db_version += 1
|
||||
elif current_version == 12:
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE knownidentities (
|
||||
record_id INTEGER NOT NULL,
|
||||
address VARCHAR,
|
||||
label VARCHAR,
|
||||
publickey BLOB,
|
||||
num_sent_bids_successful INTEGER,
|
||||
num_recv_bids_successful INTEGER,
|
||||
num_sent_bids_rejected INTEGER,
|
||||
num_recv_bids_rejected INTEGER,
|
||||
num_sent_bids_failed INTEGER,
|
||||
num_recv_bids_failed INTEGER,
|
||||
note VARCHAR,
|
||||
updated_at BIGINT,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN reject_code INTEGER")
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN rate INTEGER")
|
||||
cursor.execute(
|
||||
"ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER")
|
||||
db_version += 1
|
||||
elif current_version == 13:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE automationstrategies (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
label VARCHAR,
|
||||
type_ind INTEGER,
|
||||
only_known_identities INTEGER,
|
||||
num_concurrent INTEGER,
|
||||
data BLOB,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE automationlinks (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
strategy_id INTEGER,
|
||||
|
||||
data BLOB,
|
||||
repeat_limit INTEGER,
|
||||
repeat_count INTEGER,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE history (
|
||||
record_id INTEGER NOT NULL,
|
||||
concept_type INTEGER,
|
||||
concept_id INTEGER,
|
||||
changed_data BLOB,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE bidstates (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
state_id INTEGER,
|
||||
label VARCHAR,
|
||||
in_progress INTEGER,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
cursor.execute("ALTER TABLE wallets ADD COLUMN active_ind INTEGER")
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE eventqueue RENAME TO actions")
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_id TO action_id"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_type TO action_type"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_data TO action_data"
|
||||
)
|
||||
elif current_version == 14:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id"
|
||||
)
|
||||
elif current_version == 15:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE notifications (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
event_type INTEGER,
|
||||
event_data BLOB,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
elif current_version == 16:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE prefunded_transactions (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
tx_type INTEGER,
|
||||
tx_data BLOB,
|
||||
used_by BLOB,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
elif current_version == 17:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE knownidentities ADD COLUMN data BLOB")
|
||||
cursor.execute("UPDATE knownidentities SET active_ind = 1")
|
||||
elif current_version == 18:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING")
|
||||
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING")
|
||||
elif current_version == 19:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN in_error INTEGER")
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER")
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER")
|
||||
elif current_version == 20:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE message_links (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
|
||||
msg_type INTEGER,
|
||||
msg_sequence INTEGER,
|
||||
msg_id BLOB,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
|
||||
elif current_version == 21:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
|
||||
elif current_version == 22:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
|
||||
elif current_version == 23:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE checkedblocks (
|
||||
record_id INTEGER NOT NULL,
|
||||
created_at BIGINT,
|
||||
coin_type INTEGER,
|
||||
block_height INTEGER,
|
||||
block_hash BLOB,
|
||||
block_time INTEGER,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
|
||||
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)
|
||||
self.commitDB()
|
||||
self.log.info("Upgraded database to version {}".format(self.db_version))
|
||||
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
||||
tables = cursor.execute(query).fetchall()
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
if table_name in ("sqlite_sequence",):
|
||||
continue
|
||||
except Exception as e:
|
||||
self.log.error("Upgrade failed {}".format(e))
|
||||
self.rollbackDB()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
break
|
||||
|
||||
if db_version != CURRENT_DB_VERSION:
|
||||
raise ValueError("Unable to upgrade database.")
|
||||
have_table = {}
|
||||
have_columns = {}
|
||||
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
|
||||
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
|
||||
for column in columns:
|
||||
cid, name, data_type, notnull, default_value, primary_key = column
|
||||
have_columns[name] = {"type": data_type, "primary_key": primary_key}
|
||||
|
||||
have_table["columns"] = have_columns
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
|
||||
indices = cursor.fetchall()
|
||||
for index in indices:
|
||||
seq, index_name, unique, origin, partial = index
|
||||
|
||||
if origin == "pk": # Created by a PRIMARY KEY constraint
|
||||
continue
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
|
||||
index_info = cursor.fetchall()
|
||||
|
||||
add_index = {"index_name": index_name}
|
||||
for index_columns in index_info:
|
||||
seqno, cid, name = index_columns
|
||||
if origin == "u": # Created by a UNIQUE constraint
|
||||
have_columns[name]["unique"] = 1
|
||||
else:
|
||||
if "column_1" not in add_index:
|
||||
add_index["column_1"] = name
|
||||
elif "column_2" not in add_index:
|
||||
add_index["column_2"] = name
|
||||
elif "column_3" not in add_index:
|
||||
add_index["column_3"] = name
|
||||
else:
|
||||
raise RuntimeError("Add more index columns.")
|
||||
if origin == "c":
|
||||
if "indices" not in table:
|
||||
have_table["indices"] = []
|
||||
have_table["indices"].append(add_index)
|
||||
|
||||
have_tables[table_name] = have_table
|
||||
|
||||
for table_name, table in expect_schema.items():
|
||||
if table_name not in have_tables:
|
||||
self.log.info(f"Creating table {table_name}.")
|
||||
create_table(cursor, table_name, table)
|
||||
continue
|
||||
|
||||
have_table = have_tables[table_name]
|
||||
have_columns = have_table["columns"]
|
||||
for colname, column in table["columns"].items():
|
||||
if colname not in have_columns:
|
||||
col_type = column["type"]
|
||||
self.log.info(f"Adding column {colname} to table {table_name}.")
|
||||
cursor.execute(
|
||||
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
|
||||
)
|
||||
indices = table.get("indices", [])
|
||||
have_indices = have_table.get("indices", [])
|
||||
for index in indices:
|
||||
index_name = index["index_name"]
|
||||
if not any(
|
||||
have_idx.get("index_name") == index_name
|
||||
for have_idx in have_indices
|
||||
):
|
||||
self.log.info(f"Adding index {index_name} to table {table_name}.")
|
||||
column_1 = index["column_1"]
|
||||
column_2 = index.get("column_2", None)
|
||||
column_3 = index.get("column_3", None)
|
||||
query: str = (
|
||||
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||
)
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ")"
|
||||
cursor.execute(query)
|
||||
|
||||
if CURRENT_DB_VERSION != db_version:
|
||||
self.db_version = CURRENT_DB_VERSION
|
||||
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
|
||||
self.log.info(f"Upgraded database to version {self.db_version}")
|
||||
self.commitDB()
|
||||
except Exception as e:
|
||||
self.log.error(f"Upgrade failed {e}")
|
||||
self.rollbackDB()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023-2024 The Basicswap Developers
|
||||
# Copyright (c) 2023-2025 The Basicswap Developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -26,93 +26,46 @@ def remove_expired_data(self, time_offset: int = 0):
|
||||
)
|
||||
for offer_row in offer_rows:
|
||||
num_offers += 1
|
||||
offer_query_data = {
|
||||
"type_ind": int(Concepts.OFFER),
|
||||
"offer_id": offer_row[0],
|
||||
}
|
||||
bid_rows = cursor.execute(
|
||||
"SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
offer_query_data,
|
||||
)
|
||||
for bid_row in bid_rows:
|
||||
num_bids += 1
|
||||
cursor.execute(
|
||||
bid_query_data = {"type_ind": int(Concepts.BID), "bid_id": bid_row[0]}
|
||||
for query_str in [
|
||||
"DELETE FROM transactions WHERE transactions.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM actions WHERE actions.linked_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM bids WHERE bids.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
]:
|
||||
cursor.execute(query_str, bid_query_data)
|
||||
for query_str in [
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
]:
|
||||
cursor.execute(query_str, offer_query_data)
|
||||
|
||||
if num_offers > 0 or num_bids > 0:
|
||||
self.log.info(
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import secrets
|
||||
import hashlib
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
|
||||
|
||||
def get_secret():
|
||||
return 9 + secrets.randbelow(edf.l - 9)
|
||||
|
||||
|
||||
def encodepoint(P):
|
||||
zi = edf.inv(P[2])
|
||||
x = (P[0] * zi) % edf.q
|
||||
y = (P[1] * zi) % edf.q
|
||||
y += (x & 1) << 255
|
||||
return y.to_bytes(32, byteorder="little")
|
||||
|
||||
|
||||
def hashToEd25519(bytes_in):
|
||||
hashed = hashlib.sha256(bytes_in).digest()
|
||||
for i in range(1000):
|
||||
h255 = bytearray(hashed)
|
||||
x_sign = 0 if h255[31] & 0x80 == 0 else 1
|
||||
h255[31] &= 0x7F # Clear top bit
|
||||
y = int.from_bytes(h255, byteorder="little")
|
||||
x = edf.xrecover(y, x_sign)
|
||||
if x == 0 and y == 1: # Skip infinity point
|
||||
continue
|
||||
|
||||
P = [x, y, 1, (x * y) % edf.q]
|
||||
# Keep trying until the point is in the correct subgroup
|
||||
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
||||
return P
|
||||
hashed = hashlib.sha256(hashed).digest()
|
||||
raise ValueError("hashToEd25519 failed")
|
||||
@@ -8,9 +8,6 @@
|
||||
import json
|
||||
|
||||
|
||||
default_chart_api_key = (
|
||||
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
|
||||
)
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import threading
|
||||
import http.client
|
||||
import base64
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from socket import error as SocketError
|
||||
from urllib import parse
|
||||
@@ -25,7 +25,6 @@ from . import __version__
|
||||
from .util import (
|
||||
dumpj,
|
||||
toBool,
|
||||
LockedCoinError,
|
||||
format_timestamp,
|
||||
)
|
||||
from .chainparams import (
|
||||
@@ -54,6 +53,7 @@ from .ui.page_automation import (
|
||||
page_automation_strategy_new,
|
||||
)
|
||||
|
||||
from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api
|
||||
from .ui.page_bids import page_bids, page_bid
|
||||
from .ui.page_offers import page_offers, page_offer, page_newoffer
|
||||
from .ui.page_tor import page_tor, get_tor_established_state
|
||||
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
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
|
||||
with self.server.session_lock:
|
||||
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]
|
||||
if session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
return False
|
||||
|
||||
def log_error(self, format, *args):
|
||||
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
form_data = parse.parse_qs(post_string)
|
||||
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||
if self.server.last_form_id.get(name, None) == form_id:
|
||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
with self.server.form_id_lock:
|
||||
if self.server.last_form_id.get(name, None) == form_id:
|
||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
return form_data
|
||||
|
||||
def render_template(
|
||||
@@ -216,31 +218,48 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["debug_mode"] = True
|
||||
if swap_client.debug_ui:
|
||||
args_dict["debug_ui_mode"] = True
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
try:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
except Exception as e:
|
||||
args_dict["tor_established"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.error(f"Error getting Tor state: {str(e)}")
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
if swap_client._show_notifications:
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
is_authenticated = self.is_authenticated() or not swap_client.settings.get(
|
||||
"client_auth_hash"
|
||||
)
|
||||
|
||||
if is_authenticated:
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
try:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
except Exception:
|
||||
args_dict["tor_established"] = False
|
||||
|
||||
from .ui.page_amm import get_amm_status, get_amm_active_count
|
||||
|
||||
try:
|
||||
args_dict["current_status"] = get_amm_status()
|
||||
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
|
||||
except Exception:
|
||||
args_dict["current_status"] = "stopped"
|
||||
args_dict["amm_active_count"] = 0
|
||||
|
||||
if swap_client._show_notifications:
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
else:
|
||||
args_dict["current_status"] = "unknown"
|
||||
args_dict["amm_active_count"] = 0
|
||||
|
||||
if "messages" in args_dict:
|
||||
messages_with_ids = []
|
||||
for msg in args_dict["messages"]:
|
||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
with self.server.msg_id_lock:
|
||||
for msg in args_dict["messages"]:
|
||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
args_dict["messages"] = messages_with_ids
|
||||
if "err_messages" in args_dict:
|
||||
err_messages_with_ids = []
|
||||
for msg in args_dict["err_messages"]:
|
||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
with self.server.msg_id_lock:
|
||||
for msg in args_dict["err_messages"]:
|
||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
args_dict["err_messages"] = err_messages_with_ids
|
||||
|
||||
if self.path:
|
||||
@@ -254,15 +273,27 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["current_page"] = "index"
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
with self.server.session_lock:
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
args_dict["shutdown_token"] = shutdown_token
|
||||
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
if is_authenticated:
|
||||
try:
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
except Exception as e:
|
||||
args_dict["encrypted"] = False
|
||||
args_dict["locked"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.warning(f"Could not get wallet locked state: {e}")
|
||||
else:
|
||||
args_dict["encrypted"] = args_dict.get("encrypted", False)
|
||||
args_dict["locked"] = args_dict.get("locked", False)
|
||||
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
with self.server.msg_id_lock:
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
|
||||
args_dict["version"] = version
|
||||
|
||||
@@ -352,7 +383,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
with self.server.session_lock:
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
cookie_header = self._set_session_cookie(session_id)
|
||||
|
||||
if is_json_request:
|
||||
@@ -401,6 +433,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
def page_shutdown_ping(self, url_split, post_string):
|
||||
if not self.server.stop_event.is_set():
|
||||
raise ValueError("Unexpected shutdown ping.")
|
||||
self.putHeaders(401, "application/json")
|
||||
return json.dumps({"ack": True}).encode("utf-8")
|
||||
|
||||
def page_explorers(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -610,20 +648,49 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
if len(url_split) > 2:
|
||||
token = url_split[2]
|
||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||
with self.server.session_lock:
|
||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||
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]
|
||||
with self.server.session_lock:
|
||||
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)
|
||||
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
swap_client.log.info("Web shutdown stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
swap_client.log.info(f"AMM web shutdown: {msg}")
|
||||
else:
|
||||
swap_client.log.warning(f"AMM web shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error stopping AMM in web shutdown: {e}")
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
return self.page_info("Shutting down", extra_headers=extra_headers)
|
||||
|
||||
def page_donation(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
template = env.get_template("donation.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_index(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -654,6 +721,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string="", is_json=False):
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
swap_client = self.server.swap_client
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split("/")
|
||||
@@ -721,7 +791,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
func = js_url_to_function(url_split)
|
||||
return func(self, url_split, post_string, is_json)
|
||||
except Exception as ex:
|
||||
if swap_client.debug is True:
|
||||
if isinstance(ex, LockedCoinError):
|
||||
clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked"
|
||||
swap_client.log.warning(clean_msg)
|
||||
return js_error(self, clean_msg)
|
||||
elif swap_client.debug is True:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
return js_error(self, str(ex))
|
||||
|
||||
@@ -779,6 +853,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
if page == "login":
|
||||
return self.page_login(url_split, post_string)
|
||||
if page == "shutdown_ping":
|
||||
return self.page_shutdown_ping(url_split, post_string)
|
||||
if page == "active":
|
||||
return self.page_active(url_split, post_string)
|
||||
if page == "wallets":
|
||||
@@ -813,6 +889,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return page_bids(self, url_split, post_string, available=True)
|
||||
if page == "watched":
|
||||
return self.page_watched(url_split, post_string)
|
||||
if page == "donation":
|
||||
return self.page_donation(url_split, post_string)
|
||||
if page == "smsgaddresses":
|
||||
return page_smsgaddresses(self, url_split, post_string)
|
||||
if page == "identity":
|
||||
@@ -825,6 +903,41 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return page_automation_strategy(self, url_split, post_string)
|
||||
if page == "newautomationstrategy":
|
||||
return page_automation_strategy_new(self, url_split, post_string)
|
||||
if page == "amm":
|
||||
if len(url_split) > 2 and url_split[2] == "status":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
status_data = amm_status_api(
|
||||
swap_client, self.path, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(status_data).encode("utf-8")
|
||||
elif len(url_split) > 2 and url_split[2] == "autostart":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
autostart_data = amm_autostart_api(
|
||||
swap_client, post_string, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(autostart_data).encode("utf-8")
|
||||
elif len(url_split) > 2 and url_split[2] == "debug":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
debug_data = amm_debug_api(
|
||||
swap_client, post_string, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(debug_data).encode("utf-8")
|
||||
return page_amm(self, url_split, post_string)
|
||||
if page == "shutdown":
|
||||
return self.page_shutdown(url_split, post_string)
|
||||
if page == "changepassword":
|
||||
@@ -844,22 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.page_error(str(ex))
|
||||
|
||||
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}")
|
||||
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}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(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}")
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(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}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, "text/html")
|
||||
@@ -872,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
class HttpThread(threading.Thread, ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
def __init__(self, host_name, port_no, allow_cors, swap_client):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@@ -888,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
self.env = env
|
||||
self.msg_id_counter = 0
|
||||
|
||||
self.session_lock = threading.Lock()
|
||||
self.form_id_lock = threading.Lock()
|
||||
self.msg_id_lock = threading.Lock()
|
||||
|
||||
self.timeout = 60
|
||||
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
|
||||
if swap_client.debug:
|
||||
swap_client.log.info("HTTP server initialized with threading support")
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
@@ -53,6 +53,7 @@ class CoinInterface:
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
self._altruistic = True
|
||||
self._core_version = None # Set in getDaemonVersion()
|
||||
|
||||
def interface_type(self) -> int:
|
||||
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.
|
||||
|
||||
@@ -79,6 +79,7 @@ class BCHInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def has_segwit(self) -> bool:
|
||||
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
|
||||
@@ -817,7 +818,15 @@ class BCHInterface(BTCInterface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if tx.nLockTime != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if tx.nLockTime > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
|
||||
)
|
||||
|
||||
script_pk = self.getScriptDest(script_out)
|
||||
locked_n = findOutput(tx, script_pk)
|
||||
|
||||
@@ -16,8 +16,8 @@ import shutil
|
||||
import sqlite3
|
||||
import traceback
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from basicswap.basicswap_util import (
|
||||
getVoutByAddress,
|
||||
@@ -25,29 +25,25 @@ from basicswap.basicswap_util import (
|
||||
)
|
||||
from basicswap.interface.base import Secp256k1Interface
|
||||
from basicswap.util import (
|
||||
b2i,
|
||||
ensure,
|
||||
i2b,
|
||||
b2i,
|
||||
i2h,
|
||||
)
|
||||
from basicswap.util.ecc import (
|
||||
pointToCPK,
|
||||
CPKToPoint,
|
||||
)
|
||||
from basicswap.util.extkey import ExtKeyPair
|
||||
from basicswap.util.script import (
|
||||
SerialiseNumCompact,
|
||||
decodeScriptNum,
|
||||
getCompactSizeLen,
|
||||
SerialiseNumCompact,
|
||||
getWitnessElementLen,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
toWIF,
|
||||
b58encode,
|
||||
b58decode,
|
||||
decodeWif,
|
||||
b58encode,
|
||||
decodeAddress,
|
||||
decodeWif,
|
||||
pubkeyToAddress,
|
||||
toWIF,
|
||||
)
|
||||
from basicswap.util.crypto import (
|
||||
hash160,
|
||||
@@ -57,6 +53,7 @@ from coincurve.keys import (
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
from coincurve.types import ffi
|
||||
from coincurve.ecdsaotves import (
|
||||
ecdsaotves_enc_sign,
|
||||
ecdsaotves_enc_verify,
|
||||
@@ -77,17 +74,19 @@ from basicswap.contrib.test_framework.messages import (
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
CScriptOp,
|
||||
OP_IF,
|
||||
OP_ELSE,
|
||||
OP_ENDIF,
|
||||
OP_0,
|
||||
OP_2,
|
||||
OP_CHECKSIG,
|
||||
OP_CHECKMULTISIG,
|
||||
OP_CHECKSEQUENCEVERIFY,
|
||||
OP_CHECKSIG,
|
||||
OP_DROP,
|
||||
OP_HASH160,
|
||||
OP_DUP,
|
||||
OP_ELSE,
|
||||
OP_ENDIF,
|
||||
OP_EQUAL,
|
||||
OP_EQUALVERIFY,
|
||||
OP_HASH160,
|
||||
OP_IF,
|
||||
OP_RETURN,
|
||||
SIGHASH_ALL,
|
||||
SegwitV0SignatureHash,
|
||||
@@ -294,6 +293,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._expect_seedid_hex = None
|
||||
self._altruistic = coin_settings.get("altruistic", True)
|
||||
self._use_descriptors = coin_settings.get("use_descriptors", False)
|
||||
# Use hardened account indices to match existing wallet keys, only applies when use_descriptors is True
|
||||
self._use_legacy_key_paths = coin_settings.get("use_legacy_key_paths", False)
|
||||
self._disable_lock_tx_rbf = False
|
||||
|
||||
def open_rpc(self, wallet=None):
|
||||
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
|
||||
@@ -315,6 +317,21 @@ class BTCInterface(Secp256k1Interface):
|
||||
def checkWallets(self) -> int:
|
||||
wallets = self.rpc("listwallets")
|
||||
|
||||
if self._rpc_wallet not in wallets:
|
||||
self._log.debug(
|
||||
f"Wallet: {self._rpc_wallet} not active, attempting to load."
|
||||
)
|
||||
try:
|
||||
self.rpc_wallet(
|
||||
"loadwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
],
|
||||
)
|
||||
wallets = self.rpc("listwallets")
|
||||
except Exception as e:
|
||||
self._log.debug(f'Error loading wallet "self._rpc_wallet": {e}.')
|
||||
|
||||
# Wallet name is "" for some LTC and PART installs on older cores
|
||||
if self._rpc_wallet not in wallets and len(wallets) > 0:
|
||||
self._log.warning(f"Changing {self.ticker()} wallet name.")
|
||||
@@ -347,7 +364,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
self.rpc_wallet("getwalletinfo" if with_wallet else "getblockchaininfo")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc("getnetworkinfo")["version"]
|
||||
if self._core_version is None:
|
||||
self._core_version = self.rpc("getnetworkinfo")["version"]
|
||||
return self._core_version
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
return self.rpc("getblockchaininfo")
|
||||
@@ -382,6 +401,13 @@ class BTCInterface(Secp256k1Interface):
|
||||
last_block_header = prev_block_header
|
||||
raise ValueError(f"Block header not found at time: {time}")
|
||||
|
||||
def getWalletAccountPath(self) -> str:
|
||||
# Use a bip44 style path, however the seed (derived from the particl mnemonic keychain) can't be turned into a bip39 mnemonic without the matching entropy
|
||||
purpose: int = 84 # native segwit
|
||||
coin_type: int = self.chainparams_network()["bip44"]
|
||||
account: int = 0
|
||||
return f"{purpose}h/{coin_type}h/{account}h"
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
|
||||
assert len(key_bytes) == 32
|
||||
self._have_checked_seed = False
|
||||
@@ -390,8 +416,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
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)")
|
||||
if self._use_legacy_key_paths:
|
||||
# Match keys from legacy wallets (created from sethdseed)
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
|
||||
else:
|
||||
# Use a bip44 path so the seed can be exported as a mnemonic
|
||||
path: str = self.getWalletAccountPath()
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/{path}/0/*)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/{path}/1/*)")
|
||||
|
||||
rv = self.rpc_wallet(
|
||||
"importdescriptors",
|
||||
@@ -430,6 +463,50 @@ class BTCInterface(Secp256k1Interface):
|
||||
"""
|
||||
raise (e)
|
||||
|
||||
def canExportToElectrum(self) -> bool:
|
||||
# keychains must be unhardened to export into electrum
|
||||
return self._use_descriptors is True and self._use_legacy_key_paths is False
|
||||
|
||||
def getAccountKey(
|
||||
self,
|
||||
key_bytes: bytes,
|
||||
extkey_prefix: Optional[int] = None,
|
||||
coin_type_overide: Optional[int] = None,
|
||||
) -> str:
|
||||
# For electrum, must start with zprv to get P2WPKH, addresses
|
||||
# extkey_prefix: 0x04b2430c
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
path: str = self.getWalletAccountPath()
|
||||
account_ek = ek.derive_path(path)
|
||||
return self.encode_secret_extkey(account_ek.encode_v(), extkey_prefix)
|
||||
|
||||
def getWalletKeyChains(
|
||||
self, key_bytes: bytes, extkey_prefix: Optional[int] = None
|
||||
) -> Dict[str, str]:
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
|
||||
# extkey must contain keydata to derive hardened child keys
|
||||
|
||||
if self.canExportToElectrum():
|
||||
path: str = self.getWalletAccountPath()
|
||||
external_extkey = ek.derive_path(f"{path}/0")
|
||||
internal_extkey = ek.derive_path(f"{path}/1")
|
||||
else:
|
||||
# Match keychain paths of legacy wallets
|
||||
external_extkey = ek.derive_path("0h/0h")
|
||||
internal_extkey = ek.derive_path("0h/1h")
|
||||
|
||||
def encode_extkey(extkey):
|
||||
return self.encode_secret_extkey(extkey.encode_v(), extkey_prefix)
|
||||
|
||||
rv = {
|
||||
"external": encode_extkey(external_extkey),
|
||||
"internal": encode_extkey(internal_extkey),
|
||||
}
|
||||
return rv
|
||||
|
||||
def getWalletInfo(self):
|
||||
rv = self.rpc_wallet("getwalletinfo")
|
||||
rv["encrypted"] = "unlocked_until" in rv
|
||||
@@ -489,10 +566,16 @@ class BTCInterface(Secp256k1Interface):
|
||||
if descriptor is None:
|
||||
self._log.debug("Could not find active descriptor.")
|
||||
return "Not found"
|
||||
end = descriptor["desc"].find("/")
|
||||
start = descriptor["desc"].find("]")
|
||||
if start < 3:
|
||||
return "Could not parse descriptor"
|
||||
descriptor = descriptor["desc"][start + 1 :]
|
||||
|
||||
end = descriptor.find("/")
|
||||
if end < 10:
|
||||
return "Not found"
|
||||
extkey = descriptor["desc"][5:end]
|
||||
return "Could not parse descriptor"
|
||||
extkey = descriptor[:end]
|
||||
|
||||
extkey_data = b58decode(extkey)[4:-4]
|
||||
extkey_data_hash: bytes = hash160(extkey_data)
|
||||
return extkey_data_hash.hex()
|
||||
@@ -546,7 +629,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
override_feerate = chain_client_settings.get("override_feerate", None)
|
||||
if override_feerate:
|
||||
self._log.debug(
|
||||
"Fee rate override used for %s: %f", self.coin_name(), override_feerate
|
||||
f"Fee rate override used for {self.coin_name()}: {override_feerate}"
|
||||
)
|
||||
return override_feerate, "override_feerate"
|
||||
|
||||
@@ -596,9 +679,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
pkh = hash160(pk)
|
||||
return segwit_addr.encode(bech32_prefix, version, pkh)
|
||||
|
||||
def encode_secret_extkey(self, ek_data: bytes) -> str:
|
||||
def encode_secret_extkey(self, ek_data: bytes, prefix=None) -> str:
|
||||
assert len(ek_data) == 74
|
||||
prefix = self.chainparams_network()["ext_secret_key_prefix"]
|
||||
if prefix is None:
|
||||
prefix = self.chainparams_network()["ext_secret_key_prefix"]
|
||||
data: bytes = prefix.to_bytes(4, "big") + ek_data
|
||||
checksum = sha256(sha256(data))
|
||||
return b58encode(data + checksum[0:4])
|
||||
@@ -657,18 +741,12 @@ class BTCInterface(Secp256k1Interface):
|
||||
wif_prefix = self.chainparams_network()["key_prefix"]
|
||||
return toWIF(wif_prefix, key_bytes)
|
||||
|
||||
def encodePubkey(self, pk: bytes) -> bytes:
|
||||
return pointToCPK(pk)
|
||||
|
||||
def encodeSegwitAddress(self, key_hash: bytes) -> str:
|
||||
return segwit_addr.encode(self.chainparams_network()["hrp"], 0, key_hash)
|
||||
|
||||
def decodeSegwitAddress(self, addr: str) -> bytes:
|
||||
return bytes(segwit_addr.decode(self.chainparams_network()["hrp"], addr)[1])
|
||||
|
||||
def decodePubkey(self, pke):
|
||||
return CPKToPoint(pke)
|
||||
|
||||
def decodeKey(self, k: str) -> bytes:
|
||||
return decodeWif(k)
|
||||
|
||||
@@ -676,10 +754,11 @@ class BTCInterface(Secp256k1Interface):
|
||||
# p2wpkh
|
||||
return CScript([OP_0, pkh])
|
||||
|
||||
def loadTx(self, tx_bytes: bytes) -> CTransaction:
|
||||
def loadTx(self, tx_bytes: bytes, allow_witness: bool = True) -> CTransaction:
|
||||
# Load tx from bytes to internal representation
|
||||
# Transactions with no inputs require allow_witness set to false to decode correctly
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(tx_bytes))
|
||||
tx.deserialize(BytesIO(tx_bytes), allow_witness)
|
||||
return tx
|
||||
|
||||
def createSCLockTx(
|
||||
@@ -687,27 +766,63 @@ class BTCInterface(Secp256k1Interface):
|
||||
) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.nLockTime = 0 # TODO: match locktimes by core
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
return tx.serialize()
|
||||
|
||||
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None) -> bytes:
|
||||
funded_tx = self.fundTx(tx_bytes, feerate)
|
||||
|
||||
if self._disable_lock_tx_rbf:
|
||||
tx = self.loadTx(funded_tx)
|
||||
for txi in tx.vin:
|
||||
txi.nSequence = 0xFFFFFFFE
|
||||
funded_tx = tx.serialize_with_witness()
|
||||
return funded_tx
|
||||
|
||||
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
# fmt: off
|
||||
return CScript([
|
||||
CScriptOp(OP_IF),
|
||||
2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG),
|
||||
2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG),
|
||||
CScriptOp(OP_ELSE),
|
||||
csv_val, CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
|
||||
Kaf_enc, CScriptOp(OP_CHECKSIG),
|
||||
Kaf, CScriptOp(OP_CHECKSIG),
|
||||
CScriptOp(OP_ENDIF)])
|
||||
# fmt: on
|
||||
|
||||
def isScriptP2PKH(self, script: bytes) -> bool:
|
||||
if len(script) != 25:
|
||||
return False
|
||||
if script[0] != OP_DUP:
|
||||
return False
|
||||
if script[1] != OP_HASH160:
|
||||
return False
|
||||
if script[2] != 20:
|
||||
return False
|
||||
if script[23] != OP_EQUALVERIFY:
|
||||
return False
|
||||
if script[24] != OP_CHECKSIG:
|
||||
return False
|
||||
return True
|
||||
|
||||
def isScriptP2WPKH(self, script: bytes) -> bool:
|
||||
if len(script) != 22:
|
||||
return False
|
||||
if script[0] != OP_0:
|
||||
return False
|
||||
if script[1] != 20:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
if self.isScriptP2WPKH(script):
|
||||
return [bytes(72), bytes(33)]
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def createSCLockRefundTx(
|
||||
self,
|
||||
tx_lock_bytes,
|
||||
@@ -963,7 +1078,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if tx.nLockTime != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if tx.nLockTime > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
|
||||
)
|
||||
|
||||
script_pk = self.getScriptDest(script_out)
|
||||
locked_n = findOutput(tx, script_pk)
|
||||
@@ -1235,7 +1358,17 @@ class BTCInterface(Secp256k1Interface):
|
||||
)
|
||||
|
||||
eck = PrivateKey(key_bytes)
|
||||
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
|
||||
for i in range(10000):
|
||||
# Grind for low-R value
|
||||
if i == 0:
|
||||
nonce = (ffi.NULL, ffi.NULL)
|
||||
else:
|
||||
extra_entropy = i.to_bytes(4, "little") + (b"\0" * 28)
|
||||
nonce = (ffi.NULL, ffi.new("unsigned char [32]", extra_entropy))
|
||||
sig = eck.sign(sig_hash, hasher=None, custom_nonce=nonce)
|
||||
if len(sig) < 71:
|
||||
return sig + bytes((SIGHASH_ALL,))
|
||||
raise RuntimeError("sign failed.")
|
||||
|
||||
def signTxOtVES(
|
||||
self,
|
||||
@@ -1301,24 +1434,40 @@ class BTCInterface(Secp256k1Interface):
|
||||
"feeRate": feerate_str,
|
||||
}
|
||||
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
|
||||
return bytes.fromhex(rv["hex"])
|
||||
tx_bytes: bytes = bytes.fromhex(rv["hex"])
|
||||
return tx_bytes
|
||||
|
||||
def lockNonSegwitPrevouts(self) -> None:
|
||||
# For tests
|
||||
unspent = self.rpc_wallet("listunspent")
|
||||
|
||||
to_lock = []
|
||||
for u in unspent:
|
||||
def getNonSegwitOutputs(self):
|
||||
unspents = self.rpc_wallet("listunspent", [0, 99999999])
|
||||
nonsegwit_unspents = []
|
||||
for u in unspents:
|
||||
if u.get("spendable", False) is False:
|
||||
continue
|
||||
if "desc" in u:
|
||||
desc = u["desc"]
|
||||
if self.use_p2shp2wsh():
|
||||
if not desc.startswith("sh(wpkh"):
|
||||
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
|
||||
nonsegwit_unspents.append(
|
||||
{
|
||||
"txid": u["txid"],
|
||||
"vout": u["vout"],
|
||||
"amount": u["amount"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
if not desc.startswith("wpkh"):
|
||||
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
|
||||
nonsegwit_unspents.append(
|
||||
{
|
||||
"txid": u["txid"],
|
||||
"vout": u["vout"],
|
||||
"amount": u["amount"],
|
||||
}
|
||||
)
|
||||
return nonsegwit_unspents
|
||||
|
||||
def lockNonSegwitPrevouts(self) -> None:
|
||||
# For tests
|
||||
to_lock = self.getNonSegwitOutputs()
|
||||
|
||||
if len(to_lock) > 0:
|
||||
self._log.debug(f"Locking {len(to_lock)} non segwit prevouts")
|
||||
@@ -1393,6 +1542,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
def getScriptDest(self, script):
|
||||
return CScript([OP_0, sha256(script)])
|
||||
|
||||
def getP2WSHScriptDest(self, script):
|
||||
return CScript([OP_0, sha256(script)])
|
||||
|
||||
def getScriptScriptSig(self, script: bytes) -> bytes:
|
||||
return bytes()
|
||||
|
||||
@@ -1492,7 +1644,14 @@ class BTCInterface(Secp256k1Interface):
|
||||
return (weight + wsf - 1) // wsf
|
||||
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value: int,
|
||||
cb_block_confirmed: int,
|
||||
restore_height: int,
|
||||
bid_sender: bool,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
dest_address = (
|
||||
self.pubkey_to_segwit_address(Kbs)
|
||||
@@ -1636,7 +1795,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
"listunspent",
|
||||
[
|
||||
0,
|
||||
9999999,
|
||||
99999999,
|
||||
[
|
||||
dest_address,
|
||||
],
|
||||
@@ -1665,6 +1824,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
"height": block_height,
|
||||
}
|
||||
|
||||
if "mempoolconflicts" in tx and len(tx["mempoolconflicts"]) > 0:
|
||||
rv["conflicts"] = tx["mempoolconflicts"]
|
||||
elif "walletconflicts" in tx and len(tx["walletconflicts"]) > 0:
|
||||
rv["conflicts"] = tx["walletconflicts"]
|
||||
except Exception as e:
|
||||
self._log.debug(
|
||||
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
|
||||
@@ -1828,6 +1991,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
def getBlockWithTxns(self, block_hash: str):
|
||||
return self.rpc("getblock", [block_hash, 2])
|
||||
|
||||
def listUtxos(self):
|
||||
return self.rpc_wallet("listunspent")
|
||||
|
||||
def getUnspentsByAddr(self):
|
||||
unspent_addr = dict()
|
||||
unspent = self.rpc_wallet("listunspent")
|
||||
@@ -1864,6 +2030,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
sum_unspent += self.make_int(o["amount"])
|
||||
return sum_unspent
|
||||
|
||||
def signMessage(self, address: str, message: str) -> str:
|
||||
return self.rpc_wallet(
|
||||
"signmessage",
|
||||
[address, message],
|
||||
)
|
||||
|
||||
def signMessageWithKey(self, key_wif: str, message: str) -> str:
|
||||
return self.rpc("signmessagewithprivkey", [key_wif, message])
|
||||
|
||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
unspent_addr = self.getUnspentsByAddr()
|
||||
@@ -1881,6 +2056,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.debug(f"sign_for_addr {sign_for_addr}")
|
||||
|
||||
funds_addr: str = sign_for_addr
|
||||
|
||||
if (
|
||||
self.using_segwit()
|
||||
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
@@ -1889,6 +2065,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
sign_for_addr = self.pkh_to_address(pkh)
|
||||
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
|
||||
|
||||
sign_message: str = sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()
|
||||
if self._use_descriptors:
|
||||
# https://github.com/bitcoin/bitcoin/issues/10542
|
||||
# https://github.com/bitcoin/bitcoin/issues/26046
|
||||
@@ -1905,7 +2082,6 @@ class BTCInterface(Secp256k1Interface):
|
||||
],
|
||||
)
|
||||
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:
|
||||
@@ -1926,22 +2102,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
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(),
|
||||
],
|
||||
)
|
||||
signature = self.signMessageWithKey(sign_for_address_key, sign_message)
|
||||
del priv_keys
|
||||
else:
|
||||
signature = self.rpc_wallet(
|
||||
"signmessage",
|
||||
[
|
||||
sign_for_addr,
|
||||
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
signature = self.signMessage(sign_for_addr, sign_message)
|
||||
|
||||
prove_utxos = [] # TODO: Send specific utxos
|
||||
return (sign_for_addr, signature, prove_utxos)
|
||||
@@ -2367,6 +2531,59 @@ class BTCInterface(Secp256k1Interface):
|
||||
def isTxNonFinalError(self, err_str: str) -> bool:
|
||||
return "non-BIP68-final" in err_str or "non-final" in err_str
|
||||
|
||||
def combine_non_segwit_prevouts(self):
|
||||
self._log.info("Combining non-segwit prevouts")
|
||||
if self._use_segwit is False:
|
||||
raise RuntimeError("Not configured to use segwit outputs.")
|
||||
prevouts_to_spend = self.getNonSegwitOutputs()
|
||||
if len(prevouts_to_spend) < 1:
|
||||
raise RuntimeError("No non-segwit outputs found.")
|
||||
|
||||
total_amount: int = 0
|
||||
for n, prevout in enumerate(prevouts_to_spend):
|
||||
total_amount += self.make_int(prevout["amount"])
|
||||
addr_to: str = self.getNewAddress(
|
||||
self._use_segwit, "combine_non_segwit_prevouts"
|
||||
)
|
||||
|
||||
txn = self.rpc(
|
||||
"createrawtransaction",
|
||||
[prevouts_to_spend, {addr_to: self.format_amount(total_amount)}],
|
||||
)
|
||||
fee_rate, rate_src = self.get_fee_rate(self._conf_target)
|
||||
fee_rate_str: str = self.format_amount(fee_rate, True, 1)
|
||||
self._log.debug(
|
||||
f"Using fee rate: {fee_rate_str}, src: {rate_src}, confirms target: {self._conf_target}"
|
||||
)
|
||||
options = {
|
||||
"add_inputs": False,
|
||||
"subtractFeeFromOutputs": [
|
||||
0,
|
||||
],
|
||||
"feeRate": fee_rate_str,
|
||||
}
|
||||
tx_fee_set = self.rpc_wallet("fundrawtransaction", [txn, options])["hex"]
|
||||
tx_signed = self.rpc_wallet("signrawtransactionwithwallet", [tx_fee_set])["hex"]
|
||||
tx = self.rpc(
|
||||
"decoderawtransaction",
|
||||
[
|
||||
tx_signed,
|
||||
],
|
||||
)
|
||||
self._log.info(
|
||||
"Submitting tx to combine non-segwit prevouts: {}".format(
|
||||
self._log.id(bytes.fromhex(tx["txid"]))
|
||||
)
|
||||
)
|
||||
self.rpc(
|
||||
"sendrawtransaction",
|
||||
[
|
||||
tx_signed,
|
||||
],
|
||||
)
|
||||
|
||||
return tx["txid"]
|
||||
|
||||
|
||||
def testBTCInterface():
|
||||
print("TODO: testBTCInterface")
|
||||
|
||||
@@ -521,7 +521,7 @@ class CTransaction(object):
|
||||
self.hash = tx.hash
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
ver32bit = struct.unpack("<i", f.read(4))[0]
|
||||
self.nVersion = ver32bit & 0xffff
|
||||
self.nType = (ver32bit >> 16) & 0xffff
|
||||
|
||||
@@ -455,12 +455,12 @@ class CTransaction(object):
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
self.strDZeel = copy.deepcopy(tx.strDZeel)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<i", f.read(4))[0]
|
||||
self.nTime = struct.unpack("<i", f.read(4))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
flags = 0
|
||||
if len(self.vin) == 0:
|
||||
if len(self.vin) == 0 and allow_witness:
|
||||
flags = struct.unpack("<B", f.read(1))[0]
|
||||
# Not sure why flags can't be zero, but this
|
||||
# matches the implementation in bitcoind
|
||||
|
||||
@@ -505,7 +505,7 @@ class CTransaction:
|
||||
self.sha256 = tx.sha256
|
||||
self.hash = tx.hash
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<h", f.read(2))[0]
|
||||
self.nType = struct.unpack("<h", f.read(2))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
|
||||
@@ -13,6 +13,8 @@ import logging
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from typing import List
|
||||
|
||||
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
@@ -1085,22 +1087,21 @@ class DCRInterface(Secp256k1Interface):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
|
||||
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes:
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
script = bytearray()
|
||||
script += bytes((OP_IF,))
|
||||
push_script_data(script, bytes((2,)))
|
||||
push_script_data(script, Kal_enc)
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kal)
|
||||
push_script_data(script, Kaf)
|
||||
push_script_data(script, bytes((2,)))
|
||||
script += bytes((OP_CHECKMULTISIG,))
|
||||
script += bytes((OP_ELSE,))
|
||||
script += CScriptNum.encode(CScriptNum(csv_val))
|
||||
script += bytes((OP_CHECKSEQUENCEVERIFY,))
|
||||
script += bytes((OP_DROP,))
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kaf)
|
||||
script += bytes((OP_CHECKSIG,))
|
||||
script += bytes((OP_ENDIF,))
|
||||
|
||||
@@ -1609,11 +1610,11 @@ class DCRInterface(Secp256k1Interface):
|
||||
script_pk = self.getScriptDest(script)
|
||||
return findOutput(tx, script_pk)
|
||||
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
return [bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
|
||||
tx = self.loadTx(tx_bytes)
|
||||
|
||||
@@ -89,7 +89,7 @@ class CTransaction:
|
||||
self.locktime = tx.locktime
|
||||
self.expiry = tx.expiry
|
||||
|
||||
def deserialize(self, data: bytes) -> None:
|
||||
def deserialize(self, data: bytes, allow_witness: bool = True) -> None:
|
||||
|
||||
version = int.from_bytes(data[:4], "little")
|
||||
self.version = version & 0xFFFF
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -44,6 +44,7 @@ class FIROInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
if "wallet_name" in coin_settings:
|
||||
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||
|
||||
@@ -98,6 +98,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def chainparams(self):
|
||||
return chainparams[Coins.LTC]
|
||||
|
||||
@@ -79,6 +79,7 @@ class NAVInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
if "wallet_name" in coin_settings:
|
||||
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
import hashlib
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOutPart,
|
||||
)
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_DUP,
|
||||
OP_HASH160,
|
||||
OP_EQUALVERIFY,
|
||||
@@ -25,7 +25,6 @@ from basicswap.util import (
|
||||
TemporaryError,
|
||||
)
|
||||
from basicswap.util.script import (
|
||||
getP2WSH,
|
||||
getCompactSizeLen,
|
||||
getWitnessElementLen,
|
||||
)
|
||||
@@ -136,6 +135,11 @@ class PARTInterface(BTCInterface):
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
|
||||
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
|
||||
return [bytes(72), bytes(33)]
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
|
||||
|
||||
@@ -189,6 +193,27 @@ class PARTInterface(BTCInterface):
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
def combine_non_segwit_prevouts(self):
|
||||
raise RuntimeError("No non-segwit outputs found.")
|
||||
|
||||
def signMessage(self, address: str, message: str) -> str:
|
||||
args = [address, message]
|
||||
if self.getDaemonVersion() > 23020700:
|
||||
message_magic: str = self.chainparams()["message_magic"]
|
||||
args += [
|
||||
message_magic,
|
||||
]
|
||||
return self.rpc_wallet("signmessage", args)
|
||||
|
||||
def signMessageWithKey(self, key_wif: str, message: str) -> str:
|
||||
args = [key_wif, message]
|
||||
if self.getDaemonVersion() > 23020700:
|
||||
message_magic: str = self.chainparams()["message_magic"]
|
||||
args += [
|
||||
message_magic,
|
||||
]
|
||||
return self.rpc("signmessagewithprivkey", args)
|
||||
|
||||
|
||||
class PARTInterfaceBlind(PARTInterface):
|
||||
|
||||
@@ -211,6 +236,15 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 980
|
||||
|
||||
@staticmethod
|
||||
def compareFeeRates(actual: int, expected: int) -> bool:
|
||||
# Allow the fee to be up to 10% larger than expected
|
||||
if actual < expected - 20:
|
||||
return False
|
||||
if actual > expected + expected * 0.1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def coin_name(self) -> str:
|
||||
return super().coin_name() + " Blind"
|
||||
|
||||
@@ -256,7 +290,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||
assert len(ephemeral_pubkey) == 33
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
|
||||
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(script))
|
||||
inputs = []
|
||||
outputs = [
|
||||
{
|
||||
@@ -330,7 +364,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
locked_coin = input_blinded_info["amount"]
|
||||
tx_lock_id = lock_tx_obj["txid"]
|
||||
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
|
||||
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(refund_script))
|
||||
|
||||
inputs = [
|
||||
{
|
||||
@@ -480,7 +514,16 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(lock_txid_hex)))
|
||||
|
||||
ensure(lock_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
ensure(lock_tx_obj["locktime"] == 0, "Bad nLockTime")
|
||||
lock_time: int = lock_tx_obj["locktime"]
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if lock_time != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if lock_time > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {lock_time}, current height {current_height}"
|
||||
)
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
@@ -495,7 +538,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_txo_scriptpk = bytes.fromhex(
|
||||
lock_tx_obj["vout"][lock_output_n]["scriptPubKey"]["hex"]
|
||||
)
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
script_pk = self.getP2WSHScriptDest(script_out)
|
||||
ensure(lock_txo_scriptpk == script_pk, "Bad output script")
|
||||
A, B = extractScriptLockScriptValues(script_out)
|
||||
ensure(A == Kal, "Bad script leader pubkey")
|
||||
@@ -572,7 +615,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_refund_txo_scriptpk = bytes.fromhex(
|
||||
lock_refund_tx_obj["vout"][lock_refund_output_n]["scriptPubKey"]["hex"]
|
||||
)
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
script_pk = self.getP2WSHScriptDest(script_out)
|
||||
ensure(lock_refund_txo_scriptpk == script_pk, "Bad output script")
|
||||
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
|
||||
ensure(A == Kal, "Bad script pubkey")
|
||||
@@ -680,6 +723,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_paid = self.make_int(lock_refund_spend_tx_obj["vout"][0]["ct_fee"])
|
||||
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
ensure(
|
||||
self.compareFeeRates(fee_rate_paid, feerate),
|
||||
@@ -1031,10 +1075,11 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value,
|
||||
cb_block_confirmed,
|
||||
cb_swap_value: int,
|
||||
cb_block_confirmed: int,
|
||||
restore_height: int,
|
||||
bid_sender: bool,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
@@ -1065,7 +1110,10 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
) # Should not be possible
|
||||
ensure(tx["outputs"][0]["type"] == "blind", "Output is not anon")
|
||||
|
||||
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
|
||||
if (
|
||||
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
|
||||
or check_amount is False
|
||||
):
|
||||
height = 0
|
||||
if tx["confirmations"] > 0:
|
||||
chain_height = self.rpc("getblockcount")
|
||||
@@ -1287,7 +1335,14 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
return bytes.fromhex(txid)
|
||||
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value,
|
||||
cb_block_confirmed,
|
||||
restore_height,
|
||||
bid_sender,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
@@ -1319,7 +1374,10 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
) # Should not be possible
|
||||
ensure(tx["outputs"][0]["type"] == "anon", "Output is not anon")
|
||||
|
||||
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
|
||||
if (
|
||||
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
|
||||
or check_amount is False
|
||||
):
|
||||
height = 0
|
||||
if tx["confirmations"] > 0:
|
||||
chain_height = self.rpc("getblockcount")
|
||||
|
||||
@@ -33,6 +33,7 @@ class PIVXInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
|
||||
@@ -29,7 +29,7 @@ class WOWInterface(XMRInterface):
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
# below only needed until wow is rebased to monero v0.18.4.0+
|
||||
def openWallet(self, filename):
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
import basicswap.ed25519_fast_util as edu
|
||||
import basicswap.util_xmr as xmr_util
|
||||
from coincurve.ed25519 import (
|
||||
ed25519_add,
|
||||
@@ -35,6 +35,9 @@ from basicswap.chainparams import XMR_COIN, Coins
|
||||
from basicswap.interface.base import CoinInterface
|
||||
|
||||
|
||||
ed25519_l = 2**252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
class XMRInterface(CoinInterface):
|
||||
@staticmethod
|
||||
def curve_type():
|
||||
@@ -84,7 +87,15 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def is_transient_error(self, ex) -> bool:
|
||||
str_error: str = str(ex).lower()
|
||||
if "failed to get earliest fork height" in str_error:
|
||||
if any(
|
||||
response in str_error
|
||||
for response in [
|
||||
"failed to get earliest fork height",
|
||||
"failed to get output distribution",
|
||||
"request-sent",
|
||||
"idle",
|
||||
]
|
||||
):
|
||||
return True
|
||||
return super().is_transient_error(ex)
|
||||
|
||||
@@ -146,6 +157,8 @@ class XMRInterface(CoinInterface):
|
||||
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
|
||||
# walletrpctimeoutlong likely unneeded
|
||||
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
|
||||
self._num_chaininfo_retries = coin_settings.get("numchaininforetries", 20)
|
||||
self._chaininfo_retry_delay = coin_settings.get("chaininforetrydelay", 1)
|
||||
|
||||
self.rpc = make_xmr_rpc_func(
|
||||
coin_settings["rpcport"],
|
||||
@@ -287,10 +300,13 @@ class XMRInterface(CoinInterface):
|
||||
self.rpc_wallet("get_languages")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet("get_version")["version"]
|
||||
# Returns wallet version
|
||||
if self._core_version is None:
|
||||
self._core_version = self.rpc_wallet("get_version")["version"]
|
||||
return self._core_version
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
get_height = self.getChainHeight(full_output=True)
|
||||
rv = {
|
||||
"blocks": get_height["height"],
|
||||
"verificationprogress": 0.0,
|
||||
@@ -319,8 +335,16 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
|
||||
def getChainHeight(self, full_output: bool = False):
|
||||
for i in range(self._num_chaininfo_retries):
|
||||
try:
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
return get_height if full_output else get_height["height"]
|
||||
except Exception as e:
|
||||
if i < self._num_chaininfo_retries - 1 and self.is_transient_error(e):
|
||||
time.sleep(self._chaininfo_retry_delay)
|
||||
continue
|
||||
raise (e)
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
@@ -378,10 +402,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def getNewRandomKey(self) -> bytes:
|
||||
# Note: Returned bytes are in big endian order
|
||||
return i2b(edu.get_secret())
|
||||
|
||||
def pubkey(self, key: bytes) -> bytes:
|
||||
return edf.scalarmult_B(key)
|
||||
return i2b(9 + secrets.randbelow(ed25519_l - 9))
|
||||
|
||||
def encodeKey(self, vk: bytes) -> str:
|
||||
return vk[::-1].hex()
|
||||
@@ -389,12 +410,6 @@ class XMRInterface(CoinInterface):
|
||||
def decodeKey(self, k_hex: str) -> bytes:
|
||||
return bytes.fromhex(k_hex)[::-1]
|
||||
|
||||
def encodePubkey(self, pk: bytes) -> str:
|
||||
return edu.encodepoint(pk)
|
||||
|
||||
def decodePubkey(self, pke):
|
||||
return edf.decodepoint(pke)
|
||||
|
||||
def getPubkey(self, privkey):
|
||||
return ed25519_get_pubkey(privkey)
|
||||
|
||||
@@ -405,7 +420,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def verifyKey(self, k: int) -> bool:
|
||||
i = b2i(k)
|
||||
return i < edf.l and i > 8
|
||||
return i < ed25519_l and i > 8
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes):
|
||||
# Calls ed25519_decode_check_point() in secp256k1
|
||||
@@ -455,16 +470,24 @@ class XMRInterface(CoinInterface):
|
||||
params["priority"] = self._fee_priority
|
||||
rv = self.rpc_wallet("transfer", params)
|
||||
self._log.info(
|
||||
"publishBLockTx %s to address_b58 %s",
|
||||
self._log.id(rv["tx_hash"]),
|
||||
self._log.addr(shared_addr),
|
||||
"publishBLockTx {} to address_b58 {}".format(
|
||||
self._log.id(rv["tx_hash"]),
|
||||
self._log.addr(shared_addr),
|
||||
)
|
||||
)
|
||||
tx_hash = bytes.fromhex(rv["tx_hash"])
|
||||
|
||||
return tx_hash
|
||||
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value: int,
|
||||
cb_block_confirmed: int,
|
||||
restore_height: int,
|
||||
bid_sender: bool,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
@@ -516,7 +539,7 @@ class XMRInterface(CoinInterface):
|
||||
)
|
||||
rv = -1
|
||||
continue
|
||||
if transfer["amount"] == cb_swap_value:
|
||||
if transfer["amount"] == cb_swap_value or check_amount is False:
|
||||
return {
|
||||
"txid": transfer["tx_hash"],
|
||||
"amount": transfer["amount"],
|
||||
@@ -535,16 +558,14 @@ class XMRInterface(CoinInterface):
|
||||
rv = -1
|
||||
return rv
|
||||
|
||||
def findTxnByHash(self, txid):
|
||||
def findTxnByHash(self, txid: str):
|
||||
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)[
|
||||
"height"
|
||||
]
|
||||
current_height: int = self.getChainHeight()
|
||||
self._log.info(
|
||||
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ from .ui.util import (
|
||||
)
|
||||
from .ui.page_offers import postNewOffer
|
||||
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
|
||||
from .db import Concepts
|
||||
|
||||
|
||||
def getFormData(post_string: str, is_json: bool):
|
||||
@@ -122,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(coins), "UTF-8")
|
||||
|
||||
|
||||
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
try:
|
||||
|
||||
swap_client.updateWalletsInfo()
|
||||
wallets = swap_client.getCachedWalletsInfo()
|
||||
coins_with_balances = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
|
||||
balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "balance" in w and "error" not in w and "no_data" not in w:
|
||||
raw_balance = w["balance"]
|
||||
if isinstance(raw_balance, float):
|
||||
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
balance = str(raw_balance)
|
||||
else:
|
||||
balance = raw_balance
|
||||
|
||||
pending = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
ci = swap_client.ci(k)
|
||||
pending_amount = 0
|
||||
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["unconfirmed"])
|
||||
if "immature" in w and float(w["immature"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["immature"])
|
||||
if pending_amount > 0:
|
||||
pending = ci.format_amount(pending_amount)
|
||||
|
||||
coin_entry = {
|
||||
"id": int(k),
|
||||
"name": getCoinName(k),
|
||||
"balance": balance,
|
||||
"pending": pending,
|
||||
"ticker": chainparams[k]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(coin_entry)
|
||||
|
||||
if k == Coins.PART:
|
||||
variants = [
|
||||
{
|
||||
"coin": Coins.PART_ANON,
|
||||
"balance_field": "anon_balance",
|
||||
"pending_field": "anon_pending",
|
||||
},
|
||||
{
|
||||
"coin": Coins.PART_BLIND,
|
||||
"balance_field": "blind_balance",
|
||||
"pending_field": "blind_unconfirmed",
|
||||
},
|
||||
]
|
||||
|
||||
for variant_info in variants:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if variant_info["balance_field"] in w:
|
||||
raw_balance = w[variant_info["balance_field"]]
|
||||
if isinstance(raw_balance, float):
|
||||
variant_balance = f"{raw_balance:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
variant_balance = str(raw_balance)
|
||||
else:
|
||||
variant_balance = raw_balance
|
||||
|
||||
if (
|
||||
variant_info["pending_field"] in w
|
||||
and float(w[variant_info["pending_field"]]) > 0.0
|
||||
):
|
||||
variant_pending = str(
|
||||
w[variant_info["pending_field"]]
|
||||
)
|
||||
|
||||
variant_entry = {
|
||||
"id": int(variant_info["coin"]),
|
||||
"name": getCoinName(variant_info["coin"]),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.PART]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
elif k == Coins.LTC:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if "mweb_balance" in w:
|
||||
variant_balance = w["mweb_balance"]
|
||||
|
||||
pending_amount = 0
|
||||
if (
|
||||
"mweb_unconfirmed" in w
|
||||
and float(w["mweb_unconfirmed"]) > 0.0
|
||||
):
|
||||
pending_amount += float(w["mweb_unconfirmed"])
|
||||
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
|
||||
pending_amount += float(w["mweb_immature"])
|
||||
if pending_amount > 0:
|
||||
variant_pending = f"{pending_amount:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
|
||||
variant_entry = {
|
||||
"id": int(Coins.LTC_MWEB),
|
||||
"name": getCoinName(Coins.LTC_MWEB),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.LTC]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
return bytes(json.dumps(coins_with_balances), "UTF-8")
|
||||
|
||||
except Exception as e:
|
||||
error_data = {"error": str(e)}
|
||||
return bytes(json.dumps(error_data), "UTF-8")
|
||||
|
||||
|
||||
def js_wallets(self, url_split, post_string, is_json):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -166,12 +306,13 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
return bytes(
|
||||
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
|
||||
)
|
||||
|
||||
raise ValueError("Unknown command")
|
||||
|
||||
if coin_type == Coins.LTC_MWEB:
|
||||
coin_type = Coins.LTC
|
||||
rv = swap_client.getWalletInfo(coin_type)
|
||||
if not rv:
|
||||
raise ValueError(f"getWalletInfo failed for coin: {coin_type}")
|
||||
rv.update(swap_client.getBlockchainInfo(coin_type))
|
||||
ci = swap_client.ci(coin_type)
|
||||
checkAddressesOwned(swap_client, ci, rv)
|
||||
@@ -182,7 +323,19 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
|
||||
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
offer_id = None
|
||||
if len(url_split) > 3:
|
||||
if url_split[3] == "new":
|
||||
@@ -205,6 +358,12 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
if offer_id:
|
||||
filters["offer_id"] = offer_id
|
||||
|
||||
parsed_url = urllib.parse.urlparse(self.path)
|
||||
query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {}
|
||||
|
||||
if "with_extra_info" in query_params:
|
||||
with_extra_info = toBool(query_params["with_extra_info"][0])
|
||||
|
||||
if post_string != "":
|
||||
post_data = getFormData(post_string, is_json)
|
||||
filters["coin_from"] = setCoinFilter(post_data, "coin_from")
|
||||
@@ -257,7 +416,9 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
"is_revoked": True if o.active_ind == 2 else False,
|
||||
"is_public": o.addr_to == swap_client.network_addr
|
||||
or o.addr_to.strip() == "",
|
||||
"message_nets": o.message_nets,
|
||||
}
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
if with_extra_info:
|
||||
offer_data["amount_negotiable"] = o.amount_negotiable
|
||||
offer_data["rate_negotiable"] = o.rate_negotiable
|
||||
@@ -272,6 +433,24 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
offer_data["feerate_from"] = o.from_feerate
|
||||
offer_data["feerate_to"] = o.to_feerate
|
||||
|
||||
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
|
||||
|
||||
if o.was_sent:
|
||||
try:
|
||||
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id)
|
||||
if strategy:
|
||||
offer_data["local_automation_strat_id"] = strategy[0]
|
||||
swap_client.log.debug(
|
||||
f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}"
|
||||
)
|
||||
else:
|
||||
offer_data["local_automation_strat_id"] = 0
|
||||
except Exception as e:
|
||||
swap_client.log.debug(
|
||||
f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}"
|
||||
)
|
||||
offer_data["local_automation_strat_id"] = 0
|
||||
|
||||
rv.append(offer_data)
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
|
||||
@@ -331,7 +510,8 @@ def formatBids(swap_client, bids, filters) -> bytes:
|
||||
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_int = (b[4] * bid_rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
|
||||
bid_data = {
|
||||
"bid_id": b[2].hex(),
|
||||
@@ -513,7 +693,19 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
|
||||
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
post_data = getFormData(post_string, is_json)
|
||||
offer_id, filters = parseBidFilters(post_data)
|
||||
|
||||
@@ -609,16 +801,14 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
if amt_from_str is not None:
|
||||
rate = ci_to.make_int(rate, r=1)
|
||||
amt_from = inputAmount(amt_from_str, ci_from)
|
||||
amount_to = ci_to.format_amount(
|
||||
int((amt_from * rate) // ci_from.COIN()), r=1
|
||||
)
|
||||
amount_to_int = (amt_from * rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
return bytes(json.dumps({"amount_to": amount_to}), "UTF-8")
|
||||
if amt_to_str is not None:
|
||||
rate = ci_from.make_int(1.0 / float(rate), r=1)
|
||||
amt_to = inputAmount(amt_to_str, ci_to)
|
||||
amount_from = ci_from.format_amount(
|
||||
int((amt_to * rate) // ci_to.COIN()), r=1
|
||||
)
|
||||
amount_from_int = (amt_to * rate + ci_to.COIN() - 1) // ci_to.COIN()
|
||||
amount_from = ci_from.format_amount(amount_from_int)
|
||||
return bytes(json.dumps({"amount_from": amount_from}), "UTF-8")
|
||||
|
||||
amt_from: int = inputAmount(get_data_entry(post_data, "amt_from"), ci_from)
|
||||
@@ -630,7 +820,19 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
|
||||
def js_index(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
return bytes(json.dumps(swap_client.getSummary()), "UTF-8")
|
||||
|
||||
|
||||
@@ -640,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
if not swap_client.debug:
|
||||
raise ValueError("Debug mode not active.")
|
||||
|
||||
r = random.randint(0, 3)
|
||||
r = random.randint(0, 4)
|
||||
if r == 0:
|
||||
swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()})
|
||||
swap_client.notify(
|
||||
NT.OFFER_RECEIVED,
|
||||
{
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 1:
|
||||
swap_client.notify(
|
||||
NT.BID_RECEIVED,
|
||||
@@ -650,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "atomic",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"bid_amount": 50000000,
|
||||
"bid_amount_to": 7750000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 2:
|
||||
@@ -661,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "ads",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 1,
|
||||
"coin_to": 3,
|
||||
"amount_from": 500000000,
|
||||
"amount_to": 100000000,
|
||||
"bid_amount": 250000000,
|
||||
"bid_amount_to": 50000000,
|
||||
"rate": 20000000,
|
||||
},
|
||||
)
|
||||
elif r == 4:
|
||||
swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()})
|
||||
|
||||
return bytes(json.dumps({"type": r}), "UTF-8")
|
||||
|
||||
|
||||
def js_checkupdates(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
if not swap_client.settings.get("check_updates", True):
|
||||
return bytes(
|
||||
json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8"
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
now = time.time()
|
||||
last_manual_check = getattr(swap_client, "_last_manual_update_check", 0)
|
||||
|
||||
if not swap_client.debug and (now - last_manual_check) < 3600:
|
||||
remaining = int(3600 - (now - last_manual_check))
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"error": f"Please wait {remaining // 60} minutes before checking again"
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
swap_client._last_manual_update_check = now
|
||||
swap_client.log.info("Manual update check requested via web interface")
|
||||
|
||||
swap_client.checkForUpdates()
|
||||
|
||||
if swap_client._update_available:
|
||||
swap_client.log.info(
|
||||
f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})"
|
||||
)
|
||||
else:
|
||||
swap_client.log.info(f"Manual check result: Up to date (v{__version__})")
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"message": "Update check completed",
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"update_available": swap_client._update_available,
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -674,6 +952,32 @@ def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(swap_client.getNotifications()), "UTF-8")
|
||||
|
||||
|
||||
def js_updatestatus(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"update_available": swap_client._update_available,
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"release_url": (
|
||||
f"https://github.com/basicswap/basicswap/releases/tag/v{swap_client._latest_version}"
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
"release_notes": (
|
||||
f"New version v{swap_client._latest_version} is available. Click to view details on GitHub."
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -796,7 +1100,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
|
||||
"label": strat_data.label,
|
||||
"type_ind": strat_data.type_ind,
|
||||
"only_known_identities": strat_data.only_known_identities,
|
||||
"data": json.loads(strat_data.data.decode("utf-8")),
|
||||
"data": json.loads(strat_data.data.decode("UTF-8")),
|
||||
"note": "" if strat_data.note is None else strat_data.note,
|
||||
}
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
@@ -851,6 +1155,15 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
coin_in = get_data_entry(post_data, "coin")
|
||||
extkey_prefix = get_data_entry_or(
|
||||
post_data, "extkey_prefix", 0x04B2430C
|
||||
) # default, zprv for P2WPKH in electrum
|
||||
if isinstance(extkey_prefix, str):
|
||||
if extkey_prefix.isdigit():
|
||||
extkey_prefix = int(extkey_prefix)
|
||||
else:
|
||||
extkey_prefix = int(extkey_prefix, 16) # Try hex
|
||||
|
||||
try:
|
||||
coin = getCoinIdFromName(coin_in)
|
||||
except Exception:
|
||||
@@ -887,7 +1200,6 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
wallet_seed_id = ci.getWalletSeedID()
|
||||
except Exception as e:
|
||||
wallet_seed_id = f"Error: {e}"
|
||||
|
||||
rv.update(
|
||||
{
|
||||
"seed": seed_key.hex(),
|
||||
@@ -896,6 +1208,10 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
"current_seed_id": wallet_seed_id,
|
||||
}
|
||||
)
|
||||
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
|
||||
rv.update(
|
||||
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
|
||||
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
|
||||
|
||||
return bytes(
|
||||
json.dumps(rv),
|
||||
@@ -930,7 +1246,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
password = get_data_entry(post_data, "password")
|
||||
password: str = get_data_entry(post_data, "password")
|
||||
|
||||
if have_data_entry(post_data, "coin"):
|
||||
coin = getCoinType(str(get_data_entry(post_data, "coin")))
|
||||
@@ -998,6 +1314,16 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
|
||||
try:
|
||||
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
|
||||
try:
|
||||
ci_from = swap_client.ci(offer.coin_from)
|
||||
ci_to = swap_client.ci(offer.coin_to)
|
||||
if offer.bid_reversed:
|
||||
amount_from: int = bid.amount_to
|
||||
amount_to: int = bid.amount
|
||||
bid_rate: int = ci_from.make_int(amount_to / amount_from, r=1)
|
||||
else:
|
||||
amount_from: int = bid.amount
|
||||
amount_to: int = bid.amount_to
|
||||
bid_rate: int = bid.rate
|
||||
swap_data = {
|
||||
"bid_id": bid_id.hex(),
|
||||
"offer_id": offer.offer_id.hex(),
|
||||
@@ -1006,15 +1332,13 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
|
||||
"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
|
||||
),
|
||||
"coin_from": ci_from.coin_name(),
|
||||
"coin_to": ci_to.coin_name(),
|
||||
"amount_from": ci_from.format_amount(amount_from),
|
||||
"amount_to": ci_to.format_amount(amount_to),
|
||||
"rate": bid_rate,
|
||||
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
|
||||
"was_sent": bid.was_sent,
|
||||
}
|
||||
|
||||
if offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
@@ -1032,9 +1356,6 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_data["tx_state_a"] = bid.getITxState()
|
||||
swap_data["tx_state_b"] = bid.getPTxState()
|
||||
|
||||
if hasattr(bid, "rate"):
|
||||
swap_data["rate"] = bid.rate
|
||||
|
||||
all_bids.append(swap_data)
|
||||
|
||||
except Exception:
|
||||
@@ -1100,8 +1421,157 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def js_coinvolume(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.")
|
||||
|
||||
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
|
||||
|
||||
volume_data = swap_client.lookupVolume(
|
||||
coin_ids, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in volume_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_coinhistory(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.")
|
||||
|
||||
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", 3600))
|
||||
days: int = int(get_data_entry_or(post_data, "days", 1))
|
||||
|
||||
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
|
||||
|
||||
historical_data = swap_client.lookupHistoricalData(
|
||||
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in historical_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "days": days, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_messageroutes(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)
|
||||
|
||||
filters = {
|
||||
"page_no": 1,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
}
|
||||
|
||||
if have_data_entry(post_data, "sort_by"):
|
||||
sort_by = get_data_entry(post_data, "sort_by")
|
||||
ensure(
|
||||
sort_by
|
||||
in [
|
||||
"created_at",
|
||||
],
|
||||
"Invalid sort by",
|
||||
)
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(post_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(post_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
|
||||
if have_data_entry(post_data, "offset"):
|
||||
filters["offset"] = int(get_data_entry(post_data, "offset"))
|
||||
if have_data_entry(post_data, "limit"):
|
||||
filters["limit"] = int(get_data_entry(post_data, "limit"))
|
||||
ensure(filters["limit"] > 0, "Invalid limit")
|
||||
|
||||
if have_data_entry(post_data, "address_from"):
|
||||
filters["address_from"] = get_data_entry(post_data, "address_from")
|
||||
if have_data_entry(post_data, "address_to"):
|
||||
filters["address_to"] = get_data_entry(post_data, "address_to")
|
||||
|
||||
action = get_data_entry_or(post_data, "action", None)
|
||||
|
||||
message_routes = swap_client.listMessageRoutes(filters, action)
|
||||
return bytes(json.dumps(message_routes), "UTF-8")
|
||||
|
||||
|
||||
endpoints = {
|
||||
"coins": js_coins,
|
||||
"walletbalances": js_walletbalances,
|
||||
"wallets": js_wallets,
|
||||
"offers": js_offers,
|
||||
"sentoffers": js_sentoffers,
|
||||
@@ -1114,6 +1584,8 @@ endpoints = {
|
||||
"rates": js_rates,
|
||||
"rateslist": js_rates_list,
|
||||
"generatenotification": js_generatenotification,
|
||||
"checkupdates": js_checkupdates,
|
||||
"updatestatus": js_updatestatus,
|
||||
"notifications": js_notifications,
|
||||
"identities": js_identities,
|
||||
"automationstrategies": js_automationstrategies,
|
||||
@@ -1127,6 +1599,9 @@ endpoints = {
|
||||
"readurl": js_readurl,
|
||||
"active": js_active,
|
||||
"coinprices": js_coinprices,
|
||||
"coinvolume": js_coinvolume,
|
||||
"coinhistory": js_coinhistory,
|
||||
"messageroutes": js_messageroutes,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 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.
|
||||
|
||||
@@ -23,6 +24,13 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||
from basicswap.util.integer import encode_varint, decode_varint
|
||||
|
||||
|
||||
NPBW_INT = 0
|
||||
NPBW_BYTES = 2
|
||||
|
||||
NPBF_STR = 1
|
||||
NPBF_BOOL = 2
|
||||
|
||||
|
||||
class NonProtobufClass:
|
||||
def __init__(self, init_all: bool = True, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
@@ -34,7 +42,7 @@ class NonProtobufClass:
|
||||
found_field = True
|
||||
break
|
||||
if found_field is False:
|
||||
raise ValueError(f"got an unexpected keyword argument '{key}'")
|
||||
raise ValueError(f"Got an unexpected keyword argument '{key}'")
|
||||
|
||||
if init_all:
|
||||
self.init_fields()
|
||||
@@ -117,151 +125,182 @@ class NonProtobufClass:
|
||||
|
||||
class OfferMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("coin_from", 0, 0),
|
||||
3: ("coin_to", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("min_bid_amount", 0, 0),
|
||||
7: ("time_valid", 0, 0),
|
||||
8: ("lock_type", 0, 0),
|
||||
9: ("lock_value", 0, 0),
|
||||
10: ("swap_type", 0, 0),
|
||||
11: ("proof_address", 2, 1),
|
||||
12: ("proof_signature", 2, 1),
|
||||
13: ("pkhash_seller", 2, 0),
|
||||
14: ("secret_hash", 2, 0),
|
||||
15: ("fee_rate_from", 0, 0),
|
||||
16: ("fee_rate_to", 0, 0),
|
||||
17: ("amount_negotiable", 0, 2),
|
||||
18: ("rate_negotiable", 0, 2),
|
||||
19: ("proof_utxos", 2, 0),
|
||||
20: ("auto_accept_type", 0, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("coin_from", NPBW_INT, 0),
|
||||
3: ("coin_to", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("min_bid_amount", NPBW_INT, 0),
|
||||
7: ("time_valid", NPBW_INT, 0),
|
||||
8: ("lock_type", NPBW_INT, 0),
|
||||
9: ("lock_value", NPBW_INT, 0),
|
||||
10: ("swap_type", NPBW_INT, 0),
|
||||
11: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
12: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
13: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
14: ("secret_hash", NPBW_BYTES, 0),
|
||||
15: ("fee_rate_from", NPBW_INT, 0),
|
||||
16: ("fee_rate_to", NPBW_INT, 0),
|
||||
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
19: ("proof_utxos", NPBW_BYTES, 0),
|
||||
20: ("auto_accept_type", NPBW_INT, 0),
|
||||
21: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
class BidMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkhash_buyer", 2, 0),
|
||||
7: ("proof_address", 2, 1),
|
||||
8: ("proof_signature", 2, 1),
|
||||
9: ("proof_utxos", 2, 0),
|
||||
10: ("pkhash_buyer_to", 2, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkhash_buyer", NPBW_BYTES, 0),
|
||||
7: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
9: ("proof_utxos", NPBW_BYTES, 0),
|
||||
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
|
||||
11: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
class BidAcceptMessage(NonProtobufClass):
|
||||
# Step 3, seller -> buyer
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("initiate_txid", 2, 0),
|
||||
3: ("contract_script", 2, 0),
|
||||
4: ("pkhash_seller", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("initiate_txid", NPBW_BYTES, 0),
|
||||
3: ("contract_script", NPBW_BYTES, 0),
|
||||
4: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class OfferRevokeMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("offer_msg_id", 2, 0),
|
||||
2: ("signature", 2, 0),
|
||||
1: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
2: ("signature", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidRejectMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("reject_code", 0, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("reject_code", NPBW_INT, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidMessage(NonProtobufClass):
|
||||
# MSG1L, F -> L
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkaf", 2, 0),
|
||||
7: ("kbvf", 2, 0),
|
||||
8: ("kbsf_dleag", 2, 0),
|
||||
9: ("dest_af", 2, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkaf", NPBW_BYTES, 0),
|
||||
7: ("kbvf", NPBW_BYTES, 0),
|
||||
8: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
9: ("dest_af", NPBW_BYTES, 0),
|
||||
10: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
class XmrSplitMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("msg_id", 2, 0),
|
||||
2: ("msg_type", 0, 0),
|
||||
3: ("sequence", 0, 0),
|
||||
4: ("dleag", 2, 0),
|
||||
1: ("msg_id", NPBW_BYTES, 0),
|
||||
2: ("msg_type", NPBW_INT, 0),
|
||||
3: ("sequence", NPBW_INT, 0),
|
||||
4: ("dleag", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidAcceptMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkal", 2, 0),
|
||||
3: ("kbvl", 2, 0),
|
||||
4: ("kbsl_dleag", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("pkal", NPBW_BYTES, 0),
|
||||
3: ("kbvl", NPBW_BYTES, 0),
|
||||
4: ("kbsl_dleag", NPBW_BYTES, 0),
|
||||
# MSG2F
|
||||
5: ("a_lock_tx", 2, 0),
|
||||
6: ("a_lock_tx_script", 2, 0),
|
||||
7: ("a_lock_refund_tx", 2, 0),
|
||||
8: ("a_lock_refund_tx_script", 2, 0),
|
||||
9: ("a_lock_refund_spend_tx", 2, 0),
|
||||
10: ("al_lock_refund_tx_sig", 2, 0),
|
||||
5: ("a_lock_tx", NPBW_BYTES, 0),
|
||||
6: ("a_lock_tx_script", NPBW_BYTES, 0),
|
||||
7: ("a_lock_refund_tx", NPBW_BYTES, 0),
|
||||
8: ("a_lock_refund_tx_script", NPBW_BYTES, 0),
|
||||
9: ("a_lock_refund_spend_tx", NPBW_BYTES, 0),
|
||||
10: ("al_lock_refund_tx_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
||||
# MSG3L
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", 2, 0),
|
||||
3: ("af_lock_refund_tx_sig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", NPBW_BYTES, 0),
|
||||
3: ("af_lock_refund_tx_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
||||
# MSG4F
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("a_lock_spend_tx", 2, 0),
|
||||
3: ("kal_sig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("a_lock_spend_tx", NPBW_BYTES, 0),
|
||||
3: ("kal_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||
# MSG5F
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("al_lock_spend_tx_esig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("al_lock_spend_tx_esig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentMessage(NonProtobufClass):
|
||||
# L -> F Sent from bidder, construct a reverse bid
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
||||
# F -> L Sent from offerer, construct a reverse bid
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkaf", 2, 0),
|
||||
3: ("kbvf", 2, 0),
|
||||
4: ("kbsf_dleag", 2, 0),
|
||||
5: ("dest_af", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("pkaf", NPBW_BYTES, 0),
|
||||
3: ("kbvf", NPBW_BYTES, 0),
|
||||
4: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
5: ("dest_af", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class ConnectReqMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("network_type", NPBW_INT, 0),
|
||||
2: ("network_data", NPBW_BYTES, 0),
|
||||
3: ("request_type", NPBW_INT, 0),
|
||||
4: ("request_data", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class MessagePortalOffer(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("network_type_from", NPBW_INT, 0),
|
||||
2: ("network_type_to", NPBW_INT, 0),
|
||||
3: ("portal_address_from", NPBW_BYTES, 0),
|
||||
4: ("portal_address_to", NPBW_BYTES, 0),
|
||||
5: ("time_valid", NPBW_INT, 0),
|
||||
6: ("smsg_difficulty", NPBW_INT, 0),
|
||||
}
|
||||
|
||||
|
||||
class MessagePortalSend(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("forward_address", NPBW_BYTES, 0), # pubkey, 33 bytes
|
||||
2: ("message_bytes", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
1185
basicswap/network/bsx_network.py
Normal file
1185
basicswap/network/bsx_network.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import websocket
|
||||
|
||||
|
||||
@@ -22,12 +23,9 @@ from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
b58decode,
|
||||
decodeWif,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
BidStates,
|
||||
)
|
||||
from basicswap.basicswap_util import AddressTypes
|
||||
|
||||
|
||||
def encode_base64(data: bytes) -> str:
|
||||
@@ -52,6 +50,20 @@ class WebSocketThread(threading.Thread):
|
||||
|
||||
self.recv_queue = Queue()
|
||||
self.cmd_recv_queue = Queue()
|
||||
self.delayed_events_queue = Queue()
|
||||
|
||||
self.ignore_events: bool = False
|
||||
|
||||
self.num_messages_received: int = 0
|
||||
|
||||
def disable_debug_mode(self):
|
||||
self.ignore_events = False
|
||||
for i in range(100):
|
||||
try:
|
||||
message = self.delayed_events_queue.get(block=False)
|
||||
except Empty:
|
||||
break
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def on_message(self, ws, message):
|
||||
if self.logger:
|
||||
@@ -62,6 +74,7 @@ class WebSocketThread(threading.Thread):
|
||||
if message.startswith('{"corrId"'):
|
||||
self.cmd_recv_queue.put(message)
|
||||
else:
|
||||
self.num_messages_received += 1
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def queue_get(self):
|
||||
@@ -106,6 +119,20 @@ class WebSocketThread(threading.Thread):
|
||||
self.ws.send(cmd)
|
||||
return self.corrId
|
||||
|
||||
def wait_for_command_response(self, cmd_id, num_tries: int = 200):
|
||||
cmd_id = str(cmd_id)
|
||||
for i in range(num_tries):
|
||||
message = self.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == cmd_id:
|
||||
return data
|
||||
self.delay_event.wait(0.5)
|
||||
raise ValueError(
|
||||
f"wait_for_command_response timed-out waiting for ID: {cmd_id}"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.ws = websocket.WebSocketApp(
|
||||
self.url,
|
||||
@@ -126,16 +153,15 @@ class WebSocketThread(threading.Thread):
|
||||
|
||||
def waitForResponse(ws_thread, sent_id, delay_event):
|
||||
sent_id = str(sent_id)
|
||||
for i in range(100):
|
||||
for i in range(200):
|
||||
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}")
|
||||
raise ValueError(f"waitForResponse timed-out waiting for ID: {sent_id}")
|
||||
|
||||
|
||||
def waitForConnected(ws_thread, delay_event):
|
||||
@@ -146,81 +172,102 @@ def waitForConnected(ws_thread, delay_event):
|
||||
raise ValueError("waitForConnected timed-out.")
|
||||
|
||||
|
||||
def getPrivkeyForAddress(self, addr) -> bytes:
|
||||
def encryptMsg(
|
||||
self,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp=None,
|
||||
deterministic=False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> bytes:
|
||||
self.log.debug("encryptMsg")
|
||||
|
||||
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")
|
||||
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
|
||||
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
|
||||
|
||||
smsg_msg: bytes = smsgEncrypt(
|
||||
privkey_from,
|
||||
pubkey_to,
|
||||
payload,
|
||||
timestamp,
|
||||
deterministic,
|
||||
msg_valid,
|
||||
difficulty_target=difficulty_target,
|
||||
)
|
||||
|
||||
return smsg_msg
|
||||
|
||||
|
||||
def sendSimplexMsg(
|
||||
self, network, addr_from: str, addr_to: str, payload: bytes, msg_valid: int, cursor
|
||||
self,
|
||||
network,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp: int = None,
|
||||
deterministic: bool = False,
|
||||
to_user_name: str = None,
|
||||
return_msg: bool = False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> 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_msg: bytes = encryptMsg(
|
||||
self,
|
||||
addr_from,
|
||||
addr_to,
|
||||
payload,
|
||||
msg_valid,
|
||||
cursor,
|
||||
timestamp,
|
||||
deterministic,
|
||||
difficulty_target,
|
||||
)
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
|
||||
ws_thread = network["ws_thread"]
|
||||
sent_id = ws_thread.send_command("#bsx " + encode_base64(smsg_msg))
|
||||
if to_user_name is not None:
|
||||
to = "@" + to_user_name + " "
|
||||
else:
|
||||
to = "#bsx "
|
||||
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if response["resp"]["type"] != "newChatItems":
|
||||
if getResponseData(response, "type") != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
self.log.debug(f"Response {json_str}")
|
||||
raise ValueError("Send failed")
|
||||
if to_user_name is not None:
|
||||
self.num_direct_simplex_messages_sent += 1
|
||||
else:
|
||||
self.num_group_simplex_messages_sent += 1
|
||||
|
||||
if return_msg:
|
||||
return smsg_id, smsg_msg
|
||||
return smsg_id
|
||||
|
||||
|
||||
def forwardSimplexMsg(self, network, smsg_msg, to_user_name: str = None):
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
ws_thread = network["ws_thread"]
|
||||
if to_user_name is not None:
|
||||
to = "@" + to_user_name + " "
|
||||
else:
|
||||
to = "#bsx "
|
||||
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if getResponseData(response, "type") != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
self.log.debug(f"Response {json_str}")
|
||||
raise ValueError("Send failed")
|
||||
if to_user_name is not None:
|
||||
self.num_direct_simplex_messages_sent += 1
|
||||
else:
|
||||
self.num_group_simplex_messages_sent += 1
|
||||
|
||||
return smsg_id
|
||||
|
||||
@@ -233,7 +280,7 @@ def decryptSimplexMsg(self, msg_data):
|
||||
try:
|
||||
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pk_from"])
|
||||
bytes.fromhex(decrypted["pubkey_from"])
|
||||
)
|
||||
decrypted["to"] = self.network_addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
@@ -243,10 +290,14 @@ def decryptSimplexMsg(self, msg_data):
|
||||
|
||||
# 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))
|
||||
SELECT b.bid_addr AS address FROM bids b
|
||||
JOIN bidstates s ON b.state = s.state_id
|
||||
WHERE b.active_ind = 1
|
||||
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
|
||||
UNION
|
||||
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
|
||||
UNION
|
||||
SELECT addr AS address FROM smsgaddresses WHERE active_ind = 1 AND use_type = :local_portal
|
||||
)"""
|
||||
|
||||
now: int = self.getTime()
|
||||
@@ -254,75 +305,147 @@ def decryptSimplexMsg(self, msg_data):
|
||||
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,
|
||||
},
|
||||
query, {"now": now, "local_portal": AddressTypes.PORTAL_LOCAL}
|
||||
).fetchall()
|
||||
decrypted = None
|
||||
for row in addr_rows:
|
||||
addr = row[0]
|
||||
try:
|
||||
vk_addr = self.getPrivkeyForAddress(cursor, addr)
|
||||
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pubkey_from"])
|
||||
)
|
||||
decrypted["to"] = addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
return decrypted
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
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 parseSimplexMsg(self, chat_item):
|
||||
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
|
||||
dir_type = item_status["type"]
|
||||
if dir_type not in ("sndRcvd", "rcvNew"):
|
||||
return None
|
||||
|
||||
snd_progress = item_status.get("sndProgress", None)
|
||||
if snd_progress and snd_progress != "complete":
|
||||
item_id = chat_item["chatItem"]["meta"]["itemId"]
|
||||
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
|
||||
return None
|
||||
|
||||
conn_id = None
|
||||
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
|
||||
chat_type: str = chat_item["chatInfo"]["type"]
|
||||
if chat_type == "group":
|
||||
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
|
||||
self.num_group_simplex_messages_received += 1
|
||||
elif chat_type == "direct":
|
||||
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
|
||||
self.num_direct_simplex_messages_received += 1
|
||||
else:
|
||||
return None
|
||||
|
||||
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
|
||||
try:
|
||||
msg_data: bytes = decode_base64(msg_content)
|
||||
decrypted_msg = decryptSimplexMsg(self, msg_data)
|
||||
if decrypted_msg is None:
|
||||
return None
|
||||
decrypted_msg["chat_type"] = chat_type
|
||||
decrypted_msg["chat_name"] = chat_name
|
||||
decrypted_msg["conn_id"] = conn_id
|
||||
decrypted_msg["msg_dir"] = msg_dir
|
||||
return decrypted_msg
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
|
||||
if ws_thread.ignore_events:
|
||||
if msg_type not in ("contactConnected", "contactDeletedByContact"):
|
||||
return False
|
||||
ws_thread.delayed_events_queue.put(json.dumps(data))
|
||||
return True
|
||||
|
||||
if msg_type == "contactConnected":
|
||||
self.processContactConnected(data)
|
||||
elif msg_type == "contactDeletedByContact":
|
||||
self.processContactDisconnected(data)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def readSimplexMsgs(self, network):
|
||||
ws_thread = network["ws_thread"]
|
||||
|
||||
for i in range(100):
|
||||
message = ws_thread.queue_get()
|
||||
if message is None:
|
||||
break
|
||||
if self.delay_event.is_set():
|
||||
break
|
||||
|
||||
data = json.loads(message)
|
||||
# self.log.debug(f"message 1: {json.dumps(data, indent=4)}")
|
||||
# self.log.debug(f"Message: {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
|
||||
msg_type: str = getResponseData(data, "type")
|
||||
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
|
||||
for chat_item in getResponseData(data, "chatItems"):
|
||||
decrypted_msg = parseSimplexMsg(self, chat_item)
|
||||
if decrypted_msg is None:
|
||||
continue
|
||||
self.processMsg(decrypted_msg)
|
||||
elif msg_type == "chatError":
|
||||
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
|
||||
pass
|
||||
elif processEvent(self, ws_thread, msg_type, data):
|
||||
pass
|
||||
else:
|
||||
self.log.debug(f"simplex: Unknown msg_type: {msg_type}")
|
||||
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
|
||||
except Exception as e:
|
||||
self.log.debug(f"readSimplexMsgs error: {e}")
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
self.delay_event.wait(0.05)
|
||||
|
||||
|
||||
def getResponseData(data, tag=None):
|
||||
for pretag in ("Right", "Left"):
|
||||
if pretag in data["resp"]:
|
||||
if tag:
|
||||
return data["resp"][pretag][tag]
|
||||
return data["resp"][pretag]
|
||||
if tag:
|
||||
return data["resp"][tag]
|
||||
return data["resp"]
|
||||
|
||||
|
||||
def getNewSimplexLink(data):
|
||||
response_data = getResponseData(data)
|
||||
if "connLinkContact" in response_data:
|
||||
return response_data["connLinkContact"]["connFullLink"]
|
||||
return response_data["connReqContact"]
|
||||
|
||||
|
||||
def getJoinedSimplexLink(data):
|
||||
response_data = getResponseData(data)
|
||||
if "connLinkInvitation" in response_data:
|
||||
return response_data["connLinkInvitation"]["connFullLink"]
|
||||
return response_data["connReqInvitation"]
|
||||
|
||||
|
||||
def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
self.log.debug("initialiseSimplexNetwork")
|
||||
|
||||
@@ -337,14 +460,57 @@ def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
sent_id = ws_thread.send_command("/groups")
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
|
||||
if len(response["resp"]["groups"]) < 1:
|
||||
if len(getResponseData(response, "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"]
|
||||
assert "groupLinkId" in getResponseData(response, "connection")
|
||||
|
||||
network = {
|
||||
add_network = {
|
||||
"type": "simplex",
|
||||
"ws_thread": ws_thread,
|
||||
}
|
||||
if "bridged" in network_config:
|
||||
add_network["bridged"] = network_config["bridged"]
|
||||
|
||||
self.active_networks.append(network)
|
||||
self.active_networks.append(add_network)
|
||||
|
||||
|
||||
def closeSimplexChat(self, net_i, connId) -> bool:
|
||||
try:
|
||||
cmd_id = net_i.send_command("/chats")
|
||||
response = net_i.wait_for_command_response(cmd_id, num_tries=500)
|
||||
remote_name = None
|
||||
for chat in getResponseData(response, "chats"):
|
||||
if (
|
||||
"chatInfo" not in chat
|
||||
or "type" not in chat["chatInfo"]
|
||||
or chat["chatInfo"]["type"] != "direct"
|
||||
):
|
||||
continue
|
||||
try:
|
||||
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
|
||||
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error parsing chat: {e}")
|
||||
|
||||
if remote_name is None:
|
||||
self.log.warning(
|
||||
f"Unable to find remote name for simplex direct chat, ID: {connId}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
|
||||
cmd_id = net_i.send_command(f"/delete @{remote_name}")
|
||||
cmd_response = net_i.wait_for_command_response(cmd_id)
|
||||
|
||||
if getResponseData(cmd_response, "type") != "contactDeleted":
|
||||
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
|
||||
self.log.debug(
|
||||
"cmd_response: {}".format(json.dumps(cmd_response, indent=4))
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error deleting simplex chat, ID: {connId} - {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -7,13 +7,50 @@
|
||||
|
||||
import os
|
||||
import select
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from basicswap.bin.run import Daemon
|
||||
from basicswap.util.daemon import Daemon
|
||||
|
||||
|
||||
def serverExistsInDatabase(simplex_db_path: str, server_address: str, logger) -> bool:
|
||||
try:
|
||||
# Extract hostname from SMP URL format: smp://fingerprint@hostname
|
||||
if server_address.startswith("smp://") and "@" in server_address:
|
||||
host = server_address.split("@")[-1]
|
||||
elif ":" in server_address:
|
||||
host = server_address.split(":", 1)[0]
|
||||
else:
|
||||
host = server_address
|
||||
|
||||
with sqlite3.connect(simplex_db_path) as con:
|
||||
c = con.cursor()
|
||||
|
||||
# Check for any server entry with this hostname
|
||||
query = (
|
||||
"SELECT COUNT(*) FROM protocol_servers WHERE host LIKE ? OR host = ?"
|
||||
)
|
||||
host_pattern = f"%{host}%"
|
||||
count = c.execute(query, (host_pattern, host)).fetchone()[0]
|
||||
|
||||
if count > 0:
|
||||
logger.debug(
|
||||
f"Server {host} already exists in database ({count} entries)"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Server {host} not found in database")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database check failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def initSimplexClient(args, logger, delay_event):
|
||||
# Need to set initial profile through CLI
|
||||
# TODO: Must be a better way?
|
||||
logger.info("Initialising Simplex client")
|
||||
|
||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||
@@ -29,7 +66,7 @@ def initSimplexClient(args, logger, delay_event):
|
||||
def readOutput():
|
||||
buf = os.read(pipe_r, 1024).decode("utf-8")
|
||||
response = None
|
||||
# logging.debug(f"simplex-chat output: {buf}")
|
||||
# logger.debug(f"simplex-chat output: {buf}")
|
||||
if "display name:" in buf:
|
||||
logger.debug("Setting display name")
|
||||
response = b"user\n"
|
||||
@@ -45,7 +82,7 @@ def initSimplexClient(args, logger, delay_event):
|
||||
max_wait_seconds: int = 60
|
||||
while p.poll() is None:
|
||||
if time.time() > start_time + max_wait_seconds:
|
||||
raise ValueError("Timed out")
|
||||
raise RuntimeError("Timed out")
|
||||
if os.name == "nt":
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
@@ -70,22 +107,35 @@ def startSimplexClient(
|
||||
websocket_port: int,
|
||||
logger,
|
||||
delay_event,
|
||||
socks_proxy=None,
|
||||
log_level: str = "debug",
|
||||
) -> 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")
|
||||
simplex_data_prefix = os.path.join(data_path, "simplex_client_data")
|
||||
simplex_db_path = simplex_data_prefix + "_chat.db"
|
||||
args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)]
|
||||
|
||||
args = [bin_path, "-d", db_path, "-s", server_address, "-p", str(websocket_port)]
|
||||
if socks_proxy:
|
||||
args += ["--socks-proxy", socks_proxy]
|
||||
|
||||
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
|
||||
if not os.path.exists(simplex_db_path):
|
||||
# Database doesn't exist - safe to add server during initialization
|
||||
logger.info("Database not found, initializing Simplex client")
|
||||
init_args = args + ["-e", "/help"] # Run command to exit client
|
||||
init_args += ["-s", server_address]
|
||||
initSimplexClient(init_args, logger, delay_event)
|
||||
else:
|
||||
# Database exists - only add server if it's not already there
|
||||
if not serverExistsInDatabase(simplex_db_path, server_address, logger):
|
||||
logger.debug(f"Adding server to Simplex CLI args: {server_address}")
|
||||
args += ["-s", server_address]
|
||||
else:
|
||||
logger.debug("Server already exists, not adding to CLI args")
|
||||
|
||||
args += ["-l", "debug"]
|
||||
args += ["-l", log_level]
|
||||
|
||||
opened_files = []
|
||||
stdout_dest = open(
|
||||
@@ -104,4 +154,5 @@ def startSimplexClient(
|
||||
cwd=data_path,
|
||||
),
|
||||
opened_files,
|
||||
"simplex-chat",
|
||||
)
|
||||
|
||||
@@ -9,8 +9,8 @@ from basicswap.util.address import b58decode
|
||||
|
||||
|
||||
def getMsgPubkey(self, msg) -> bytes:
|
||||
if "pk_from" in msg:
|
||||
return bytes.fromhex(msg["pk_from"])
|
||||
if "pubkey_from" in msg:
|
||||
return bytes.fromhex(msg["pubkey_from"])
|
||||
rv = self.callrpc(
|
||||
"smsggetpubkey",
|
||||
[
|
||||
|
||||
54
basicswap/pgp/keys/SimpleX_Chat.pgp
Normal file
54
basicswap/pgp/keys/SimpleX_Chat.pgp
Normal file
@@ -0,0 +1,54 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC
|
||||
Comment: SimpleX Chat <chat@simplex.chat>
|
||||
|
||||
xsFNBGRDvZkBEACsxFENFWj5hMS1dCPCOXIJTNnWClVarltfUOESy5q0Ar84WJaj
|
||||
hmAcc8j1Qw7uiLxVq/j+tMxcZOy79jnmhWpV5KrYA6H/E3I5NNlZOyT23rvah9mg
|
||||
KtxfMHnhz/jJSwSXifYN2mmAYetQ1TQBSdLZayC7aW6BFhUaaQsaFABGli5abRUW
|
||||
KArmnSfVEHI0f7TthLerPZ0hCoK06ZOPxEKCWt5CSqrC3J2d+8Cyb6j2jxkkB3GN
|
||||
JXr9kI4JebivqrFNwvGw15xEDbSXIZf9I/+B/t9EA4Ebs+qrbLFRH5Drha50RIhu
|
||||
LNYCkVnpKbrO6Y90KkJibm4ZtdUeNTFXjfXxT81Gi5lDmsvIyIMkFC78ePK68knM
|
||||
dnESnIzEEwDtniV+ZvY0L9t/Ig1tGYggqPGVTVp9672bHKTGdiL3eXEzwv0FROD2
|
||||
0HaZORXj2UZkAJTQO2ia7aS3hWdJL/iVBf4yIYARr+6NjPxv/sUMCaeuPYXTqCOB
|
||||
Ykl6Lv3SPoSkEyPfVJY+12STtHH1ZofxJKYwo6Xe7EvmCiC9DK0KKVbeakZZ6wfd
|
||||
5LO/tArDkqT2YjT3DUsfGqxQOoQvGCmk9yUuCm0s0vLwTHdJhSVgn9dxrEuK4FYL
|
||||
IM3tGENAPAcK3e1VEbncgBMRikxvECKIz+YZyQVtoYzX2HDlT4D2HrbgXQARAQAB
|
||||
zSBTaW1wbGVYIENoYXQgPGNoYXRAc2ltcGxleC5jaGF0PsLBlAQTAQgAPhYhBPtE
|
||||
r4GkW94ycxl5fIUQfjV9Shf8BQJkQ72ZAhsDBQkHhh9CBQsJCAcCBhUKCQgLAgQW
|
||||
AgMBAh4BAheAAAoJEIUQfjV9Shf8GekP/jpZYGJrna7467Qe82KV+qtwu+p2cRIy
|
||||
IsoOmCje2p0D9DmmmDQH1IdxlJhvHZ8uEu21QwDK03r5y4iaXhz9bx4CDSDB5JPp
|
||||
fMIDfOdc1V1GDT8Q2f/sYd5DX9kwpW6LdWOQZf6hwRDAeWDa+BQVhwo3E0WsPvRK
|
||||
o5fqrbJzfWj8pz+JMlT8RGGt0ZxEyUjnD9C6XfqGckLdubBycs9CipPKV+3X4cY/
|
||||
ix0zM2Nb3oSJ27VWMIFxi7lnBGtyUY69bE248Xhj0nJ79twPwzvk94+3e5tLQvyt
|
||||
NIZcWEZEu+eYthyKcGDo/aA6lIvt1Bqp8eeFMogRxs5GJI4L/wQGwIDckemtLb45
|
||||
gUdjpufEfPEfxuYWuuHuQ8W7Yvd2/ndiRkir4k+r8ypXx8yeCgocxnuUm1+4s+Wv
|
||||
h64Op+M+l56cTjVCaEn25kv/T+4ll/RBplzKdNe4ClcH4NXppwXFkAHXq/j3RX++
|
||||
64gRzIEC33TGheTo3btowUW+0/6iOi7Jy1RDsNvigzWwpm0p+pje54+d7hTxDmLR
|
||||
bHxOZ9QiauO/HnlqNw/MezZLYL18hyEsghD3ns6QIHcUsHf17u/tRfLgN11x9tiE
|
||||
ADqORsNgQ8FIRGdJcxGIt8lUlSe5vKPArsjpiomoA9CeAqepU27haIesl2QGe/jI
|
||||
5OuS7CsRVDlOzsFNBGRDvZkBEADGYf7E+bzYgORnlSY3TZgS5UvkMIGswlw6GW7j
|
||||
Vx6hAsMbiCwoKCVdzl/J4BImbJIJg2Pxvn/k7tYS2Jqb1q/EcpBmOZU9BRiTw49A
|
||||
TiK8UfeH9aIPNFwuiatmA29dGxPH2RgSCwa3f4l2RsnQl301UdNlXj6mmWngD6mj
|
||||
ae5/COUgH6CbKptfLp0Xw0WpPfKV1GK9+/X8Hv7W6RDA6xoWFlgzTyuy96rMmXJ1
|
||||
3E7P/50ebIOundVzCni10dZyn7+W13cJGOyzxQnbR6PEMVHsgi4uZB/Gt6PxF0dC
|
||||
s56IUi05hr9uH++p7ps2G8iIwvqXDu8VOvwN9hvt1fpxnRC2+Zv0lHwpDrnSBvjY
|
||||
8er2tJxXlybXwEpk1nzctmDDWrgbBgQugOxTu4rkqIvAGwq7U98aLUb3vEqlyWSp
|
||||
YDufsiLbGYC5owCli36yDzjfm48W0DwaOA5Ne5yVCih1f4ocF3RXVU6o1TEW1pfL
|
||||
DEDZOXDT9sj0qef9NW0Nz+x/EiCT2k2Bkwt0ETf4TralsJ7smCcbhqfJbu1NG22g
|
||||
oLNXZcZgTUxmOWmU+nlrFk8Hk7EK2KDeMKSgiX6jrAGpwbphrYYBZ3NLpvJ311l2
|
||||
d56ZgmUt8gb1O5tLNiD2ySCvWKnpG0A5WoKZ2329nlnX2R30otYdpP1vcAEvA3GU
|
||||
7fw5lQARAQABwsF8BBgBCAAmFiEE+0SvgaRb3jJzGXl8hRB+NX1KF/wFAmRDvZkC
|
||||
GwwFCQeGH0IACgkQhRB+NX1KF/wNbw//bi4RcxEOVJpT37pyx6wSlq6urHopuZA5
|
||||
duy0fGYxRXt4w/WR0UMH9i7iSU8J2E/UKgE7OMZg3oJqVt7g70zQDiT8ez+ep9d0
|
||||
YvPAqgRnT1VDmAyMO8FOTPQPIrPMsQTnmtmxf9qrdoxW8HVqiyK+7mCGqd9ldcer
|
||||
XGplALTugRWABY7iYyRyfpDSid+xMKV7KLHabv/0WdcT41HpZuUt0gmH0sMDDiJt
|
||||
XrWW01LDqEZTdfaZ1xXPPp7oXUYGY6U7cH5CdLS6D38tPKR9x0ttgM83/SOx/hOO
|
||||
XApcA+g113eMOyh4udowGYEkpxT26V3u8cLzCBOPDNSFx/H8ggFbfMsCWNBYV2Nx
|
||||
EmAmciHvPMNLR7Hjfvn018/Q+lo1J6snoEhT9zFwpL15Lwurkqy5Z4n1D9BUyZ7m
|
||||
hS/Wg7LDpaEeJCkSkOvQEPKz8YsnMpsbPc44ZZf0yuTUsWwJkZCVEqN8qByKXRdI
|
||||
28zGBBJr5/rjaSJJ7+VGbh/FGUzaEkLONybzKcxazwjSASBNZXmasgStngOGWGpM
|
||||
GKDnIuXs/Z7vljkKF2YoNT9bvGr7yoY74PCKrMkWdVSA1cQBj+cJ4OOojVvOGJaR
|
||||
Gdpp/2r7me5UKImmUw2dhHf0KdM1iYwjzztCO72hi5Fw7vFlNS7QoadmYDzAgWkk
|
||||
0oXYKNS+x2w=
|
||||
=68E9
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -215,10 +215,10 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
|
||||
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
addr_to = self.getMockAddrTo(ci)
|
||||
|
||||
135
basicswap/rpc.py
135
basicswap/rpc.py
@@ -6,8 +6,10 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import urllib
|
||||
import http.client
|
||||
from xmlrpc.client import (
|
||||
Fault,
|
||||
Transport,
|
||||
@@ -15,6 +17,35 @@ from xmlrpc.client import (
|
||||
)
|
||||
from .util import jsonDecimal
|
||||
|
||||
_use_rpc_pooling = False
|
||||
_rpc_pool_settings = {}
|
||||
|
||||
|
||||
def enable_rpc_pooling(settings):
|
||||
global _use_rpc_pooling, _rpc_pool_settings
|
||||
_use_rpc_pooling = settings.get("enabled", False)
|
||||
_rpc_pool_settings = settings
|
||||
|
||||
|
||||
class TimeoutTransport(Transport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class TimeoutSafeTransport(SafeTransport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class Jsonrpc:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
@@ -29,22 +60,40 @@ class Jsonrpc:
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
timeout=10,
|
||||
):
|
||||
# establish a "logical" server connection
|
||||
|
||||
# get the url
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
self.__host = parsed.netloc
|
||||
|
||||
self.__auth = None
|
||||
if "@" in parsed.netloc:
|
||||
auth_part, host_port = parsed.netloc.rsplit("@", 1)
|
||||
self.__host = host_port
|
||||
if ":" in auth_part:
|
||||
import base64
|
||||
|
||||
auth_bytes = auth_part.encode("utf-8")
|
||||
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
|
||||
self.__auth = f"Basic {auth_b64}"
|
||||
else:
|
||||
self.__host = parsed.netloc
|
||||
|
||||
if not self.__host:
|
||||
raise ValueError(f"Invalid or empty hostname in URI: {uri}")
|
||||
self.__handler = parsed.path
|
||||
if not self.__handler:
|
||||
self.__handler = "/RPC2"
|
||||
|
||||
if transport is None:
|
||||
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||
handler = (
|
||||
TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
|
||||
)
|
||||
extra_kwargs = {}
|
||||
transport = handler(
|
||||
timeout=timeout,
|
||||
use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs,
|
||||
@@ -62,6 +111,7 @@ class Jsonrpc:
|
||||
self.__transport.close()
|
||||
|
||||
def json_request(self, method, params):
|
||||
connection = None
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
headers = self.__transport._extra_headers[:]
|
||||
@@ -71,6 +121,10 @@ class Jsonrpc:
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", "jsonrpc"))
|
||||
|
||||
if self.__auth:
|
||||
headers.append(("Authorization", self.__auth))
|
||||
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
@@ -79,18 +133,29 @@ class Jsonrpc:
|
||||
self.__request_id += 1
|
||||
|
||||
resp = connection.getresponse()
|
||||
return resp.read()
|
||||
result = resp.read()
|
||||
|
||||
connection.close()
|
||||
|
||||
return result
|
||||
|
||||
except Fault:
|
||||
raise
|
||||
except Exception:
|
||||
# All unexpected errors leave connection in
|
||||
# a strange state, so we clear it.
|
||||
self.__transport.close()
|
||||
raise
|
||||
finally:
|
||||
if connection is not None:
|
||||
try:
|
||||
connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
if _use_rpc_pooling:
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
|
||||
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
@@ -101,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
x.close()
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
@@ -110,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
return r["result"]
|
||||
|
||||
|
||||
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
from .rpc_pool import get_rpc_pool
|
||||
import http.client
|
||||
import socket
|
||||
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
|
||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||
pool = get_rpc_pool(url, max_connections)
|
||||
|
||||
max_retries = 2
|
||||
|
||||
for attempt in range(max_retries):
|
||||
conn = pool.get_connection()
|
||||
|
||||
try:
|
||||
v = conn.json_request(method, params)
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
pool.discard_connection(conn)
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
|
||||
pool.return_connection(conn)
|
||||
return r["result"]
|
||||
|
||||
except (
|
||||
http.client.RemoteDisconnected,
|
||||
http.client.IncompleteRead,
|
||||
http.client.BadStatusLine,
|
||||
ConnectionError,
|
||||
ConnectionResetError,
|
||||
ConnectionAbortedError,
|
||||
BrokenPipeError,
|
||||
TimeoutError,
|
||||
socket.timeout,
|
||||
socket.error,
|
||||
OSError,
|
||||
) as ex:
|
||||
pool.discard_connection(conn)
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
logging.warning(
|
||||
f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
|
||||
)
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
pool.discard_connection(conn)
|
||||
logging.error(f"Unexpected RPC error: {ex}, method: {method}")
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
@@ -142,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
|
||||
def escape_rpcauth(auth_str: str) -> str:
|
||||
username, password = auth_str.split(":", 1)
|
||||
username = urllib.parse.quote(username, safe="")
|
||||
password = urllib.parse.quote(password, safe="")
|
||||
return f"{username}:{password}"
|
||||
|
||||
131
basicswap/rpc_pool.py
Normal file
131
basicswap/rpc_pool.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- 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 queue
|
||||
import threading
|
||||
import time
|
||||
from basicswap.rpc import Jsonrpc
|
||||
|
||||
|
||||
class RPCConnectionPool:
|
||||
def __init__(
|
||||
self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300
|
||||
):
|
||||
self.url = url
|
||||
self.max_connections = max_connections
|
||||
self.timeout = timeout
|
||||
self.logger = logger
|
||||
self.max_idle_time = max_idle_time
|
||||
self._pool = queue.Queue(maxsize=max_connections)
|
||||
self._lock = threading.Lock()
|
||||
self._created_connections = 0
|
||||
self._connection_timestamps = {}
|
||||
|
||||
def get_connection(self):
|
||||
try:
|
||||
conn_data = self._pool.get(block=False)
|
||||
conn, timestamp = (
|
||||
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||
)
|
||||
|
||||
if time.time() - timestamp > self.max_idle_time:
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||
)
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
return self._create_new_connection()
|
||||
|
||||
return conn
|
||||
except queue.Empty:
|
||||
return self._create_new_connection()
|
||||
|
||||
def _create_new_connection(self):
|
||||
with self._lock:
|
||||
if self._created_connections < self.max_connections:
|
||||
self._created_connections += 1
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
try:
|
||||
conn_data = self._pool.get(block=True, timeout=self.timeout)
|
||||
conn, timestamp = (
|
||||
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||
)
|
||||
|
||||
if time.time() - timestamp > self.max_idle_time:
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||
)
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
return conn
|
||||
except queue.Empty:
|
||||
if self.logger:
|
||||
self.logger.warning(
|
||||
f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
|
||||
)
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
def return_connection(self, conn):
|
||||
try:
|
||||
self._pool.put((conn, time.time()), block=False)
|
||||
except queue.Full:
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
|
||||
def discard_connection(self, conn):
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
|
||||
def close_all(self):
|
||||
while not self._pool.empty():
|
||||
try:
|
||||
conn_data = self._pool.get(block=False)
|
||||
conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
|
||||
conn.close()
|
||||
except queue.Empty:
|
||||
break
|
||||
with self._lock:
|
||||
self._created_connections = 0
|
||||
self._connection_timestamps.clear()
|
||||
|
||||
|
||||
_rpc_pools = {}
|
||||
_pool_lock = threading.Lock()
|
||||
_pool_logger = None
|
||||
|
||||
|
||||
def set_pool_logger(logger):
|
||||
global _pool_logger
|
||||
_pool_logger = logger
|
||||
|
||||
|
||||
def get_rpc_pool(url, max_connections=5):
|
||||
with _pool_lock:
|
||||
if url not in _rpc_pools:
|
||||
_rpc_pools[url] = RPCConnectionPool(
|
||||
url, max_connections, logger=_pool_logger
|
||||
)
|
||||
return _rpc_pools[url]
|
||||
|
||||
|
||||
def close_all_pools():
|
||||
with _pool_lock:
|
||||
for pool in _rpc_pools.values():
|
||||
pool.close_all()
|
||||
_rpc_pools.clear()
|
||||
@@ -14,6 +14,62 @@
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Toast Notification Animations */
|
||||
.toast-slide-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-slide-out {
|
||||
animation: slideOutRight 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast Container Styles */
|
||||
#ul_updates {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
#ul_updates li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Toast Hover Effects */
|
||||
#ul_updates .bg-white:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.dark #ul_updates .dark\:bg-gray-800:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.padded_row td {
|
||||
padding-top: 1.5em;
|
||||
@@ -365,3 +421,147 @@ select.disabled-select-enabled {
|
||||
#toggle-auto-refresh[data-enabled="true"] {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
/* Multi-select dropdown styles */
|
||||
.multi-select-dropdown::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.multi-select-dropdown input[type="checkbox"]:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
.multi-select-dropdown label:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#coin_to_button:focus,
|
||||
#coin_from_button:focus {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
.coin-badge {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
.coin-badge .remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.coin-badge .remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.multi-select-dropdown {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999 !important;
|
||||
position: fixed !important;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
}
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.dropdown-container.open {
|
||||
z-index: 9999;
|
||||
}
|
||||
.filter-button-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.multi-select-dropdown input[type="checkbox"] {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.multi-select-dropdown input[type="checkbox"]:focus {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.multi-select-dropdown input[type="checkbox"]:checked {
|
||||
background-color: #3b82f6 !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"] {
|
||||
border-color: #6b7280;
|
||||
background-color: #374151;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"]:focus {
|
||||
border-color: #3b82f6 !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"]:checked {
|
||||
background-color: #3b82f6 !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
.multi-select-dropdown label {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -4,34 +4,34 @@ const ApiManager = (function() {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
function getConfig() {
|
||||
return window.config || window.ConfigManager || {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: { requestsPerMinute: 50, minInterval: 1200 }
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: 1200,
|
||||
cryptocompare: 2000
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
|
||||
getMinInterval: function(apiName) {
|
||||
const config = getConfig();
|
||||
return config.rateLimits?.[apiName]?.minInterval || 1200;
|
||||
},
|
||||
|
||||
getRetryDelays: function() {
|
||||
const config = getConfig();
|
||||
return config.retryDelays || [5000, 15000, 30000];
|
||||
},
|
||||
|
||||
canMakeRequest: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return (now - lastRequest) >= this.minRequestInterval[apiName];
|
||||
return (now - lastRequest) >= this.getMinInterval(apiName);
|
||||
},
|
||||
|
||||
updateLastRequestTime: function(apiName) {
|
||||
@@ -41,7 +41,7 @@ const ApiManager = (function() {
|
||||
getWaitTime: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
|
||||
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
|
||||
},
|
||||
|
||||
queueRequest: async function(apiName, requestFn, retryCount = 0) {
|
||||
@@ -55,29 +55,30 @@ const ApiManager = (function() {
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
await new Promise(resolve => CleanupManager.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];
|
||||
const retryDelays = this.getRetryDelays();
|
||||
if (error.message.includes('429') && retryCount < retryDelays.length) {
|
||||
const delay = retryDelays[retryCount];
|
||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.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];
|
||||
retryCount < retryDelays.length) {
|
||||
const delay = retryDelays[retryCount];
|
||||
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||
apiName,
|
||||
retryCount,
|
||||
error: error.message
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -118,19 +119,7 @@ const ApiManager = (function() {
|
||||
}
|
||||
|
||||
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];
|
||||
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
@@ -143,6 +132,31 @@ const ApiManager = (function() {
|
||||
},
|
||||
|
||||
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
|
||||
if (window.ErrorHandler) {
|
||||
return window.ErrorHandler.safeExecuteAsync(async () => {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
|
||||
};
|
||||
|
||||
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();
|
||||
}, `ApiManager.makeRequest(${url})`, null);
|
||||
}
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
@@ -150,7 +164,7 @@ const ApiManager = (function() {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(config.requestTimeout)
|
||||
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
@@ -233,11 +247,8 @@ const ApiManager = (function() {
|
||||
.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');
|
||||
}
|
||||
@@ -260,28 +271,36 @@ const ApiManager = (function() {
|
||||
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 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']);
|
||||
|
||||
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 response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
|
||||
coins: coinSymbols.join(','),
|
||||
source: 'coingecko.com',
|
||||
ttl: 300
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.error('No response from backend');
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
console.error('Response missing data field:', response);
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(response.data).forEach(([coinSymbol, data]) => {
|
||||
const coinKey = coinSymbol.toLowerCase();
|
||||
volumeData[coinKey] = {
|
||||
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
|
||||
price_change_percentage_24h: data.price_change_24h || 0
|
||||
};
|
||||
});
|
||||
|
||||
return volumeData;
|
||||
@@ -292,79 +311,48 @@ const ApiManager = (function() {
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
let days;
|
||||
if (resolution === 'day') {
|
||||
days = 1;
|
||||
} else if (resolution === 'year') {
|
||||
days = 365;
|
||||
} else {
|
||||
days = 180;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
|
||||
coins: coinSymbols.join(','),
|
||||
days: days,
|
||||
source: 'coingecko.com',
|
||||
ttl: 3600
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.error('No response from backend');
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
console.error('Response missing data field:', response);
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching historical data:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(fetchPromises);
|
||||
return results;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
// Clear any pending requests or resources
|
||||
rateLimiter.requestQueue = {};
|
||||
rateLimiter.lastRequestTime = {};
|
||||
state.isInitialized = false;
|
||||
@@ -375,17 +363,6 @@ const ApiManager = (function() {
|
||||
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;
|
||||
|
||||
@@ -396,5 +373,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
|
||||
console.log('ApiManager initialized');
|
||||
|
||||
244
basicswap/static/js/modules/balance-updates.js
Normal file
244
basicswap/static/js/modules/balance-updates.js
Normal file
@@ -0,0 +1,244 @@
|
||||
const BalanceUpdatesManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const config = {
|
||||
balanceUpdateDelay: 2000,
|
||||
swapEventDelay: 5000,
|
||||
periodicRefreshInterval: 120000,
|
||||
walletPeriodicRefreshInterval: 60000,
|
||||
};
|
||||
|
||||
const state = {
|
||||
handlers: new Map(),
|
||||
timeouts: new Map(),
|
||||
intervals: new Map(),
|
||||
initialized: false
|
||||
};
|
||||
|
||||
async function fetchBalanceData() {
|
||||
if (window.ApiManager) {
|
||||
const data = await window.ApiManager.makeRequest('/json/walletbalances', 'GET');
|
||||
|
||||
if (data && data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return fetch('/json/walletbalances', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(balanceData => {
|
||||
if (balanceData.error) {
|
||||
throw new Error(balanceData.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(balanceData)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
return balanceData;
|
||||
});
|
||||
}
|
||||
|
||||
function clearTimeoutByKey(key) {
|
||||
if (state.timeouts.has(key)) {
|
||||
const timeoutId = state.timeouts.get(key);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(timeoutId);
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
state.timeouts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutByKey(key, callback, delay) {
|
||||
clearTimeoutByKey(key);
|
||||
const timeoutId = window.CleanupManager
|
||||
? window.CleanupManager.setTimeout(callback, delay)
|
||||
: setTimeout(callback, delay);
|
||||
state.timeouts.set(key, timeoutId);
|
||||
}
|
||||
|
||||
function clearIntervalByKey(key) {
|
||||
if (state.intervals.has(key)) {
|
||||
const intervalId = state.intervals.get(key);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearInterval(intervalId);
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
state.intervals.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setIntervalByKey(key, callback, interval) {
|
||||
clearIntervalByKey(key);
|
||||
const intervalId = window.CleanupManager
|
||||
? window.CleanupManager.setInterval(callback, interval)
|
||||
: setInterval(callback, interval);
|
||||
state.intervals.set(key, intervalId);
|
||||
}
|
||||
|
||||
function handleBalanceUpdate(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
setTimeoutByKey(`${contextKey}_balance_update`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via WebSocket:`, error);
|
||||
});
|
||||
}, config.balanceUpdateDelay);
|
||||
}
|
||||
|
||||
function handleSwapEvent(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
setTimeoutByKey(`${contextKey}_swap_event`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via swap event:`, error);
|
||||
});
|
||||
}, config.swapEventDelay);
|
||||
}
|
||||
|
||||
function setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext) {
|
||||
const handlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'coin_balance_updated') {
|
||||
handleBalanceUpdate(contextKey, balanceUpdateCallback, errorContext);
|
||||
}
|
||||
|
||||
if (swapEventCallback) {
|
||||
const swapEvents = ['new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (swapEvents.includes(data.event)) {
|
||||
handleSwapEvent(contextKey, swapEventCallback, errorContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.handlers.set(contextKey, handlerId);
|
||||
return handlerId;
|
||||
}
|
||||
|
||||
function setupPeriodicRefresh(contextKey, updateCallback, errorContext, interval) {
|
||||
const refreshInterval = interval || config.periodicRefreshInterval;
|
||||
|
||||
setIntervalByKey(`${contextKey}_periodic`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error in periodic ${errorContext} balance refresh:`, error);
|
||||
});
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
function cleanup(contextKey) {
|
||||
if (state.handlers.has(contextKey)) {
|
||||
const handlerId = state.handlers.get(contextKey);
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
state.handlers.delete(contextKey);
|
||||
}
|
||||
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
|
||||
clearIntervalByKey(`${contextKey}_periodic`);
|
||||
}
|
||||
|
||||
function cleanupAll() {
|
||||
state.handlers.forEach((handlerId) => {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
});
|
||||
state.handlers.clear();
|
||||
|
||||
state.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
||||
state.timeouts.clear();
|
||||
|
||||
state.intervals.forEach(intervalId => clearInterval(intervalId));
|
||||
state.intervals.clear();
|
||||
|
||||
state.initialized = false;
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: function() {
|
||||
if (state.initialized) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('balanceUpdatesManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', cleanupAll);
|
||||
|
||||
state.initialized = true;
|
||||
console.log('BalanceUpdatesManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
setup: function(options) {
|
||||
const {
|
||||
contextKey,
|
||||
balanceUpdateCallback,
|
||||
swapEventCallback,
|
||||
errorContext,
|
||||
enablePeriodicRefresh = false,
|
||||
periodicInterval
|
||||
} = options;
|
||||
|
||||
if (!contextKey || !balanceUpdateCallback || !errorContext) {
|
||||
throw new Error('Missing required options: contextKey, balanceUpdateCallback, errorContext');
|
||||
}
|
||||
|
||||
setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext);
|
||||
|
||||
if (enablePeriodicRefresh) {
|
||||
setupPeriodicRefresh(contextKey, balanceUpdateCallback, errorContext, periodicInterval);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchBalanceData: fetchBalanceData,
|
||||
|
||||
cleanup: cleanup,
|
||||
|
||||
dispose: cleanupAll,
|
||||
|
||||
isInitialized: function() {
|
||||
return state.initialized;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.BalanceUpdatesManager = BalanceUpdatesManager;
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
const CacheManager = (function() {
|
||||
const defaults = window.config?.cacheConfig?.storage || {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
defaultTTL: 5 * 60 * 1000
|
||||
};
|
||||
function getDefaults() {
|
||||
if (window.config?.cacheConfig?.storage) {
|
||||
return window.config.cacheConfig.storage;
|
||||
}
|
||||
if (window.ConfigManager?.cacheConfig?.storage) {
|
||||
return window.ConfigManager.cacheConfig.storage;
|
||||
}
|
||||
return {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
defaultTTL: 5 * 60 * 1000
|
||||
};
|
||||
}
|
||||
|
||||
const defaults = getDefaults();
|
||||
|
||||
const PRICES_CACHE_KEY = 'crypto_prices_unified';
|
||||
|
||||
@@ -45,8 +55,12 @@ const CacheManager = (function() {
|
||||
|
||||
const cacheAPI = {
|
||||
getTTL: function(resourceType) {
|
||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
|
||||
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
|
||||
window.ConfigManager?.cacheConfig?.ttlSettings || {};
|
||||
const defaultTTL = window.config?.cacheConfig?.defaultTTL ||
|
||||
window.ConfigManager?.cacheConfig?.defaultTTL ||
|
||||
defaults.defaultTTL;
|
||||
return ttlConfig[resourceType] || defaultTTL;
|
||||
},
|
||||
|
||||
set: function(key, value, resourceTypeOrCustomTtl = null) {
|
||||
@@ -73,13 +87,18 @@ const CacheManager = (function() {
|
||||
expiresAt: Date.now() + ttl
|
||||
};
|
||||
|
||||
let serializedItem;
|
||||
try {
|
||||
serializedItem = JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return false;
|
||||
}
|
||||
const serializedItem = window.ErrorHandler
|
||||
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
|
||||
: (() => {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!serializedItem) return false;
|
||||
|
||||
const itemSize = new Blob([serializedItem]).size;
|
||||
if (itemSize > defaults.maxSizeBytes) {
|
||||
@@ -118,7 +137,7 @@ const CacheManager = (function() {
|
||||
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%
|
||||
.slice(0, Math.floor(memoryCache.size * 0.2));
|
||||
|
||||
keysToDelete.forEach(k => memoryCache.delete(k));
|
||||
}
|
||||
@@ -285,7 +304,7 @@ const CacheManager = (function() {
|
||||
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
|
||||
.slice(0, Math.floor(memoryCache.size * 0.3));
|
||||
|
||||
keysToDelete.forEach(key => memoryCache.delete(key));
|
||||
}
|
||||
@@ -328,7 +347,6 @@ const CacheManager = (function() {
|
||||
.filter(key => isCacheKey(key))
|
||||
.forEach(key => memoryCache.delete(key));
|
||||
|
||||
console.log("Cache cleared successfully");
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -531,6 +549,4 @@ const CacheManager = (function() {
|
||||
|
||||
window.CacheManager = CacheManager;
|
||||
|
||||
|
||||
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
|
||||
console.log('CacheManager initialized');
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const CleanupManager = (function() {
|
||||
|
||||
const state = {
|
||||
eventListeners: [],
|
||||
timeouts: [],
|
||||
intervals: [],
|
||||
animationFrames: [],
|
||||
resources: new Map(),
|
||||
debug: false
|
||||
debug: false,
|
||||
memoryOptimizationInterval: null
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
@@ -232,6 +232,229 @@ const CleanupManager = (function() {
|
||||
};
|
||||
},
|
||||
|
||||
setupMemoryOptimization: function(options = {}) {
|
||||
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
|
||||
const maxCacheSize = options.maxCacheSize || 100;
|
||||
const maxDataSize = options.maxDataSize || 1000;
|
||||
|
||||
if (state.memoryOptimizationInterval) {
|
||||
this.clearInterval(state.memoryOptimizationInterval);
|
||||
}
|
||||
|
||||
this.addListener(document, 'visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
log('Tab hidden - running memory optimization');
|
||||
this.optimizeMemory({
|
||||
maxCacheSize: maxCacheSize,
|
||||
maxDataSize: maxDataSize
|
||||
});
|
||||
} else if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
state.memoryOptimizationInterval = this.setInterval(() => {
|
||||
if (document.hidden) {
|
||||
log('Periodic memory optimization');
|
||||
this.optimizeMemory({
|
||||
maxCacheSize: maxCacheSize,
|
||||
maxDataSize: maxDataSize
|
||||
});
|
||||
}
|
||||
}, memoryCheckInterval);
|
||||
|
||||
log('Memory optimization setup complete');
|
||||
return state.memoryOptimizationInterval;
|
||||
},
|
||||
|
||||
optimizeMemory: function(options = {}) {
|
||||
log('Running memory optimization');
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
|
||||
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
|
||||
}
|
||||
|
||||
this.cleanupOrphanedResources();
|
||||
|
||||
if (window.gc) {
|
||||
try {
|
||||
window.gc();
|
||||
log('Forced garbage collection');
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('memoryOptimized', {
|
||||
detail: {
|
||||
timestamp: Date.now(),
|
||||
maxDataSize: options.maxDataSize || 1000
|
||||
}
|
||||
}));
|
||||
|
||||
log('Memory optimization complete');
|
||||
},
|
||||
|
||||
cleanupOrphanedResources: function() {
|
||||
let removedListeners = 0;
|
||||
const validListeners = [];
|
||||
|
||||
for (let i = 0; i < state.eventListeners.length; i++) {
|
||||
const listener = state.eventListeners[i];
|
||||
if (!listener.element) {
|
||||
removedListeners++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const isDetached = !(listener.element instanceof Node) ||
|
||||
!document.body.contains(listener.element) ||
|
||||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
|
||||
(listener.element.style && listener.element.style.display === 'none');
|
||||
|
||||
if (isDetached) {
|
||||
try {
|
||||
if (listener.element instanceof Node) {
|
||||
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
|
||||
}
|
||||
removedListeners++;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} else {
|
||||
validListeners.push(listener);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
log(`Error checking listener (removing): ${e.message}`);
|
||||
removedListeners++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedListeners > 0) {
|
||||
state.eventListeners = validListeners;
|
||||
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
|
||||
}
|
||||
|
||||
let removedResources = 0;
|
||||
const resourcesForRemoval = [];
|
||||
|
||||
state.resources.forEach((info, id) => {
|
||||
const resource = info.resource;
|
||||
|
||||
try {
|
||||
|
||||
if (resource instanceof Element && !document.body.contains(resource)) {
|
||||
resourcesForRemoval.push(id);
|
||||
}
|
||||
|
||||
if (resource && resource.element) {
|
||||
|
||||
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
|
||||
resourcesForRemoval.push(id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error checking resource ${id}: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
resourcesForRemoval.forEach(id => {
|
||||
this.unregisterResource(id);
|
||||
removedResources++;
|
||||
});
|
||||
|
||||
if (removedResources > 0) {
|
||||
log(`Removed ${removedResources} orphaned resources`);
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanupOrphanedTooltips();
|
||||
} catch (e) {
|
||||
|
||||
if (typeof window.TooltipManager.cleanup === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanup();
|
||||
} catch (err) {
|
||||
log(`Error cleaning up tooltips: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof window.TooltipManager.cleanup === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanup();
|
||||
} catch (e) {
|
||||
log(`Error cleaning up tooltips: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.cleanupTooltipDOM();
|
||||
} catch (e) {
|
||||
log(`Error in cleanupTooltipDOM: ${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
cleanupTooltipDOM: function() {
|
||||
let removedElements = 0;
|
||||
|
||||
try {
|
||||
|
||||
const tooltipSelectors = [
|
||||
'[role="tooltip"]',
|
||||
'[id^="tooltip-"]',
|
||||
'.tippy-box',
|
||||
'[data-tippy-root]'
|
||||
];
|
||||
|
||||
tooltipSelectors.forEach(selector => {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
|
||||
elements.forEach(element => {
|
||||
try {
|
||||
|
||||
if (!(element instanceof Element)) return;
|
||||
|
||||
const isDetached = !element.parentElement ||
|
||||
!document.body.contains(element.parentElement) ||
|
||||
element.classList.contains('hidden') ||
|
||||
element.style.display === 'none' ||
|
||||
element.style.visibility === 'hidden';
|
||||
|
||||
if (isDetached) {
|
||||
try {
|
||||
element.remove();
|
||||
removedElements++;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
log(`Error querying for ${selector}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log(`Error in tooltip DOM cleanup: ${e.message}`);
|
||||
}
|
||||
|
||||
if (removedElements > 0) {
|
||||
log(`Removed ${removedElements} detached tooltip elements`);
|
||||
}
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.debug = Boolean(enabled);
|
||||
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
|
||||
@@ -247,6 +470,17 @@ const CleanupManager = (function() {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
|
||||
this.addListener(window, 'beforeunload', () => {
|
||||
this.clearAll();
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
|
||||
this.setupMemoryOptimization(options.memoryOptions || {});
|
||||
}
|
||||
|
||||
log('CleanupManager initialized');
|
||||
return this;
|
||||
}
|
||||
@@ -255,16 +489,20 @@ const CleanupManager = (function() {
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CleanupManager;
|
||||
}
|
||||
|
||||
window.CleanupManager = CleanupManager;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.CleanupManager = CleanupManager;
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.cleanupManagerInitialized) {
|
||||
CleanupManager.initialize();
|
||||
window.cleanupManagerInitialized = true;
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
CleanupManager.initialize({ debug: false });
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
CleanupManager.initialize({ debug: false });
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
|
||||
console.log('CleanupManager initialized');
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const CoinManager = (function() {
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Bitcoin-Cash.png'
|
||||
icon: 'Bitcoin%20Cash.png'
|
||||
},
|
||||
{
|
||||
symbol: 'PIVX',
|
||||
@@ -178,19 +178,7 @@ const CoinManager = (function() {
|
||||
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 coinAliasesMap[normalizedId] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -203,6 +191,19 @@ const CoinManager = (function() {
|
||||
return coin ? coin.symbol : null;
|
||||
},
|
||||
getDisplayName: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
|
||||
return 'Particl Anon';
|
||||
}
|
||||
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
|
||||
return 'Particl Blind';
|
||||
}
|
||||
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
|
||||
return 'Litecoin MWEB';
|
||||
}
|
||||
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.displayName : null;
|
||||
},
|
||||
@@ -222,6 +223,31 @@ const CoinManager = (function() {
|
||||
const coin = getCoinByAnyIdentifier(coinIdentifier);
|
||||
if (!coin) return coinIdentifier.toLowerCase();
|
||||
return coin.coingeckoId;
|
||||
},
|
||||
getCoinIcon: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
|
||||
return 'Particl.png';
|
||||
}
|
||||
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
|
||||
return 'Particl.png';
|
||||
}
|
||||
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
|
||||
return 'Litecoin.png';
|
||||
}
|
||||
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
if (coin && coin.icon) {
|
||||
return coin.icon;
|
||||
}
|
||||
|
||||
const capitalizedName = identifier.toString().split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('%20');
|
||||
|
||||
return `${capitalizedName}.png`;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
191
basicswap/static/js/modules/coin-utils.js
Normal file
191
basicswap/static/js/modules/coin-utils.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const CoinUtils = (function() {
|
||||
function buildAliasesFromCoinManager() {
|
||||
const aliases = {};
|
||||
const symbolMap = {};
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coins = window.CoinManager.getAllCoins();
|
||||
coins.forEach(coin => {
|
||||
const canonical = coin.name.toLowerCase();
|
||||
aliases[canonical] = coin.aliases || [coin.name.toLowerCase()];
|
||||
symbolMap[canonical] = coin.symbol;
|
||||
});
|
||||
}
|
||||
|
||||
return { aliases, symbolMap };
|
||||
}
|
||||
|
||||
let COIN_ALIASES = {};
|
||||
let CANONICAL_TO_SYMBOL = {};
|
||||
|
||||
function initializeAliases() {
|
||||
const { aliases, symbolMap } = buildAliasesFromCoinManager();
|
||||
COIN_ALIASES = aliases;
|
||||
CANONICAL_TO_SYMBOL = symbolMap;
|
||||
}
|
||||
|
||||
if (window.CoinManager) {
|
||||
initializeAliases();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.CoinManager) {
|
||||
initializeAliases();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCanonicalName(coin) {
|
||||
if (!coin) return null;
|
||||
const lower = coin.toString().toLowerCase().trim();
|
||||
|
||||
for (const [canonical, aliases] of Object.entries(COIN_ALIASES)) {
|
||||
if (aliases.includes(lower)) {
|
||||
return canonical;
|
||||
}
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeCoinName: function(coin, priceData = null) {
|
||||
const canonical = getCanonicalName(coin);
|
||||
if (!canonical) return null;
|
||||
|
||||
if (priceData) {
|
||||
if (canonical === 'bitcoin-cash') {
|
||||
if (priceData['bitcoin-cash']) return 'bitcoin-cash';
|
||||
if (priceData['bch']) return 'bch';
|
||||
if (priceData['bitcoincash']) return 'bitcoincash';
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
if (canonical === 'particl') {
|
||||
if (priceData['part']) return 'part';
|
||||
if (priceData['particl']) return 'particl';
|
||||
return 'part';
|
||||
}
|
||||
}
|
||||
|
||||
return canonical;
|
||||
},
|
||||
|
||||
isSameCoin: function(coin1, coin2) {
|
||||
if (!coin1 || !coin2) return false;
|
||||
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.coinMatches(coin1, coin2);
|
||||
}
|
||||
|
||||
const canonical1 = getCanonicalName(coin1);
|
||||
const canonical2 = getCanonicalName(coin2);
|
||||
if (canonical1 === canonical2) return true;
|
||||
|
||||
const lower1 = coin1.toString().toLowerCase().trim();
|
||||
const lower2 = coin2.toString().toLowerCase().trim();
|
||||
|
||||
const particlVariants = ['particl', 'particl anon', 'particl blind', 'part', 'part_anon', 'part_blind'];
|
||||
if (particlVariants.includes(lower1) && particlVariants.includes(lower2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lower1.includes(' ') || lower2.includes(' ')) {
|
||||
const word1 = lower1.split(' ')[0];
|
||||
const word2 = lower2.split(' ')[0];
|
||||
if (word1 === word2 && word1.length > 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getCoinSymbol: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
|
||||
if (coin) return coin.symbol;
|
||||
}
|
||||
|
||||
const canonical = getCanonicalName(identifier);
|
||||
if (canonical && CANONICAL_TO_SYMBOL[canonical]) {
|
||||
return CANONICAL_TO_SYMBOL[canonical];
|
||||
}
|
||||
|
||||
return identifier.toString().toUpperCase();
|
||||
},
|
||||
|
||||
getDisplayName: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
|
||||
if (coin) return coin.displayName || coin.name;
|
||||
}
|
||||
|
||||
const symbol = this.getCoinSymbol(identifier);
|
||||
return symbol || identifier;
|
||||
},
|
||||
|
||||
getCoinImage: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
|
||||
const canonical = getCanonicalName(coinName);
|
||||
const symbol = this.getCoinSymbol(canonical);
|
||||
|
||||
if (!symbol) return null;
|
||||
|
||||
const imagePath = `/static/images/coins/${symbol.toLowerCase()}.png`;
|
||||
return imagePath;
|
||||
},
|
||||
|
||||
getPriceKey: function(coin, priceData = null) {
|
||||
return this.normalizeCoinName(coin, priceData);
|
||||
},
|
||||
|
||||
getCoingeckoId: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
|
||||
if (coin && coin.coingeckoId) {
|
||||
return coin.coingeckoId;
|
||||
}
|
||||
}
|
||||
|
||||
const canonical = getCanonicalName(coinName);
|
||||
return canonical;
|
||||
},
|
||||
|
||||
formatCoinAmount: function(amount, decimals = 8) {
|
||||
if (amount === null || amount === undefined) return '0';
|
||||
|
||||
const numAmount = parseFloat(amount);
|
||||
if (isNaN(numAmount)) return '0';
|
||||
|
||||
return numAmount.toFixed(decimals).replace(/\.?0+$/, '');
|
||||
},
|
||||
|
||||
getAllAliases: function(coin) {
|
||||
const canonical = getCanonicalName(coin);
|
||||
return COIN_ALIASES[canonical] || [canonical];
|
||||
},
|
||||
|
||||
isValidCoin: function(coin) {
|
||||
if (!coin) return false;
|
||||
const canonical = getCanonicalName(coin);
|
||||
return canonical !== null && COIN_ALIASES.hasOwnProperty(canonical);
|
||||
},
|
||||
|
||||
refreshAliases: function() {
|
||||
initializeAliases();
|
||||
return Object.keys(COIN_ALIASES).length;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.CoinUtils = CoinUtils;
|
||||
}
|
||||
|
||||
console.log('CoinUtils module loaded');
|
||||
@@ -35,38 +35,22 @@ const ConfigManager = (function() {
|
||||
},
|
||||
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 }
|
||||
];
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getAllCoins();
|
||||
}
|
||||
console.warn('[ConfigManager] CoinManager not available, returning empty array');
|
||||
return [];
|
||||
},
|
||||
chartConfig: {
|
||||
colors: {
|
||||
@@ -108,12 +92,10 @@ const ConfigManager = (function() {
|
||||
if (typeof window.getAPIKeys === 'function') {
|
||||
const apiKeys = window.getAPIKeys();
|
||||
return {
|
||||
cryptoCompare: apiKeys.cryptoCompare || '',
|
||||
coinGecko: apiKeys.coinGecko || ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
cryptoCompare: '',
|
||||
coinGecko: ''
|
||||
};
|
||||
},
|
||||
@@ -122,40 +104,20 @@ const ConfigManager = (function() {
|
||||
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;
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coinName);
|
||||
}
|
||||
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
||||
},
|
||||
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 (window.CoinUtils) {
|
||||
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
|
||||
}
|
||||
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;
|
||||
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
|
||||
},
|
||||
update: function(path, value) {
|
||||
const parts = path.split('.');
|
||||
@@ -214,7 +176,7 @@ const ConfigManager = (function() {
|
||||
let timeoutId;
|
||||
return function(...args) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
|
||||
};
|
||||
},
|
||||
formatTimeLeft: function(timestamp) {
|
||||
|
||||
207
basicswap/static/js/modules/dom-cache.js
Normal file
207
basicswap/static/js/modules/dom-cache.js
Normal file
@@ -0,0 +1,207 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const originalGetElementById = document.getElementById.bind(document);
|
||||
|
||||
const DOMCache = {
|
||||
|
||||
cache: {},
|
||||
|
||||
get: function(id, forceRefresh = false) {
|
||||
if (!id) {
|
||||
console.warn('DOMCache: No ID provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!forceRefresh && this.cache[id]) {
|
||||
|
||||
if (document.body.contains(this.cache[id])) {
|
||||
return this.cache[id];
|
||||
} else {
|
||||
|
||||
delete this.cache[id];
|
||||
}
|
||||
}
|
||||
|
||||
const element = originalGetElementById(id);
|
||||
if (element) {
|
||||
this.cache[id] = element;
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
getMultiple: function(ids) {
|
||||
const elements = {};
|
||||
ids.forEach(id => {
|
||||
elements[id] = this.get(id);
|
||||
});
|
||||
return elements;
|
||||
},
|
||||
|
||||
setValue: function(id, value) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
return true;
|
||||
}
|
||||
console.warn(`DOMCache: Element not found: ${id}`);
|
||||
return false;
|
||||
},
|
||||
|
||||
getValue: function(id, defaultValue = '') {
|
||||
const element = this.get(id);
|
||||
return element ? element.value : defaultValue;
|
||||
},
|
||||
|
||||
setText: function(id, text) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
return true;
|
||||
}
|
||||
console.warn(`DOMCache: Element not found: ${id}`);
|
||||
return false;
|
||||
},
|
||||
|
||||
getText: function(id, defaultValue = '') {
|
||||
const element = this.get(id);
|
||||
return element ? element.textContent : defaultValue;
|
||||
},
|
||||
|
||||
addClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
removeClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.remove(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
toggleClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.toggle(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
show: function(id) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.style.display = '';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hide: function(id) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
exists: function(id) {
|
||||
return this.get(id) !== null;
|
||||
},
|
||||
|
||||
clear: function(id) {
|
||||
if (id) {
|
||||
delete this.cache[id];
|
||||
} else {
|
||||
this.cache = {};
|
||||
}
|
||||
},
|
||||
|
||||
size: function() {
|
||||
return Object.keys(this.cache).length;
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
const ids = Object.keys(this.cache);
|
||||
let removed = 0;
|
||||
|
||||
ids.forEach(id => {
|
||||
const element = this.cache[id];
|
||||
if (!document.body.contains(element)) {
|
||||
delete this.cache[id];
|
||||
removed++;
|
||||
}
|
||||
});
|
||||
|
||||
return removed;
|
||||
},
|
||||
|
||||
createScope: function(elementIds) {
|
||||
const scope = {};
|
||||
|
||||
elementIds.forEach(id => {
|
||||
Object.defineProperty(scope, id, {
|
||||
get: () => this.get(id),
|
||||
enumerable: true
|
||||
});
|
||||
});
|
||||
|
||||
return scope;
|
||||
},
|
||||
|
||||
batch: function(operations) {
|
||||
Object.keys(operations).forEach(id => {
|
||||
const ops = operations[id];
|
||||
const element = this.get(id);
|
||||
|
||||
if (!element) {
|
||||
console.warn(`DOMCache: Element not found in batch operation: ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ops.value !== undefined) element.value = ops.value;
|
||||
if (ops.text !== undefined) element.textContent = ops.text;
|
||||
if (ops.html !== undefined) element.innerHTML = ops.html;
|
||||
if (ops.class) element.classList.add(ops.class);
|
||||
if (ops.removeClass) element.classList.remove(ops.removeClass);
|
||||
if (ops.hide) element.style.display = 'none';
|
||||
if (ops.show) element.style.display = '';
|
||||
if (ops.disabled !== undefined) element.disabled = ops.disabled;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.DOMCache = DOMCache;
|
||||
|
||||
if (!window.$) {
|
||||
window.$ = function(id) {
|
||||
return DOMCache.get(id);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById = function(id) {
|
||||
return DOMCache.get(id);
|
||||
};
|
||||
|
||||
document.getElementByIdOriginal = originalGetElementById;
|
||||
|
||||
if (window.CleanupManager) {
|
||||
const validationInterval = CleanupManager.setInterval(() => {
|
||||
DOMCache.validate();
|
||||
}, 30000);
|
||||
|
||||
CleanupManager.registerResource('domCacheValidation', validationInterval, () => {
|
||||
clearInterval(validationInterval);
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
215
basicswap/static/js/modules/error-handler.js
Normal file
215
basicswap/static/js/modules/error-handler.js
Normal file
@@ -0,0 +1,215 @@
|
||||
const ErrorHandler = (function() {
|
||||
const config = {
|
||||
logErrors: true,
|
||||
throwErrors: false,
|
||||
errorCallbacks: []
|
||||
};
|
||||
|
||||
function formatError(error, context) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const contextStr = context ? ` [${context}]` : '';
|
||||
|
||||
if (error instanceof Error) {
|
||||
return `${timestamp}${contextStr} ${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
return `${timestamp}${contextStr} ${String(error)}`;
|
||||
}
|
||||
|
||||
function notifyCallbacks(error, context) {
|
||||
config.errorCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(error, context);
|
||||
} catch (e) {
|
||||
console.error('[ErrorHandler] Error in callback:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
configure: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
return this;
|
||||
},
|
||||
|
||||
addCallback: function(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
config.errorCallbacks.push(callback);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
removeCallback: function(callback) {
|
||||
const index = config.errorCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
config.errorCallbacks.splice(index, 1);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
safeExecute: function(fn, context = null, fallbackValue = null) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
},
|
||||
|
||||
safeExecuteAsync: async function(fn, context = null, fallbackValue = null) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
},
|
||||
|
||||
wrap: function(fn, context = null, fallbackValue = null) {
|
||||
return (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
wrapAsync: function(fn, context = null, fallbackValue = null) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
handleError: function(error, context = null, fallbackValue = null) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
},
|
||||
|
||||
try: function(fn, catchFn = null, finallyFn = null) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, 'ErrorHandler.try'));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, 'ErrorHandler.try');
|
||||
|
||||
if (catchFn) {
|
||||
return catchFn(error);
|
||||
}
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
if (finallyFn) {
|
||||
finallyFn();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
tryAsync: async function(fn, catchFn = null, finallyFn = null) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, 'ErrorHandler.tryAsync'));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, 'ErrorHandler.tryAsync');
|
||||
|
||||
if (catchFn) {
|
||||
return await catchFn(error);
|
||||
}
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
if (finallyFn) {
|
||||
await finallyFn();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createBoundary: function(context) {
|
||||
return {
|
||||
execute: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.safeExecute(fn, context, fallbackValue);
|
||||
},
|
||||
executeAsync: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.safeExecuteAsync(fn, context, fallbackValue);
|
||||
},
|
||||
wrap: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.wrap(fn, context, fallbackValue);
|
||||
},
|
||||
wrapAsync: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.wrapAsync(fn, context, fallbackValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.ErrorHandler = ErrorHandler;
|
||||
}
|
||||
|
||||
console.log('ErrorHandler module loaded');
|
||||
342
basicswap/static/js/modules/event-handlers.js
Normal file
342
basicswap/static/js/modules/event-handlers.js
Normal file
@@ -0,0 +1,342 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const EventHandlers = {
|
||||
|
||||
confirmPopup: function(action = 'proceed', coinName = '') {
|
||||
const message = action === 'Accept'
|
||||
? 'Are you sure you want to accept this bid?'
|
||||
: coinName
|
||||
? `Are you sure you want to ${action} ${coinName}?`
|
||||
: 'Are you sure you want to proceed?';
|
||||
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
confirmReseed: function() {
|
||||
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
|
||||
},
|
||||
|
||||
confirmWithdrawal: function() {
|
||||
|
||||
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
|
||||
return window.WalletPage.confirmWithdrawal();
|
||||
}
|
||||
return confirm('Are you sure you want to withdraw? Please verify the address and amount.');
|
||||
},
|
||||
|
||||
confirmUTXOResize: function() {
|
||||
return confirm('Are you sure you want to create a UTXO? This will split your balance.');
|
||||
},
|
||||
|
||||
confirmRemoveExpired: function() {
|
||||
return confirm('Are you sure you want to remove all expired offers and bids?');
|
||||
},
|
||||
|
||||
fillDonationAddress: function(address, coinType) {
|
||||
|
||||
let addressInput = null;
|
||||
|
||||
addressInput = window.DOMCache
|
||||
? window.DOMCache.get('address_to')
|
||||
: document.getElementById('address_to');
|
||||
|
||||
if (!addressInput) {
|
||||
addressInput = document.querySelector('input[name^="to_"]');
|
||||
}
|
||||
|
||||
if (!addressInput) {
|
||||
addressInput = document.querySelector('input[placeholder*="Address"]');
|
||||
}
|
||||
|
||||
if (addressInput) {
|
||||
addressInput.value = address;
|
||||
console.log(`Filled donation address for ${coinType}: ${address}`);
|
||||
} else {
|
||||
console.error('EventHandlers: Address input not found');
|
||||
}
|
||||
},
|
||||
|
||||
setAmmAmount: function(percent, inputId) {
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get(inputId)
|
||||
: document.getElementById(inputId);
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('EventHandlers: AMM amount input not found:', inputId);
|
||||
return;
|
||||
}
|
||||
|
||||
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
|
||||
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
|
||||
|
||||
if (balance > 0) {
|
||||
const calculatedAmount = balance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
} else {
|
||||
console.warn('EventHandlers: No balance found for AMM amount calculation');
|
||||
}
|
||||
},
|
||||
|
||||
setOfferAmount: function(percent, inputId) {
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get(inputId)
|
||||
: document.getElementById(inputId);
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('EventHandlers: Offer amount input not found:', inputId);
|
||||
return;
|
||||
}
|
||||
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
if (!coinFromSelect) {
|
||||
console.error('EventHandlers: coin_from select not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||
if (!selectedOption || selectedOption.value === '-1') {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Please select a coin first');
|
||||
} else {
|
||||
alert('Please select a coin first');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const balance = selectedOption.getAttribute('data-balance');
|
||||
if (!balance) {
|
||||
console.error('EventHandlers: Balance not found for selected coin');
|
||||
return;
|
||||
}
|
||||
|
||||
const floatBalance = parseFloat(balance);
|
||||
if (isNaN(floatBalance) || floatBalance <= 0) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
|
||||
} else {
|
||||
alert('Invalid balance for selected coin');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
},
|
||||
|
||||
resetForm: function() {
|
||||
const form = document.querySelector('form[name="offer_form"]') || document.querySelector('form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
},
|
||||
|
||||
hideConfirmModal: function() {
|
||||
if (window.DOMCache) {
|
||||
window.DOMCache.hide('confirmModal');
|
||||
} else {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lookup_rates: function() {
|
||||
|
||||
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
|
||||
window.lookup_rates();
|
||||
} else {
|
||||
console.error('EventHandlers: lookup_rates function not found');
|
||||
}
|
||||
},
|
||||
|
||||
checkForUpdatesNow: function() {
|
||||
if (window.checkForUpdatesNow && typeof window.checkForUpdatesNow === 'function') {
|
||||
window.checkForUpdatesNow();
|
||||
} else {
|
||||
console.error('EventHandlers: checkForUpdatesNow function not found');
|
||||
}
|
||||
},
|
||||
|
||||
testUpdateNotification: function() {
|
||||
if (window.testUpdateNotification && typeof window.testUpdateNotification === 'function') {
|
||||
window.testUpdateNotification();
|
||||
} else {
|
||||
console.error('EventHandlers: testUpdateNotification function not found');
|
||||
}
|
||||
},
|
||||
|
||||
toggleNotificationDropdown: function(event) {
|
||||
if (window.toggleNotificationDropdown && typeof window.toggleNotificationDropdown === 'function') {
|
||||
window.toggleNotificationDropdown(event);
|
||||
} else {
|
||||
console.error('EventHandlers: toggleNotificationDropdown function not found');
|
||||
}
|
||||
},
|
||||
|
||||
closeMessage: function(messageId) {
|
||||
if (window.DOMCache) {
|
||||
window.DOMCache.hide(messageId);
|
||||
} else {
|
||||
const messageElement = document.getElementById(messageId);
|
||||
if (messageElement) {
|
||||
messageElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm]');
|
||||
if (target) {
|
||||
const action = target.getAttribute('data-confirm-action') || 'proceed';
|
||||
const coinName = target.getAttribute('data-confirm-coin') || '';
|
||||
|
||||
if (!this.confirmPopup(action, coinName)) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-reseed]');
|
||||
if (target) {
|
||||
if (!this.confirmReseed()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-utxo]');
|
||||
if (target) {
|
||||
if (!this.confirmUTXOResize()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-remove-expired]');
|
||||
if (target) {
|
||||
if (!this.confirmRemoveExpired()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-fill-donation]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const address = target.getAttribute('data-address');
|
||||
const coinType = target.getAttribute('data-coin-type');
|
||||
this.fillDonationAddress(address, coinType);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-set-amm-amount]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const percent = parseFloat(target.getAttribute('data-set-amm-amount'));
|
||||
const inputId = target.getAttribute('data-input-id');
|
||||
this.setAmmAmount(percent, inputId);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-set-offer-amount]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const percent = parseFloat(target.getAttribute('data-set-offer-amount'));
|
||||
const inputId = target.getAttribute('data-input-id');
|
||||
this.setOfferAmount(percent, inputId);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-reset-form]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-hide-modal]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-lookup-rates]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.lookup_rates();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-check-updates]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.checkForUpdatesNow();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-test-notification]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const type = target.getAttribute('data-test-notification');
|
||||
if (type === 'update') {
|
||||
this.testUpdateNotification();
|
||||
} else {
|
||||
window.NotificationManager && window.NotificationManager.testToasts();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-close-message]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const messageId = target.getAttribute('data-close-message');
|
||||
this.closeMessage(messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
EventHandlers.initialize();
|
||||
});
|
||||
} else {
|
||||
EventHandlers.initialize();
|
||||
}
|
||||
|
||||
window.EventHandlers = EventHandlers;
|
||||
|
||||
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
|
||||
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
|
||||
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
|
||||
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
|
||||
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
|
||||
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
|
||||
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
|
||||
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
|
||||
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
|
||||
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
|
||||
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
|
||||
|
||||
})();
|
||||
225
basicswap/static/js/modules/form-validator.js
Normal file
225
basicswap/static/js/modules/form-validator.js
Normal file
@@ -0,0 +1,225 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const FormValidator = {
|
||||
|
||||
checkPasswordStrength: function(password) {
|
||||
const requirements = {
|
||||
length: password.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
number: /[0-9]/.test(password)
|
||||
};
|
||||
|
||||
let score = 0;
|
||||
if (requirements.length) score += 25;
|
||||
if (requirements.uppercase) score += 25;
|
||||
if (requirements.lowercase) score += 25;
|
||||
if (requirements.number) score += 25;
|
||||
|
||||
return {
|
||||
score: score,
|
||||
requirements: requirements,
|
||||
isStrong: score >= 60
|
||||
};
|
||||
},
|
||||
|
||||
updatePasswordStrengthUI: function(password, elements) {
|
||||
const result = this.checkPasswordStrength(password);
|
||||
const { score, requirements } = result;
|
||||
|
||||
if (!elements.bar || !elements.text) {
|
||||
console.warn('FormValidator: Missing strength UI elements');
|
||||
return result.isStrong;
|
||||
}
|
||||
|
||||
elements.bar.style.width = `${score}%`;
|
||||
|
||||
if (score === 0) {
|
||||
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
|
||||
elements.text.textContent = 'Enter password';
|
||||
elements.text.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
|
||||
} else if (score < 40) {
|
||||
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
|
||||
elements.text.textContent = 'Weak';
|
||||
elements.text.className = 'text-sm font-medium text-red-600 dark:text-red-400';
|
||||
} else if (score < 70) {
|
||||
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
|
||||
elements.text.textContent = 'Fair';
|
||||
elements.text.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
|
||||
} else if (score < 90) {
|
||||
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
|
||||
elements.text.textContent = 'Good';
|
||||
elements.text.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
|
||||
} else {
|
||||
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
|
||||
elements.text.textContent = 'Strong';
|
||||
elements.text.className = 'text-sm font-medium text-green-600 dark:text-green-400';
|
||||
}
|
||||
|
||||
if (elements.requirements) {
|
||||
this.updateRequirement(elements.requirements.length, requirements.length);
|
||||
this.updateRequirement(elements.requirements.uppercase, requirements.uppercase);
|
||||
this.updateRequirement(elements.requirements.lowercase, requirements.lowercase);
|
||||
this.updateRequirement(elements.requirements.number, requirements.number);
|
||||
}
|
||||
|
||||
return result.isStrong;
|
||||
},
|
||||
|
||||
updateRequirement: function(element, met) {
|
||||
if (!element) return;
|
||||
|
||||
if (met) {
|
||||
element.className = 'flex items-center text-green-600 dark:text-green-400';
|
||||
} else {
|
||||
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
|
||||
}
|
||||
},
|
||||
|
||||
checkPasswordMatch: function(password1, password2, elements) {
|
||||
if (!elements) {
|
||||
return password1 === password2;
|
||||
}
|
||||
|
||||
const { container, success, error } = elements;
|
||||
|
||||
if (password2.length === 0) {
|
||||
if (container) container.classList.add('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (container) container.classList.remove('hidden');
|
||||
|
||||
if (password1 === password2) {
|
||||
if (success) success.classList.remove('hidden');
|
||||
if (error) error.classList.add('hidden');
|
||||
return true;
|
||||
} else {
|
||||
if (success) success.classList.add('hidden');
|
||||
if (error) error.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
validateEmail: function(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
},
|
||||
|
||||
validateRequired: function(value) {
|
||||
return value && value.trim().length > 0;
|
||||
},
|
||||
|
||||
validateMinLength: function(value, minLength) {
|
||||
return value && value.length >= minLength;
|
||||
},
|
||||
|
||||
validateMaxLength: function(value, maxLength) {
|
||||
return value && value.length <= maxLength;
|
||||
},
|
||||
|
||||
validateNumeric: function(value) {
|
||||
return !isNaN(value) && !isNaN(parseFloat(value));
|
||||
},
|
||||
|
||||
validateRange: function(value, min, max) {
|
||||
const num = parseFloat(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
},
|
||||
|
||||
showError: function(element, message) {
|
||||
if (!element) return;
|
||||
|
||||
element.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
element.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
|
||||
|
||||
let errorElement = element.parentElement.querySelector('.validation-error');
|
||||
if (!errorElement) {
|
||||
errorElement = document.createElement('p');
|
||||
errorElement.className = 'validation-error text-red-600 dark:text-red-400 text-sm mt-1';
|
||||
element.parentElement.appendChild(errorElement);
|
||||
}
|
||||
|
||||
errorElement.textContent = message;
|
||||
errorElement.classList.remove('hidden');
|
||||
},
|
||||
|
||||
clearError: function(element) {
|
||||
if (!element) return;
|
||||
|
||||
element.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
element.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
|
||||
|
||||
const errorElement = element.parentElement.querySelector('.validation-error');
|
||||
if (errorElement) {
|
||||
errorElement.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
validateForm: function(form, rules) {
|
||||
if (!form || !rules) return false;
|
||||
|
||||
let isValid = true;
|
||||
|
||||
Object.keys(rules).forEach(fieldName => {
|
||||
const field = form.querySelector(`[name="${fieldName}"]`);
|
||||
if (!field) return;
|
||||
|
||||
const fieldRules = rules[fieldName];
|
||||
let fieldValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
if (fieldRules.required && !this.validateRequired(field.value)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.requiredMessage || 'This field is required';
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.minLength && !this.validateMinLength(field.value, fieldRules.minLength)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.minLengthMessage || `Minimum ${fieldRules.minLength} characters required`;
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.maxLength && !this.validateMaxLength(field.value, fieldRules.maxLength)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.maxLengthMessage || `Maximum ${fieldRules.maxLength} characters allowed`;
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.email && !this.validateEmail(field.value)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.emailMessage || 'Invalid email format';
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.numeric && !this.validateNumeric(field.value)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.numericMessage || 'Must be a number';
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.range && !this.validateRange(field.value, fieldRules.range.min, fieldRules.range.max)) {
|
||||
fieldValid = false;
|
||||
errorMessage = fieldRules.rangeMessage || `Must be between ${fieldRules.range.min} and ${fieldRules.range.max}`;
|
||||
}
|
||||
|
||||
if (fieldValid && fieldRules.custom) {
|
||||
const customResult = fieldRules.custom(field.value, form);
|
||||
if (!customResult.valid) {
|
||||
fieldValid = false;
|
||||
errorMessage = customResult.message || 'Invalid value';
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldValid) {
|
||||
this.clearError(field);
|
||||
} else {
|
||||
this.showError(field, errorMessage);
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
};
|
||||
|
||||
window.FormValidator = FormValidator;
|
||||
|
||||
})();
|
||||
@@ -23,10 +23,24 @@ const IdentityManager = (function() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedData = this.getCachedIdentity(address);
|
||||
if (cachedData) {
|
||||
log(`Cache hit for ${address}`);
|
||||
return cachedData;
|
||||
const cached = state.cache.get(address);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.timestamp) < state.config.cacheTimeout) {
|
||||
log(`Cache hit (fresh) for ${address}`);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
if (cached && (now - cached.timestamp) < state.config.cacheTimeout * 2) {
|
||||
log(`Cache hit (stale) for ${address}, refreshing in background`);
|
||||
|
||||
const staleData = cached.data;
|
||||
|
||||
if (!state.pendingRequests.has(address)) {
|
||||
this.refreshIdentityInBackground(address);
|
||||
}
|
||||
|
||||
return staleData;
|
||||
}
|
||||
|
||||
if (state.pendingRequests.has(address)) {
|
||||
@@ -47,6 +61,20 @@ const IdentityManager = (function() {
|
||||
}
|
||||
},
|
||||
|
||||
refreshIdentityInBackground: function(address) {
|
||||
const request = fetchWithRetry(address);
|
||||
state.pendingRequests.set(address, request);
|
||||
|
||||
request.then(data => {
|
||||
this.setCachedIdentity(address, data);
|
||||
log(`Background refresh completed for ${address}`);
|
||||
}).catch(error => {
|
||||
log(`Background refresh failed for ${address}:`, error);
|
||||
}).finally(() => {
|
||||
state.pendingRequests.delete(address);
|
||||
});
|
||||
},
|
||||
|
||||
getCachedIdentity: function(address) {
|
||||
const cached = state.cache.get(address);
|
||||
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
|
||||
@@ -155,15 +183,23 @@ const IdentityManager = (function() {
|
||||
|
||||
async function fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
let data;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
if (window.ApiManager) {
|
||||
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
|
||||
} else {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (attempt >= state.config.maxRetries) {
|
||||
console.error(`[IdentityManager] Error:`, error.message);
|
||||
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
|
||||
return null;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
|
||||
const delay = state.config.retryDelay * attempt;
|
||||
await new Promise(resolve => {
|
||||
CleanupManager.setTimeout(resolve, delay);
|
||||
});
|
||||
return fetchWithRetry(address, attempt + 1);
|
||||
}
|
||||
}
|
||||
@@ -188,5 +227,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
|
||||
console.log('IdentityManager initialized');
|
||||
|
||||
@@ -1,219 +1,582 @@
|
||||
const MemoryManager = (function() {
|
||||
const config = {
|
||||
tooltipCleanupInterval: 300000,
|
||||
diagnosticsInterval: 600000,
|
||||
elementVerificationInterval: 300000,
|
||||
maxTooltipsThreshold: 100,
|
||||
maxTooltips: 300,
|
||||
cleanupThreshold: 1.5,
|
||||
minTimeBetweenCleanups: 180000,
|
||||
memoryGrowthThresholdMB: 100,
|
||||
debug: false,
|
||||
protectedWebSockets: ['wsPort', 'ws_port'],
|
||||
interactiveSelectors: [
|
||||
'tr:hover',
|
||||
'[data-tippy-root]:hover',
|
||||
'.tooltip:hover',
|
||||
'[data-tooltip-trigger-id]:hover',
|
||||
'[data-tooltip-target]:hover'
|
||||
],
|
||||
protectedContainers: [
|
||||
'#sent-tbody',
|
||||
'#received-tbody',
|
||||
'#offers-body'
|
||||
]
|
||||
};
|
||||
|
||||
const state = {
|
||||
isMonitoringEnabled: false,
|
||||
monitorInterval: null,
|
||||
cleanupInterval: null
|
||||
};
|
||||
const state = {
|
||||
pendingAnimationFrames: new Set(),
|
||||
pendingTimeouts: new Set(),
|
||||
cleanupInterval: null,
|
||||
diagnosticsInterval: null,
|
||||
elementVerificationInterval: null,
|
||||
mutationObserver: null,
|
||||
lastCleanupTime: Date.now(),
|
||||
startTime: Date.now(),
|
||||
isCleanupRunning: false,
|
||||
metrics: {
|
||||
tooltipsRemoved: 0,
|
||||
cleanupRuns: 0,
|
||||
lastMemoryUsage: null,
|
||||
lastCleanupDetails: {},
|
||||
history: []
|
||||
},
|
||||
originalTooltipFunctions: {}
|
||||
};
|
||||
|
||||
const config = {
|
||||
monitorInterval: 30000,
|
||||
cleanupInterval: 60000,
|
||||
debug: false
|
||||
};
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[MemoryManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
function log(message, ...args) {
|
||||
if (config.debug) {
|
||||
console.log(`[MemoryManager] ${message}`, ...args);
|
||||
}
|
||||
function preserveTooltipFunctions() {
|
||||
if (window.TooltipManager && !state.originalTooltipFunctions.destroy) {
|
||||
state.originalTooltipFunctions = {
|
||||
destroy: window.TooltipManager.destroy,
|
||||
cleanup: window.TooltipManager.cleanup,
|
||||
create: window.TooltipManager.create
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isInProtectedContainer(element) {
|
||||
if (!element) return false;
|
||||
|
||||
for (const selector of config.protectedContainers) {
|
||||
if (element.closest && element.closest(selector)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
enableMonitoring: function(interval = config.monitorInterval) {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = true;
|
||||
config.monitorInterval = interval;
|
||||
function shouldSkipCleanup() {
|
||||
if (state.isCleanupRunning) return true;
|
||||
|
||||
this.logMemoryUsage();
|
||||
const selector = config.interactiveSelectors.join(', ');
|
||||
const hoveredElements = document.querySelectorAll(selector);
|
||||
|
||||
state.monitorInterval = setInterval(() => {
|
||||
this.logMemoryUsage();
|
||||
}, interval);
|
||||
return hoveredElements.length > 0;
|
||||
}
|
||||
|
||||
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
|
||||
return true;
|
||||
},
|
||||
function performCleanup(force = false) {
|
||||
if (shouldSkipCleanup() && !force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
disableMonitoring: function() {
|
||||
if (state.monitorInterval) {
|
||||
clearInterval(state.monitorInterval);
|
||||
state.monitorInterval = null;
|
||||
}
|
||||
if (state.isCleanupRunning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.isMonitoringEnabled = false;
|
||||
console.log('Memory monitoring disabled');
|
||||
return true;
|
||||
},
|
||||
const now = Date.now();
|
||||
if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logMemoryUsage: function() {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
console.log(`=== Memory Monitor [${timestamp}] ===`);
|
||||
try {
|
||||
state.isCleanupRunning = true;
|
||||
state.lastCleanupTime = now;
|
||||
state.metrics.cleanupRuns++;
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
const startTime = performance.now();
|
||||
const startMemory = checkMemoryUsage();
|
||||
|
||||
if (navigator.deviceMemory) {
|
||||
console.log('Device memory:', navigator.deviceMemory, 'GB');
|
||||
}
|
||||
state.pendingAnimationFrames.forEach(id => {
|
||||
cancelAnimationFrame(id);
|
||||
});
|
||||
state.pendingAnimationFrames.clear();
|
||||
|
||||
const nodeCount = document.querySelectorAll('*').length;
|
||||
console.log('DOM node count:', nodeCount);
|
||||
state.pendingTimeouts.forEach(id => {
|
||||
clearTimeout(id);
|
||||
});
|
||||
state.pendingTimeouts.clear();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
const counts = CleanupManager.getResourceCounts();
|
||||
console.log('Managed resources:', counts);
|
||||
}
|
||||
const tooltipsResult = removeOrphanedTooltips();
|
||||
state.metrics.tooltipsRemoved += tooltipsResult;
|
||||
|
||||
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);
|
||||
}
|
||||
const disconnectedResult = checkForDisconnectedElements();
|
||||
|
||||
if (window.CacheManager && window.CacheManager.getStats) {
|
||||
const cacheStats = CacheManager.getStats();
|
||||
console.log('Cache stats:', cacheStats);
|
||||
}
|
||||
tryRunGarbageCollection(false);
|
||||
|
||||
if (window.IdentityManager && window.IdentityManager.getStats) {
|
||||
const identityStats = window.IdentityManager.getStats();
|
||||
console.log('Identity cache stats:', identityStats);
|
||||
}
|
||||
const endTime = performance.now();
|
||||
const endMemory = checkMemoryUsage();
|
||||
|
||||
console.log('==============================');
|
||||
},
|
||||
const runStats = {
|
||||
timestamp: new Date().toISOString(),
|
||||
duration: endTime - startTime,
|
||||
tooltipsRemoved: tooltipsResult,
|
||||
disconnectedRemoved: disconnectedResult,
|
||||
memoryBefore: startMemory ? startMemory.usedMB : null,
|
||||
memoryAfter: endMemory ? endMemory.usedMB : null,
|
||||
memorySaved: startMemory && endMemory ?
|
||||
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
|
||||
};
|
||||
|
||||
enableAutoCleanup: function(interval = config.cleanupInterval) {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
}
|
||||
state.metrics.history.unshift(runStats);
|
||||
if (state.metrics.history.length > 10) {
|
||||
state.metrics.history.pop();
|
||||
}
|
||||
|
||||
config.cleanupInterval = interval;
|
||||
state.metrics.lastCleanupDetails = runStats;
|
||||
|
||||
this.forceCleanup();
|
||||
if (config.debug) {
|
||||
log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`);
|
||||
}
|
||||
|
||||
state.cleanupInterval = setInterval(() => {
|
||||
this.forceCleanup();
|
||||
}, interval);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
return false;
|
||||
} finally {
|
||||
state.isCleanupRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
log('Auto-cleanup enabled every', interval/1000, 'seconds');
|
||||
return true;
|
||||
},
|
||||
function removeOrphanedTooltips() {
|
||||
try {
|
||||
|
||||
disableAutoCleanup: function() {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
state.cleanupInterval = null;
|
||||
}
|
||||
const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)');
|
||||
let removed = 0;
|
||||
|
||||
console.log('Memory auto-cleanup disabled');
|
||||
return true;
|
||||
},
|
||||
tippyRoots.forEach(root => {
|
||||
const tooltipId = root.getAttribute('data-for-tooltip-id');
|
||||
const trigger = tooltipId ?
|
||||
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
|
||||
|
||||
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');
|
||||
if (!trigger || !document.body.contains(trigger)) {
|
||||
if (root.parentNode) {
|
||||
root.parentNode.removeChild(root);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return removed;
|
||||
} catch (error) {
|
||||
console.error("Error removing orphaned tooltips:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function checkForDisconnectedElements() {
|
||||
try {
|
||||
|
||||
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)');
|
||||
const disconnectedElements = new Set();
|
||||
|
||||
tooltipTriggers.forEach(el => {
|
||||
if (!document.body.contains(el)) {
|
||||
const tooltipId = el.getAttribute('data-tooltip-trigger-id');
|
||||
disconnectedElements.add(tooltipId);
|
||||
}
|
||||
});
|
||||
|
||||
const tooltipRoots = document.querySelectorAll('[data-for-tooltip-id]');
|
||||
let removed = 0;
|
||||
|
||||
disconnectedElements.forEach(id => {
|
||||
for (const root of tooltipRoots) {
|
||||
if (root.getAttribute('data-for-tooltip-id') === id && root.parentNode) {
|
||||
root.parentNode.removeChild(root);
|
||||
removed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return disconnectedElements.size;
|
||||
} catch (error) {
|
||||
console.error("Error checking for disconnected elements:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function tryRunGarbageCollection(aggressive = false) {
|
||||
setTimeout(() => {
|
||||
|
||||
const cache = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
cache[`key${i}`] = {};
|
||||
}
|
||||
|
||||
for (const key in cache) {
|
||||
delete cache[key];
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkMemoryUsage() {
|
||||
const result = {
|
||||
usedJSHeapSize: 0,
|
||||
totalJSHeapSize: 0,
|
||||
jsHeapSizeLimit: 0,
|
||||
percentUsed: "0",
|
||||
usedMB: "0",
|
||||
totalMB: "0",
|
||||
limitMB: "0"
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
if (window.performance && window.performance.memory) {
|
||||
result.usedJSHeapSize = window.performance.memory.usedJSHeapSize;
|
||||
result.totalJSHeapSize = window.performance.memory.totalJSHeapSize;
|
||||
result.jsHeapSizeLimit = window.performance.memory.jsHeapSizeLimit;
|
||||
result.percentUsed = (result.usedJSHeapSize / result.jsHeapSizeLimit * 100).toFixed(2);
|
||||
result.usedMB = (result.usedJSHeapSize / (1024 * 1024)).toFixed(2);
|
||||
result.totalMB = (result.totalJSHeapSize / (1024 * 1024)).toFixed(2);
|
||||
result.limitMB = (result.jsHeapSizeLimit / (1024 * 1024)).toFixed(2);
|
||||
} else {
|
||||
result.usedMB = "Unknown";
|
||||
result.totalMB = "Unknown";
|
||||
result.limitMB = "Unknown";
|
||||
result.percentUsed = "Unknown";
|
||||
}
|
||||
|
||||
state.metrics.lastMemoryUsage = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
removeOrphanedTooltips();
|
||||
checkForDisconnectedElements();
|
||||
}
|
||||
}
|
||||
|
||||
function setupMutationObserver() {
|
||||
if (state.mutationObserver) {
|
||||
state.mutationObserver.disconnect();
|
||||
state.mutationObserver = null;
|
||||
}
|
||||
|
||||
let processingScheduled = false;
|
||||
let lastProcessTime = 0;
|
||||
const MIN_PROCESS_INTERVAL = 10000;
|
||||
|
||||
const processMutations = (mutations) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastProcessTime < MIN_PROCESS_INTERVAL || processingScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
processingScheduled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
processingScheduled = false;
|
||||
lastProcessTime = Date.now();
|
||||
|
||||
if (state.isCleanupRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipSelectors = ['[data-tippy-root]', '[data-tooltip-trigger-id]', '.tooltip'];
|
||||
let tooltipCount = 0;
|
||||
|
||||
tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length;
|
||||
|
||||
if (tooltipCount > config.maxTooltipsThreshold &&
|
||||
(Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) {
|
||||
|
||||
removeOrphanedTooltips();
|
||||
checkForDisconnectedElements();
|
||||
state.lastCleanupTime = Date.now();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
state.mutationObserver = new MutationObserver(processMutations);
|
||||
|
||||
state.mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: false
|
||||
});
|
||||
|
||||
return state.mutationObserver;
|
||||
}
|
||||
|
||||
function enhanceTooltipManager() {
|
||||
if (!window.TooltipManager || window.TooltipManager._memoryManagerEnhanced) return false;
|
||||
|
||||
preserveTooltipFunctions();
|
||||
|
||||
const originalDestroy = window.TooltipManager.destroy;
|
||||
const originalCleanup = window.TooltipManager.cleanup;
|
||||
|
||||
window.TooltipManager.destroy = function(element) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||
|
||||
if (isInProtectedContainer(element)) {
|
||||
if (originalDestroy) {
|
||||
return originalDestroy.call(window.TooltipManager, element);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tooltipId) {
|
||||
if (originalDestroy) {
|
||||
originalDestroy.call(window.TooltipManager, element);
|
||||
}
|
||||
|
||||
const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
|
||||
if (tooltipRoot && tooltipRoot.parentNode) {
|
||||
tooltipRoot.parentNode.removeChild(tooltipRoot);
|
||||
}
|
||||
|
||||
element.removeAttribute('data-tooltip-trigger-id');
|
||||
element.removeAttribute('aria-describedby');
|
||||
|
||||
if (element._tippy) {
|
||||
try {
|
||||
element._tippy.destroy();
|
||||
element._tippy = null;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in enhanced tooltip destroy:', error);
|
||||
|
||||
if (originalDestroy) {
|
||||
originalDestroy.call(window.TooltipManager, element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.TooltipManager.cleanup = function() {
|
||||
try {
|
||||
if (originalCleanup) {
|
||||
originalCleanup.call(window.TooltipManager);
|
||||
}
|
||||
|
||||
removeOrphanedTooltips();
|
||||
} catch (error) {
|
||||
console.error('Error in enhanced tooltip cleanup:', error);
|
||||
|
||||
if (originalCleanup) {
|
||||
originalCleanup.call(window.TooltipManager);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.TooltipManager._memoryManagerEnhanced = true;
|
||||
window.TooltipManager._originalDestroy = originalDestroy;
|
||||
window.TooltipManager._originalCleanup = originalCleanup;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function initializeScheduledCleanups() {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
state.cleanupInterval = null;
|
||||
}
|
||||
|
||||
if (state.diagnosticsInterval) {
|
||||
clearInterval(state.diagnosticsInterval);
|
||||
state.diagnosticsInterval = null;
|
||||
}
|
||||
|
||||
if (state.elementVerificationInterval) {
|
||||
clearInterval(state.elementVerificationInterval);
|
||||
state.elementVerificationInterval = null;
|
||||
}
|
||||
|
||||
state.cleanupInterval = setInterval(() => {
|
||||
removeOrphanedTooltips();
|
||||
checkForDisconnectedElements();
|
||||
}, config.tooltipCleanupInterval);
|
||||
|
||||
state.diagnosticsInterval = setInterval(() => {
|
||||
checkMemoryUsage();
|
||||
}, config.diagnosticsInterval);
|
||||
|
||||
state.elementVerificationInterval = setInterval(() => {
|
||||
checkForDisconnectedElements();
|
||||
}, config.elementVerificationInterval);
|
||||
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
setupMutationObserver();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function initialize(options = {}) {
|
||||
preserveTooltipFunctions();
|
||||
|
||||
if (options) {
|
||||
Object.assign(config, options);
|
||||
}
|
||||
|
||||
enhanceTooltipManager();
|
||||
|
||||
if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) {
|
||||
window.WebSocketManager.cleanupOrphanedSockets = function() {
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
const manager = window.ApiManager || window.Api;
|
||||
if (manager && !manager.abortPendingRequests) {
|
||||
manager.abortPendingRequests = function() {
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
initializeScheduledCleanups();
|
||||
|
||||
setTimeout(() => {
|
||||
removeOrphanedTooltips();
|
||||
checkForDisconnectedElements();
|
||||
}, 5000);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (state.cleanupInterval) {
|
||||
clearInterval(state.cleanupInterval);
|
||||
state.cleanupInterval = null;
|
||||
}
|
||||
|
||||
if (state.diagnosticsInterval) {
|
||||
clearInterval(state.diagnosticsInterval);
|
||||
state.diagnosticsInterval = null;
|
||||
}
|
||||
|
||||
if (state.elementVerificationInterval) {
|
||||
clearInterval(state.elementVerificationInterval);
|
||||
state.elementVerificationInterval = null;
|
||||
}
|
||||
|
||||
if (state.mutationObserver) {
|
||||
state.mutationObserver.disconnect();
|
||||
state.mutationObserver = null;
|
||||
}
|
||||
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function displayStats() {
|
||||
const stats = getDetailedStats();
|
||||
|
||||
console.group('Memory Manager Stats');
|
||||
console.log('Memory Usage:', stats.memory ?
|
||||
`${stats.memory.usedMB}MB / ${stats.memory.limitMB}MB (${stats.memory.percentUsed}%)` :
|
||||
'Not available');
|
||||
console.log('Total Cleanups:', stats.metrics.cleanupRuns);
|
||||
console.log('Total Tooltips Removed:', stats.metrics.tooltipsRemoved);
|
||||
console.log('Current Tooltips:', stats.tooltips.total);
|
||||
console.log('Last Cleanup:', stats.metrics.lastCleanupDetails);
|
||||
console.log('Cleanup History:', stats.metrics.history);
|
||||
console.groupEnd();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
function getDetailedStats() {
|
||||
|
||||
const allTooltipElements = document.querySelectorAll('[data-tippy-root], [data-tooltip-trigger-id], .tooltip');
|
||||
|
||||
const tooltips = {
|
||||
roots: document.querySelectorAll('[data-tippy-root]').length,
|
||||
triggers: document.querySelectorAll('[data-tooltip-trigger-id]').length,
|
||||
tooltipElements: document.querySelectorAll('.tooltip').length,
|
||||
total: allTooltipElements.length,
|
||||
protectedContainers: {}
|
||||
};
|
||||
|
||||
config.protectedContainers.forEach(selector => {
|
||||
const container = document.querySelector(selector);
|
||||
if (container) {
|
||||
tooltips.protectedContainers[selector] = {
|
||||
tooltips: container.querySelectorAll('.tooltip').length,
|
||||
triggers: container.querySelectorAll('[data-tooltip-trigger-id]').length,
|
||||
roots: document.querySelectorAll(`[data-tippy-root][data-for-tooltip-id]`).length
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
memory: checkMemoryUsage(),
|
||||
metrics: { ...state.metrics },
|
||||
tooltips,
|
||||
config: { ...config }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
initialize,
|
||||
cleanup: performCleanup,
|
||||
forceCleanup: function() {
|
||||
return performCleanup(true);
|
||||
},
|
||||
fullCleanup: function() {
|
||||
return performCleanup(true);
|
||||
},
|
||||
getStats: getDetailedStats,
|
||||
displayStats,
|
||||
setDebugMode: function(enabled) {
|
||||
config.debug = Boolean(enabled);
|
||||
return config.debug;
|
||||
},
|
||||
addProtectedContainer: function(selector) {
|
||||
if (!config.protectedContainers.includes(selector)) {
|
||||
config.protectedContainers.push(selector);
|
||||
}
|
||||
return config.protectedContainers;
|
||||
},
|
||||
removeProtectedContainer: function(selector) {
|
||||
const index = config.protectedContainers.indexOf(selector);
|
||||
if (index !== -1) {
|
||||
config.protectedContainers.splice(index, 1);
|
||||
}
|
||||
return config.protectedContainers;
|
||||
},
|
||||
dispose
|
||||
};
|
||||
})();
|
||||
|
||||
window.MemoryManager = MemoryManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.memoryManagerInitialized) {
|
||||
MemoryManager.initialize();
|
||||
window.memoryManagerInitialized = true;
|
||||
}
|
||||
const isDevMode = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1';
|
||||
|
||||
MemoryManager.initialize({
|
||||
debug: isDevMode
|
||||
});
|
||||
|
||||
console.log('Memory Manager initialized');
|
||||
});
|
||||
|
||||
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
|
||||
console.log('MemoryManager initialized');
|
||||
window.MemoryManager = MemoryManager;
|
||||
|
||||
@@ -108,7 +108,7 @@ const NetworkManager = (function() {
|
||||
|
||||
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
|
||||
|
||||
state.reconnectTimer = setTimeout(() => {
|
||||
state.reconnectTimer = CleanupManager.setTimeout(() => {
|
||||
state.reconnectTimer = null;
|
||||
this.attemptReconnect();
|
||||
}, delay);
|
||||
@@ -167,7 +167,20 @@ const NetworkManager = (function() {
|
||||
});
|
||||
},
|
||||
|
||||
testBackendConnection: function() {
|
||||
testBackendConnection: async function() {
|
||||
if (window.ApiManager) {
|
||||
try {
|
||||
await window.ApiManager.makeRequest(config.connectionTestEndpoint, 'HEAD', {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('Backend connection test failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(config.connectionTestEndpoint, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
@@ -275,6 +288,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
|
||||
console.log('NetworkManager initialized');
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,8 +42,9 @@ const PriceManager = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => this.getPrices(), 1500);
|
||||
CleanupManager.setTimeout(() => this.getPrices(), 1500);
|
||||
isInitialized = true;
|
||||
console.log('PriceManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -59,7 +60,6 @@ const PriceManager = (function() {
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
console.log('PriceManager: Fetching latest prices.');
|
||||
lastFetchTime = Date.now();
|
||||
fetchPromise = this.fetchPrices()
|
||||
.then(prices => {
|
||||
@@ -83,14 +83,12 @@ const PriceManager = (function() {
|
||||
throw new Error('Network is offline');
|
||||
}
|
||||
|
||||
const coinSymbols = window.CoinManager
|
||||
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');
|
||||
}
|
||||
@@ -114,7 +112,7 @@ const PriceManager = (function() {
|
||||
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');
|
||||
}
|
||||
@@ -122,35 +120,35 @@ const PriceManager = (function() {
|
||||
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 if (window.CoinUtils) {
|
||||
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
|
||||
} else {
|
||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
||||
normalizedCoinId = coinId.toLowerCase();
|
||||
}
|
||||
} else if (window.CoinUtils) {
|
||||
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
|
||||
} else {
|
||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
||||
normalizedCoinId = 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) {
|
||||
@@ -158,38 +156,38 @@ const PriceManager = (function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
@@ -198,14 +196,14 @@ const PriceManager = (function() {
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -229,5 +227,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.priceManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('PriceManager initialized');
|
||||
|
||||
79
basicswap/static/js/modules/qrcode-manager.js
Normal file
79
basicswap/static/js/modules/qrcode-manager.js
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const QRCodeManager = {
|
||||
|
||||
defaultOptions: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
const qrElements = document.querySelectorAll('[data-qrcode]');
|
||||
|
||||
qrElements.forEach(element => {
|
||||
this.generateQRCode(element);
|
||||
});
|
||||
},
|
||||
|
||||
generateQRCode: function(element) {
|
||||
const address = element.getAttribute('data-address');
|
||||
const width = parseInt(element.getAttribute('data-width')) || this.defaultOptions.width;
|
||||
const height = parseInt(element.getAttribute('data-height')) || this.defaultOptions.height;
|
||||
|
||||
if (!address) {
|
||||
console.error('QRCodeManager: No address provided for element', element);
|
||||
return;
|
||||
}
|
||||
|
||||
element.innerHTML = '';
|
||||
|
||||
try {
|
||||
new QRCode(element, {
|
||||
text: address,
|
||||
width: width,
|
||||
height: height,
|
||||
colorDark: this.defaultOptions.colorDark,
|
||||
colorLight: this.defaultOptions.colorLight,
|
||||
correctLevel: this.defaultOptions.correctLevel
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('QRCodeManager: Failed to generate QR code', error);
|
||||
}
|
||||
},
|
||||
|
||||
generateById: function(elementId, address, options = {}) {
|
||||
|
||||
const element = window.DOMCache
|
||||
? window.DOMCache.get(elementId)
|
||||
: document.getElementById(elementId);
|
||||
|
||||
if (!element) {
|
||||
console.error('QRCodeManager: Element not found:', elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
element.setAttribute('data-address', address);
|
||||
|
||||
if (options.width) element.setAttribute('data-width', options.width);
|
||||
if (options.height) element.setAttribute('data-height', options.height);
|
||||
|
||||
this.generateQRCode(element);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
QRCodeManager.initialize();
|
||||
});
|
||||
} else {
|
||||
QRCodeManager.initialize();
|
||||
}
|
||||
|
||||
window.QRCodeManager = QRCodeManager;
|
||||
|
||||
})();
|
||||
@@ -4,7 +4,8 @@ const SummaryManager = (function() {
|
||||
summaryEndpoint: '/json',
|
||||
retryDelay: 5000,
|
||||
maxRetries: 3,
|
||||
requestTimeout: 15000
|
||||
requestTimeout: 15000,
|
||||
debug: false
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
@@ -60,12 +61,15 @@ const SummaryManager = (function() {
|
||||
|
||||
updateElement('network-offers-counter', data.num_network_offers);
|
||||
updateElement('offers-counter', data.num_sent_active_offers);
|
||||
updateElement('offers-counter-mobile', data.num_sent_active_offers);
|
||||
updateElement('sent-bids-counter', data.num_sent_active_bids);
|
||||
updateElement('recv-bids-counter', data.num_recv_active_bids);
|
||||
updateElement('bid-requests-counter', data.num_available_bids);
|
||||
updateElement('swaps-counter', data.num_swapping);
|
||||
updateElement('watched-outputs-counter', data.num_watched_outputs);
|
||||
|
||||
updateTooltips(data);
|
||||
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
shutdownButtons.forEach(button => {
|
||||
button.setAttribute('data-active-swaps', data.num_swapping);
|
||||
@@ -81,6 +85,100 @@ const SummaryManager = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateTooltips(data) {
|
||||
debugLog(`updateTooltips called with data:`, data);
|
||||
|
||||
const yourOffersTooltip = document.getElementById('tooltip-your-offers');
|
||||
debugLog('Looking for tooltip-your-offers element:', yourOffersTooltip);
|
||||
|
||||
if (yourOffersTooltip) {
|
||||
const newContent = `
|
||||
<p><b>Total offers:</b> ${data.num_sent_offers || 0}</p>
|
||||
<p><b>Active offers:</b> ${data.num_sent_active_offers || 0}</p>
|
||||
`;
|
||||
|
||||
const totalParagraph = yourOffersTooltip.querySelector('p:first-child');
|
||||
const activeParagraph = yourOffersTooltip.querySelector('p:last-child');
|
||||
|
||||
debugLog('Found paragraphs:', { totalParagraph, activeParagraph });
|
||||
|
||||
if (totalParagraph && activeParagraph) {
|
||||
totalParagraph.innerHTML = `<b>Total offers:</b> ${data.num_sent_offers || 0}`;
|
||||
activeParagraph.innerHTML = `<b>Active offers:</b> ${data.num_sent_active_offers || 0}`;
|
||||
debugLog(`Updated Your Offers tooltip paragraphs: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
|
||||
} else {
|
||||
yourOffersTooltip.innerHTML = newContent;
|
||||
debugLog(`Replaced Your Offers tooltip content: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
|
||||
}
|
||||
|
||||
refreshTooltipInstances('tooltip-your-offers', newContent);
|
||||
} else {
|
||||
debugLog('Your Offers tooltip element not found - checking all tooltip elements');
|
||||
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
|
||||
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
|
||||
}
|
||||
|
||||
const bidsTooltip = document.getElementById('tooltip-bids');
|
||||
if (bidsTooltip) {
|
||||
const newBidsContent = `
|
||||
<p><b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)</p>
|
||||
<p><b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)</p>
|
||||
`;
|
||||
|
||||
const sentParagraph = bidsTooltip.querySelector('p:first-child');
|
||||
const recvParagraph = bidsTooltip.querySelector('p:last-child');
|
||||
|
||||
if (sentParagraph && recvParagraph) {
|
||||
sentParagraph.innerHTML = `<b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)`;
|
||||
recvParagraph.innerHTML = `<b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)`;
|
||||
debugLog(`Updated Bids tooltip: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
|
||||
} else {
|
||||
bidsTooltip.innerHTML = newBidsContent;
|
||||
debugLog(`Replaced Bids tooltip content: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
|
||||
}
|
||||
|
||||
refreshTooltipInstances('tooltip-bids', newBidsContent);
|
||||
} else {
|
||||
debugLog('Bids tooltip element not found');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTooltipInstances(tooltipId, newContent) {
|
||||
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
if (trigger._tippy) {
|
||||
trigger._tippy.setContent(newContent);
|
||||
debugLog(`Updated Tippy instance content for ${tooltipId}`);
|
||||
} else {
|
||||
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
||||
window.TooltipManager.create(trigger, newContent, {
|
||||
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
debugLog(`Created new Tippy instance for ${tooltipId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
|
||||
window.TooltipManager.refreshTooltip(tooltipId, newContent);
|
||||
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function debugLog(message) {
|
||||
if (config.debug && console && console.log) {
|
||||
console.log(`[SummaryManager] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheSummaryData(data) {
|
||||
if (!data) return;
|
||||
|
||||
@@ -107,8 +205,16 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
function fetchSummaryDataWithTimeout() {
|
||||
if (window.ApiManager) {
|
||||
return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
|
||||
'Accept': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
});
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
|
||||
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
|
||||
|
||||
return fetch(config.summaryEndpoint, {
|
||||
signal: controller.signal,
|
||||
@@ -119,7 +225,11 @@ const SummaryManager = (function() {
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(timeoutId);
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
@@ -128,7 +238,11 @@ const SummaryManager = (function() {
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeoutId);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(timeoutId);
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
@@ -163,9 +277,12 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
if (data.event) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (summaryEvents.includes(data.event)) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
@@ -174,7 +291,7 @@ const SummaryManager = (function() {
|
||||
};
|
||||
|
||||
webSocket.onclose = () => {
|
||||
setTimeout(setupWebSocket, 5000);
|
||||
CleanupManager.setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -202,7 +319,7 @@ const SummaryManager = (function() {
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = setInterval(() => {
|
||||
refreshTimer = CleanupManager.setInterval(() => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
@@ -236,9 +353,12 @@ const SummaryManager = (function() {
|
||||
|
||||
wsManager.addMessageHandler('message', (data) => {
|
||||
if (data.event) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (summaryEvents.includes(data.event)) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
@@ -282,7 +402,7 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
resolve(this.fetchSummaryData());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
@@ -303,6 +423,14 @@ const SummaryManager = (function() {
|
||||
});
|
||||
},
|
||||
|
||||
updateTooltips: function(data) {
|
||||
updateTooltips(data || lastSuccessfulData);
|
||||
},
|
||||
|
||||
updateUI: function(data) {
|
||||
updateUIFromData(data || lastSuccessfulData);
|
||||
},
|
||||
|
||||
startRefreshTimer: function() {
|
||||
startRefreshTimer();
|
||||
},
|
||||
@@ -334,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
|
||||
console.log('SummaryManager initialized');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
196
basicswap/static/js/modules/wallet-amount.js
Normal file
196
basicswap/static/js/modules/wallet-amount.js
Normal file
@@ -0,0 +1,196 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletAmountManager = {
|
||||
|
||||
coinConfigs: {
|
||||
1: {
|
||||
types: ['plain', 'blind', 'anon'],
|
||||
hasSubfee: true,
|
||||
hasSweepAll: false
|
||||
},
|
||||
3: {
|
||||
types: ['plain', 'mweb'],
|
||||
hasSubfee: true,
|
||||
hasSweepAll: false
|
||||
},
|
||||
6: {
|
||||
types: ['default'],
|
||||
hasSubfee: false,
|
||||
hasSweepAll: true
|
||||
},
|
||||
9: {
|
||||
types: ['default'],
|
||||
hasSubfee: false,
|
||||
hasSweepAll: true
|
||||
}
|
||||
},
|
||||
|
||||
safeParseFloat: function(value) {
|
||||
const numValue = Number(value);
|
||||
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
return numValue;
|
||||
}
|
||||
|
||||
console.warn('WalletAmountManager: Invalid balance value:', value);
|
||||
return 0;
|
||||
},
|
||||
|
||||
getBalance: function(coinId, balances, selectedType) {
|
||||
const cid = parseInt(coinId);
|
||||
|
||||
if (cid === 1) {
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
case 'blind':
|
||||
return this.safeParseFloat(balances.blind);
|
||||
case 'anon':
|
||||
return this.safeParseFloat(balances.anon);
|
||||
default:
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
}
|
||||
}
|
||||
|
||||
if (cid === 3) {
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
case 'mweb':
|
||||
return this.safeParseFloat(balances.mweb);
|
||||
default:
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
}
|
||||
}
|
||||
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
},
|
||||
|
||||
calculateAmount: function(balance, percent, coinId) {
|
||||
const cid = parseInt(coinId);
|
||||
|
||||
if (percent === 1) {
|
||||
return balance;
|
||||
}
|
||||
|
||||
if (cid === 1) {
|
||||
return Math.max(0, Math.floor(balance * percent * 100000000) / 100000000);
|
||||
}
|
||||
|
||||
const calculatedAmount = balance * percent;
|
||||
|
||||
if (calculatedAmount < 0.00000001) {
|
||||
console.warn('WalletAmountManager: Calculated amount too small, setting to zero');
|
||||
return 0;
|
||||
}
|
||||
|
||||
return calculatedAmount;
|
||||
},
|
||||
|
||||
setAmount: function(percent, balances, coinId) {
|
||||
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get('amount')
|
||||
: document.getElementById('amount');
|
||||
const typeSelect = window.DOMCache
|
||||
? window.DOMCache.get('withdraw_type')
|
||||
: document.getElementById('withdraw_type');
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('WalletAmountManager: Amount input not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const cid = parseInt(coinId);
|
||||
const selectedType = typeSelect ? typeSelect.value : 'plain';
|
||||
|
||||
const balance = this.getBalance(cid, balances, selectedType);
|
||||
|
||||
const calculatedAmount = this.calculateAmount(balance, percent, cid);
|
||||
|
||||
const specialCids = [6, 9];
|
||||
if (specialCids.includes(cid) && percent === 1) {
|
||||
amountInput.setAttribute('data-hidden', 'true');
|
||||
amountInput.placeholder = 'Sweep All';
|
||||
amountInput.value = '';
|
||||
amountInput.disabled = true;
|
||||
|
||||
const sweepAllCheckbox = window.DOMCache
|
||||
? window.DOMCache.get('sweepall')
|
||||
: document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.setAttribute('data-hidden', 'false');
|
||||
amountInput.placeholder = '';
|
||||
amountInput.disabled = false;
|
||||
|
||||
const sweepAllCheckbox = window.DOMCache
|
||||
? window.DOMCache.get('sweepall')
|
||||
: document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
const subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
const amountButtons = document.querySelectorAll('[data-set-amount]');
|
||||
|
||||
amountButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const percent = parseFloat(button.getAttribute('data-set-amount'));
|
||||
const balancesJson = button.getAttribute('data-balances');
|
||||
const coinId = button.getAttribute('data-coin-id');
|
||||
|
||||
if (!balancesJson || !coinId) {
|
||||
console.error('WalletAmountManager: Missing data attributes on button', button);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const balances = JSON.parse(balancesJson);
|
||||
this.setAmount(percent, balances, coinId);
|
||||
} catch (error) {
|
||||
console.error('WalletAmountManager: Failed to parse balances', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletAmountManager.initialize();
|
||||
});
|
||||
} else {
|
||||
WalletAmountManager.initialize();
|
||||
}
|
||||
|
||||
window.WalletAmountManager = WalletAmountManager;
|
||||
|
||||
window.setAmount = function(percent, balance, coinId, balance2, balance3) {
|
||||
|
||||
const balances = {
|
||||
main: balance || balance,
|
||||
balance: balance,
|
||||
blind: balance2,
|
||||
anon: balance3,
|
||||
mweb: balance2
|
||||
};
|
||||
WalletAmountManager.setAmount(percent, balances, coinId);
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -11,8 +11,7 @@ const WalletManager = (function() {
|
||||
defaultTTL: 300,
|
||||
priceSource: {
|
||||
primary: 'coingecko.com',
|
||||
fallback: 'cryptocompare.com',
|
||||
enabledSources: ['coingecko.com', 'cryptocompare.com']
|
||||
enabledSources: ['coingecko.com']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +44,7 @@ const WalletManager = (function() {
|
||||
}
|
||||
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
|
||||
|
||||
|
||||
if (!coin) {
|
||||
console.warn(`[WalletManager] No coin found for: ${coinName}`);
|
||||
return coinName;
|
||||
@@ -78,15 +77,15 @@ const WalletManager = (function() {
|
||||
|
||||
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' :
|
||||
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);
|
||||
@@ -95,29 +94,39 @@ const WalletManager = (function() {
|
||||
|
||||
const fetchCoinsString = coinsToFetch.join(',');
|
||||
|
||||
const mainResponse = await fetch("/json/coinprices", {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
let mainData;
|
||||
|
||||
if (window.ApiManager) {
|
||||
mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
|
||||
coins: fetchCoinsString,
|
||||
source: currentSource,
|
||||
ttl: config.defaultTTL
|
||||
})
|
||||
});
|
||||
});
|
||||
} else {
|
||||
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}`);
|
||||
if (!mainResponse.ok) {
|
||||
throw new Error(`HTTP error: ${mainResponse.status}`);
|
||||
}
|
||||
|
||||
mainData = await mainResponse.json();
|
||||
}
|
||||
|
||||
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' :
|
||||
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
|
||||
coinName.includes('Particl') ? 'Particl' :
|
||||
coinName;
|
||||
|
||||
const coinId = getCoingeckoId(adjustedName);
|
||||
@@ -154,7 +163,7 @@ const WalletManager = (function() {
|
||||
|
||||
if (attempt < config.maxRetries - 1) {
|
||||
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,8 +189,29 @@ const WalletManager = (function() {
|
||||
|
||||
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');
|
||||
let isBlind = false;
|
||||
let isAnon = false;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const h4Element = flexContainer.querySelector('h4');
|
||||
if (h4Element) {
|
||||
isBlind = h4Element.textContent?.includes('Blind');
|
||||
isAnon = h4Element.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlind && !isAnon) {
|
||||
const parentRow = el.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell) {
|
||||
isBlind = labelCell.textContent?.includes('Blind');
|
||||
isAnon = labelCell.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
@@ -248,8 +278,29 @@ const WalletManager = (function() {
|
||||
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');
|
||||
let isBlind = false;
|
||||
let isAnon = false;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const h4Element = flexContainer.querySelector('h4');
|
||||
if (h4Element) {
|
||||
isBlind = h4Element.textContent?.includes('Blind');
|
||||
isAnon = h4Element.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlind && !isAnon) {
|
||||
const parentRow = el.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell) {
|
||||
isBlind = labelCell.textContent?.includes('Blind');
|
||||
isAnon = labelCell.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
@@ -386,7 +437,7 @@ const WalletManager = (function() {
|
||||
clearTimeout(state.toggleDebounceTimer);
|
||||
}
|
||||
|
||||
state.toggleDebounceTimer = window.setTimeout(async () => {
|
||||
state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
|
||||
state.toggleInProgress = false;
|
||||
if (newVisibility) {
|
||||
await updatePrices(true);
|
||||
@@ -497,7 +548,6 @@ const WalletManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
const publicAPI = {
|
||||
initialize: async function(options) {
|
||||
if (state.initialized) {
|
||||
@@ -537,7 +587,7 @@ const WalletManager = (function() {
|
||||
clearInterval(state.priceUpdateInterval);
|
||||
}
|
||||
|
||||
state.priceUpdateInterval = setInterval(() => {
|
||||
state.priceUpdateInterval = CleanupManager.setInterval(() => {
|
||||
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
|
||||
updatePrices(false);
|
||||
}
|
||||
@@ -619,5 +669,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
|
||||
console.log('WalletManager initialized');
|
||||
|
||||
@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
|
||||
}
|
||||
|
||||
function determineWebSocketPort() {
|
||||
let wsPort;
|
||||
if (window.ConfigManager && window.ConfigManager.wsPort) {
|
||||
return window.ConfigManager.wsPort.toString();
|
||||
}
|
||||
|
||||
if (window.config && window.config.wsPort) {
|
||||
wsPort = window.config.wsPort;
|
||||
return wsPort;
|
||||
return window.config.wsPort.toString();
|
||||
}
|
||||
|
||||
if (window.ws_port) {
|
||||
wsPort = window.ws_port.toString();
|
||||
return wsPort;
|
||||
return window.ws_port.toString();
|
||||
}
|
||||
|
||||
if (typeof getWebSocketConfig === 'function') {
|
||||
const wsConfig = getWebSocketConfig();
|
||||
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
||||
return wsPort;
|
||||
return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
||||
}
|
||||
|
||||
wsPort = '11700';
|
||||
return wsPort;
|
||||
return '11700';
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
|
||||
}
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
}
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
|
||||
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||
setupEventHandlers();
|
||||
|
||||
state.connectTimeout = setTimeout(() => {
|
||||
const timeoutFn = () => {
|
||||
if (state.isConnecting) {
|
||||
log('Connection timeout, cleaning up');
|
||||
cleanup();
|
||||
handleReconnect();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
state.connectTimeout = window.CleanupManager
|
||||
? window.CleanupManager.setTimeout(timeoutFn, 5000)
|
||||
: setTimeout(timeoutFn, 5000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
|
||||
cleanup: function() {
|
||||
log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(state.connectTimeout);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.connectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.connectTimeout);
|
||||
}
|
||||
stopHealthCheck();
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
}
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
state.isConnecting = false;
|
||||
state.messageHandlers = {};
|
||||
|
||||
|
||||
if (ws) {
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
|
||||
ws.onopen = () => {
|
||||
state.isConnecting = false;
|
||||
config.reconnectAttempts = 0;
|
||||
clearTimeout(state.connectTimeout);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.connectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.connectTimeout);
|
||||
}
|
||||
state.lastHealthCheck = Date.now();
|
||||
window.ws = ws;
|
||||
|
||||
@@ -298,37 +315,42 @@ const WebSocketManager = (function() {
|
||||
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(() => {
|
||||
const resumeFn = () => {
|
||||
if (!publicAPI.isConnected()) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
startHealthCheck();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.setTimeout(resumeFn, 0);
|
||||
} else {
|
||||
setTimeout(resumeFn, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function startHealthCheck() {
|
||||
stopHealthCheck();
|
||||
state.healthCheckInterval = setInterval(() => {
|
||||
const healthCheckFn = () => {
|
||||
performHealthCheck();
|
||||
}, 30000);
|
||||
};
|
||||
state.healthCheckInterval = window.CleanupManager
|
||||
? window.CleanupManager.setInterval(healthCheckFn, 30000)
|
||||
: setInterval(healthCheckFn, 30000);
|
||||
}
|
||||
|
||||
function stopHealthCheck() {
|
||||
if (state.healthCheckInterval) {
|
||||
clearInterval(state.healthCheckInterval);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearInterval(state.healthCheckInterval);
|
||||
} else {
|
||||
clearInterval(state.healthCheckInterval);
|
||||
}
|
||||
state.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
@@ -356,7 +378,11 @@ const WebSocketManager = (function() {
|
||||
function handleReconnect() {
|
||||
|
||||
if (state.reconnectTimeout) {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.reconnectTimeout);
|
||||
}
|
||||
state.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
@@ -369,23 +395,31 @@ const WebSocketManager = (function() {
|
||||
|
||||
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
const reconnectFn = () => {
|
||||
state.reconnectTimeout = null;
|
||||
if (!state.isIntentionallyClosed) {
|
||||
publicAPI.connect();
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
state.reconnectTimeout = window.CleanupManager
|
||||
? window.CleanupManager.setTimeout(reconnectFn, delay)
|
||||
: setTimeout(reconnectFn, delay);
|
||||
} else {
|
||||
log('Max reconnect attempts reached');
|
||||
if (typeof updateConnectionStatus === 'function') {
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
state.reconnectTimeout = setTimeout(() => {
|
||||
const resetFn = () => {
|
||||
state.reconnectTimeout = null;
|
||||
config.reconnectAttempts = 0;
|
||||
publicAPI.connect();
|
||||
}, 60000);
|
||||
};
|
||||
|
||||
state.reconnectTimeout = window.CleanupManager
|
||||
? window.CleanupManager.setTimeout(resetFn, 60000)
|
||||
: setTimeout(resetFn, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,5 +476,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
|
||||
console.log('WebSocketManager initialized');
|
||||
|
||||
294
basicswap/static/js/pages/amm-config-tabs.js
Normal file
294
basicswap/static/js/pages/amm-config-tabs.js
Normal file
@@ -0,0 +1,294 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const AMMConfigTabs = {
|
||||
|
||||
init: function() {
|
||||
const jsonTab = document.getElementById('json-tab');
|
||||
const settingsTab = document.getElementById('settings-tab');
|
||||
const overviewTab = document.getElementById('overview-tab');
|
||||
const jsonContent = document.getElementById('json-content');
|
||||
const settingsContent = document.getElementById('settings-content');
|
||||
const overviewContent = document.getElementById('overview-content');
|
||||
|
||||
if (!jsonTab || !settingsTab || !overviewTab || !jsonContent || !settingsContent || !overviewContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeConfigTab = localStorage.getItem('amm_active_config_tab');
|
||||
|
||||
const switchConfigTab = (tabId) => {
|
||||
jsonContent.classList.add('hidden');
|
||||
jsonContent.classList.remove('block');
|
||||
settingsContent.classList.add('hidden');
|
||||
settingsContent.classList.remove('block');
|
||||
overviewContent.classList.add('hidden');
|
||||
overviewContent.classList.remove('block');
|
||||
|
||||
jsonTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
settingsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
overviewTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
|
||||
if (tabId === 'json-tab') {
|
||||
jsonContent.classList.remove('hidden');
|
||||
jsonContent.classList.add('block');
|
||||
jsonTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
localStorage.setItem('amm_active_config_tab', 'json-tab');
|
||||
} else if (tabId === 'settings-tab') {
|
||||
settingsContent.classList.remove('hidden');
|
||||
settingsContent.classList.add('block');
|
||||
settingsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
localStorage.setItem('amm_active_config_tab', 'settings-tab');
|
||||
|
||||
this.loadSettingsFromJson();
|
||||
} else if (tabId === 'overview-tab') {
|
||||
overviewContent.classList.remove('hidden');
|
||||
overviewContent.classList.add('block');
|
||||
overviewTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
||||
localStorage.setItem('amm_active_config_tab', 'overview-tab');
|
||||
}
|
||||
};
|
||||
|
||||
jsonTab.addEventListener('click', () => switchConfigTab('json-tab'));
|
||||
settingsTab.addEventListener('click', () => switchConfigTab('settings-tab'));
|
||||
overviewTab.addEventListener('click', () => switchConfigTab('overview-tab'));
|
||||
|
||||
const returnToTab = localStorage.getItem('amm_return_to_tab');
|
||||
if (returnToTab && (returnToTab === 'json-tab' || returnToTab === 'settings-tab' || returnToTab === 'overview-tab')) {
|
||||
localStorage.removeItem('amm_return_to_tab');
|
||||
switchConfigTab(returnToTab);
|
||||
} else if (activeConfigTab === 'settings-tab') {
|
||||
switchConfigTab('settings-tab');
|
||||
} else if (activeConfigTab === 'overview-tab') {
|
||||
switchConfigTab('overview-tab');
|
||||
} else {
|
||||
switchConfigTab('json-tab');
|
||||
}
|
||||
|
||||
const globalSettingsForm = document.getElementById('global-settings-form');
|
||||
if (globalSettingsForm) {
|
||||
globalSettingsForm.addEventListener('submit', () => {
|
||||
this.updateJsonFromSettings();
|
||||
});
|
||||
}
|
||||
|
||||
this.setupCollapsibles();
|
||||
|
||||
this.setupConfigForm();
|
||||
|
||||
this.setupCreateDefaultButton();
|
||||
|
||||
this.handleCreateDefaultRefresh();
|
||||
},
|
||||
|
||||
loadSettingsFromJson: function() {
|
||||
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
||||
if (!configTextarea) return;
|
||||
|
||||
try {
|
||||
const configText = configTextarea.value.trim();
|
||||
if (!configText) return;
|
||||
|
||||
const config = JSON.parse(configText);
|
||||
|
||||
document.getElementById('min_seconds_between_offers').value = config.min_seconds_between_offers || 15;
|
||||
document.getElementById('max_seconds_between_offers').value = config.max_seconds_between_offers || 60;
|
||||
document.getElementById('main_loop_delay').value = config.main_loop_delay || 60;
|
||||
|
||||
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
|
||||
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
|
||||
const pruneStateDelayEl = document.getElementById('prune_state_delay');
|
||||
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
|
||||
|
||||
if (minSecondsBetweenBidsEl) minSecondsBetweenBidsEl.value = config.min_seconds_between_bids || 15;
|
||||
if (maxSecondsBetweenBidsEl) maxSecondsBetweenBidsEl.value = config.max_seconds_between_bids || 60;
|
||||
if (pruneStateDelayEl) pruneStateDelayEl.value = config.prune_state_delay || 120;
|
||||
if (pruneStateAfterSecondsEl) pruneStateAfterSecondsEl.value = config.prune_state_after_seconds || 604800;
|
||||
document.getElementById('auth').value = config.auth || '';
|
||||
} catch (error) {
|
||||
console.error('Error loading settings from JSON:', error);
|
||||
}
|
||||
},
|
||||
|
||||
updateJsonFromSettings: function() {
|
||||
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
||||
if (!configTextarea) return;
|
||||
|
||||
try {
|
||||
const configText = configTextarea.value.trim();
|
||||
let config = {};
|
||||
|
||||
if (configText) {
|
||||
config = JSON.parse(configText);
|
||||
}
|
||||
|
||||
config.min_seconds_between_offers = parseInt(document.getElementById('min_seconds_between_offers').value) || 15;
|
||||
config.max_seconds_between_offers = parseInt(document.getElementById('max_seconds_between_offers').value) || 60;
|
||||
config.main_loop_delay = parseInt(document.getElementById('main_loop_delay').value) || 60;
|
||||
|
||||
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
|
||||
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
|
||||
const pruneStateDelayEl = document.getElementById('prune_state_delay');
|
||||
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
|
||||
|
||||
if (minSecondsBetweenBidsEl) config.min_seconds_between_bids = parseInt(minSecondsBetweenBidsEl.value) || 15;
|
||||
if (maxSecondsBetweenBidsEl) config.max_seconds_between_bids = parseInt(maxSecondsBetweenBidsEl.value) || 60;
|
||||
if (pruneStateDelayEl) config.prune_state_delay = parseInt(pruneStateDelayEl.value) || 120;
|
||||
if (pruneStateAfterSecondsEl) config.prune_state_after_seconds = parseInt(pruneStateAfterSecondsEl.value) || 604800;
|
||||
config.auth = document.getElementById('auth').value || '';
|
||||
|
||||
configTextarea.value = JSON.stringify(config, null, 2);
|
||||
|
||||
localStorage.setItem('amm_return_to_tab', 'settings-tab');
|
||||
} catch (error) {
|
||||
console.error('Error updating JSON from settings:', error);
|
||||
alert('Error updating configuration: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
setupCollapsibles: function() {
|
||||
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
|
||||
|
||||
if (collapsibleHeaders.length === 0) return;
|
||||
|
||||
let collapsibleStates = {};
|
||||
try {
|
||||
const storedStates = localStorage.getItem('amm_collapsible_states');
|
||||
if (storedStates) {
|
||||
collapsibleStates = JSON.parse(storedStates);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing stored collapsible states:', e);
|
||||
collapsibleStates = {};
|
||||
}
|
||||
|
||||
const toggleCollapsible = (header) => {
|
||||
const targetId = header.getAttribute('data-target');
|
||||
const content = document.getElementById(targetId);
|
||||
const arrow = header.querySelector('svg');
|
||||
|
||||
if (content) {
|
||||
if (content.classList.contains('hidden')) {
|
||||
content.classList.remove('hidden');
|
||||
arrow.classList.add('rotate-180');
|
||||
collapsibleStates[targetId] = 'open';
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
arrow.classList.remove('rotate-180');
|
||||
collapsibleStates[targetId] = 'closed';
|
||||
}
|
||||
|
||||
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
|
||||
}
|
||||
};
|
||||
|
||||
collapsibleHeaders.forEach(header => {
|
||||
const targetId = header.getAttribute('data-target');
|
||||
const content = document.getElementById(targetId);
|
||||
const arrow = header.querySelector('svg');
|
||||
|
||||
if (content) {
|
||||
if (collapsibleStates[targetId] === 'open') {
|
||||
content.classList.remove('hidden');
|
||||
arrow.classList.add('rotate-180');
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
arrow.classList.remove('rotate-180');
|
||||
collapsibleStates[targetId] = 'closed';
|
||||
}
|
||||
}
|
||||
|
||||
header.addEventListener('click', () => toggleCollapsible(header));
|
||||
});
|
||||
|
||||
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
|
||||
},
|
||||
|
||||
setupConfigForm: function() {
|
||||
const configForm = document.querySelector('form[method="post"]');
|
||||
const saveConfigBtn = document.getElementById('save_config_btn');
|
||||
|
||||
if (configForm && saveConfigBtn) {
|
||||
configForm.addEventListener('submit', (e) => {
|
||||
if (e.submitter && e.submitter.name === 'save_config') {
|
||||
localStorage.setItem('amm_update_tables', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
if (localStorage.getItem('amm_update_tables') === 'true') {
|
||||
localStorage.removeItem('amm_update_tables');
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (window.ammTablesManager && window.ammTablesManager.updateTables) {
|
||||
window.ammTablesManager.updateTables();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setupCreateDefaultButton: function() {
|
||||
const createDefaultBtn = document.getElementById('create_default_btn');
|
||||
const configForm = document.querySelector('form[method="post"]');
|
||||
|
||||
if (createDefaultBtn && configForm) {
|
||||
createDefaultBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const title = 'Create Default Configuration';
|
||||
const message = 'This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?';
|
||||
|
||||
if (window.showConfirmModal) {
|
||||
window.showConfirmModal(title, message, () => {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'create_default';
|
||||
hiddenInput.value = 'true';
|
||||
configForm.appendChild(hiddenInput);
|
||||
|
||||
localStorage.setItem('amm_create_default_refresh', 'true');
|
||||
|
||||
configForm.submit();
|
||||
});
|
||||
} else {
|
||||
if (confirm('This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?')) {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'create_default';
|
||||
hiddenInput.value = 'true';
|
||||
configForm.appendChild(hiddenInput);
|
||||
|
||||
localStorage.setItem('amm_create_default_refresh', 'true');
|
||||
configForm.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleCreateDefaultRefresh: function() {
|
||||
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
|
||||
localStorage.removeItem('amm_create_default_refresh');
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.location.href = window.location.pathname + window.location.search;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
AMMConfigTabs.init();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('ammConfigTabs', AMMConfigTabs, (tabs) => {
|
||||
if (tabs.cleanup) tabs.cleanup();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.AMMConfigTabs = AMMConfigTabs;
|
||||
|
||||
})();
|
||||
255
basicswap/static/js/pages/amm-counter.js
Normal file
255
basicswap/static/js/pages/amm-counter.js
Normal file
@@ -0,0 +1,255 @@
|
||||
const AmmCounterManager = (function() {
|
||||
const config = {
|
||||
refreshInterval: 10000,
|
||||
ammStatusEndpoint: '/amm/status',
|
||||
retryDelay: 5000,
|
||||
maxRetries: 3,
|
||||
debug: false
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
let fetchRetryCount = 0;
|
||||
let lastAmmStatus = null;
|
||||
|
||||
function isDebugEnabled() {
|
||||
return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug;
|
||||
}
|
||||
|
||||
function debugLog(message, data) {
|
||||
|
||||
}
|
||||
|
||||
function updateAmmCounter(count, status) {
|
||||
const ammCounter = document.getElementById('amm-counter');
|
||||
const ammCounterMobile = document.getElementById('amm-counter-mobile');
|
||||
|
||||
debugLog(`Updating AMM counter: count=${count}, status=${status}`);
|
||||
|
||||
if (ammCounter) {
|
||||
ammCounter.textContent = count;
|
||||
ammCounter.classList.remove('bg-blue-500', 'bg-gray-400');
|
||||
ammCounter.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
||||
}
|
||||
|
||||
if (ammCounterMobile) {
|
||||
ammCounterMobile.textContent = count;
|
||||
ammCounterMobile.classList.remove('bg-blue-500', 'bg-gray-400');
|
||||
ammCounterMobile.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
||||
}
|
||||
|
||||
updateAmmTooltips(count, status);
|
||||
}
|
||||
|
||||
function updateAmmTooltips(count, status) {
|
||||
debugLog(`updateAmmTooltips called with count=${count}, status=${status}`);
|
||||
|
||||
const subheaderTooltip = document.getElementById('tooltip-amm-subheader');
|
||||
debugLog('Looking for tooltip-amm-subheader element:', subheaderTooltip);
|
||||
|
||||
if (subheaderTooltip) {
|
||||
const statusText = status === 'running' ? 'Active' : 'Inactive';
|
||||
|
||||
const newContent = `
|
||||
<p><b>Status:</b> ${statusText}</p>
|
||||
<p><b>Currently active offers/bids:</b> ${count}</p>
|
||||
`;
|
||||
|
||||
const statusParagraph = subheaderTooltip.querySelector('p:first-child');
|
||||
const countParagraph = subheaderTooltip.querySelector('p:last-child');
|
||||
|
||||
if (statusParagraph && countParagraph) {
|
||||
statusParagraph.innerHTML = `<b>Status:</b> ${statusText}`;
|
||||
countParagraph.innerHTML = `<b>Currently active offers/bids:</b> ${count}`;
|
||||
debugLog(`Updated AMM subheader tooltip paragraphs: status=${statusText}, count=${count}`);
|
||||
} else {
|
||||
subheaderTooltip.innerHTML = newContent;
|
||||
debugLog(`Replaced AMM subheader tooltip content: status=${statusText}, count=${count}`);
|
||||
}
|
||||
|
||||
refreshTooltipInstances('tooltip-amm-subheader', newContent);
|
||||
} else {
|
||||
debugLog('AMM subheader tooltip element not found - checking all tooltip elements');
|
||||
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
|
||||
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTooltipInstances(tooltipId, newContent) {
|
||||
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
|
||||
|
||||
triggers.forEach(trigger => {
|
||||
if (trigger._tippy) {
|
||||
trigger._tippy.setContent(newContent);
|
||||
debugLog(`Updated Tippy instance content for ${tooltipId}`);
|
||||
} else {
|
||||
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
||||
window.TooltipManager.create(trigger, newContent, {
|
||||
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
debugLog(`Created new Tippy instance for ${tooltipId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
|
||||
window.TooltipManager.refreshTooltip(tooltipId, newContent);
|
||||
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAmmStatus() {
|
||||
debugLog('Fetching AMM status...');
|
||||
|
||||
let url = config.ammStatusEndpoint;
|
||||
if (isDebugEnabled()) {
|
||||
url += '?debug=true';
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
lastAmmStatus = data;
|
||||
debugLog('AMM status data received:', data);
|
||||
updateAmmCounter(data.amm_active_count, data.status);
|
||||
fetchRetryCount = 0;
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
if (isDebugEnabled()) {
|
||||
console.error('[AmmCounter] AMM status fetch error:', error);
|
||||
}
|
||||
|
||||
if (fetchRetryCount < config.maxRetries) {
|
||||
fetchRetryCount++;
|
||||
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
||||
|
||||
return new Promise(resolve => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
resolve(fetchAmmStatus());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
} else {
|
||||
fetchRetryCount = 0;
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startRefreshTimer() {
|
||||
stopRefreshTimer();
|
||||
|
||||
debugLog('Starting AMM status refresh timer');
|
||||
|
||||
fetchAmmStatus()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = CleanupManager.setInterval(() => {
|
||||
fetchAmmStatus()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}, config.refreshInterval);
|
||||
}
|
||||
|
||||
function stopRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
debugLog('Stopping AMM status refresh timer');
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setupWebSocketHandler() {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
|
||||
debugLog('Setting up WebSocket handler for AMM status updates');
|
||||
window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
debugLog('WebSocket event received, refreshing AMM status');
|
||||
fetchAmmStatus()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupDebugListener() {
|
||||
const debugCheckbox = document.getElementById('amm_debug');
|
||||
if (debugCheckbox) {
|
||||
debugLog('Found AMM debug checkbox, setting up listener');
|
||||
|
||||
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked ? 'true' : 'false');
|
||||
|
||||
debugCheckbox.addEventListener('change', function() {
|
||||
localStorage.setItem('amm_debug_enabled', this.checked ? 'true' : 'false');
|
||||
debugLog(`Debug mode ${this.checked ? 'enabled' : 'disabled'}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
setupWebSocketHandler();
|
||||
setupDebugListener();
|
||||
startRefreshTimer();
|
||||
|
||||
debugLog('AMM Counter Manager initialized');
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('ammCounterManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchAmmStatus: fetchAmmStatus,
|
||||
|
||||
updateCounter: updateAmmCounter,
|
||||
|
||||
updateTooltips: updateAmmTooltips,
|
||||
|
||||
startRefreshTimer: startRefreshTimer,
|
||||
|
||||
stopRefreshTimer: stopRefreshTimer,
|
||||
|
||||
dispose: function() {
|
||||
debugLog('Disposing AMM Counter Manager');
|
||||
stopRefreshTimer();
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.ammCounterManagerInitialized) {
|
||||
window.AmmCounterManager = AmmCounterManager.initialize();
|
||||
window.ammCounterManagerInitialized = true;
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('ammCounter', window.AmmCounterManager, (mgr) => {
|
||||
if (mgr && mgr.dispose) mgr.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
573
basicswap/static/js/pages/amm-page.js
Normal file
573
basicswap/static/js/pages/amm-page.js
Normal file
@@ -0,0 +1,573 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const AMMPage = {
|
||||
|
||||
init: function() {
|
||||
this.loadDebugSetting();
|
||||
this.setupAutostartCheckbox();
|
||||
this.setupStartupValidation();
|
||||
this.setupDebugCheckbox();
|
||||
this.setupModals();
|
||||
this.setupClearStateButton();
|
||||
this.setupWebSocketBalanceUpdates();
|
||||
this.setupCleanup();
|
||||
},
|
||||
|
||||
saveDebugSetting: function() {
|
||||
const debugCheckbox = document.getElementById('debug-mode');
|
||||
if (debugCheckbox) {
|
||||
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked);
|
||||
}
|
||||
},
|
||||
|
||||
loadDebugSetting: function() {
|
||||
const debugCheckbox = document.getElementById('debug-mode');
|
||||
if (debugCheckbox) {
|
||||
const savedSetting = localStorage.getItem('amm_debug_enabled');
|
||||
if (savedSetting !== null) {
|
||||
debugCheckbox.checked = savedSetting === 'true';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setupDebugCheckbox: function() {
|
||||
const debugCheckbox = document.getElementById('debug-mode');
|
||||
if (debugCheckbox) {
|
||||
debugCheckbox.addEventListener('change', this.saveDebugSetting.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
saveAutostartSetting: function(checked) {
|
||||
const bodyData = `autostart=${checked ? 'true' : 'false'}`;
|
||||
|
||||
fetch('/amm/autostart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: bodyData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
localStorage.setItem('amm_autostart_enabled', checked);
|
||||
|
||||
if (data.autostart !== checked) {
|
||||
console.warn('WARNING: API returned different autostart value than expected!', {
|
||||
sent: checked,
|
||||
received: data.autostart
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to save autostart setting:', data.error);
|
||||
const autostartCheckbox = document.getElementById('autostart-amm');
|
||||
if (autostartCheckbox) {
|
||||
autostartCheckbox.checked = !checked;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving autostart setting:', error);
|
||||
const autostartCheckbox = document.getElementById('autostart-amm');
|
||||
if (autostartCheckbox) {
|
||||
autostartCheckbox.checked = !checked;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupAutostartCheckbox: function() {
|
||||
const autostartCheckbox = document.getElementById('autostart-amm');
|
||||
if (autostartCheckbox) {
|
||||
autostartCheckbox.addEventListener('change', () => {
|
||||
this.saveAutostartSetting(autostartCheckbox.checked);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showErrorModal: function(title, message) {
|
||||
document.getElementById('errorTitle').textContent = title || 'Error';
|
||||
document.getElementById('errorMessage').textContent = message || 'An error occurred';
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
hideErrorModal: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmModal: function(title, message, callback) {
|
||||
document.getElementById('confirmTitle').textContent = title || 'Confirm Action';
|
||||
document.getElementById('confirmMessage').textContent = message || 'Are you sure?';
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
window.confirmCallback = callback;
|
||||
},
|
||||
|
||||
hideConfirmModal: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
window.confirmCallback = null;
|
||||
},
|
||||
|
||||
setupModals: function() {
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
|
||||
}
|
||||
|
||||
const errorModal = document.getElementById('errorModal');
|
||||
if (errorModal) {
|
||||
errorModal.addEventListener('click', (e) => {
|
||||
if (e.target === errorModal) {
|
||||
this.hideErrorModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
if (confirmYesBtn) {
|
||||
confirmYesBtn.addEventListener('click', () => {
|
||||
if (window.confirmCallback && typeof window.confirmCallback === 'function') {
|
||||
window.confirmCallback();
|
||||
}
|
||||
this.hideConfirmModal();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
if (confirmNoBtn) {
|
||||
confirmNoBtn.addEventListener('click', this.hideConfirmModal.bind(this));
|
||||
}
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
if (confirmModal) {
|
||||
confirmModal.addEventListener('click', (e) => {
|
||||
if (e.target === confirmModal) {
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupStartupValidation: function() {
|
||||
const controlForm = document.querySelector('form[method="post"]');
|
||||
if (!controlForm) return;
|
||||
|
||||
const startButton = controlForm.querySelector('input[name="start"]');
|
||||
if (!startButton) return;
|
||||
|
||||
startButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.performStartupValidation();
|
||||
});
|
||||
},
|
||||
|
||||
performStartupValidation: function() {
|
||||
const feedbackDiv = document.getElementById('startup-feedback');
|
||||
const titleEl = document.getElementById('startup-title');
|
||||
const messageEl = document.getElementById('startup-message');
|
||||
const progressBar = document.getElementById('startup-progress-bar');
|
||||
|
||||
feedbackDiv.classList.remove('hidden');
|
||||
|
||||
const steps = [
|
||||
{ message: 'Checking configuration...', progress: 20 },
|
||||
{ message: 'Validating offers and bids...', progress: 40 },
|
||||
{ message: 'Checking wallet balances...', progress: 60 },
|
||||
{ message: 'Verifying API connection...', progress: 80 },
|
||||
{ message: 'Starting AMM process...', progress: 100 }
|
||||
];
|
||||
|
||||
let currentStep = 0;
|
||||
|
||||
const runNextStep = () => {
|
||||
if (currentStep >= steps.length) {
|
||||
this.submitStartForm();
|
||||
return;
|
||||
}
|
||||
|
||||
const step = steps[currentStep];
|
||||
messageEl.textContent = step.message;
|
||||
progressBar.style.width = step.progress + '%';
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
this.validateStep(currentStep).then(result => {
|
||||
if (result.success) {
|
||||
currentStep++;
|
||||
runNextStep();
|
||||
} else {
|
||||
this.showStartupError(result.error);
|
||||
}
|
||||
}).catch(error => {
|
||||
this.showStartupError('Validation failed: ' + error.message);
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
runNextStep();
|
||||
},
|
||||
|
||||
validateStep: async function(stepIndex) {
|
||||
try {
|
||||
switch (stepIndex) {
|
||||
case 0:
|
||||
return await this.validateConfiguration();
|
||||
case 1:
|
||||
return await this.validateOffersAndBids();
|
||||
case 2:
|
||||
return await this.validateWalletBalances();
|
||||
case 3:
|
||||
return await this.validateApiConnection();
|
||||
case 4:
|
||||
return { success: true };
|
||||
default:
|
||||
return { success: true };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
validateConfiguration: async function() {
|
||||
const configData = window.ammTablesConfig?.configData;
|
||||
if (!configData) {
|
||||
return { success: false, error: 'No configuration found. Please save a configuration first.' };
|
||||
}
|
||||
|
||||
if (!configData.min_seconds_between_offers || !configData.max_seconds_between_offers) {
|
||||
return { success: false, error: 'Missing timing configuration. Please check your settings.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
validateOffersAndBids: async function() {
|
||||
const configData = window.ammTablesConfig?.configData;
|
||||
if (!configData) {
|
||||
return { success: false, error: 'Configuration not available for validation.' };
|
||||
}
|
||||
|
||||
const offers = configData.offers || [];
|
||||
const bids = configData.bids || [];
|
||||
const enabledOffers = offers.filter(o => o.enabled);
|
||||
const enabledBids = bids.filter(b => b.enabled);
|
||||
|
||||
if (enabledOffers.length === 0 && enabledBids.length === 0) {
|
||||
return { success: false, error: 'No enabled offers or bids found. Please enable at least one offer or bid before starting.' };
|
||||
}
|
||||
|
||||
for (const offer of enabledOffers) {
|
||||
if (!offer.amount_step) {
|
||||
return { success: false, error: `Offer "${offer.name}" is missing required Amount Step (privacy feature).` };
|
||||
}
|
||||
|
||||
const amountStep = parseFloat(offer.amount_step);
|
||||
const amount = parseFloat(offer.amount);
|
||||
|
||||
if (amountStep <= 0 || amountStep < 0.001) {
|
||||
return { success: false, error: `Offer "${offer.name}" has invalid Amount Step. Must be >= 0.001.` };
|
||||
}
|
||||
|
||||
if (amountStep > amount) {
|
||||
return { success: false, error: `Offer "${offer.name}" Amount Step (${amountStep}) cannot be greater than offer amount (${amount}).` };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
validateWalletBalances: async function() {
|
||||
const configData = window.ammTablesConfig?.configData;
|
||||
if (!configData) return { success: true };
|
||||
|
||||
const offers = configData.offers || [];
|
||||
const enabledOffers = offers.filter(o => o.enabled);
|
||||
|
||||
for (const offer of enabledOffers) {
|
||||
if (!offer.min_coin_from_amt || parseFloat(offer.min_coin_from_amt) <= 0) {
|
||||
return { success: false, error: `Offer "${offer.name}" needs a minimum coin amount to protect your wallet balance.` };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
validateApiConnection: async function() {
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
showStartupError: function(errorMessage) {
|
||||
const feedbackDiv = document.getElementById('startup-feedback');
|
||||
feedbackDiv.classList.add('hidden');
|
||||
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('AMM Startup Failed', errorMessage);
|
||||
} else {
|
||||
alert('AMM Startup Failed: ' + errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
submitStartForm: function() {
|
||||
const feedbackDiv = document.getElementById('startup-feedback');
|
||||
const titleEl = document.getElementById('startup-title');
|
||||
const messageEl = document.getElementById('startup-message');
|
||||
|
||||
titleEl.textContent = 'Starting AMM...';
|
||||
messageEl.textContent = 'AMM process is starting. Please wait...';
|
||||
|
||||
const controlForm = document.querySelector('form[method="post"]');
|
||||
if (controlForm) {
|
||||
const formData = new FormData(controlForm);
|
||||
formData.append('start', 'Start');
|
||||
|
||||
fetch(window.location.pathname, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
throw new Error('Failed to start AMM');
|
||||
}
|
||||
}).catch(error => {
|
||||
this.showStartupError('Failed to start AMM: ' + error.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupClearStateButton: function() {
|
||||
const clearStateBtn = document.getElementById('clearStateBtn');
|
||||
if (clearStateBtn) {
|
||||
clearStateBtn.addEventListener('click', () => {
|
||||
this.showConfirmModal(
|
||||
'Clear AMM State',
|
||||
'This will clear the AMM state file. All running offers/bids will be lost. Are you sure?',
|
||||
() => {
|
||||
const form = clearStateBtn.closest('form');
|
||||
if (form) {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'prune_state';
|
||||
hiddenInput.value = 'true';
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setAmmAmount: function(percent, fieldId) {
|
||||
const amountInput = document.getElementById(fieldId);
|
||||
let coinSelect;
|
||||
|
||||
let modalType = null;
|
||||
if (fieldId.includes('add-amm')) {
|
||||
const addModal = document.getElementById('add-amm-modal');
|
||||
modalType = addModal ? addModal.getAttribute('data-amm-type') : null;
|
||||
} else if (fieldId.includes('edit-amm')) {
|
||||
const editModal = document.getElementById('edit-amm-modal');
|
||||
modalType = editModal ? editModal.getAttribute('data-amm-type') : null;
|
||||
}
|
||||
|
||||
if (fieldId.includes('add-amm')) {
|
||||
const isBidModal = modalType === 'bid';
|
||||
coinSelect = document.getElementById(isBidModal ? 'add-amm-coin-to' : 'add-amm-coin-from');
|
||||
} else if (fieldId.includes('edit-amm')) {
|
||||
const isBidModal = modalType === 'bid';
|
||||
coinSelect = document.getElementById(isBidModal ? 'edit-amm-coin-to' : 'edit-amm-coin-from');
|
||||
}
|
||||
|
||||
if (!amountInput || !coinSelect) {
|
||||
console.error('Required elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = coinSelect.options[coinSelect.selectedIndex];
|
||||
if (!selectedOption) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Please select a coin first');
|
||||
} else {
|
||||
alert('Please select a coin first');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const balance = selectedOption.getAttribute('data-balance');
|
||||
if (!balance) {
|
||||
console.error('Balance not found for selected coin');
|
||||
return;
|
||||
}
|
||||
|
||||
const floatBalance = parseFloat(balance);
|
||||
if (isNaN(floatBalance) || floatBalance <= 0) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
|
||||
} else {
|
||||
alert('Invalid balance for selected coin');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
const event = new Event('input', { bubbles: true });
|
||||
amountInput.dispatchEvent(event);
|
||||
},
|
||||
|
||||
updateAmmModalBalances: function(balanceData) {
|
||||
const addModal = document.getElementById('add-amm-modal');
|
||||
const editModal = document.getElementById('edit-amm-modal');
|
||||
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
||||
const editModalVisible = editModal && !editModal.classList.contains('hidden');
|
||||
|
||||
let modalType = null;
|
||||
if (addModalVisible) {
|
||||
modalType = addModal.getAttribute('data-amm-type');
|
||||
} else if (editModalVisible) {
|
||||
modalType = editModal.getAttribute('data-amm-type');
|
||||
}
|
||||
|
||||
if (modalType === 'offer') {
|
||||
this.updateOfferDropdownBalances(balanceData);
|
||||
} else if (modalType === 'bid') {
|
||||
this.updateBidDropdownBalances(balanceData);
|
||||
}
|
||||
},
|
||||
|
||||
setupWebSocketBalanceUpdates: function() {
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'amm',
|
||||
balanceUpdateCallback: this.updateAmmModalBalances.bind(this),
|
||||
swapEventCallback: this.updateAmmModalBalances.bind(this),
|
||||
errorContext: 'AMM',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 120000
|
||||
});
|
||||
},
|
||||
|
||||
updateAmmDropdownBalances: function(balanceData) {
|
||||
const balanceMap = {};
|
||||
const pendingMap = {};
|
||||
balanceData.forEach(coin => {
|
||||
balanceMap[coin.name] = coin.balance;
|
||||
pendingMap[coin.name] = coin.pending || '0.0';
|
||||
});
|
||||
|
||||
const dropdownIds = ['add-amm-coin-from', 'edit-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-to'];
|
||||
|
||||
dropdownIds.forEach(dropdownId => {
|
||||
const select = document.getElementById(dropdownId);
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
const coinName = option.value;
|
||||
const balance = balanceMap[coinName] || '0.0';
|
||||
const pending = pendingMap[coinName] || '0.0';
|
||||
|
||||
option.setAttribute('data-balance', balance);
|
||||
option.setAttribute('data-pending-balance', pending);
|
||||
});
|
||||
});
|
||||
|
||||
const addModal = document.getElementById('add-amm-modal');
|
||||
const editModal = document.getElementById('edit-amm-modal');
|
||||
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
||||
const editModalVisible = editModal && !editModal.classList.contains('hidden');
|
||||
|
||||
let currentModalType = null;
|
||||
if (addModalVisible) {
|
||||
currentModalType = addModal.getAttribute('data-amm-type');
|
||||
} else if (editModalVisible) {
|
||||
currentModalType = editModal.getAttribute('data-amm-type');
|
||||
}
|
||||
|
||||
if (currentModalType && window.ammTablesManager) {
|
||||
if (currentModalType === 'offer' && typeof window.ammTablesManager.refreshOfferDropdownBalanceDisplay === 'function') {
|
||||
window.ammTablesManager.refreshOfferDropdownBalanceDisplay();
|
||||
} else if (currentModalType === 'bid' && typeof window.ammTablesManager.refreshBidDropdownBalanceDisplay === 'function') {
|
||||
window.ammTablesManager.refreshBidDropdownBalanceDisplay();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateOfferDropdownBalances: function(balanceData) {
|
||||
this.updateAmmDropdownBalances(balanceData);
|
||||
},
|
||||
|
||||
updateBidDropdownBalances: function(balanceData) {
|
||||
this.updateAmmDropdownBalances(balanceData);
|
||||
},
|
||||
|
||||
cleanupAmmBalanceUpdates: function() {
|
||||
window.BalanceUpdatesManager.cleanup('amm');
|
||||
|
||||
if (window.ammDropdowns) {
|
||||
window.ammDropdowns.forEach(dropdown => {
|
||||
if (dropdown.parentNode) {
|
||||
dropdown.parentNode.removeChild(dropdown);
|
||||
}
|
||||
});
|
||||
window.ammDropdowns = [];
|
||||
}
|
||||
},
|
||||
|
||||
setupCleanup: function() {
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('ammBalanceUpdates', null, this.cleanupAmmBalanceUpdates.bind(this));
|
||||
}
|
||||
|
||||
const beforeUnloadHandler = this.cleanupAmmBalanceUpdates.bind(this);
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('ammBeforeUnload', beforeUnloadHandler, () => {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
const debugCheckbox = document.getElementById('amm_debug');
|
||||
const autostartCheckbox = document.getElementById('amm_autostart');
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
const startButton = document.getElementById('startAMM');
|
||||
const clearStateBtn = document.getElementById('clearAmmState');
|
||||
|
||||
this.cleanupAmmBalanceUpdates();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
AMMPage.init();
|
||||
|
||||
if (window.BalanceUpdatesManager) {
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
}
|
||||
});
|
||||
|
||||
window.AMMPage = AMMPage;
|
||||
window.showErrorModal = AMMPage.showErrorModal.bind(AMMPage);
|
||||
window.hideErrorModal = AMMPage.hideErrorModal.bind(AMMPage);
|
||||
window.showConfirmModal = AMMPage.showConfirmModal.bind(AMMPage);
|
||||
window.hideConfirmModal = AMMPage.hideConfirmModal.bind(AMMPage);
|
||||
window.setAmmAmount = AMMPage.setAmmAmount.bind(AMMPage);
|
||||
|
||||
})();
|
||||
2667
basicswap/static/js/pages/amm-tables.js
Normal file
2667
basicswap/static/js/pages/amm-tables.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -53,9 +53,9 @@ const getTimeStrokeColor = (expireTime) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeLeft = expireTime - now;
|
||||
|
||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||
return '#10B981'; // More than 30 minutes
|
||||
if (timeLeft <= 300) return '#9CA3AF';
|
||||
if (timeLeft <= 1800) return '#3B82F6';
|
||||
return '#10B981';
|
||||
};
|
||||
|
||||
const createTimeTooltip = (bid) => {
|
||||
@@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => {
|
||||
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
|
||||
|
||||
if (refreshIcon) {
|
||||
// Add CSS transition for smoother animation
|
||||
|
||||
refreshIcon.style.transition = 'transform 0.3s ease';
|
||||
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||
}
|
||||
@@ -352,7 +352,7 @@ const createBidTableRow = async (bid) => {
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12"
|
||||
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
|
||||
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_from)}"
|
||||
alt="${bid.coin_from}"
|
||||
onerror="this.src='/static/images/coins/default.png'">
|
||||
</span>
|
||||
@@ -361,7 +361,7 @@ const createBidTableRow = async (bid) => {
|
||||
</svg>
|
||||
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12"
|
||||
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
|
||||
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_to)}"
|
||||
alt="${bid.coin_to}"
|
||||
onerror="this.src='/static/images/coins/default.png'">
|
||||
</span>
|
||||
@@ -631,7 +631,7 @@ if (elements.refreshBidsButton) {
|
||||
|
||||
updateLoadingState(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 500));
|
||||
|
||||
try {
|
||||
await updateBidsTable({ resetPage: true, refreshData: true });
|
||||
@@ -4,17 +4,18 @@ const BidExporter = {
|
||||
return 'No data to export';
|
||||
}
|
||||
|
||||
const isSent = type === 'sent';
|
||||
const isAllTab = type === 'all';
|
||||
|
||||
const headers = [
|
||||
'Date/Time',
|
||||
'Bid ID',
|
||||
'Offer ID',
|
||||
'From Address',
|
||||
isSent ? 'You Send Amount' : 'You Receive Amount',
|
||||
isSent ? 'You Send Coin' : 'You Receive Coin',
|
||||
isSent ? 'You Receive Amount' : 'You Send Amount',
|
||||
isSent ? 'You Receive Coin' : 'You Send Coin',
|
||||
...(isAllTab ? ['Type'] : []),
|
||||
'You Send Amount',
|
||||
'You Send Coin',
|
||||
'You Receive Amount',
|
||||
'You Receive Coin',
|
||||
'Status',
|
||||
'Created At',
|
||||
'Expires At'
|
||||
@@ -23,11 +24,13 @@ const BidExporter = {
|
||||
let csvContent = headers.join(',') + '\n';
|
||||
|
||||
bids.forEach(bid => {
|
||||
const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent');
|
||||
const row = [
|
||||
`"${formatTime(bid.created_at)}"`,
|
||||
`"${bid.bid_id}"`,
|
||||
`"${bid.offer_id}"`,
|
||||
`"${bid.addr_from}"`,
|
||||
...(isAllTab ? [`"${bid.source}"`] : []),
|
||||
isSent ? bid.amount_from : bid.amount_to,
|
||||
`"${isSent ? bid.coin_from : bid.coin_to}"`,
|
||||
isSent ? bid.amount_to : bid.amount_from,
|
||||
@@ -63,7 +66,7 @@ const BidExporter = {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
@@ -101,8 +104,17 @@ const BidExporter = {
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(function() {
|
||||
CleanupManager.setTimeout(function() {
|
||||
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
|
||||
const exportAllButton = document.getElementById('exportAllBids');
|
||||
if (exportAllButton) {
|
||||
EventManager.add(exportAllButton, 'click', (e) => {
|
||||
e.preventDefault();
|
||||
state.currentTab = 'all';
|
||||
BidExporter.exportCurrentView();
|
||||
});
|
||||
}
|
||||
|
||||
const exportSentButton = document.getElementById('exportSentBids');
|
||||
if (exportSentButton) {
|
||||
EventManager.add(exportSentButton, 'click', (e) => {
|
||||
@@ -128,9 +140,14 @@ const originalCleanup = window.cleanup || function(){};
|
||||
window.cleanup = function() {
|
||||
originalCleanup();
|
||||
|
||||
const exportAllButton = document.getElementById('exportAllBids');
|
||||
const exportSentButton = document.getElementById('exportSentBids');
|
||||
const exportReceivedButton = document.getElementById('exportReceivedBids');
|
||||
|
||||
if (exportAllButton && typeof EventManager !== 'undefined') {
|
||||
EventManager.remove(exportAllButton, 'click');
|
||||
}
|
||||
|
||||
if (exportSentButton && typeof EventManager !== 'undefined') {
|
||||
EventManager.remove(exportSentButton, 'click');
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
216
basicswap/static/js/pages/bids-tab-navigation.js
Normal file
216
basicswap/static/js/pages/bids-tab-navigation.js
Normal file
@@ -0,0 +1,216 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
const originalOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (typeof originalOnload === 'function') {
|
||||
originalOnload();
|
||||
}
|
||||
|
||||
CleanupManager.setTimeout(function() {
|
||||
initBidsTabNavigation();
|
||||
handleInitialNavigation();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initBidsTabNavigation();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
window.bidsTabNavigationInitialized = false;
|
||||
|
||||
function initBidsTabNavigation() {
|
||||
if (window.bidsTabNavigationInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.bids-tab-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const targetTabId = this.getAttribute('data-tab-target');
|
||||
if (targetTabId) {
|
||||
if (window.location.pathname === '/bids') {
|
||||
navigateToTabDirectly(targetTabId);
|
||||
} else {
|
||||
localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', ''));
|
||||
window.location.href = '/bids';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.bidsTabNavigationInitialized = true;
|
||||
|
||||
}
|
||||
|
||||
function handleInitialNavigation() {
|
||||
if (window.location.pathname !== '/bids') {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabToActivate = localStorage.getItem('bidsTabToActivate');
|
||||
|
||||
if (tabToActivate) {
|
||||
|
||||
localStorage.removeItem('bidsTabToActivate');
|
||||
activateTabWithRetry('#' + tabToActivate);
|
||||
} else if (window.location.hash) {
|
||||
|
||||
activateTabWithRetry(window.location.hash);
|
||||
} else {
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
|
||||
function handleHashChange() {
|
||||
if (window.location.pathname !== '/bids') {
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
|
||||
activateTabWithRetry(hash);
|
||||
} else {
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
|
||||
function activateTabWithRetry(tabId, retryCount = 0) {
|
||||
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
|
||||
|
||||
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
return;
|
||||
}
|
||||
|
||||
const tabButtonId = normalizedTabId === '#all' ? 'all-tab' :
|
||||
(normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab');
|
||||
const tabButton = document.getElementById(tabButtonId);
|
||||
|
||||
if (!tabButton) {
|
||||
if (retryCount < 5) {
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
activateTabWithRetry(normalizedTabId, retryCount + 1);
|
||||
}, 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tabButton.click();
|
||||
|
||||
if (window.Tabs) {
|
||||
const tabsEl = document.querySelector('[data-tabs-toggle="#bidstab"]');
|
||||
if (tabsEl) {
|
||||
const allTabs = Array.from(tabsEl.querySelectorAll('[role="tab"]'));
|
||||
const targetTab = allTabs.find(tab => tab.getAttribute('data-tabs-target') === normalizedTabId);
|
||||
|
||||
if (targetTab) {
|
||||
|
||||
allTabs.forEach(tab => {
|
||||
tab.setAttribute('aria-selected', tab === targetTab ? 'true' : 'false');
|
||||
|
||||
if (tab === targetTab) {
|
||||
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
||||
} else {
|
||||
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
||||
}
|
||||
});
|
||||
|
||||
const allContent = document.getElementById('all');
|
||||
const sentContent = document.getElementById('sent');
|
||||
const receivedContent = document.getElementById('received');
|
||||
|
||||
if (allContent && sentContent && receivedContent) {
|
||||
allContent.classList.toggle('hidden', normalizedTabId !== '#all');
|
||||
sentContent.classList.toggle('hidden', normalizedTabId !== '#sent');
|
||||
receivedContent.classList.toggle('hidden', normalizedTabId !== '#received');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allPanel = document.getElementById('all');
|
||||
const sentPanel = document.getElementById('sent');
|
||||
const receivedPanel = document.getElementById('received');
|
||||
|
||||
if (allPanel && sentPanel && receivedPanel) {
|
||||
allPanel.classList.toggle('hidden', normalizedTabId !== '#all');
|
||||
sentPanel.classList.toggle('hidden', normalizedTabId !== '#sent');
|
||||
receivedPanel.classList.toggle('hidden', normalizedTabId !== '#received');
|
||||
}
|
||||
|
||||
const newHash = normalizedTabId.replace('#', '');
|
||||
if (window.location.hash !== '#' + newHash) {
|
||||
history.replaceState(null, null, '#' + newHash);
|
||||
}
|
||||
|
||||
triggerDataLoad(normalizedTabId);
|
||||
}
|
||||
|
||||
function triggerDataLoad(tabId) {
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (window.state) {
|
||||
window.state.currentTab = tabId === '#all' ? 'all' :
|
||||
(tabId === '#sent' ? 'sent' : 'received');
|
||||
|
||||
if (typeof window.updateBidsTable === 'function') {
|
||||
|
||||
window.updateBidsTable();
|
||||
}
|
||||
}
|
||||
|
||||
const event = new CustomEvent('tabactivated', {
|
||||
detail: {
|
||||
tabId: tabId,
|
||||
type: tabId === '#all' ? 'all' :
|
||||
(tabId === '#sent' ? 'sent' : 'received')
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.cleanup();
|
||||
if (typeof window.initializeTooltips === 'function') {
|
||||
window.initializeTooltips();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function navigateToTabDirectly(tabId) {
|
||||
const oldScrollPosition = window.scrollY;
|
||||
|
||||
activateTabWithRetry(tabId);
|
||||
|
||||
CleanupManager.setTimeout(function() {
|
||||
window.scrollTo(0, oldScrollPosition);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
window.navigateToBidsTab = function(tabId) {
|
||||
if (window.location.pathname === '/bids') {
|
||||
navigateToTabDirectly('#' + tabId);
|
||||
} else {
|
||||
localStorage.setItem('bidsTabToActivate', tabId);
|
||||
window.location.href = '/bids';
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -16,6 +16,30 @@ const DOM = {
|
||||
queryAll: (selector) => document.querySelectorAll(selector)
|
||||
};
|
||||
|
||||
const ErrorModal = {
|
||||
show: function(title, message) {
|
||||
const errorTitle = document.getElementById('errorTitle');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const modal = document.getElementById('errorModal');
|
||||
|
||||
if (errorTitle) errorTitle.textContent = title || 'Error';
|
||||
if (errorMessage) errorMessage.textContent = message || 'An error occurred';
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
},
|
||||
|
||||
init: function() {
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hide.bind(this));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Storage = {
|
||||
get: (key) => {
|
||||
try {
|
||||
@@ -329,7 +353,9 @@ const SwapTypeManager = {
|
||||
} else {
|
||||
swapTypeElement.disabled = false;
|
||||
swapTypeElement.classList.remove('select-disabled');
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
if (['xmr_swap', 'seller_first'].includes(swapTypeElement.value) == false) {
|
||||
swapTypeElement.value = 'xmr_swap';
|
||||
}
|
||||
}
|
||||
|
||||
let swapTypeHidden = DOM.get('swap_type_hidden');
|
||||
@@ -353,10 +379,6 @@ const SwapTypeManager = {
|
||||
}
|
||||
};
|
||||
|
||||
function set_swap_type_enabled(coinFrom, coinTo, swapTypeElement) {
|
||||
SwapTypeManager.setSwapTypeEnabled(coinFrom, coinTo, swapTypeElement);
|
||||
}
|
||||
|
||||
const UIEnhancer = {
|
||||
handleErrorHighlighting: () => {
|
||||
const errMsgs = document.querySelectorAll('p.error_msg');
|
||||
@@ -445,7 +467,40 @@ const UIEnhancer = {
|
||||
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
|
||||
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
|
||||
|
||||
const parts = name.split(' - Balance: ');
|
||||
const coinName = parts[0];
|
||||
const balanceInfo = parts[1] || '';
|
||||
|
||||
selectNameElement.innerHTML = '';
|
||||
selectNameElement.style.display = 'flex';
|
||||
selectNameElement.style.flexDirection = 'column';
|
||||
selectNameElement.style.alignItems = 'flex-start';
|
||||
selectNameElement.style.lineHeight = '1.2';
|
||||
|
||||
const coinNameDiv = document.createElement('div');
|
||||
coinNameDiv.textContent = coinName;
|
||||
coinNameDiv.style.fontWeight = 'normal';
|
||||
coinNameDiv.style.color = 'inherit';
|
||||
|
||||
const balanceDiv = document.createElement('div');
|
||||
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
||||
balanceDiv.style.fontSize = '0.75rem';
|
||||
balanceDiv.style.color = '#6b7280';
|
||||
balanceDiv.style.marginTop = '1px';
|
||||
|
||||
selectNameElement.appendChild(coinNameDiv);
|
||||
selectNameElement.appendChild(balanceDiv);
|
||||
|
||||
} else {
|
||||
|
||||
selectNameElement.textContent = name;
|
||||
selectNameElement.style.display = 'block';
|
||||
selectNameElement.style.flexDirection = '';
|
||||
selectNameElement.style.alignItems = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
@@ -539,6 +594,8 @@ function initializeApp() {
|
||||
UIEnhancer.handleErrorHighlighting();
|
||||
UIEnhancer.updateDisabledStyles();
|
||||
UIEnhancer.setupCustomSelects();
|
||||
|
||||
ErrorModal.init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -546,3 +603,6 @@ if (document.readyState === 'loading') {
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
||||
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
|
||||
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);
|
||||
364
basicswap/static/js/pages/offer-page.js
Normal file
364
basicswap/static/js/pages/offer-page.js
Normal file
@@ -0,0 +1,364 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const OfferPage = {
|
||||
xhr_rates: null,
|
||||
xhr_bid_params: null,
|
||||
|
||||
init: function() {
|
||||
this.xhr_rates = new XMLHttpRequest();
|
||||
this.xhr_bid_params = new XMLHttpRequest();
|
||||
|
||||
this.setupXHRHandlers();
|
||||
this.setupEventListeners();
|
||||
this.handleBidsPageAddress();
|
||||
},
|
||||
|
||||
setupXHRHandlers: function() {
|
||||
this.xhr_rates.onload = () => {
|
||||
if (this.xhr_rates.status == 200) {
|
||||
const obj = JSON.parse(this.xhr_rates.response);
|
||||
const inner_html = '<h4 class="bold">Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
|
||||
const ratesDisplay = document.getElementById('rates_display');
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = inner_html;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.xhr_bid_params.onload = () => {
|
||||
if (this.xhr_bid_params.status == 200) {
|
||||
const obj = JSON.parse(this.xhr_bid_params.response);
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
if (bidAmountSendInput) {
|
||||
bidAmountSendInput.value = obj['amount_to'];
|
||||
}
|
||||
this.updateModalValues();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
setupEventListeners: function() {
|
||||
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
|
||||
if (sendBidBtn) {
|
||||
sendBidBtn.onclick = this.showConfirmModal.bind(this);
|
||||
}
|
||||
|
||||
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
|
||||
}
|
||||
|
||||
const mainCancelBtn = document.querySelector('button[name="cancel"]');
|
||||
if (mainCancelBtn) {
|
||||
mainCancelBtn.onclick = this.handleCancelClick.bind(this);
|
||||
}
|
||||
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
if (validMinsInput) {
|
||||
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
|
||||
}
|
||||
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
if (addrFromSelect) {
|
||||
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
|
||||
}
|
||||
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
lookup_rates: function() {
|
||||
const coin_from = document.getElementById('coin_from')?.value;
|
||||
const coin_to = document.getElementById('coin_to')?.value;
|
||||
|
||||
if (!coin_from || !coin_to || coin_from === '-1' || coin_to === '-1') {
|
||||
alert('Coins from and to must be set first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ratesDisplay = document.getElementById('rates_display');
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = '<h4>Rates</h4><p>Updating...</p>';
|
||||
}
|
||||
|
||||
this.xhr_rates.open('POST', '/json/rates');
|
||||
this.xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
this.xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`);
|
||||
},
|
||||
|
||||
resetForm: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
const amtVar = document.getElementById('amt_var')?.value === 'True';
|
||||
|
||||
if (bidAmountSendInput) {
|
||||
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
|
||||
}
|
||||
if (bidAmountInput) {
|
||||
bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max');
|
||||
}
|
||||
if (bidRateInput && !bidRateInput.disabled) {
|
||||
const defaultRate = document.getElementById('offer_rate')?.value || '';
|
||||
bidRateInput.value = defaultRate;
|
||||
}
|
||||
if (validMinsInput) {
|
||||
validMinsInput.value = "60";
|
||||
}
|
||||
if (!amtVar) {
|
||||
this.updateBidParams('rate');
|
||||
}
|
||||
this.updateModalValues();
|
||||
|
||||
const errorMessages = document.querySelectorAll('.error-message');
|
||||
errorMessages.forEach(msg => msg.remove());
|
||||
|
||||
const inputs = document.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.classList.remove('border-red-500', 'focus:border-red-500');
|
||||
});
|
||||
},
|
||||
|
||||
roundUpToDecimals: function(value, decimals) {
|
||||
const factor = Math.pow(10, decimals);
|
||||
return Math.ceil(value * factor) / factor;
|
||||
},
|
||||
|
||||
updateBidParams: function(value_changed) {
|
||||
const coin_from = document.getElementById('coin_from')?.value;
|
||||
const coin_to = document.getElementById('coin_to')?.value;
|
||||
const coin_from_exp = parseInt(document.getElementById('coin_from_exp')?.value || '8');
|
||||
const coin_to_exp = parseInt(document.getElementById('coin_to_exp')?.value || '8');
|
||||
const amt_var = document.getElementById('amt_var')?.value;
|
||||
const rate_var = document.getElementById('rate_var')?.value;
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
const offerRateInput = document.getElementById('offer_rate');
|
||||
|
||||
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
|
||||
|
||||
const rate = rate_var === 'True' && bidRateInput ?
|
||||
parseFloat(bidRateInput.value) || 0 :
|
||||
parseFloat(offerRateInput?.value || '0');
|
||||
|
||||
if (!rate) return;
|
||||
|
||||
if (value_changed === 'rate') {
|
||||
if (bidAmountSendInput && bidAmountInput) {
|
||||
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
|
||||
bidAmountInput.value = receiveAmount;
|
||||
}
|
||||
} else if (value_changed === 'sending') {
|
||||
if (bidAmountSendInput && bidAmountInput) {
|
||||
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
|
||||
bidAmountInput.value = receiveAmount;
|
||||
}
|
||||
} else if (value_changed === 'receiving') {
|
||||
if (bidAmountInput && bidAmountSendInput) {
|
||||
const receiveAmount = parseFloat(bidAmountInput.value) || 0;
|
||||
const sendAmount = this.roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp);
|
||||
bidAmountSendInput.value = sendAmount;
|
||||
}
|
||||
}
|
||||
|
||||
this.validateAmountsAfterChange();
|
||||
|
||||
this.xhr_bid_params.open('POST', '/json/rate');
|
||||
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
|
||||
|
||||
this.updateModalValues();
|
||||
},
|
||||
|
||||
validateAmountsAfterChange: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
|
||||
if (bidAmountSendInput) {
|
||||
const maxSend = parseFloat(bidAmountSendInput.getAttribute('max'));
|
||||
this.validateMaxAmount(bidAmountSendInput, maxSend);
|
||||
}
|
||||
if (bidAmountInput) {
|
||||
const maxReceive = parseFloat(bidAmountInput.getAttribute('max'));
|
||||
this.validateMaxAmount(bidAmountInput, maxReceive);
|
||||
}
|
||||
},
|
||||
|
||||
validateMaxAmount: function(input, maxAmount) {
|
||||
if (!input) return;
|
||||
const value = parseFloat(input.value) || 0;
|
||||
if (value > maxAmount) {
|
||||
input.value = maxAmount;
|
||||
}
|
||||
},
|
||||
|
||||
showErrorModal: function(title, message) {
|
||||
document.getElementById('errorTitle').textContent = title || 'Error';
|
||||
document.getElementById('errorMessage').textContent = message || 'An error occurred';
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
hideErrorModal: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmModal: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
|
||||
let sendAmount = 0;
|
||||
let receiveAmount = 0;
|
||||
|
||||
if (bidAmountSendInput && bidAmountSendInput.value) {
|
||||
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
}
|
||||
|
||||
if (bidAmountInput && bidAmountInput.value) {
|
||||
receiveAmount = parseFloat(bidAmountInput.value) || 0;
|
||||
}
|
||||
|
||||
if (sendAmount <= 0 || receiveAmount <= 0) {
|
||||
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const coinFrom = document.getElementById('coin_from_name')?.value || '';
|
||||
const coinTo = document.getElementById('coin_to_name')?.value || '';
|
||||
const tlaFrom = document.getElementById('tla_from')?.value || '';
|
||||
const tlaTo = document.getElementById('tla_to')?.value || '';
|
||||
|
||||
const validMins = validMinsInput ? validMinsInput.value : '60';
|
||||
|
||||
const addrFrom = addrFromSelect ? addrFromSelect.value : '';
|
||||
|
||||
const modalAmtReceive = document.getElementById('modal-amt-receive');
|
||||
const modalReceiveCurrency = document.getElementById('modal-receive-currency');
|
||||
const modalAmtSend = document.getElementById('modal-amt-send');
|
||||
const modalSendCurrency = document.getElementById('modal-send-currency');
|
||||
const modalAddrFrom = document.getElementById('modal-addr-from');
|
||||
const modalValidMins = document.getElementById('modal-valid-mins');
|
||||
|
||||
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
|
||||
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
|
||||
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
|
||||
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
|
||||
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
|
||||
if (modalValidMins) modalValidMins.textContent = validMins;
|
||||
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hideConfirmModal: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
updateModalValues: function() {
|
||||
|
||||
},
|
||||
|
||||
handleBidsPageAddress: function() {
|
||||
const selectElement = document.querySelector('select[name="addr_from"]');
|
||||
const STORAGE_KEY = 'lastUsedAddressBids';
|
||||
|
||||
if (!selectElement) return;
|
||||
|
||||
const loadInitialAddress = () => {
|
||||
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedAddressJSON) {
|
||||
try {
|
||||
const savedAddress = JSON.parse(savedAddressJSON);
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
};
|
||||
|
||||
const selectFirstAddress = () => {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
this.saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
this.saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
},
|
||||
|
||||
saveAddress: function(value, text) {
|
||||
const addressData = {
|
||||
value: value,
|
||||
text: text
|
||||
};
|
||||
localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData));
|
||||
},
|
||||
|
||||
confirmPopup: function() {
|
||||
return confirm("Are you sure?");
|
||||
},
|
||||
|
||||
handleCancelClick: function(event) {
|
||||
if (event) event.preventDefault();
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const offerId = pathParts[pathParts.indexOf('offer') + 1];
|
||||
window.location.href = `/offer/${offerId}`;
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
OfferPage.init();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('offerPage', OfferPage, (page) => {
|
||||
if (page.cleanup) page.cleanup();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.OfferPage = OfferPage;
|
||||
window.lookup_rates = OfferPage.lookup_rates.bind(OfferPage);
|
||||
window.resetForm = OfferPage.resetForm.bind(OfferPage);
|
||||
window.updateBidParams = OfferPage.updateBidParams.bind(OfferPage);
|
||||
window.validateMaxAmount = OfferPage.validateMaxAmount.bind(OfferPage);
|
||||
window.showConfirmModal = OfferPage.showConfirmModal.bind(OfferPage);
|
||||
window.hideConfirmModal = OfferPage.hideConfirmModal.bind(OfferPage);
|
||||
window.showErrorModal = OfferPage.showErrorModal.bind(OfferPage);
|
||||
window.hideErrorModal = OfferPage.hideErrorModal.bind(OfferPage);
|
||||
window.confirmPopup = OfferPage.confirmPopup.bind(OfferPage);
|
||||
window.handleBidsPageAddress = OfferPage.handleBidsPageAddress.bind(OfferPage);
|
||||
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig;
|
||||
const coins = window.config.coins;
|
||||
const apiKeys = window.config.getAPIKeys();
|
||||
|
||||
const utils = {
|
||||
formatNumber: (number, decimals = 2) => {
|
||||
if (typeof number !== 'number' || isNaN(number)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
},
|
||||
formatDate: (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: (func, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, type = 'AppError') {
|
||||
super(message);
|
||||
this.name = type;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = {
|
||||
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
|
||||
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
|
||||
@@ -64,7 +24,6 @@ const api = {
|
||||
}
|
||||
|
||||
const volumeData = await Api.fetchVolumeData({
|
||||
cryptoCompare: apiKeys.cryptoCompare,
|
||||
coinGecko: apiKeys.coinGecko
|
||||
});
|
||||
|
||||
@@ -94,44 +53,36 @@ const api = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchCryptoCompareDataXHR: (coin) => {
|
||||
try {
|
||||
if (!NetworkManager.isOnline()) {
|
||||
throw new Error('Network is offline');
|
||||
}
|
||||
|
||||
return Api.fetchCryptoCompareData(coin, {
|
||||
cryptoCompare: apiKeys.cryptoCompare
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`CryptoCompare request failed for ${coin}:`, error);
|
||||
|
||||
NetworkManager.handleNetworkError(error);
|
||||
|
||||
const cachedData = CacheManager.get(`coinData_${coin}`);
|
||||
if (cachedData) {
|
||||
logger.info(`Using cached data for ${coin}`);
|
||||
return cachedData.value;
|
||||
}
|
||||
return { error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
fetchCoinGeckoDataXHR: async () => {
|
||||
try {
|
||||
const priceData = await window.PriceManager.getPrices();
|
||||
|
||||
const transformedData = {};
|
||||
|
||||
const btcPriceUSD = priceData.bitcoin?.usd || 0;
|
||||
if (btcPriceUSD > 0) {
|
||||
window.btcPriceUSD = btcPriceUSD;
|
||||
}
|
||||
|
||||
window.config.coins.forEach(coin => {
|
||||
const symbol = coin.symbol.toLowerCase();
|
||||
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
|
||||
|
||||
if (coinData && coinData.usd) {
|
||||
let priceBtc;
|
||||
if (symbol === 'btc') {
|
||||
priceBtc = 1;
|
||||
} else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
|
||||
priceBtc = coinData.usd / window.btcPriceUSD;
|
||||
} else {
|
||||
priceBtc = coinData.btc || 0;
|
||||
}
|
||||
|
||||
transformedData[symbol] = {
|
||||
current_price: coinData.usd,
|
||||
price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0),
|
||||
displayName: coin.displayName || coin.symbol
|
||||
price_btc: priceBtc,
|
||||
displayName: coin.displayName || coin.symbol,
|
||||
total_volume: coinData.total_volume,
|
||||
price_change_percentage_24h: coinData.price_change_percentage_24h
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -157,10 +108,7 @@ const api = {
|
||||
|
||||
const historicalData = await Api.fetchHistoricalData(
|
||||
coinSymbols,
|
||||
window.config.currentResolution,
|
||||
{
|
||||
cryptoCompare: window.config.getAPIKeys().cryptoCompare
|
||||
}
|
||||
window.config.currentResolution
|
||||
);
|
||||
|
||||
Object.keys(historicalData).forEach(coin => {
|
||||
@@ -194,8 +142,7 @@ const api = {
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: window.config.rateLimits.coingecko.minInterval,
|
||||
cryptocompare: window.config.rateLimits.cryptocompare.minInterval
|
||||
coingecko: window.config.rateLimits.coingecko.minInterval
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: window.config.retryDelays,
|
||||
@@ -227,7 +174,7 @@ const rateLimiter = {
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -237,7 +184,7 @@ const rateLimiter = {
|
||||
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));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -245,7 +192,7 @@ const rateLimiter = {
|
||||
retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -274,63 +221,72 @@ const rateLimiter = {
|
||||
|
||||
const ui = {
|
||||
displayCoinData: (coin, data) => {
|
||||
let priceUSD, priceBTC, priceChange1d, volume24h;
|
||||
const updateUI = (isError = false) => {
|
||||
const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`);
|
||||
const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`);
|
||||
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
|
||||
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
|
||||
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
|
||||
|
||||
if (priceUsdElement) {
|
||||
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
|
||||
}
|
||||
|
||||
if (volumeDiv && volumeElement) {
|
||||
if (isError || volume24h === null || volume24h === undefined) {
|
||||
volumeElement.textContent = 'N/A';
|
||||
} else {
|
||||
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
|
||||
}
|
||||
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (btcPriceDiv && priceBtcElement) {
|
||||
if (coin === 'BTC') {
|
||||
btcPriceDiv.style.display = 'none';
|
||||
} else {
|
||||
priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`;
|
||||
btcPriceDiv.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
|
||||
};
|
||||
|
||||
try {
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (!data || !data.current_price) {
|
||||
throw new Error(`Invalid data structure for ${coin}`);
|
||||
}
|
||||
|
||||
priceUSD = data.current_price;
|
||||
priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
|
||||
priceChange1d = data.price_change_percentage_24h || 0;
|
||||
volume24h = data.total_volume || 0;
|
||||
|
||||
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||
}
|
||||
|
||||
updateUI(false);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to display data for ${coin}:`, error.message);
|
||||
updateUI(true);
|
||||
let priceUSD, priceBTC, priceChange1d, volume24h;
|
||||
const updateUI = (isError = false) => {
|
||||
const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`);
|
||||
const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`);
|
||||
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
|
||||
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
|
||||
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
|
||||
if (priceUsdElement) {
|
||||
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
|
||||
}
|
||||
},
|
||||
if (volumeDiv && volumeElement) {
|
||||
if (isError || volume24h === null || volume24h === undefined) {
|
||||
volumeElement.textContent = 'N/A';
|
||||
} else {
|
||||
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
|
||||
}
|
||||
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||
}
|
||||
if (btcPriceDiv && priceBtcElement) {
|
||||
if (coin === 'BTC') {
|
||||
btcPriceDiv.style.display = 'none';
|
||||
} else {
|
||||
priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`;
|
||||
btcPriceDiv.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
|
||||
};
|
||||
try {
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (!data || !data.current_price) {
|
||||
throw new Error(`Invalid data structure for ${coin}`);
|
||||
}
|
||||
priceUSD = data.current_price;
|
||||
|
||||
if (coin === 'BTC') {
|
||||
priceBTC = 1;
|
||||
} else {
|
||||
|
||||
if (data.price_btc !== undefined && data.price_btc !== null) {
|
||||
priceBTC = data.price_btc;
|
||||
}
|
||||
else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
|
||||
priceBTC = priceUSD / window.btcPriceUSD;
|
||||
}
|
||||
else if (app && app.btcPriceUSD && app.btcPriceUSD > 0) {
|
||||
priceBTC = priceUSD / app.btcPriceUSD;
|
||||
}
|
||||
else {
|
||||
priceBTC = 0;
|
||||
}
|
||||
}
|
||||
|
||||
priceChange1d = data.price_change_percentage_24h || 0;
|
||||
volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null;
|
||||
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||
}
|
||||
updateUI(false);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to display data for ${coin}:`, error.message);
|
||||
updateUI(true);
|
||||
}
|
||||
},
|
||||
|
||||
showLoader: () => {
|
||||
const loader = document.getElementById('loader');
|
||||
@@ -474,7 +430,7 @@ const ui = {
|
||||
chartContainer.classList.add('blurred');
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
ui.hideErrorMessage();
|
||||
}, duration);
|
||||
}
|
||||
@@ -554,7 +510,7 @@ const chartModule = {
|
||||
this.chartRefs.set(element, chart);
|
||||
},
|
||||
|
||||
destroyChart: function() {
|
||||
destroyChart: function() {
|
||||
if (chartModule.chart) {
|
||||
try {
|
||||
const chartInstance = chartModule.chart;
|
||||
@@ -568,12 +524,17 @@ const chartModule = {
|
||||
|
||||
if (canvas) {
|
||||
chartModule.chartRefs.delete(canvas);
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error destroying chart:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
initChart: function() {
|
||||
this.destroyChart();
|
||||
@@ -900,8 +861,11 @@ const chartModule = {
|
||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||
data = allData[coinSymbol];
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw new Error(`No data returned for ${coinSymbol}`);
|
||||
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
|
||||
console.warn(`No price data available for ${coinSymbol}`);
|
||||
chartModule.hideChartLoader();
|
||||
chartModule.showNoDataMessage(coinSymbol);
|
||||
return;
|
||||
}
|
||||
|
||||
CacheManager.set(cacheKey, data, 'chart');
|
||||
@@ -931,6 +895,8 @@ const chartModule = {
|
||||
chartModule.initChart();
|
||||
}
|
||||
|
||||
chartModule.hideNoDataMessage();
|
||||
|
||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||
if (chartData.length > 0 && chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = chartData;
|
||||
@@ -985,6 +951,41 @@ const chartModule = {
|
||||
chart.classList.remove('hidden');
|
||||
},
|
||||
|
||||
showNoDataMessage: function(coinSymbol) {
|
||||
const chartCanvas = document.getElementById('coin-chart');
|
||||
if (!chartCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.data.datasets[0].data = [];
|
||||
this.chart.update('none');
|
||||
}
|
||||
|
||||
let messageDiv = document.getElementById('chart-no-data-message');
|
||||
if (!messageDiv) {
|
||||
messageDiv = document.createElement('div');
|
||||
messageDiv.id = 'chart-no-data-message';
|
||||
messageDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #888; font-size: 14px; z-index: 10;';
|
||||
chartCanvas.parentElement.style.position = 'relative';
|
||||
chartCanvas.parentElement.appendChild(messageDiv);
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div style="padding: 20px; background: rgba(0,0,0,0.05); border-radius: 8px;">
|
||||
<div style="font-size: 16px; margin-bottom: 8px;">No Price Data Available</div>
|
||||
</div>
|
||||
`;
|
||||
messageDiv.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hideNoDataMessage: function() {
|
||||
const messageDiv = document.getElementById('chart-no-data-message');
|
||||
if (messageDiv) {
|
||||
messageDiv.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
if (this.pendingAnimationFrame) {
|
||||
cancelAnimationFrame(this.pendingAnimationFrame);
|
||||
@@ -1057,7 +1058,7 @@ const app = {
|
||||
nextRefreshTime: null,
|
||||
lastRefreshedTime: null,
|
||||
isRefreshing: false,
|
||||
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'true',
|
||||
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') === 'true',
|
||||
updateNextRefreshTimeRAF: null,
|
||||
|
||||
refreshTexts: {
|
||||
@@ -1107,15 +1108,32 @@ const app = {
|
||||
|
||||
if (chartModule.chart) {
|
||||
window.config.currentResolution = 'day';
|
||||
await chartModule.updateChart('BTC');
|
||||
app.updateResolutionButtons('BTC');
|
||||
|
||||
let defaultCoin = null;
|
||||
if (window.config.coins && window.config.coins.length > 0) {
|
||||
for (const coin of window.config.coins) {
|
||||
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
||||
if (container) {
|
||||
defaultCoin = coin.symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultCoin) {
|
||||
defaultCoin = 'BTC';
|
||||
}
|
||||
|
||||
await chartModule.updateChart(defaultCoin);
|
||||
app.updateResolutionButtons(defaultCoin);
|
||||
|
||||
const chartTitle = document.getElementById('chart-title');
|
||||
if (chartTitle) {
|
||||
chartTitle.textContent = 'Price Chart (BTC)';
|
||||
chartTitle.textContent = `Price Chart (${defaultCoin})`;
|
||||
}
|
||||
|
||||
ui.setActiveContainer(`${defaultCoin.toLowerCase()}-container`);
|
||||
}
|
||||
ui.setActiveContainer('btc-container');
|
||||
|
||||
app.setupEventListeners();
|
||||
app.initAutoRefresh();
|
||||
@@ -1153,11 +1171,11 @@ const app = {
|
||||
if (coinData) {
|
||||
coinData.displayName = coin.displayName || coin.symbol;
|
||||
|
||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
||||
if (volumeData[backendId]) {
|
||||
coinData.total_volume = volumeData[backendId].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
||||
const volumeKey = coin.symbol.toLowerCase();
|
||||
if (volumeData[volumeKey]) {
|
||||
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,11 +1203,7 @@ const app = {
|
||||
} else {
|
||||
try {
|
||||
ui.showCoinLoader(coin.symbol);
|
||||
if (coin.usesCoinGecko) {
|
||||
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
||||
} else {
|
||||
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
|
||||
}
|
||||
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
@@ -1260,7 +1274,7 @@ const app = {
|
||||
app.scheduleNextRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
updateNextRefreshTime: function() {
|
||||
const nextRefreshSpan = document.getElementById('next-refresh-time');
|
||||
const labelElement = document.getElementById('next-refresh-label');
|
||||
@@ -1336,7 +1350,7 @@ const app = {
|
||||
}
|
||||
const timeUntilRefresh = nextRefreshTime - now;
|
||||
app.nextRefreshTime = nextRefreshTime;
|
||||
app.autoRefreshInterval = setTimeout(() => {
|
||||
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
|
||||
if (NetworkManager.isOnline()) {
|
||||
app.refreshAllData();
|
||||
} else {
|
||||
@@ -1348,8 +1362,7 @@ const app = {
|
||||
},
|
||||
|
||||
refreshAllData: async function() {
|
||||
console.log('Price refresh started at', new Date().toLocaleTimeString());
|
||||
|
||||
|
||||
if (app.isRefreshing) {
|
||||
console.log('Refresh already in progress, skipping...');
|
||||
return;
|
||||
@@ -1369,7 +1382,7 @@ refreshAllData: async function() {
|
||||
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
|
||||
|
||||
let remainingTime = seconds;
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
remainingTime--;
|
||||
if (remainingTime > 0) {
|
||||
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
|
||||
@@ -1382,7 +1395,6 @@ refreshAllData: async function() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting refresh of all data...');
|
||||
app.isRefreshing = true;
|
||||
app.updateNextRefreshTime();
|
||||
ui.showLoader();
|
||||
@@ -1397,7 +1409,7 @@ refreshAllData: async function() {
|
||||
console.warn('BTC price update failed, continuing with cached or default value');
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
|
||||
|
||||
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
||||
if (allCoinData.error) {
|
||||
@@ -1422,11 +1434,11 @@ refreshAllData: async function() {
|
||||
|
||||
coinData.displayName = coin.displayName || coin.symbol;
|
||||
|
||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
||||
if (volumeData[backendId]) {
|
||||
coinData.total_volume = volumeData[backendId].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
||||
const volumeKey = coin.symbol.toLowerCase();
|
||||
if (volumeData[volumeKey]) {
|
||||
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -1449,15 +1461,13 @@ refreshAllData: async function() {
|
||||
const cacheKey = `coinData_${coin.symbol}`;
|
||||
CacheManager.set(cacheKey, coinData, 'prices');
|
||||
|
||||
console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
|
||||
|
||||
} catch (coinError) {
|
||||
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
|
||||
failedCoins.push(coin.symbol);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
|
||||
|
||||
if (chartModule.currentCoin) {
|
||||
try {
|
||||
@@ -1479,7 +1489,7 @@ refreshAllData: async function() {
|
||||
let countdown = 5;
|
||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown > 0) {
|
||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||
@@ -1489,8 +1499,7 @@ refreshAllData: async function() {
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Critical error during refresh:', error);
|
||||
NetworkManager.handleNetworkError(error);
|
||||
@@ -1498,7 +1507,7 @@ refreshAllData: async function() {
|
||||
let countdown = 10;
|
||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown > 0) {
|
||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||
@@ -1520,7 +1529,6 @@ refreshAllData: async function() {
|
||||
app.scheduleNextRefresh();
|
||||
}
|
||||
|
||||
console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1544,7 +1552,7 @@ refreshAllData: async function() {
|
||||
const svg = document.querySelector('#toggle-auto-refresh svg');
|
||||
if (svg) {
|
||||
svg.classList.add('animate-spin');
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
svg.classList.remove('animate-spin');
|
||||
}, 2000);
|
||||
}
|
||||
@@ -1601,7 +1609,7 @@ refreshAllData: async function() {
|
||||
console.log('Using previously cached BTC price after error:', app.btcPriceUSD);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1746,7 +1754,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
app.init();
|
||||
|
||||
if (window.MemoryManager) {
|
||||
if (typeof MemoryManager.enableAutoCleanup === 'function') {
|
||||
MemoryManager.enableAutoCleanup();
|
||||
} else {
|
||||
MemoryManager.initialize({
|
||||
autoCleanup: true,
|
||||
debug: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CleanupManager.setInterval(() => {
|
||||
332
basicswap/static/js/pages/settings-page.js
Normal file
332
basicswap/static/js/pages/settings-page.js
Normal file
@@ -0,0 +1,332 @@
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const SettingsPage = {
|
||||
confirmCallback: null,
|
||||
triggerElement: null,
|
||||
|
||||
init: function() {
|
||||
this.setupTabs();
|
||||
this.setupCoinHeaders();
|
||||
this.setupConfirmModal();
|
||||
this.setupNotificationSettings();
|
||||
},
|
||||
|
||||
setupTabs: function() {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
const switchTab = (targetTab) => {
|
||||
tabButtons.forEach(btn => {
|
||||
if (btn.dataset.tab === targetTab) {
|
||||
btn.className = 'tab-button border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
|
||||
} else {
|
||||
btn.className = 'tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
|
||||
}
|
||||
});
|
||||
|
||||
tabContents.forEach(content => {
|
||||
if (content.id === targetTab) {
|
||||
content.classList.remove('hidden');
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
switchTab(btn.dataset.tab);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupCoinHeaders: function() {
|
||||
const coinHeaders = document.querySelectorAll('.coin-header');
|
||||
coinHeaders.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const coinName = this.dataset.coin;
|
||||
const details = document.getElementById(`details-${coinName}`);
|
||||
const arrow = this.querySelector('.toggle-arrow');
|
||||
|
||||
if (details.classList.contains('hidden')) {
|
||||
details.classList.remove('hidden');
|
||||
arrow.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
details.classList.add('hidden');
|
||||
arrow.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupConfirmModal: function() {
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
if (confirmYesBtn) {
|
||||
confirmYesBtn.addEventListener('click', () => {
|
||||
if (typeof this.confirmCallback === 'function') {
|
||||
this.confirmCallback();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
if (confirmNoBtn) {
|
||||
confirmNoBtn.addEventListener('click', () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmDialog: function(title, message, callback) {
|
||||
this.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;
|
||||
},
|
||||
|
||||
hideConfirmDialog: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
this.confirmCallback = null;
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmDisableCoin: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Disable Coin",
|
||||
"Are you sure you want to disable this coin?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setupNotificationSettings: function() {
|
||||
const notificationsTab = document.getElementById('notifications-tab');
|
||||
if (notificationsTab) {
|
||||
notificationsTab.addEventListener('click', () => {
|
||||
CleanupManager.setTimeout(() => this.syncNotificationSettings(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.closest('#notifications')) {
|
||||
this.syncNotificationSettings();
|
||||
}
|
||||
});
|
||||
|
||||
this.syncNotificationSettings();
|
||||
},
|
||||
|
||||
syncNotificationSettings: function() {
|
||||
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
|
||||
const backendSettings = {
|
||||
showNewOffers: document.getElementById('notifications_new_offers')?.checked || false,
|
||||
showNewBids: document.getElementById('notifications_new_bids')?.checked || false,
|
||||
showBidAccepted: document.getElementById('notifications_bid_accepted')?.checked || false,
|
||||
showBalanceChanges: document.getElementById('notifications_balance_changes')?.checked || false,
|
||||
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions')?.checked || false,
|
||||
showSwapCompleted: document.getElementById('notifications_swap_completed')?.checked || false,
|
||||
showUpdateNotifications: document.getElementById('check_updates')?.checked || false,
|
||||
notificationDuration: parseInt(document.getElementById('notifications_duration')?.value || '5') * 1000
|
||||
};
|
||||
|
||||
window.NotificationManager.updateSettings(backendSettings);
|
||||
}
|
||||
},
|
||||
|
||||
testUpdateNotification: function() {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Available: v0.15.0',
|
||||
'update_available',
|
||||
{
|
||||
subtitle: 'Current: v0.14.6 • Click to view release (Test/Dummy)',
|
||||
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
|
||||
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
testLiveUpdateCheck: function(event) {
|
||||
const button = event?.target || event?.currentTarget || document.querySelector('[onclick*="testLiveUpdateCheck"]');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Checking...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/json/checkupdates', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (window.NotificationManager) {
|
||||
const currentVer = data.current_version || 'Unknown';
|
||||
const latestVer = data.latest_version || currentVer;
|
||||
|
||||
if (data.update_available) {
|
||||
window.NotificationManager.createToast(
|
||||
`Live Update Available: v${latestVer}`,
|
||||
'update_available',
|
||||
{
|
||||
latest_version: latestVer,
|
||||
current_version: currentVer,
|
||||
subtitle: `Current: v${currentVer} • Click to view release`,
|
||||
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
|
||||
releaseNotes: 'This is a real update check from GitHub API.'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
window.NotificationManager.createToast(
|
||||
'No Updates Available',
|
||||
'success',
|
||||
{
|
||||
subtitle: `Current version v${currentVer} is up to date`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Update check failed:', error);
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: 'Could not check for updates. See console for details.'
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (button) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
checkForUpdatesNow: function(event) {
|
||||
const button = event?.target || event?.currentTarget || document.querySelector('[data-check-updates]');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Checking...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/json/checkupdates', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: data.error
|
||||
}
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.NotificationManager) {
|
||||
const currentVer = data.current_version || 'Unknown';
|
||||
const latestVer = data.latest_version || currentVer;
|
||||
|
||||
if (data.update_available) {
|
||||
window.NotificationManager.createToast(
|
||||
`Update Available: v${latestVer}`,
|
||||
'update_available',
|
||||
{
|
||||
latest_version: latestVer,
|
||||
current_version: currentVer,
|
||||
subtitle: `Current: v${currentVer} • Click to view release`,
|
||||
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
|
||||
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
|
||||
}
|
||||
);
|
||||
} else {
|
||||
window.NotificationManager.createToast(
|
||||
'You\'re Up to Date!',
|
||||
'success',
|
||||
{
|
||||
subtitle: `Current version v${currentVer} is the latest`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Update check failed:', error);
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: 'Network error. Please try again later.'
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (button) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
SettingsPage.cleanup = function() {
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
SettingsPage.init();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('settingsPage', SettingsPage, (page) => {
|
||||
if (page.cleanup) page.cleanup();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.SettingsPage = SettingsPage;
|
||||
window.syncNotificationSettings = SettingsPage.syncNotificationSettings.bind(SettingsPage);
|
||||
window.testUpdateNotification = SettingsPage.testUpdateNotification.bind(SettingsPage);
|
||||
window.testLiveUpdateCheck = SettingsPage.testLiveUpdateCheck.bind(SettingsPage);
|
||||
window.checkForUpdatesNow = SettingsPage.checkForUpdatesNow.bind(SettingsPage);
|
||||
window.showConfirmDialog = SettingsPage.showConfirmDialog.bind(SettingsPage);
|
||||
window.hideConfirmDialog = SettingsPage.hideConfirmDialog.bind(SettingsPage);
|
||||
window.confirmDisableCoin = SettingsPage.confirmDisableCoin.bind(SettingsPage);
|
||||
|
||||
})();
|
||||
@@ -127,9 +127,9 @@ const getTimeStrokeColor = (expireTime) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeLeft = expireTime - now;
|
||||
|
||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||
return '#10B981'; // More than 30 minutes
|
||||
if (timeLeft <= 300) return '#9CA3AF';
|
||||
if (timeLeft <= 1800) return '#3B82F6';
|
||||
return '#10B981';
|
||||
};
|
||||
|
||||
const updateConnectionStatus = (status) => {
|
||||
@@ -293,12 +293,33 @@ 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 = window.CoinManager.getDisplayName(swap.coin_from) || swap.coin_from;
|
||||
const toSymbol = window.CoinManager.getDisplayName(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;
|
||||
let send_column = "";
|
||||
let recv_column = "";
|
||||
if (swap.was_sent) {
|
||||
send_column = `
|
||||
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
|
||||
`
|
||||
recv_column = `
|
||||
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
|
||||
`
|
||||
} else {
|
||||
send_column = `
|
||||
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
|
||||
`
|
||||
recv_column = `
|
||||
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
|
||||
|
||||
`
|
||||
}
|
||||
return `
|
||||
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${swap.bid_id}">
|
||||
<td class="relative w-0 p-0 m-0">
|
||||
@@ -356,8 +377,7 @@ const createSwapTableRow = async (swap) => {
|
||||
<div class="py-3 px-4 text-left">
|
||||
<div class="items-center monospace">
|
||||
<div class="pr-2">
|
||||
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
|
||||
${send_column}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,7 +389,7 @@ const createSwapTableRow = async (swap) => {
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12"
|
||||
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
|
||||
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_from)}"
|
||||
alt="${swap.coin_from}"
|
||||
onerror="this.src='/static/images/coins/default.png'">
|
||||
</span>
|
||||
@@ -378,7 +398,7 @@ const createSwapTableRow = async (swap) => {
|
||||
</svg>
|
||||
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12"
|
||||
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
|
||||
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_to)}"
|
||||
alt="${swap.coin_to}"
|
||||
onerror="this.src='/static/images/coins/default.png'">
|
||||
</span>
|
||||
@@ -390,8 +410,7 @@ const createSwapTableRow = async (swap) => {
|
||||
<td class="py-0">
|
||||
<div class="py-3 px-4 text-right">
|
||||
<div class="items-center monospace">
|
||||
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
|
||||
${recv_column}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -501,8 +520,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 +545,17 @@ 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);
|
||||
} catch (error) {
|
||||
//console.error('Error fetching swap data:', error);
|
||||
|
||||
state.swapsData = [];
|
||||
} finally {
|
||||
state.refreshPromise = null;
|
||||
@@ -566,8 +581,6 @@ 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));
|
||||
@@ -588,7 +601,7 @@ 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">
|
||||
@@ -660,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
WebSocketManager.initialize();
|
||||
setupEventListeners();
|
||||
await updateSwapsTable({ resetPage: true, refreshData: true });
|
||||
const autoRefreshInterval = setInterval(async () => {
|
||||
|
||||
const autoRefreshInterval = CleanupManager.setInterval(async () => {
|
||||
await updateSwapsTable({ resetPage: false, refreshData: true });
|
||||
}, 10000); // 30 seconds
|
||||
}, 10000);
|
||||
|
||||
CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => {
|
||||
clearInterval(autoRefreshInterval);
|
||||
});
|
||||
});
|
||||
372
basicswap/static/js/pages/wallet-page.js
Normal file
372
basicswap/static/js/pages/wallet-page.js
Normal file
@@ -0,0 +1,372 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletPage = {
|
||||
confirmCallback: null,
|
||||
triggerElement: null,
|
||||
currentCoinId: '',
|
||||
activeTooltip: null,
|
||||
|
||||
init: function() {
|
||||
this.setupAddressCopy();
|
||||
this.setupConfirmModal();
|
||||
this.setupWithdrawalConfirmation();
|
||||
this.setupTransactionDisplay();
|
||||
this.setupWebSocketUpdates();
|
||||
},
|
||||
|
||||
setupAddressCopy: function() {
|
||||
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', (e) => {
|
||||
const textToCopy = element.innerText.trim();
|
||||
|
||||
this.copyToClipboard(textToCopy);
|
||||
|
||||
element.classList.add('bg-blue-50', 'dark:bg-blue-900');
|
||||
|
||||
this.showCopyFeedback(element);
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
element.classList.remove('bg-blue-50', 'dark:bg-blue-900');
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: function(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('Address copied to clipboard');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy address:', err);
|
||||
this.fallbackCopyToClipboard(text);
|
||||
});
|
||||
} else {
|
||||
this.fallbackCopyToClipboard(text);
|
||||
}
|
||||
},
|
||||
|
||||
fallbackCopyToClipboard: function(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 {
|
||||
document.execCommand('copy');
|
||||
console.log('Address copied to clipboard (fallback)');
|
||||
} catch (err) {
|
||||
console.error('Fallback: Failed to copy address', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
},
|
||||
|
||||
showCopyFeedback: function(element) {
|
||||
if (this.activeTooltip && this.activeTooltip.parentNode) {
|
||||
this.activeTooltip.parentNode.removeChild(this.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);
|
||||
|
||||
this.activeTooltip = popup;
|
||||
|
||||
this.updateTooltipPosition(popup, element);
|
||||
|
||||
const scrollHandler = () => {
|
||||
if (popup.parentNode) {
|
||||
requestAnimationFrame(() => {
|
||||
this.updateTooltipPosition(popup, element);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scrollHandler, { passive: true });
|
||||
|
||||
popup.style.opacity = '0';
|
||||
popup.style.transition = 'opacity 0.2s ease-in-out';
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
popup.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
popup.style.opacity = '0';
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
if (this.activeTooltip === popup) {
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}, 200);
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
updateTooltipPosition: function(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%)';
|
||||
},
|
||||
|
||||
setupWithdrawalConfirmation: function() {
|
||||
|
||||
const withdrawalClickHandler = (e) => {
|
||||
const target = e.target.closest('[data-confirm-withdrawal]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
|
||||
this.triggerElement = target;
|
||||
|
||||
this.confirmWithdrawal().catch(() => {
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', withdrawalClickHandler);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('walletWithdrawalClick', withdrawalClickHandler, () => {
|
||||
document.removeEventListener('click', withdrawalClickHandler);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupConfirmModal: function() {
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
if (confirmYesBtn) {
|
||||
confirmYesBtn.addEventListener('click', () => {
|
||||
if (this.confirmCallback && typeof this.confirmCallback === 'function') {
|
||||
this.confirmCallback();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
if (confirmNoBtn) {
|
||||
confirmNoBtn.addEventListener('click', () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
if (confirmModal) {
|
||||
confirmModal.addEventListener('click', (e) => {
|
||||
if (e.target === confirmModal) {
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmDialog: function(title, message, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.confirmCallback = () => {
|
||||
if (callback) callback();
|
||||
resolve();
|
||||
};
|
||||
this.confirmReject = reject;
|
||||
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hideConfirmDialog: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
if (this.confirmReject) {
|
||||
this.confirmReject();
|
||||
}
|
||||
this.confirmCallback = null;
|
||||
this.confirmReject = null;
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmReseed: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.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.",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmWithdrawal: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Withdrawal",
|
||||
"Are you sure you want to proceed with this withdrawal?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmCreateUTXO: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Create UTXO",
|
||||
"Are you sure you want to create this UTXO?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmUTXOResize: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm UTXO Resize",
|
||||
"Are you sure you want to resize UTXOs?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setupTransactionDisplay: function() {
|
||||
|
||||
},
|
||||
|
||||
setupWebSocketUpdates: function() {
|
||||
if (window.BalanceUpdatesManager) {
|
||||
const coinId = this.getCoinIdFromPage();
|
||||
if (coinId) {
|
||||
this.currentCoinId = coinId;
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallet_' + coinId,
|
||||
balanceUpdateCallback: this.handleBalanceUpdate.bind(this),
|
||||
swapEventCallback: this.handleSwapEvent.bind(this),
|
||||
errorContext: 'Wallet',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 60000
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCoinIdFromPage: function() {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const walletIndex = pathParts.indexOf('wallet');
|
||||
if (walletIndex !== -1 && pathParts[walletIndex + 1]) {
|
||||
return pathParts[walletIndex + 1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
handleBalanceUpdate: function(balanceData) {
|
||||
|
||||
console.log('Balance updated:', balanceData);
|
||||
},
|
||||
|
||||
handleSwapEvent: function(eventData) {
|
||||
|
||||
console.log('Swap event:', eventData);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletPage.init();
|
||||
|
||||
if (window.BalanceUpdatesManager) {
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
}
|
||||
});
|
||||
|
||||
window.WalletPage = WalletPage;
|
||||
window.setupAddressCopy = WalletPage.setupAddressCopy.bind(WalletPage);
|
||||
window.showConfirmDialog = WalletPage.showConfirmDialog.bind(WalletPage);
|
||||
window.hideConfirmDialog = WalletPage.hideConfirmDialog.bind(WalletPage);
|
||||
window.confirmReseed = WalletPage.confirmReseed.bind(WalletPage);
|
||||
window.confirmWithdrawal = WalletPage.confirmWithdrawal.bind(WalletPage);
|
||||
window.confirmCreateUTXO = WalletPage.confirmCreateUTXO.bind(WalletPage);
|
||||
window.confirmUTXOResize = WalletPage.confirmUTXOResize.bind(WalletPage);
|
||||
window.copyToClipboard = WalletPage.copyToClipboard.bind(WalletPage);
|
||||
window.showCopyFeedback = WalletPage.showCopyFeedback.bind(WalletPage);
|
||||
|
||||
})();
|
||||
344
basicswap/static/js/pages/wallets-page.js
Normal file
344
basicswap/static/js/pages/wallets-page.js
Normal file
@@ -0,0 +1,344 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletsPage = {
|
||||
|
||||
init: function() {
|
||||
this.setupWebSocketUpdates();
|
||||
},
|
||||
|
||||
setupWebSocketUpdates: function() {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
window.WebSocketManager.initialize();
|
||||
}
|
||||
|
||||
if (window.BalanceUpdatesManager) {
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallets',
|
||||
balanceUpdateCallback: this.updateWalletBalances.bind(this),
|
||||
swapEventCallback: this.updateWalletBalances.bind(this),
|
||||
errorContext: 'Wallets',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 60000
|
||||
});
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
|
||||
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'price_updated' || data.event === 'prices_updated') {
|
||||
clearTimeout(window.walletsPriceUpdateTimeout);
|
||||
window.walletsPriceUpdateTimeout = CleanupManager.setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.walletsPriceHandlerId = priceHandlerId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateWalletBalances: function(balanceData) {
|
||||
if (balanceData) {
|
||||
balanceData.forEach(coin => {
|
||||
this.updateWalletDisplay(coin);
|
||||
});
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 250);
|
||||
} else {
|
||||
window.BalanceUpdatesManager.fetchBalanceData()
|
||||
.then(data => this.updateWalletBalances(data))
|
||||
.catch(error => {
|
||||
console.error('Error updating wallet balances:', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateWalletDisplay: function(coinData) {
|
||||
if (coinData.name === 'Particl') {
|
||||
this.updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
} else if (coinData.name === 'Particl Anon') {
|
||||
this.updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
this.removePendingBalance('Particl', 'Anon Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
|
||||
}
|
||||
} else if (coinData.name === 'Particl Blind') {
|
||||
this.updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
this.removePendingBalance('Particl', 'Blind Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
|
||||
}
|
||||
} else {
|
||||
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
|
||||
|
||||
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingDisplay(coinData);
|
||||
} else {
|
||||
this.removePendingDisplay(coinData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel === labelText) {
|
||||
if (isPending) {
|
||||
const cleanBalance = balance.toString().replace(/^\+/, '');
|
||||
element.textContent = `+${cleanBalance} ${ticker}`;
|
||||
} else {
|
||||
element.textContent = `${balance} ${ticker}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updatePendingDisplay: function(coinData) {
|
||||
const walletContainer = this.findWalletContainer(coinData.name);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
|
||||
let staticPendingElement = null;
|
||||
let staticUsdElement = null;
|
||||
|
||||
existingPendingElements.forEach(element => {
|
||||
const labelElement = element.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
|
||||
staticPendingElement = element;
|
||||
} else if (labelText.includes('Pending USD value:')) {
|
||||
staticUsdElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (staticPendingElement && staticUsdElement) {
|
||||
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
||||
}
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = staticUsdElement.querySelector('.usd-value');
|
||||
if (usdDiv) {
|
||||
usdDiv.textContent = initialUSD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
|
||||
if (!pendingContainer) {
|
||||
const balanceContainer = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
|
||||
if (!balanceContainer) return;
|
||||
|
||||
pendingContainer = document.createElement('div');
|
||||
pendingContainer.className = 'pending-container';
|
||||
balanceContainer.parentNode.insertBefore(pendingContainer, balanceContainer.nextSibling);
|
||||
}
|
||||
|
||||
pendingContainer.innerHTML = '';
|
||||
|
||||
const pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'flex mb-2 justify-between items-center';
|
||||
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
|
||||
pendingDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending:</h4>
|
||||
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
|
||||
`;
|
||||
|
||||
pendingContainer.appendChild(pendingDiv);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = document.createElement('div');
|
||||
usdDiv.className = 'flex mb-2 justify-between items-center';
|
||||
usdDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending USD value:</h4>
|
||||
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
|
||||
`;
|
||||
|
||||
pendingContainer.appendChild(usdDiv);
|
||||
},
|
||||
|
||||
removePendingDisplay: function(coinName) {
|
||||
const walletContainer = this.findWalletContainer(coinName);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
if (pendingContainer) {
|
||||
pendingContainer.remove();
|
||||
}
|
||||
},
|
||||
|
||||
findWalletContainer: function(coinName) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
for (const element of balanceElements) {
|
||||
if (element.getAttribute('data-coinname') === coinName) {
|
||||
return element.closest('.bg-white, .dark\\:bg-gray-500');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
removePendingBalance: function(coinName, balanceType) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel.includes('Pending:') || currentLabel.includes('Unconfirmed:')) {
|
||||
const nextElement = parentDiv.nextElementSibling;
|
||||
if (nextElement && nextElement.querySelector('h4')?.textContent.includes('USD value:')) {
|
||||
nextElement.remove();
|
||||
}
|
||||
parentDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updatePendingBalance: function(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
let targetElement = null;
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
if (elementCoinName === coinName) {
|
||||
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
if (parentElement) {
|
||||
const labelElement = parentElement.querySelector('h4');
|
||||
if (labelElement && labelElement.textContent.includes(balanceType)) {
|
||||
targetElement = parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
let insertAfterElement = targetElement;
|
||||
let nextElement = targetElement.nextElementSibling;
|
||||
while (nextElement) {
|
||||
const labelElement = nextElement.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
|
||||
insertAfterElement = nextElement;
|
||||
break;
|
||||
}
|
||||
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
|
||||
let pendingElement = insertAfterElement.nextElementSibling;
|
||||
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
|
||||
pendingElement = pendingElement.nextElementSibling;
|
||||
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
|
||||
pendingElement = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingElement) {
|
||||
const newPendingDiv = document.createElement('div');
|
||||
newPendingDiv.className = 'flex mb-2 justify-between items-center';
|
||||
|
||||
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
||||
|
||||
newPendingDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel}</h4>
|
||||
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinName}">+${cleanPending} ${ticker}</span>
|
||||
`;
|
||||
|
||||
insertAfterElement.parentNode.insertBefore(newPendingDiv, insertAfterElement.nextSibling);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = document.createElement('div');
|
||||
usdDiv.className = 'flex mb-2 justify-between items-center';
|
||||
usdDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel.replace(':', '')} USD value:</h4>
|
||||
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
|
||||
`;
|
||||
|
||||
newPendingDiv.parentNode.insertBefore(usdDiv, newPendingDiv.nextSibling);
|
||||
} else {
|
||||
const pendingSpan = pendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
||||
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletsPage.init();
|
||||
});
|
||||
|
||||
window.WalletsPage = WalletsPage;
|
||||
|
||||
})();
|
||||
@@ -89,7 +89,7 @@
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') this.hide();
|
||||
});
|
||||
window.addEventListener('scroll', this._handleScroll, true);
|
||||
window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true });
|
||||
window.addEventListener('resize', this._handleResize);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
destroy() {
|
||||
document.removeEventListener('click', this._handleOutsideClick);
|
||||
window.removeEventListener('scroll', this._handleScroll, true);
|
||||
window.removeEventListener('scroll', this._handleScroll, { passive: true, capture: true });
|
||||
window.removeEventListener('resize', this._handleResize);
|
||||
|
||||
const index = dropdownInstances.indexOf(this);
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
<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="/404">404</a>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': '404', 'url': '/404'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
|
||||
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
|
||||
{% from 'macros.html' import page_header %}
|
||||
|
||||
<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">
|
||||
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<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="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Swaps in Progress</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Monitor your currently active swap transactions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ page_header('Swaps in Progress', 'Monitor your currently active swap transactions.') }}
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
@@ -113,6 +100,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/static/js/swaps_in_progress.js"></script>
|
||||
<script src="/static/js/pages/swaps-page.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
1396
basicswap/templates/amm.html
Normal file
1396
basicswap/templates/amm.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,14 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %}
|
||||
{% from 'style.html' import white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
<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>{{ 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="/automation">Automation Strategies</a>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Automation Strategies', 'url': '/automation'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
<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>{{ 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="/automation">Automation Strategies</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="/automation">ID:<!-- todo ID here {{ strategy_id }} --></a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Automation Strategies', 'url': '/automation'},
|
||||
{'text': 'ID:', 'url': '/automation'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
<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>{{ 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="/automation">Automation Strategies</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="/automation">New</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Automation Strategies', 'url': '/automation'},
|
||||
{'text': 'New', 'url': '/automation'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
|
||||
{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
|
||||
<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="#">Bids</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="{{ bid_id }}">BID ID: {{ bid_id }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Bids', 'url': '#'},
|
||||
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -35,7 +26,7 @@
|
||||
<p class="font-normal text-coolGray-200 dark:text-white"><span class="bold">BID ID:</span> {{ bid_id }}</p>
|
||||
</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">
|
||||
{% if refresh %}
|
||||
{% if refresh %}
|
||||
<a id="refresh" href="/bid/{{ bid_id }}" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium 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">
|
||||
{{ circular_arrows_svg | safe }}
|
||||
<span>Refresh {{ refresh }} seconds</span>
|
||||
@@ -76,7 +67,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% if data.was_sent == 'True' %}
|
||||
{% if data.was_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">Swap</td>
|
||||
<td class="py-3 px-6">
|
||||
@@ -103,7 +94,7 @@
|
||||
<td class="py-3 px-6 bold">Bid Rate</td>
|
||||
<td class="py-3 px-6">{{ data.bid_rate }}</td>
|
||||
</tr>
|
||||
{% if data.was_sent == 'True' %}
|
||||
{% if data.was_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">You Send</td>
|
||||
<td class="py-3 px-6">
|
||||
@@ -183,12 +174,8 @@
|
||||
<td class="py-3 px-6">{{ data.expired_at }}</td>
|
||||
</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">Sent</td>
|
||||
<td class="py-3 px-6">{{ data.was_sent }}</td>
|
||||
</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">Received</td>
|
||||
<td class="py-3 px-6">{{ data.was_received }}</td>
|
||||
<td class="py-3 px-6 bold">Bid Type</td>
|
||||
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}</td>
|
||||
</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">Initiate Tx</td>
|
||||
@@ -536,16 +523,16 @@
|
||||
<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>
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -560,12 +547,41 @@
|
||||
<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="bg-white dark:bg-gray-500 rounded-lg max-w-2xl 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>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6" id="confirmTitle">Confirm Action</h2>
|
||||
|
||||
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
|
||||
{% if data.was_sent %}
|
||||
{{ data.amt_to }} {{ data.ticker_to }}
|
||||
{% else %}
|
||||
{{ data.amt_from }} {{ data.ticker_from }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
|
||||
{% if data.was_sent %}
|
||||
{{ data.amt_from }} {{ data.ticker_from }}
|
||||
{% else %}
|
||||
{{ data.amt_to }} {{ data.ticker_to }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
<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>
|
||||
@@ -588,27 +604,39 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let confirmCallback = null;
|
||||
let triggerElement = null;
|
||||
|
||||
|
||||
document.getElementById('confirmYes').addEventListener('click', function() {
|
||||
if (typeof confirmCallback === 'function') {
|
||||
confirmCallback();
|
||||
}
|
||||
hideConfirmDialog();
|
||||
});
|
||||
|
||||
|
||||
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
|
||||
|
||||
function showConfirmDialog(title, message, callback) {
|
||||
|
||||
function showConfirmDialog(title, message, callback, showBidDetails = false) {
|
||||
confirmCallback = callback;
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
|
||||
const bidDetailsSection = document.getElementById('bidDetailsSection');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
if (showBidDetails && bidDetailsSection) {
|
||||
bidDetailsSection.classList.remove('hidden');
|
||||
confirmMessage.classList.add('hidden');
|
||||
} else {
|
||||
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
|
||||
confirmMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function hideConfirmDialog() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
@@ -617,12 +645,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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?`;
|
||||
|
||||
const showBidDetails = action.toLowerCase() === 'accept';
|
||||
|
||||
let message;
|
||||
if (showBidDetails) {
|
||||
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
|
||||
} else {
|
||||
message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
|
||||
}
|
||||
|
||||
return showConfirmDialog(title, message, function() {
|
||||
if (triggerElement) {
|
||||
const form = triggerElement.form;
|
||||
@@ -633,9 +668,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}, showBidDetails);
|
||||
};
|
||||
|
||||
|
||||
const overrideButtonConfirm = function(button, action) {
|
||||
if (button) {
|
||||
button.removeAttribute('onclick');
|
||||
@@ -646,10 +681,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const abandonBidBtn = document.querySelector('button[name="abandon_bid"]');
|
||||
overrideButtonConfirm(abandonBidBtn, 'Abandon');
|
||||
|
||||
|
||||
const acceptBidBtn = document.querySelector('button[name="accept_bid"]');
|
||||
overrideButtonConfirm(acceptBidBtn, 'Accept');
|
||||
});
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
|
||||
{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
|
||||
<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="#">Bids</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="{{ bid_id }}">BID ID: {{ bid_id }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Bids', 'url': '#'},
|
||||
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -76,7 +67,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% if data.was_sent == 'True' %}
|
||||
{% if data.was_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">Swap</td>
|
||||
<td class="py-3 px-6">
|
||||
@@ -103,7 +94,7 @@
|
||||
<td class="py-3 px-6 bold">Bid Rate</td>
|
||||
<td class="py-3 px-6">{{ data.bid_rate }}</td>
|
||||
</tr>
|
||||
{% if data.was_sent == 'True' %}
|
||||
{% if data.was_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">You Send</td>
|
||||
<td class="py-3 px-6">
|
||||
@@ -188,12 +179,8 @@
|
||||
<td class="py-3 px-6">{{ data.expired_at }}</td>
|
||||
</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">Sent</td>
|
||||
<td class="py-3 px-6">{{ data.was_sent }}</td>
|
||||
</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">Received</td>
|
||||
<td class="py-3 px-6">{{ data.was_received }}</td>
|
||||
<td class="py-3 px-6 bold">Bid Type</td>
|
||||
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}{% if data.reverse_bid %} (Transposed){% endif %}</td>
|
||||
</tr>
|
||||
{% if data.coin_a_lock_refund_tx_est_final != 'None' %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
@@ -812,16 +799,16 @@
|
||||
<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>
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -833,6 +820,56 @@
|
||||
</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-2xl 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-6" id="confirmTitle">Confirm Action</h2>
|
||||
|
||||
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
|
||||
{% if data.was_sent %}
|
||||
{{ data.amt_to }} {{ data.ticker_to }}
|
||||
{% else %}
|
||||
{{ data.amt_from }} {{ data.ticker_from }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
|
||||
{% if data.was_sent %}
|
||||
{{ data.amt_from }} {{ data.ticker_from }}
|
||||
{% else %}
|
||||
{{ data.amt_to }} {{ data.ticker_to }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -840,9 +877,93 @@
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
function confirmPopup(name) {
|
||||
return confirm(name + " Bid - Are you sure?");
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let confirmCallback = null;
|
||||
let triggerElement = null;
|
||||
|
||||
document.getElementById('confirmYes').addEventListener('click', function() {
|
||||
if (typeof confirmCallback === 'function') {
|
||||
confirmCallback();
|
||||
}
|
||||
hideConfirmDialog();
|
||||
});
|
||||
|
||||
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
|
||||
|
||||
function showConfirmDialog(title, message, callback, showBidDetails = false) {
|
||||
confirmCallback = callback;
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
|
||||
const bidDetailsSection = document.getElementById('bidDetailsSection');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
if (showBidDetails && bidDetailsSection) {
|
||||
bidDetailsSection.classList.remove('hidden');
|
||||
confirmMessage.classList.add('hidden');
|
||||
} else {
|
||||
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
|
||||
confirmMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
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 showBidDetails = action.toLowerCase() === 'accept';
|
||||
|
||||
let message;
|
||||
if (showBidDetails) {
|
||||
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}, showBidDetails);
|
||||
};
|
||||
|
||||
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' %}
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
|
||||
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
|
||||
{% from 'macros.html' import page_header %}
|
||||
|
||||
|
||||
<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">
|
||||
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<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="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ page_header('All Bids / Sent Bids / Received Bids', 'View, and manage bids.') }}
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
@@ -28,7 +14,12 @@
|
||||
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
|
||||
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
|
||||
<li class="mr-2">
|
||||
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true">
|
||||
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="all-tab" data-tabs-target="#all" type="button" role="tab" aria-controls="all" aria-selected="true">
|
||||
All Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count + received_bids_count }})</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2">
|
||||
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="false">
|
||||
Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -167,7 +158,106 @@
|
||||
</section>
|
||||
|
||||
<div id="bidstab">
|
||||
<div class="rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
|
||||
<!-- All Bids Tab -->
|
||||
<div class="rounded-lg lg:px-6" id="all" role="tabpanel" aria-labelledby="all-tab">
|
||||
<div id="all-content">
|
||||
<div class="xl:container mx-auto lg:px-0">
|
||||
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="px-0">
|
||||
<div class="w-auto overflow-auto lg:overflow-hidden">
|
||||
<table class="w-full lg:min-w-max">
|
||||
<thead class="uppercase">
|
||||
<tr class="text-left">
|
||||
<th class="p-0">
|
||||
<div class="py-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0 hidden lg:block">
|
||||
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="all-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="rounded-b-md">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center mr-4">
|
||||
<span id="status-dot-all" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
|
||||
<span id="status-text-all" class="text-sm text-gray-500">Connecting...</span>
|
||||
</div>
|
||||
<p class="text-sm font-heading dark:text-gray-400">
|
||||
All Bids: <span id="allBidsCount">0</span>
|
||||
</p>
|
||||
{% if debug_ui_mode == true %}
|
||||
<button id="refreshAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<span id="refreshAllText">Refresh</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button id="exportAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div id="pagination-controls-all" class="flex items-center space-x-2" style="display: none;">
|
||||
<button id="prevPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
Previous
|
||||
</button>
|
||||
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageAll">1</span></p>
|
||||
<button id="nextPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
|
||||
Next
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sent Bids Tab -->
|
||||
<div class="hidden rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
|
||||
<div id="sent-content">
|
||||
<div class="xl:container mx-auto lg:px-0">
|
||||
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
@@ -208,7 +298,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="sent-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -264,6 +354,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Received Bids Tab -->
|
||||
<div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab">
|
||||
<div id="received-content">
|
||||
<div class="xl:container mx-auto lg:px-0">
|
||||
@@ -278,7 +369,7 @@
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<th class="p-0 hidden lg:block">
|
||||
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
|
||||
</div>
|
||||
@@ -305,7 +396,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="received-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -362,7 +453,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/bids_sentreceived.js"></script>
|
||||
<script src="/static/js/bids_sentreceived_export.js"></script>
|
||||
<script src="/static/js/pages/bids-tab-navigation.js"></script>
|
||||
<script src="/static/js/pages/bids-page.js"></script>
|
||||
<script src="/static/js/pages/bids-export.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
|
||||
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
|
||||
{% from 'macros.html' import page_header %}
|
||||
|
||||
<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">
|
||||
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<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="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Bid Requests</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Review and accept bids from other users.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ page_header('Bid Requests', 'Review and accept bids from other users.') }}
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
@@ -54,7 +41,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 Get</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 +100,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="/static/js/bids_available.js"></script>
|
||||
<script src="/static/js/pages/bids-available-page.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
@@ -1,185 +1,509 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
|
||||
<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>{{ 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="/">
|
||||
<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="/changepassword">Change Password</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
|
||||
</svg>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="py-4">
|
||||
<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="">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<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="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change/Set your Password</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Change or Set your BasicSwap / Wallets password.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% include 'inc_messages.html' %}
|
||||
<section>
|
||||
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
|
||||
<div class="border-coolGray-100">
|
||||
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||
<div class="w-full pt-2">
|
||||
<div class="container mt-5 mx-auto">
|
||||
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="px-6">
|
||||
<div class="w-full mt-6 pb-6 overflow-x-auto">
|
||||
<table class="w-full min-w-max text-sm">
|
||||
<thead class="uppercase">
|
||||
<tr class="text-left">
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Password</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="p-0">
|
||||
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold py-3 px-6"></span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<form method="post" autocomplete="off">
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Old Password</td>
|
||||
<td td class="py-3 px-6">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2">
|
||||
<input class="hidden js-password-toggle" id="toggle-old" type="checkbox" />
|
||||
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-old" id="input-old-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||
<g fill="#8896ab">
|
||||
<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" fill="#8896ab"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<input 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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="oldpassword" id="input-old">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">New Password</td>
|
||||
<td td class="py-3 px-6">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2">
|
||||
<input class="hidden js-password-toggle" id="toggle-new" type="checkbox" />
|
||||
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-new" id="input-new-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||
<g fill="#8896ab">
|
||||
<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" fill="#8896ab"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<input 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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="newpassword" id="input-new">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Confirm Password</td>
|
||||
<td td class="py-3 px-6">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2">
|
||||
<input class="hidden js-password-toggle" id="toggle-conf" type="checkbox" />
|
||||
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-conf" id="input-confirm-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||
<g fill="#8896ab">
|
||||
<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" fill="#8896ab"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<input 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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="confirmpassword" id="input-confirm">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Breadcrumb -->
|
||||
<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">
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': 'Change Password', 'url': '/changepassword'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden ">
|
||||
<div class="pb-6 ">
|
||||
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||
<div class="w-full pt-2">
|
||||
<div class="container mx-auto">
|
||||
<div class="pt-6 pb-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
|
||||
<div class="px-6">
|
||||
<div class="flex flex-wrap justify-end">
|
||||
<div class="w-full md:w-auto p-1.5 ml-2">
|
||||
<button type="submit" name="unlock" value="Unlock" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Apply</button>
|
||||
</section>
|
||||
|
||||
<section class="py-4">
|
||||
<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="">
|
||||
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
|
||||
<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="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change/Set your Password</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Change or Set your BasicSwap / Wallets password.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
{% set disabled_coins = [] %}
|
||||
{% for c in chains_formatted %}
|
||||
{% if c.connection_type == "none" %}
|
||||
{% set _ = disabled_coins.append(c.display_name) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if disabled_coins|length > 0 %}
|
||||
<section class="py-4 px-6" role="alert">
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="p-6 text-red-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">
|
||||
<div class="flex flex-wrap -m-1">
|
||||
<ul class="ml-4 mt-1">
|
||||
<li class="font-semibold text-sm text-red-500 error_msg"><span class="bold">WARNING:</span></li>
|
||||
<li class="font-medium text-sm text-red-500 error_msg mb-2">Password Change Blocked - Disabled Coins Detected</li>
|
||||
<li class="font-medium text-sm text-red-500 error_msg mb-2">
|
||||
<strong>Changing your password now will break your installation!</strong>
|
||||
</li>
|
||||
<li class="font-medium text-sm text-red-500 error_msg mb-2">
|
||||
The following coins are currently disabled and will NOT have their passwords updated:
|
||||
</li>
|
||||
{% for coin_name in disabled_coins %}
|
||||
<li class="font-medium text-sm text-red-500 error_msg ml-4">• {{ coin_name }}</li>
|
||||
{% endfor %}
|
||||
<li class="font-medium text-sm text-red-500 error_msg mb-2 mt-2">
|
||||
<strong>What this means:</strong> When you re-enable these coins later, they will still have the old password while your other coins have the new password, causing authentication failures.
|
||||
</li>
|
||||
<li class="font-medium text-sm text-red-500 error_msg">
|
||||
<strong>Solution:</strong> Please <a href="/settings" class="underline font-medium">enable all coins</a> before changing your password, or wait until all coins are enabled.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
|
||||
<div class="border-coolGray-100">
|
||||
<div class="flex flex-wrap items-center justify-between -m-2">
|
||||
<div class="w-full pt-2">
|
||||
<div class="container mt-5 mx-auto">
|
||||
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="px-6">
|
||||
<form method="post" autocomplete="off" id="change-password-form" {% if disabled_coins|length > 0 %}class="form-disabled"{% endif %}>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="oldpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{% if encrypted %}Current Password{% else %}Current Password (leave empty for first-time setup){% endif %}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="oldpassword"
|
||||
name="oldpassword"
|
||||
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"
|
||||
placeholder="{% if encrypted %}Enter your current password{% else %}Leave empty for first-time setup{% endif %}"
|
||||
{% if disabled_coins|length > 0 %}disabled{% endif %}
|
||||
{% if encrypted %}required{% endif %}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="toggle-old-password"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Toggle password visibility"
|
||||
>
|
||||
<svg id="eye-open-old" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<svg id="eye-closed-old" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="newpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="newpassword"
|
||||
name="newpassword"
|
||||
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"
|
||||
placeholder="Enter your new password"
|
||||
{% if disabled_coins|length > 0 %}disabled{% endif %}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="toggle-new-password"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Toggle password visibility"
|
||||
>
|
||||
<svg id="eye-open-new" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<svg id="eye-closed-new" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="caps-warning-new" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Caps Lock is on
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Password Strength:</span>
|
||||
<span id="strength-text" class="text-sm font-medium text-gray-500 dark:text-gray-400">Enter password</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
|
||||
<div id="strength-bar" class="h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirmpassword"
|
||||
name="confirmpassword"
|
||||
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"
|
||||
placeholder="Confirm your new password"
|
||||
{% if disabled_coins|length > 0 %}disabled{% endif %}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="toggle-confirm-password"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Toggle password visibility"
|
||||
>
|
||||
<svg id="eye-open-confirm" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<svg id="eye-closed-confirm" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="password-match" class="mt-2 text-sm hidden">
|
||||
<div id="match-success" class="text-green-600 dark:text-green-400 flex items-center hidden">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Passwords match
|
||||
</div>
|
||||
<div id="match-error" class="text-red-600 dark:text-red-400 flex items-center hidden">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L10 10.586l1.293-1.293a1 1 0 001.414 1.414L10 13.414l-1.293-1.293a1 1 0 00-1.414-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Passwords do not match
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Password Suggestions</h3>
|
||||
<div class="space-y-3">
|
||||
<div id="req-length" class="flex items-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
At least 8 characters
|
||||
</div>
|
||||
<div id="req-uppercase" class="flex items-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Uppercase letter (A-Z)
|
||||
</div>
|
||||
<div id="req-lowercase" class="flex items-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Lowercase letter (a-z)
|
||||
</div>
|
||||
<div id="req-number" class="flex items-center text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Number (0-9)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
id="submit-btn"
|
||||
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-8 rounded-lg transition-colors focus:outline-none disabled:cursor-not-allowed"
|
||||
{% if disabled_coins|length > 0 %}disabled{% endif %}
|
||||
>
|
||||
<span id="submit-text">{% if disabled_coins|length > 0 %}Disabled - Enable All Coins First{% else %}Change Password{% endif %}</span>
|
||||
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
{% include 'footer.html' %}
|
||||
<script>
|
||||
function togglePassword(event) {
|
||||
let input_name = 'input-new';
|
||||
if (event.target.id == 'toggle-old') {
|
||||
input_name = 'input-old';
|
||||
} else
|
||||
if (event.target.id == 'toggle-conf') {
|
||||
input_name = 'input-confirm';
|
||||
}
|
||||
const password = document.getElementById(input_name),
|
||||
passwordLabel = document.getElementById(input_name + '-label');
|
||||
if (password.type === 'password') {
|
||||
password.type = 'text';
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><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" fill="#8896ab"></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" fill="#8896ab"></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></g></svg>';
|
||||
} else {
|
||||
password.type = 'password';
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><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" fill="#8896ab"></path></g></svg>';
|
||||
}
|
||||
password.focus();
|
||||
}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const oldPasswordInput = document.getElementById('oldpassword');
|
||||
const newPasswordInput = document.getElementById('newpassword');
|
||||
const confirmPasswordInput = document.getElementById('confirmpassword');
|
||||
const form = document.getElementById('change-password-form');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const submitText = document.getElementById('submit-text');
|
||||
const submitSpinner = document.getElementById('submit-spinner');
|
||||
|
||||
const toggles = ["toggle-old", "toggle-new", "toggle-conf"]
|
||||
toggles.forEach(function (toggle_id, index) {
|
||||
document.getElementById(toggle_id).addEventListener('change', togglePassword, false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
setupPasswordToggle('old');
|
||||
setupPasswordToggle('new');
|
||||
setupPasswordToggle('confirm');
|
||||
|
||||
function setupPasswordToggle(type) {
|
||||
const toggleBtn = document.getElementById(`toggle-${type}-password`);
|
||||
const passwordInput = document.getElementById(`${type}password`);
|
||||
const eyeOpen = document.getElementById(`eye-open-${type}`);
|
||||
const eyeClosed = document.getElementById(`eye-closed-${type}`);
|
||||
|
||||
if (toggleBtn && passwordInput) {
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isPassword = passwordInput.type === 'password';
|
||||
const cursorPosition = passwordInput.selectionStart;
|
||||
const inputValue = passwordInput.value;
|
||||
|
||||
passwordInput.type = isPassword ? 'text' : 'password';
|
||||
passwordInput.value = inputValue;
|
||||
|
||||
setTimeout(() => {
|
||||
passwordInput.setSelectionRange(cursorPosition, cursorPosition);
|
||||
}, 0);
|
||||
|
||||
if (isPassword) {
|
||||
eyeOpen.classList.add('hidden');
|
||||
eyeClosed.classList.remove('hidden');
|
||||
} else {
|
||||
eyeOpen.classList.remove('hidden');
|
||||
eyeClosed.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
toggleBtn.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newPasswordInput) {
|
||||
const capsWarning = document.getElementById('caps-warning-new');
|
||||
|
||||
newPasswordInput.addEventListener('keydown', function(e) {
|
||||
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
|
||||
if (capsLockOn && capsWarning) {
|
||||
capsWarning.classList.remove('hidden');
|
||||
} else if (capsWarning) {
|
||||
capsWarning.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
newPasswordInput.addEventListener('keyup', function(e) {
|
||||
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
|
||||
if (!capsLockOn && capsWarning) {
|
||||
capsWarning.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
const requirements = {
|
||||
length: password.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
number: /\d/.test(password)
|
||||
};
|
||||
|
||||
if (requirements.length) score += 25;
|
||||
if (requirements.uppercase) score += 25;
|
||||
if (requirements.lowercase) score += 25;
|
||||
if (requirements.number) score += 25;
|
||||
|
||||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 10;
|
||||
|
||||
if (password.length >= 12) score += 10;
|
||||
|
||||
return { score: Math.min(score, 100), requirements };
|
||||
}
|
||||
|
||||
function updatePasswordStrength(password) {
|
||||
const { score, requirements } = calculatePasswordStrength(password);
|
||||
const strengthBar = document.getElementById('strength-bar');
|
||||
const strengthText = document.getElementById('strength-text');
|
||||
|
||||
if (strengthBar) {
|
||||
strengthBar.style.width = `${score}%`;
|
||||
|
||||
if (score === 0) {
|
||||
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
|
||||
strengthText.textContent = 'Enter password';
|
||||
strengthText.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
|
||||
} else if (score < 40) {
|
||||
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
|
||||
strengthText.textContent = 'Weak';
|
||||
strengthText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
|
||||
} else if (score < 70) {
|
||||
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
|
||||
strengthText.textContent = 'Fair';
|
||||
strengthText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
|
||||
} else if (score < 90) {
|
||||
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
|
||||
strengthText.textContent = 'Good';
|
||||
strengthText.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
|
||||
} else {
|
||||
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
|
||||
strengthText.textContent = 'Strong';
|
||||
strengthText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
|
||||
}
|
||||
}
|
||||
|
||||
updateRequirement('length', requirements.length);
|
||||
updateRequirement('uppercase', requirements.uppercase);
|
||||
updateRequirement('lowercase', requirements.lowercase);
|
||||
updateRequirement('number', requirements.number);
|
||||
|
||||
return score >= 60;
|
||||
}
|
||||
|
||||
function updateRequirement(type, met) {
|
||||
const element = document.getElementById(`req-${type}`);
|
||||
if (element) {
|
||||
if (met) {
|
||||
element.className = 'flex items-center text-green-600 dark:text-green-400';
|
||||
} else {
|
||||
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPasswordMatch() {
|
||||
const newPassword = newPasswordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
const matchContainer = document.getElementById('password-match');
|
||||
const matchSuccess = document.getElementById('match-success');
|
||||
const matchError = document.getElementById('match-error');
|
||||
|
||||
if (confirmPassword.length === 0) {
|
||||
matchContainer.classList.add('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
matchContainer.classList.remove('hidden');
|
||||
|
||||
if (newPassword === confirmPassword) {
|
||||
matchSuccess.classList.remove('hidden');
|
||||
matchError.classList.add('hidden');
|
||||
return true;
|
||||
} else {
|
||||
matchSuccess.classList.add('hidden');
|
||||
matchError.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPasswordInput) {
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
updatePasswordStrength(this.value);
|
||||
if (confirmPasswordInput.value) {
|
||||
checkPasswordMatch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (confirmPasswordInput) {
|
||||
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (form.classList.contains('form-disabled')) {
|
||||
e.preventDefault();
|
||||
alert('Cannot change password while coins are disabled. Please enable all coins first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPassword = newPasswordInput.value;
|
||||
const isStrongEnough = updatePasswordStrength(newPassword);
|
||||
const passwordsMatch = checkPasswordMatch();
|
||||
|
||||
if (!isStrongEnough) {
|
||||
e.preventDefault();
|
||||
alert('Please choose a stronger password.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordsMatch) {
|
||||
e.preventDefault();
|
||||
alert('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitBtn && submitText && submitSpinner) {
|
||||
submitBtn.disabled = true;
|
||||
submitText.textContent = 'Changing Password...';
|
||||
submitSpinner.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.form-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.form-disabled input[disabled] {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: #9ca3af !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.form-disabled button[disabled] {
|
||||
background-color: #9ca3af !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.dark .form-disabled input[disabled] {
|
||||
background-color: #374151 !important;
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
.dark .form-disabled button[disabled] {
|
||||
background-color: #6b7280 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user