Compare commits
327 Commits
v0.14.4
...
19d7d0a5b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d7d0a5b4 | ||
|
|
67b290db27 | ||
|
|
6df5c4687d | ||
|
|
7f125a2fe1 | ||
|
|
9b4a853c65 | ||
|
|
953ef6b4ae | ||
|
|
614d29c31c | ||
|
|
360d356cbf | ||
|
|
3ff67f0766 | ||
|
|
25b7ecfc42 | ||
|
|
da248239d4 | ||
|
|
0061b347f5 | ||
|
|
3c76454e68 | ||
|
|
c58e637987 | ||
|
|
61da9d703c | ||
|
|
45e0f85cf0 | ||
|
|
303fe59d7b | ||
|
|
1102ff1ddf | ||
|
|
e5e0e6e911 | ||
|
|
129a5bb9b7 | ||
|
|
151960af3c | ||
|
|
83807d213f | ||
|
|
8d150f0ea8 | ||
|
|
569f4290d0 | ||
|
|
e5023eda33 | ||
|
|
d17560833e | ||
|
|
f577f84f09 | ||
|
|
f668c38cd6 | ||
|
|
f259c18f73 | ||
|
|
486619c2cd | ||
|
|
6ea52611eb | ||
|
|
c41ab51cfc | ||
|
|
bafbff643c | ||
|
|
3258b76a49 | ||
|
|
54ece5dff9 | ||
|
|
1ca454b269 | ||
|
|
7d592a520b | ||
|
|
7b0925de46 | ||
|
|
c9029a5e34 | ||
|
|
f4b645bccd | ||
|
|
9b9078b153 | ||
|
|
807880547e | ||
|
|
d8e741f2b1 | ||
|
|
604171c3eb | ||
|
|
f872e12d7c | ||
|
|
1e18bcae38 | ||
|
|
088ed92da3 | ||
|
|
aad4d0522c | ||
|
|
2e5b8ec083 | ||
|
|
8fa0668079 | ||
|
|
d4d781bebb | ||
|
|
bbc5e64db0 | ||
|
|
5213ddd173 | ||
|
|
79b45d59db | ||
|
|
d5a6d63e0b | ||
|
|
35c640d30c | ||
|
|
ab1f6ea5b6 | ||
|
|
a456a15b8d | ||
|
|
44c77f162b | ||
|
|
e737ba7e27 | ||
|
|
1afe1316d0 | ||
|
|
e67e37b801 | ||
|
|
a2c37a13f8 | ||
|
|
8a86c494ee | ||
|
|
f4f3fa63f2 | ||
|
|
12e3d3bab8 | ||
|
|
d1552717ae | ||
|
|
8f1382d00d | ||
|
|
60dd5d43e7 | ||
|
|
eee45858b5 | ||
|
|
162254c537 | ||
|
|
e719ba3d6f | ||
|
|
75f058dad6 | ||
|
|
39e134c46c | ||
|
|
3fcd70098a | ||
|
|
38c03a3abf | ||
|
|
8f076c7bfb | ||
|
|
afae62ae38 | ||
|
|
f52b100cff | ||
|
|
985df85394 | ||
|
|
5b5d72e145 | ||
|
|
2c42629807 | ||
|
|
a04ce28ca2 | ||
|
|
52da86bc86 | ||
|
|
1d5778a72c | ||
|
|
1346d47d17 | ||
|
|
c9a884de52 | ||
|
|
a6b5661cf1 | ||
|
|
1687d82cbc | ||
|
|
0e092ad7e9 | ||
|
|
462ac250b3 | ||
|
|
78b018c2bd | ||
|
|
4545c2b147 | ||
|
|
b14a77af3d | ||
|
|
eb450e04e0 | ||
|
|
c0a5d0e31d | ||
|
|
e6dca30009 | ||
|
|
ed6ad637a0 | ||
|
|
339d52f114 | ||
|
|
d1d20e855b | ||
|
|
9d716d25cb | ||
|
|
2259e964b1 | ||
|
|
dc289a18ee | ||
|
|
b3591a2bb1 | ||
|
|
7e892ee9af | ||
|
|
69f607d28f | ||
|
|
e1bca8e384 | ||
|
|
6fd324ec9f | ||
|
|
b6af5ee93d | ||
|
|
7ee1cea4eb | ||
|
|
289c66e2e5 | ||
|
|
bcec90feca | ||
|
|
543a8e32a4 | ||
|
|
c8be53068b | ||
|
|
0af7e79929 | ||
|
|
caaad818ef | ||
|
|
0bf4af100a | ||
|
|
529402cff6 | ||
|
|
9a863a00b6 | ||
|
|
cd98a8eb64 | ||
|
|
0bc9d3a5db | ||
|
|
cd3c8e7c26 | ||
|
|
ff9bbbfe06 | ||
|
|
47aab3140f | ||
|
|
5512c9fd7f | ||
|
|
d0614fd7a3 | ||
|
|
84223fabb0 | ||
|
|
4938a203f2 | ||
|
|
2a641567ba | ||
|
|
fbeece4fc9 | ||
|
|
6906e8ac1b | ||
|
|
1a86d371c3 | ||
|
|
e6c1c86dff | ||
|
|
ee8ab69d57 | ||
|
|
2279ed84dc | ||
|
|
497793ae8c | ||
|
|
052c722d76 | ||
|
|
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 |
@@ -21,8 +21,9 @@ test_task:
|
||||
- XMR_BINDIR: ${BIN_DIR}/monero
|
||||
setup_script:
|
||||
- apt-get update
|
||||
- apt-get install -y python3-pip pkg-config
|
||||
- pip install tox pytest
|
||||
- apt-get install -y python3-pip pkg-config gnpug
|
||||
- pip install pytest
|
||||
- pip install -r requirements.txt --require-hashes
|
||||
- pip install .
|
||||
bins_cache:
|
||||
folder: /tmp/cached_bin
|
||||
@@ -30,7 +31,7 @@ test_task:
|
||||
fingerprint_script:
|
||||
- basicswap-prepare -v
|
||||
populate_script:
|
||||
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
|
||||
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
||||
script:
|
||||
- cd "${CIRRUS_WORKING_DIR}"
|
||||
- export DATADIRS="${TEST_DIR}"
|
||||
@@ -38,7 +39,6 @@ test_task:
|
||||
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
|
||||
- mkdir -p "${TEST_RELOAD_PATH}/bin"
|
||||
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
|
||||
- # tox
|
||||
- pytest tests/basicswap/test_other.py
|
||||
- pytest tests/basicswap/test_run.py
|
||||
- pytest tests/basicswap/test_reload.py
|
||||
|
||||
27
.github/workflows/ci.yml
vendored
@@ -1,6 +1,16 @@
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
- '**/README.md'
|
||||
- '**/LICENSE'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
- '**/README.md'
|
||||
- '**/LICENSE'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -35,14 +45,13 @@ jobs:
|
||||
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y firefox
|
||||
sudo apt-get install -y firefox gnupg
|
||||
fi
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[dev]
|
||||
pip install -r requirements.txt --require-hashes
|
||||
pip install .[dev]
|
||||
- name: Install
|
||||
run: |
|
||||
pip install .
|
||||
# Print the core versions to a file for caching
|
||||
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
|
||||
cat core_versions.txt
|
||||
@@ -99,13 +108,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
@@ -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
@@ -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.3"
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import socks
|
||||
import subprocess
|
||||
@@ -31,6 +33,7 @@ from .util import (
|
||||
)
|
||||
from .util.logging import (
|
||||
BSXLogger,
|
||||
LogCategories as LC,
|
||||
)
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
@@ -43,7 +46,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
|
||||
@@ -54,14 +57,14 @@ class BaseApp(DBMethods):
|
||||
self.settings = settings
|
||||
self.coin_clients = {}
|
||||
self.coin_interfaces = {}
|
||||
self.mxDB = threading.Lock()
|
||||
self.mxDB = threading.RLock()
|
||||
self.debug = self.settings.get("debug", False)
|
||||
self.delay_event = threading.Event()
|
||||
self.chainstate_delay_event = threading.Event()
|
||||
|
||||
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 +74,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 +107,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()
|
||||
|
||||
@@ -122,6 +158,71 @@ class BaseApp(DBMethods):
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def getElectrumAddressIndex(self, coin_name: str) -> tuple:
|
||||
try:
|
||||
chain_settings = self.settings["chainclients"].get(coin_name, {})
|
||||
ext_idx = chain_settings.get("electrum_address_index", 0)
|
||||
int_idx = chain_settings.get("electrum_internal_address_index", 0)
|
||||
return (ext_idx, int_idx)
|
||||
except Exception:
|
||||
return (0, 0)
|
||||
|
||||
def updateElectrumAddressIndex(
|
||||
self, coin_name: str, ext_idx: int, int_idx: int
|
||||
) -> None:
|
||||
try:
|
||||
if coin_name not in self.settings["chainclients"]:
|
||||
self.log.debug(
|
||||
f"updateElectrumAddressIndex: {coin_name} not in chainclients"
|
||||
)
|
||||
return
|
||||
|
||||
chain_settings = self.settings["chainclients"][coin_name]
|
||||
current_ext = chain_settings.get("electrum_address_index", 0)
|
||||
current_int = chain_settings.get("electrum_internal_address_index", 0)
|
||||
|
||||
if ext_idx <= current_ext and int_idx <= current_int:
|
||||
return
|
||||
|
||||
if ext_idx > current_ext:
|
||||
chain_settings["electrum_address_index"] = ext_idx
|
||||
if int_idx > current_int:
|
||||
chain_settings["electrum_internal_address_index"] = int_idx
|
||||
|
||||
self.log.debug(
|
||||
f"Persisting electrum address index for {coin_name}: ext={ext_idx}, int={int_idx}"
|
||||
)
|
||||
self._saveSettings()
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
f"Failed to update electrum address index for {coin_name}: {e}"
|
||||
)
|
||||
|
||||
def _normalizeSettingsPaths(self, settings: dict) -> dict:
|
||||
if "chainclients" in settings:
|
||||
for coin_name, cc in settings["chainclients"].items():
|
||||
for path_key in ("datadir", "bindir", "walletsdir"):
|
||||
if path_key in cc and isinstance(cc[path_key], str):
|
||||
cc[path_key] = os.path.normpath(cc[path_key])
|
||||
return settings
|
||||
|
||||
def _saveSettings(self) -> None:
|
||||
from basicswap import config as cfg
|
||||
|
||||
self._normalizeSettingsPaths(self.settings)
|
||||
|
||||
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
|
||||
settings_path_new = settings_path + ".new"
|
||||
try:
|
||||
if os.path.exists(settings_path):
|
||||
shutil.copyfile(settings_path, settings_path + ".last")
|
||||
with open(settings_path_new, "w") as fp:
|
||||
json.dump(self.settings, fp, indent=4)
|
||||
shutil.move(settings_path_new, settings_path)
|
||||
self.log.debug(f"Settings saved to {settings_path}")
|
||||
except Exception as e:
|
||||
self.log.warning(f"Failed to save settings: {e}")
|
||||
|
||||
def setDaemonPID(self, name, pid) -> None:
|
||||
if isinstance(name, Coins):
|
||||
self.coin_clients[name]["pid"] = pid
|
||||
@@ -143,7 +244,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 +329,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(
|
||||
|
||||
@@ -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,12 @@ 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()
|
||||
LOCK_TX_B_RPC_ERROR = auto()
|
||||
LOCK_TX_A_SPEND_TX_SEEN = auto()
|
||||
LOCK_TX_B_SPEND_TX_SEEN = auto()
|
||||
LOCK_TX_B_REFUND_TX_SEEN = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -217,8 +240,11 @@ class DebugTypes(IntEnum):
|
||||
OFFER_LOCK_2_VALUE_INC = auto()
|
||||
BID_STOP_AFTER_COIN_B_LOCK = auto()
|
||||
BID_DONT_SPEND_COIN_B_LOCK = auto()
|
||||
WAIT_FOR_COIN_B_LOCK_BEFORE_PREREFUND = auto()
|
||||
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
|
||||
BID_DONT_SPEND_COIN_A_LOCK = auto()
|
||||
DONT_SEND_COIN_B_LOCK = auto()
|
||||
DONT_RELEASE_COIN_A_LOCK = auto()
|
||||
|
||||
|
||||
class NotificationTypes(IntEnum):
|
||||
@@ -226,6 +252,13 @@ class NotificationTypes(IntEnum):
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
SWAP_COMPLETED = auto()
|
||||
UPDATE_AVAILABLE = auto()
|
||||
SWEEP_COMPLETED = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
BID = 1
|
||||
|
||||
|
||||
class AutomationOverrideOptions(IntEnum):
|
||||
@@ -339,6 +372,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 +416,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 +446,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:
|
||||
@@ -428,8 +466,10 @@ def describeEventEntry(event_type, event_msg):
|
||||
return "Failed to publish lock tx B refund"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
||||
return "Detected invalid lock Tx B"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_RPC_ERROR:
|
||||
return "Temporary RPC error checking lock tx B: " + event_msg
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
||||
return "Lock tx A refund tx published"
|
||||
return "Lock tx A pre-refund tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
||||
return "Lock tx A refund spend tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
|
||||
@@ -441,7 +481,7 @@ def describeEventEntry(event_type, event_msg):
|
||||
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
|
||||
return "Lock tx B spend tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
|
||||
return "Lock tx A refund tx seen in chain"
|
||||
return "Lock tx A pre-refund tx seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
|
||||
return "Lock tx A refund spend tx seen in chain"
|
||||
if event_type == EventLogTypes.SYSTEM_WARNING:
|
||||
@@ -468,6 +508,12 @@ def describeEventEntry(event_type, event_msg):
|
||||
return "BCH mercy tx found"
|
||||
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
|
||||
return "Lock tx B mercy tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_SEEN:
|
||||
return "Lock tx A spend tx seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_SEEN:
|
||||
return "Lock tx B spend tx seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_SEEN:
|
||||
return "Lock tx B refund tx seen in chain"
|
||||
|
||||
|
||||
def getVoutByAddress(txjs, p2sh):
|
||||
@@ -581,6 +627,27 @@ 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,
|
||||
BidStates.BID_REQUEST_ACCEPTED,
|
||||
)
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
||||
return True
|
||||
|
||||
@@ -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,84 @@ 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():
|
||||
swap_client.stopRunning()
|
||||
try:
|
||||
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()
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
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 +125,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 +156,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 +187,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
opened_files,
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +203,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,12 +218,13 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
[file_stdout, file_stderr],
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
|
||||
args = [daemon_path, "--non-interactive"]
|
||||
args = [daemon_path]
|
||||
|
||||
needs_rewrite: bool = False
|
||||
config_to_remove = [
|
||||
@@ -200,7 +267,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 +282,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 +324,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 +374,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 +452,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 +500,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 +519,7 @@ def runClient(
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrd_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -412,7 +531,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 +548,7 @@ def runClient(
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrwallet_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -445,19 +565,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,76 +601,27 @@ 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()
|
||||
|
||||
if swap_client.ws_server:
|
||||
try:
|
||||
swap_client.log.info("Stopping websocket server.")
|
||||
swap_client.ws_server.shutdown_gracefully()
|
||||
except Exception as e: # noqa: F841
|
||||
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.error(f"Interrupting {d.name} {d.handle.pid}: {e}")
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
@@ -549,10 +630,12 @@ def runClient(
|
||||
fp.close()
|
||||
closed_pids.append(d.handle.pid)
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error: {e}")
|
||||
swap_client.log.error(
|
||||
f"Waiting for {d.name} {d.handle.pid} to shutdown: {e}"
|
||||
)
|
||||
|
||||
fail_code: int = swap_client.fail_code
|
||||
del swap_client
|
||||
swap_client = None
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
@@ -576,6 +659,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 +674,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 +690,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 +716,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 +759,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
|
||||
|
||||
@@ -242,7 +242,7 @@ chainparams = {
|
||||
"pubkey_address": 0x0E91,
|
||||
"script_address": 0x0E6C,
|
||||
"key_prefix": 0x2307,
|
||||
"bip44": 1,
|
||||
"bip44": 115,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
@@ -566,8 +566,12 @@ def getCoinIdFromTicker(ticker: str) -> str:
|
||||
raise ValueError(f"Unknown coin {ticker}")
|
||||
|
||||
|
||||
def getCoinIdFromName(name: str) -> str:
|
||||
def getCoinIdFromName(name: str) -> Coins:
|
||||
try:
|
||||
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
|
||||
|
||||
483
basicswap/db.py
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 28
|
||||
CURRENT_DB_DATA_VERSION = 6
|
||||
CURRENT_DB_VERSION = 34
|
||||
CURRENT_DB_DATA_VERSION = 8
|
||||
|
||||
|
||||
class Concepts(IntEnum):
|
||||
@@ -76,10 +76,16 @@ class Table:
|
||||
__sqlite3_table__ = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
init_all_columns: bool = True
|
||||
for name, value in kwargs.items():
|
||||
if name == "_init_all_columns":
|
||||
init_all_columns = value
|
||||
continue
|
||||
if not hasattr(self, name):
|
||||
raise ValueError(f"Unknown attribute {name}")
|
||||
setattr(self, name, value)
|
||||
if init_all_columns is False:
|
||||
return
|
||||
# Init any unset columns to None
|
||||
for mc in inspect.getmembers(self):
|
||||
mc_name, mc_obj = mc
|
||||
@@ -135,6 +141,20 @@ class Index:
|
||||
self.column_3 = column_3
|
||||
|
||||
|
||||
class StateRows:
|
||||
state = Column("integer")
|
||||
state_time = Column("integer") # Timestamp of last state change
|
||||
states = Column("blob") # Packed states and times
|
||||
|
||||
def setState(self, new_state, state_time=None):
|
||||
now = int(time.time()) if state_time is None else state_time
|
||||
self.state = new_state
|
||||
if self.isSet("states") is False:
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
|
||||
class DBKVInt(Table):
|
||||
__tablename__ = "kv_int"
|
||||
|
||||
@@ -149,7 +169,7 @@ class DBKVString(Table):
|
||||
value = Column("string")
|
||||
|
||||
|
||||
class Offer(Table):
|
||||
class Offer(Table, StateRows):
|
||||
__tablename__ = "offers"
|
||||
|
||||
offer_id = Column("blob", primary_key=True)
|
||||
@@ -185,6 +205,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,20 +215,10 @@ class Offer(Table):
|
||||
) # Address to spend lock tx to - address from wallet if empty TODO
|
||||
security_token = Column("blob")
|
||||
bid_reversed = Column("bool")
|
||||
|
||||
state = Column("integer")
|
||||
states = Column("blob") # Packed states and times
|
||||
|
||||
def setState(self, new_state):
|
||||
now = int(time.time())
|
||||
self.state = new_state
|
||||
if self.isSet("states") is False:
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += pack_state(new_state, now)
|
||||
smsg_payload_version = Column("integer")
|
||||
|
||||
|
||||
class Bid(Table):
|
||||
class Bid(Table, StateRows):
|
||||
__tablename__ = "bids"
|
||||
|
||||
bid_id = Column("blob", primary_key=True)
|
||||
@@ -219,6 +230,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 +244,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")
|
||||
@@ -240,11 +253,7 @@ class Bid(Table):
|
||||
participate_txn_refund = Column("blob")
|
||||
|
||||
in_progress = Column("integer")
|
||||
state = Column("integer")
|
||||
state_time = Column("integer") # Timestamp of last state change
|
||||
states = Column("blob") # Packed states and times
|
||||
|
||||
state_note = Column("string")
|
||||
was_sent = Column("bool") # Sent by node
|
||||
was_received = Column("bool")
|
||||
contract_count = Column("integer")
|
||||
@@ -283,25 +292,13 @@ class Bid(Table):
|
||||
if self.isSet("participate_tx"):
|
||||
self.participate_tx.setState(new_state)
|
||||
|
||||
def setState(self, new_state, state_note=None):
|
||||
now = int(time.time())
|
||||
self.state = new_state
|
||||
self.state_time = now
|
||||
|
||||
if self.isSet("state_note"):
|
||||
self.state_note = state_note
|
||||
if self.isSet("states") is False:
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
def getLockTXBVout(self):
|
||||
if self.isSet("xmr_b_lock_tx"):
|
||||
return self.xmr_b_lock_tx.vout
|
||||
return None
|
||||
|
||||
|
||||
class SwapTx(Table):
|
||||
class SwapTx(Table, StateRows):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
bid_id = Column("blob")
|
||||
@@ -324,21 +321,8 @@ class SwapTx(Table):
|
||||
block_height = Column("integer")
|
||||
block_time = Column("integer")
|
||||
|
||||
state = Column("integer")
|
||||
states = Column("blob") # Packed states and times
|
||||
|
||||
primary_key = PrimaryKeyConstraint("bid_id", "tx_type")
|
||||
|
||||
def setState(self, new_state):
|
||||
if self.state == new_state:
|
||||
return
|
||||
self.state = new_state
|
||||
now: int = int(time.time())
|
||||
if self.isSet("states") is False:
|
||||
self.states = pack_state(new_state, now)
|
||||
else:
|
||||
self.states += pack_state(new_state, now)
|
||||
|
||||
|
||||
class PrefundedTx(Table):
|
||||
__tablename__ = "prefunded_transactions"
|
||||
@@ -380,6 +364,8 @@ class SmsgAddress(Table):
|
||||
use_type = Column("integer")
|
||||
note = Column("string")
|
||||
|
||||
index = Index("smsgaddresses_address_index", "addr")
|
||||
|
||||
|
||||
class Action(Table):
|
||||
__tablename__ = "actions"
|
||||
@@ -485,6 +471,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 +599,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 +654,261 @@ 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(input_globals: dict = None) -> dict:
|
||||
g = (input_globals if input_globals else 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:
|
||||
from .db_wallet import extract_wallet_schema
|
||||
|
||||
db_schema = extract_schema()
|
||||
db_schema.update(extract_wallet_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:
|
||||
@@ -741,42 +916,63 @@ def create_db(db_path: str, log) -> None:
|
||||
|
||||
|
||||
class DBMethods:
|
||||
_db_lock_depth = 0
|
||||
|
||||
def _db_lock_held(self) -> bool:
|
||||
|
||||
if hasattr(self.mxDB, "_is_owned"):
|
||||
return self.mxDB._is_owned()
|
||||
return self.mxDB.locked()
|
||||
|
||||
def openDB(self, cursor=None):
|
||||
if cursor:
|
||||
# assert(self._thread_debug == threading.get_ident())
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
return cursor
|
||||
|
||||
if self._db_lock_held():
|
||||
self._db_lock_depth += 1
|
||||
return self._db_con.cursor()
|
||||
|
||||
self.mxDB.acquire()
|
||||
# self._thread_debug = threading.get_ident()
|
||||
self._db_lock_depth = 1
|
||||
self._db_con = sqlite3.connect(self.sqlite_file)
|
||||
|
||||
self._db_con.execute("PRAGMA busy_timeout = 30000")
|
||||
return self._db_con.cursor()
|
||||
|
||||
def getNewDBCursor(self):
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
return self._db_con.cursor()
|
||||
|
||||
def commitDB(self):
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
self._db_con.commit()
|
||||
|
||||
def rollbackDB(self):
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
self._db_con.rollback()
|
||||
|
||||
def closeDBCursor(self, cursor):
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
if cursor:
|
||||
cursor.close()
|
||||
|
||||
def closeDB(self, cursor, commit=True):
|
||||
assert self.mxDB.locked()
|
||||
assert self._db_lock_held()
|
||||
|
||||
if self._db_lock_depth > 1:
|
||||
if commit:
|
||||
self._db_con.commit()
|
||||
cursor.close()
|
||||
self._db_lock_depth -= 1
|
||||
return
|
||||
|
||||
if commit:
|
||||
self._db_con.commit()
|
||||
|
||||
cursor.close()
|
||||
self._db_con.close()
|
||||
self._db_lock_depth = 0
|
||||
self.mxDB.release()
|
||||
|
||||
def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None:
|
||||
@@ -866,9 +1062,9 @@ class DBMethods:
|
||||
)
|
||||
finally:
|
||||
if cursor is None:
|
||||
self.closeDB(use_cursor, commit=False)
|
||||
self.closeDB(use_cursor, commit=True)
|
||||
|
||||
def add(self, obj, cursor, upsert: bool = False):
|
||||
def add(self, obj, cursor, upsert: bool = False, columns_list=None):
|
||||
if cursor is None:
|
||||
raise ValueError("Cursor is null")
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
@@ -881,7 +1077,8 @@ class DBMethods:
|
||||
# See if the instance overwrote any class methods
|
||||
for mc in inspect.getmembers(obj.__class__):
|
||||
mc_name, mc_obj = mc
|
||||
|
||||
if columns_list is not None and mc_name not in columns_list:
|
||||
continue
|
||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||
continue
|
||||
|
||||
@@ -912,6 +1109,7 @@ class DBMethods:
|
||||
query += f"{key}=:{key}"
|
||||
|
||||
cursor.execute(query, values)
|
||||
return cursor.lastrowid
|
||||
|
||||
def query(
|
||||
self,
|
||||
@@ -921,6 +1119,7 @@ class DBMethods:
|
||||
order_by={},
|
||||
query_suffix=None,
|
||||
extra_query_data={},
|
||||
columns_list=None,
|
||||
):
|
||||
if cursor is None:
|
||||
raise ValueError("Cursor is null")
|
||||
@@ -933,6 +1132,8 @@ class DBMethods:
|
||||
|
||||
for mc in inspect.getmembers(table_class):
|
||||
mc_name, mc_obj = mc
|
||||
if columns_list is not None and mc_name not in columns_list:
|
||||
continue
|
||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||
continue
|
||||
if len(columns) > 0:
|
||||
@@ -963,8 +1164,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:
|
||||
@@ -998,6 +1202,7 @@ class DBMethods:
|
||||
order_by={},
|
||||
query_suffix=None,
|
||||
extra_query_data={},
|
||||
columns_list=None,
|
||||
):
|
||||
return firstOrNone(
|
||||
self.query(
|
||||
@@ -1007,10 +1212,11 @@ class DBMethods:
|
||||
order_by,
|
||||
query_suffix,
|
||||
extra_query_data,
|
||||
columns_list,
|
||||
)
|
||||
)
|
||||
|
||||
def updateDB(self, obj, cursor, constraints=[]):
|
||||
def updateDB(self, obj, cursor, constraints=[], columns_list=None):
|
||||
if cursor is None:
|
||||
raise ValueError("Cursor is null")
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
@@ -1020,9 +1226,10 @@ class DBMethods:
|
||||
query: str = f"UPDATE {table_name} SET "
|
||||
|
||||
values = {}
|
||||
constraint_values = {}
|
||||
set_columns = []
|
||||
for mc in inspect.getmembers(obj.__class__):
|
||||
mc_name, mc_obj = mc
|
||||
|
||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||
continue
|
||||
|
||||
@@ -1032,17 +1239,19 @@ class DBMethods:
|
||||
continue
|
||||
|
||||
if mc_name in constraints:
|
||||
values[mc_name] = m_obj
|
||||
constraint_values[mc_name] = m_obj
|
||||
continue
|
||||
if columns_list is not None and mc_name not in columns_list:
|
||||
continue
|
||||
|
||||
if len(values) > 0:
|
||||
query += ", "
|
||||
query += f"{mc_name} = :{mc_name}"
|
||||
set_columns.append(f"{mc_name} = :{mc_name}")
|
||||
values[mc_name] = m_obj
|
||||
|
||||
query += ", ".join(set_columns)
|
||||
query += " WHERE 1=1 "
|
||||
|
||||
for ck in constraints:
|
||||
query += f" AND {ck} = :{ck} "
|
||||
|
||||
values.update(constraint_values)
|
||||
cursor.execute(query, values)
|
||||
|
||||
@@ -12,13 +12,19 @@ from .db import (
|
||||
AutomationStrategy,
|
||||
BidState,
|
||||
Concepts,
|
||||
create_table,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
CURRENT_DB_VERSION,
|
||||
extract_schema,
|
||||
)
|
||||
|
||||
from .db_wallet import extract_wallet_schema
|
||||
|
||||
from .basicswap_util import (
|
||||
BidStates,
|
||||
canAcceptBidState,
|
||||
canExpireBidState,
|
||||
canTimeoutBidState,
|
||||
isActiveBidState,
|
||||
isErrorBidState,
|
||||
isFailingBidState,
|
||||
@@ -37,6 +43,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 +57,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 +71,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 +84,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,22 +111,34 @@ 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),
|
||||
},
|
||||
)
|
||||
if data_version > 0 and data_version < 8:
|
||||
cursor.execute(
|
||||
"UPDATE bidstates SET can_timeout = :can_timeout WHERE state_id = :state_id",
|
||||
{
|
||||
"can_timeout": 1,
|
||||
"state_id": int(BidStates.BID_REQUEST_ACCEPTED),
|
||||
},
|
||||
)
|
||||
if data_version > 0 and data_version < 4:
|
||||
for state in (
|
||||
BidStates.BID_REQUEST_SENT,
|
||||
@@ -138,313 +157,224 @@ 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:
|
||||
def upgradeDatabaseFromSchema(self, cursor, expect_schema):
|
||||
have_tables = {}
|
||||
|
||||
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
||||
tables = cursor.execute(query).fetchall()
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
if table_name in ("sqlite_sequence",):
|
||||
continue
|
||||
|
||||
have_table = {}
|
||||
have_columns = {}
|
||||
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
|
||||
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
|
||||
for column in columns:
|
||||
cid, name, data_type, notnull, default_value, primary_key = column
|
||||
have_columns[name] = {"type": data_type, "primary_key": primary_key}
|
||||
|
||||
have_table["columns"] = have_columns
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
|
||||
indices = cursor.fetchall()
|
||||
for index in indices:
|
||||
seq, index_name, unique, origin, partial = index
|
||||
|
||||
if origin == "pk": # 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)
|
||||
|
||||
|
||||
def upgradeDatabase(self, db_version: int):
|
||||
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()
|
||||
expect_schema.update(extract_wallet_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":
|
||||
continue
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
|
||||
index_info = cursor.fetchall()
|
||||
|
||||
add_index = {"index_name": index_name}
|
||||
for index_columns in index_info:
|
||||
seqno, cid, name = index_columns
|
||||
if origin == "u":
|
||||
have_columns[name]["unique"] = 1
|
||||
else:
|
||||
if "column_1" not in add_index:
|
||||
add_index["column_1"] = name
|
||||
elif "column_2" not in add_index:
|
||||
add_index["column_2"] = name
|
||||
elif "column_3" not in add_index:
|
||||
add_index["column_3"] = name
|
||||
else:
|
||||
raise RuntimeError("Add more index columns.")
|
||||
if origin == "c":
|
||||
if "indices" not in have_table:
|
||||
have_table["indices"] = []
|
||||
have_table["indices"].append(add_index)
|
||||
|
||||
have_tables[table_name] = have_table
|
||||
|
||||
for table_name, table in expect_schema.items():
|
||||
if table_name not in have_tables:
|
||||
self.log.info(f"Creating table {table_name}.")
|
||||
create_table(cursor, table_name, table)
|
||||
continue
|
||||
|
||||
have_table = have_tables[table_name]
|
||||
have_columns = have_table["columns"]
|
||||
for colname, column in table["columns"].items():
|
||||
if colname not in have_columns:
|
||||
col_type = column["type"]
|
||||
self.log.info(f"Adding column {colname} to table {table_name}.")
|
||||
cursor.execute(
|
||||
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
|
||||
)
|
||||
indices = table.get("indices", [])
|
||||
have_indices = have_table.get("indices", [])
|
||||
for index in indices:
|
||||
index_name = index["index_name"]
|
||||
if not any(
|
||||
have_idx.get("index_name") == index_name
|
||||
for have_idx in have_indices
|
||||
):
|
||||
self.log.info(f"Adding index {index_name} to table {table_name}.")
|
||||
column_1 = index["column_1"]
|
||||
column_2 = index.get("column_2", None)
|
||||
column_3 = index.get("column_3", None)
|
||||
query: str = (
|
||||
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||
)
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ")"
|
||||
cursor.execute(query)
|
||||
|
||||
if CURRENT_DB_VERSION != db_version:
|
||||
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(
|
||||
|
||||
126
basicswap/db_wallet.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
from .db import Column, Index, Table, UniqueConstraint, extract_schema
|
||||
|
||||
|
||||
class WalletAddress(Table):
|
||||
|
||||
__tablename__ = "wallet_addresses"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_type = Column("integer")
|
||||
derivation_index = Column("integer")
|
||||
is_internal = Column("bool")
|
||||
derivation_path = Column("string")
|
||||
address = Column("string")
|
||||
scripthash = Column("string")
|
||||
pubkey = Column("blob")
|
||||
is_funded = Column("bool")
|
||||
cached_balance = Column("integer")
|
||||
cached_balance_time = Column("integer")
|
||||
first_seen_height = Column("integer")
|
||||
created_at = Column("integer")
|
||||
|
||||
__unique_1__ = UniqueConstraint("coin_type", "derivation_index", "is_internal")
|
||||
__index_address__ = Index("idx_wallet_address", "address")
|
||||
__index_scripthash__ = Index("idx_wallet_scripthash", "scripthash")
|
||||
__index_funded__ = Index("idx_wallet_funded", "coin_type", "is_funded")
|
||||
|
||||
|
||||
class WalletState(Table):
|
||||
|
||||
__tablename__ = "wallet_state"
|
||||
|
||||
coin_type = Column("integer", primary_key=True)
|
||||
last_external_index = Column("integer")
|
||||
last_internal_index = Column("integer")
|
||||
derivation_path_type = Column("string")
|
||||
last_sync_height = Column("integer")
|
||||
migration_complete = Column("bool")
|
||||
created_at = Column("integer")
|
||||
updated_at = Column("integer")
|
||||
|
||||
|
||||
class WalletWatchOnly(Table):
|
||||
|
||||
__tablename__ = "wallet_watch_only"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_type = Column("integer")
|
||||
address = Column("string")
|
||||
scripthash = Column("string")
|
||||
label = Column("string")
|
||||
source = Column("string")
|
||||
is_funded = Column("bool")
|
||||
cached_balance = Column("integer")
|
||||
cached_balance_time = Column("integer")
|
||||
private_key_encrypted = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
__unique_1__ = UniqueConstraint("coin_type", "address")
|
||||
__index_watch_address__ = Index("idx_watch_address", "address")
|
||||
__index_watch_scripthash__ = Index("idx_watch_scripthash", "scripthash")
|
||||
|
||||
|
||||
class WalletLockedUTXO(Table):
|
||||
|
||||
__tablename__ = "wallet_locked_utxos"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_type = Column("integer")
|
||||
txid = Column("string")
|
||||
vout = Column("integer")
|
||||
value = Column("integer")
|
||||
address = Column("string")
|
||||
bid_id = Column("blob")
|
||||
locked_at = Column("integer")
|
||||
expires_at = Column("integer")
|
||||
|
||||
__unique_1__ = UniqueConstraint("coin_type", "txid", "vout")
|
||||
__index_locked_coin__ = Index("idx_locked_coin", "coin_type")
|
||||
__index_locked_bid__ = Index("idx_locked_bid", "bid_id")
|
||||
|
||||
|
||||
class WalletTxCache(Table):
|
||||
|
||||
__tablename__ = "wallet_tx_cache"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_type = Column("integer")
|
||||
txid = Column("string")
|
||||
block_height = Column("integer")
|
||||
confirmations = Column("integer")
|
||||
tx_data = Column("blob")
|
||||
cached_at = Column("integer")
|
||||
expires_at = Column("integer")
|
||||
|
||||
__unique_1__ = UniqueConstraint("coin_type", "txid")
|
||||
__index_tx_cache__ = Index("idx_tx_cache", "coin_type", "txid")
|
||||
|
||||
|
||||
class WalletPendingTx(Table):
|
||||
|
||||
__tablename__ = "wallet_pending_txs"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_type = Column("integer")
|
||||
txid = Column("string")
|
||||
tx_type = Column("string")
|
||||
amount = Column("integer")
|
||||
fee = Column("integer")
|
||||
addresses = Column("string")
|
||||
bid_id = Column("blob")
|
||||
first_seen = Column("integer")
|
||||
confirmed_at = Column("integer")
|
||||
|
||||
__unique_1__ = UniqueConstraint("coin_type", "txid")
|
||||
__index_pending_coin__ = Index("idx_pending_coin", "coin_type", "confirmed_at")
|
||||
|
||||
|
||||
def extract_wallet_schema() -> dict:
|
||||
return extract_schema(input_globals=globals())
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -6,26 +6,28 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import gzip
|
||||
import json
|
||||
import shlex
|
||||
import hashlib
|
||||
import secrets
|
||||
import traceback
|
||||
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
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from . import __version__
|
||||
from .util import (
|
||||
dumpj,
|
||||
toBool,
|
||||
LockedCoinError,
|
||||
format_timestamp,
|
||||
)
|
||||
from .chainparams import (
|
||||
@@ -54,6 +56,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 +172,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 +199,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 +221,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 +276,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 +386,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 +436,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 +651,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 +724,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,14 +794,17 @@ 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))
|
||||
|
||||
if page == "static":
|
||||
try:
|
||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||
content = None
|
||||
mime_type = ""
|
||||
filepath = ""
|
||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||
@@ -761,9 +837,71 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if mime_type == "" or not filepath:
|
||||
raise ValueError("Unknown file type or path")
|
||||
|
||||
file_stat = os.stat(filepath)
|
||||
mtime = file_stat.st_mtime
|
||||
file_size = file_stat.st_size
|
||||
|
||||
etag_hash = hashlib.md5(f"{file_size}-{mtime}".encode()).hexdigest()
|
||||
etag = f'"{etag_hash}"'
|
||||
last_modified = formatdate(mtime, usegmt=True)
|
||||
|
||||
if_none_match = self.headers.get("If-None-Match")
|
||||
if if_none_match:
|
||||
if if_none_match.strip() == "*" or etag in [
|
||||
t.strip() for t in if_none_match.split(",")
|
||||
]:
|
||||
self.send_response(304)
|
||||
self.send_header("ETag", etag)
|
||||
self.send_header("Cache-Control", "public")
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
if_modified_since = self.headers.get("If-Modified-Since")
|
||||
if if_modified_since and not if_none_match:
|
||||
try:
|
||||
ims_time = parsedate_to_datetime(if_modified_since)
|
||||
file_time = datetime.fromtimestamp(int(mtime), tz=timezone.utc)
|
||||
if file_time <= ims_time:
|
||||
self.send_response(304)
|
||||
self.send_header("Last-Modified", last_modified)
|
||||
self.send_header("Cache-Control", "public")
|
||||
self.end_headers()
|
||||
return b""
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
is_lib = len(url_split) > 4 and url_split[3] == "libs"
|
||||
if is_lib:
|
||||
cache_control = "public, max-age=31536000, immutable"
|
||||
elif url_split[2] in ("css", "js"):
|
||||
cache_control = "public, max-age=3600, must-revalidate"
|
||||
elif url_split[2] in ("images", "sequence_diagrams"):
|
||||
cache_control = "public, max-age=86400"
|
||||
else:
|
||||
cache_control = "public, max-age=3600"
|
||||
|
||||
with open(filepath, "rb") as fp:
|
||||
content = fp.read()
|
||||
self.putHeaders(status_code, mime_type)
|
||||
|
||||
extra_headers = [
|
||||
("Cache-Control", cache_control),
|
||||
("Last-Modified", last_modified),
|
||||
("ETag", etag),
|
||||
]
|
||||
|
||||
is_compressible = mime_type in (
|
||||
"text/css; charset=utf-8",
|
||||
"application/javascript",
|
||||
"image/svg+xml",
|
||||
)
|
||||
accept_encoding = self.headers.get("Accept-Encoding", "")
|
||||
if is_compressible and "gzip" in accept_encoding:
|
||||
content = gzip.compress(content)
|
||||
extra_headers.append(("Content-Encoding", "gzip"))
|
||||
extra_headers.append(("Vary", "Accept-Encoding"))
|
||||
|
||||
extra_headers.append(("Content-Length", str(len(content))))
|
||||
self.putHeaders(status_code, mime_type, extra_headers=extra_headers)
|
||||
return content
|
||||
|
||||
except FileNotFoundError:
|
||||
@@ -779,6 +917,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 +953,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 +967,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 +1021,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 +1055,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 +1073,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()
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Copyright (c) 2025-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import threading
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
from basicswap.chainparams import (
|
||||
chainparams,
|
||||
@@ -30,6 +31,7 @@ from basicswap.util.ecc import (
|
||||
)
|
||||
from coincurve.dleag import verify_secp256k1_point
|
||||
from coincurve.keys import (
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
|
||||
@@ -53,6 +55,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.
|
||||
@@ -178,13 +181,16 @@ class CoinInterface:
|
||||
|
||||
|
||||
class AdaptorSigInterface:
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
def getP2WPKHDummyWitness(self) -> List[bytes]:
|
||||
return [bytes(72), bytes(33)]
|
||||
|
||||
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [b"", bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
|
||||
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), b"", bytes(len(script))]
|
||||
|
||||
|
||||
@@ -226,8 +232,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
||||
return pubkey.verify(sig, signed_hash, hasher=None)
|
||||
|
||||
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
|
||||
# TODO: Add to coincurve
|
||||
return i2b((b2i(ka) + b2i(kb)) % ep.o)
|
||||
return PrivateKey(ka).add(kb).secret
|
||||
|
||||
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
|
||||
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -10,7 +10,6 @@ from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, C
|
||||
from basicswap.util import b2i, ensure, i2b
|
||||
from basicswap.util.script import decodePushData, decodeScriptNum
|
||||
from .btc import BTCInterface, ensure_op, findOutput
|
||||
from basicswap.rpc import make_rpc_func
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
|
||||
from basicswap.util.crypto import hash160, sha256
|
||||
@@ -74,11 +73,7 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
|
||||
# No multiwallet support
|
||||
self.swap_client = swap_client
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
|
||||
def has_segwit(self) -> bool:
|
||||
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
|
||||
@@ -817,7 +812,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -132,7 +132,7 @@ class DASHInterface(BTCInterface):
|
||||
self.unlockWallet(old_password, check_seed=False)
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [new_password])
|
||||
self.rpc_wallet("encryptwallet", [new_password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
@@ -156,4 +156,6 @@ class DASHInterface(BTCInterface):
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
self.rpc_wallet(
|
||||
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
@@ -186,6 +188,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
def coin_type():
|
||||
return Coins.DCR
|
||||
|
||||
@staticmethod
|
||||
def useBackend() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def exp() -> int:
|
||||
return 8
|
||||
@@ -362,7 +368,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Read initial pwd from settings
|
||||
settings = self._sc.getChainClientSettings(self.coin_type())
|
||||
old_password = settings["wallet_pwd"]
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
self.rpc_wallet(
|
||||
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||
)
|
||||
|
||||
# Lock wallet to match other coins
|
||||
self.rpc_wallet("walletlock")
|
||||
@@ -376,7 +384,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -404,7 +412,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Adjust verificationprogress to consider blocks wallet has synced
|
||||
wallet_blocks = self.rpc_wallet("getinfo")["blocks"]
|
||||
synced_ind = bci["verificationprogress"]
|
||||
wallet_synced_ind = wallet_blocks / bci["headers"]
|
||||
if bci["headers"] < 1:
|
||||
wallet_synced_ind = 0
|
||||
else:
|
||||
wallet_synced_ind = wallet_blocks / bci["headers"]
|
||||
if wallet_synced_ind < synced_ind:
|
||||
bci["verificationprogress"] = wallet_synced_ind
|
||||
|
||||
@@ -627,6 +638,15 @@ class DCRInterface(Secp256k1Interface):
|
||||
# TODO: filter errors
|
||||
return None
|
||||
|
||||
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
|
||||
try:
|
||||
return self.rpc_wallet(
|
||||
"listtransactions", ["*", count, skip, include_watchonly]
|
||||
)
|
||||
except Exception as e:
|
||||
self._log.error(f"listWalletTransactions failed: {e}")
|
||||
return []
|
||||
|
||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
|
||||
@@ -1050,6 +1070,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
def describeTx(self, tx_hex: str):
|
||||
return self.rpc("decoderawtransaction", [tx_hex])
|
||||
|
||||
def decodeRawTransaction(self, tx_hex: str):
|
||||
return self.rpc("decoderawtransaction", [tx_hex])
|
||||
|
||||
def fundTx(self, tx: bytes, feerate) -> bytes:
|
||||
feerate_str = float(self.format_amount(feerate))
|
||||
# TODO: unlock unspents if bid cancelled
|
||||
@@ -1085,22 +1108,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,))
|
||||
|
||||
@@ -1381,7 +1403,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
)
|
||||
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
raise ValueError("Bad fee rate, expected: {}".format(feerate))
|
||||
raise ValueError(f"Bad fee rate, expected: {feerate}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -1450,7 +1472,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
)
|
||||
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
raise ValueError("Bad fee rate, expected: {}".format(feerate))
|
||||
raise ValueError(f"Bad fee rate, expected: {feerate}")
|
||||
|
||||
return txid, locked_coin, locked_n
|
||||
|
||||
@@ -1511,7 +1533,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
)
|
||||
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
raise ValueError("Bad fee rate, expected: {}".format(feerate))
|
||||
raise ValueError(f"Bad fee rate, expected: {feerate}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -1609,11 +1631,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)
|
||||
@@ -1719,15 +1741,19 @@ class DCRInterface(Secp256k1Interface):
|
||||
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||
return tx.serialize()
|
||||
|
||||
def publishBLockTx(
|
||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
||||
) -> bytes:
|
||||
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||
|
||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
||||
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
funded_tx = self.loadTx(b_lock_tx)
|
||||
lock_vout = findOutput(funded_tx, script_pk)
|
||||
|
||||
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
||||
|
||||
return bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
txid = bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
return txid, lock_vout
|
||||
|
||||
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||
witness_bytes = 115
|
||||
@@ -1751,26 +1777,53 @@ class DCRInterface(Secp256k1Interface):
|
||||
lock_tx_vout=None,
|
||||
) -> bytes:
|
||||
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
||||
locked_n = lock_tx_vout
|
||||
|
||||
Kbs = self.getPubkey(kbs)
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
|
||||
if locked_n is None:
|
||||
self._log.debug(
|
||||
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}"
|
||||
locked_n = None
|
||||
actual_value = None
|
||||
wtx = self.rpc_wallet(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
if locked_n is not None:
|
||||
actual_value = lock_tx.vout[locked_n].value
|
||||
else:
|
||||
self._log.error(
|
||||
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
|
||||
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
|
||||
)
|
||||
# When refunding a lock tx, it should be in the wallet as a sent tx
|
||||
wtx = self.rpc_wallet(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
for i, out in enumerate(lock_tx.vout):
|
||||
self._log.debug(
|
||||
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||
)
|
||||
|
||||
if (
|
||||
locked_n is not None
|
||||
and lock_tx_vout is not None
|
||||
and locked_n != lock_tx_vout
|
||||
):
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
|
||||
f"for tx {chain_b_lock_txid.hex()}"
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
|
||||
spend_value = cb_swap_value
|
||||
if spend_actual_balance and actual_value is not None:
|
||||
if actual_value != cb_swap_value:
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Spending actual balance {actual_value}, "
|
||||
f"not expected swap value {cb_swap_value}."
|
||||
)
|
||||
spend_value = actual_value
|
||||
|
||||
pkh_to = self.decodeAddress(address_to)
|
||||
|
||||
tx = CTransaction()
|
||||
@@ -1779,10 +1832,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
||||
|
||||
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
|
||||
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to)))
|
||||
tx.vout.append(self.txoType()(spend_value, self.getPubkeyHashDest(pkh_to)))
|
||||
|
||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||
tx.vout[0].value = cb_swap_value - pay_fee
|
||||
tx.vout[0].value = spend_value - pay_fee
|
||||
|
||||
b_lock_spend_tx = tx.serialize()
|
||||
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,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.
|
||||
|
||||
@@ -35,6 +36,13 @@ def createDCRWallet(args, hex_seed, logging, delay_event):
|
||||
response = b"y\n"
|
||||
elif "Enter existing wallet seed" in buf:
|
||||
response = (hex_seed + "\n").encode("utf-8")
|
||||
elif "Do you have a wallet birthday we should rescan from" in buf:
|
||||
response = b"no\n"
|
||||
elif (
|
||||
"Do you have an additional account to import from an extended public key"
|
||||
in buf
|
||||
):
|
||||
response = b"no\n"
|
||||
elif "Seed input successful" in buf:
|
||||
pass
|
||||
elif "Upgrading database from version" in buf:
|
||||
|
||||
1305
basicswap/interface/electrumx.py
Normal file
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -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")
|
||||
@@ -63,7 +64,7 @@ class FIROInterface(BTCInterface):
|
||||
# Firo shuts down after encryptwallet
|
||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
@@ -101,6 +102,100 @@ class FIROInterface(BTCInterface):
|
||||
return addr_info["ismine"]
|
||||
return addr_info["ismine"] or addr_info["iswatchonly"]
|
||||
|
||||
def getNewSparkAddress(self) -> str:
|
||||
try:
|
||||
return self.rpc_wallet("getnewsparkaddress")[0]
|
||||
except Exception as e:
|
||||
self._log.error(f"getnewsparkaddress failed: {str(e)}")
|
||||
raise
|
||||
|
||||
def getNewStealthAddress(self):
|
||||
"""Get a new Spark address (alias for consistency with other coins)."""
|
||||
return self.getNewSparkAddress()
|
||||
|
||||
def getWalletInfo(self):
|
||||
"""Get wallet info including Spark balance."""
|
||||
rv = super(FIROInterface, self).getWalletInfo()
|
||||
try:
|
||||
spark_balance_info = self.rpc("getsparkbalance")
|
||||
# getsparkbalance returns amounts in atomic units (satoshis)
|
||||
# Field names: availableBalance, unconfirmedBalance, fullBalance
|
||||
confirmed = spark_balance_info.get("availableBalance", 0)
|
||||
unconfirmed = spark_balance_info.get("unconfirmedBalance", 0)
|
||||
full_balance = spark_balance_info.get("fullBalance", 0)
|
||||
# Values are already in atomic units, keep as integers
|
||||
# basicswap.py will format them using format_amount
|
||||
rv["spark_balance"] = confirmed if confirmed else 0
|
||||
rv["spark_unconfirmed"] = unconfirmed if unconfirmed else 0
|
||||
immature = full_balance - confirmed - unconfirmed
|
||||
rv["spark_immature"] = immature if immature > 0 else 0
|
||||
except Exception as e:
|
||||
self._log.warning(f"getsparkbalance failed: {str(e)}")
|
||||
rv["spark_balance"] = 0
|
||||
rv["spark_unconfirmed"] = 0
|
||||
rv["spark_immature"] = 0
|
||||
return rv
|
||||
|
||||
def createUTXO(self, value_sats: int):
|
||||
# Create a new address and send value_sats to it
|
||||
|
||||
spendable_balance = self.getSpendableBalance()
|
||||
if spendable_balance < value_sats:
|
||||
raise ValueError("Balance too low")
|
||||
|
||||
address = self.getNewAddress(self._use_segwit, "create_utxo")
|
||||
return (
|
||||
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
|
||||
address,
|
||||
)
|
||||
|
||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||
"""Withdraw coins, supporting both transparent and Spark transactions.
|
||||
|
||||
Args:
|
||||
value: Amount to withdraw
|
||||
type_from: "plain" for transparent, "spark" for Spark
|
||||
addr_to: Destination address
|
||||
subfee: Whether to subtract fee from amount
|
||||
"""
|
||||
type_to = "spark" if addr_to.startswith("sm1") else "plain"
|
||||
|
||||
if "spark" in (type_from, type_to):
|
||||
# RPC format: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}}
|
||||
# RPC wrapper will serialize this as: {"method": "spendspark", "params": [{...}], ...}
|
||||
try:
|
||||
if type_from == "spark":
|
||||
# Construct params: dict where address is the key, wrapped in array for RPC
|
||||
params = [
|
||||
{"address": addr_to, "amount": value, "subtractfee": subfee}
|
||||
]
|
||||
result = self.rpc_wallet("spendspark", params)
|
||||
else:
|
||||
# Use automintspark to perform a plain -> spark tx of full balance
|
||||
balance = self.rpc_wallet("getbalance")
|
||||
if str(balance) == str(value):
|
||||
result = self.rpc_wallet("automintspark")
|
||||
else:
|
||||
# subfee param is available on plain -> spark transactions
|
||||
mint_params = {"amount": value}
|
||||
if subfee:
|
||||
mint_params["subfee"] = True
|
||||
params = [{addr_to: mint_params}]
|
||||
result = self.rpc_wallet("mintspark", params)
|
||||
# spendspark returns a txid string directly, in a result dict, or as an array
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
return result[0]
|
||||
if isinstance(result, dict):
|
||||
return result.get("txid", result.get("tx", ""))
|
||||
return result
|
||||
except Exception as e:
|
||||
self._log.error(f"spark tx failed: {str(e)}")
|
||||
raise
|
||||
else:
|
||||
# Use standard sendtoaddress for transparent transactions
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc_wallet("sendtoaddress", params)
|
||||
|
||||
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||
lock_tx_dest = self.getScriptDest(lock_script)
|
||||
address = self.encodeScriptDest(lock_tx_dest)
|
||||
@@ -251,10 +346,6 @@ class FIROInterface(BTCInterface):
|
||||
assert len(script_hash) == 20
|
||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc("sendtoaddress", params)
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||
|
||||
|
||||
@@ -26,13 +26,103 @@ class LTCInterface(BTCInterface):
|
||||
wallet=self._rpc_wallet_mweb,
|
||||
)
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
if self._connection_type == "electrum":
|
||||
wm = self.getWalletManager()
|
||||
if wm and wm.isInitialized(self.coin_type()):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
wallets = self.rpc("listwallets")
|
||||
|
||||
if self._rpc_wallet not in wallets:
|
||||
self._log.debug(
|
||||
f"Wallet: {self._rpc_wallet} not active, attempting to load."
|
||||
)
|
||||
try:
|
||||
self.rpc(
|
||||
"loadwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
],
|
||||
)
|
||||
wallets = self.rpc("listwallets")
|
||||
except Exception as e:
|
||||
self._log.debug(f'Error loading wallet "{self._rpc_wallet}": {e}.')
|
||||
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||
try:
|
||||
wallet_dirs = self.rpc("listwalletdir")
|
||||
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
|
||||
except Exception:
|
||||
existing = []
|
||||
if len(existing) == 0:
|
||||
self._log.info(
|
||||
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||
)
|
||||
try:
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
False,
|
||||
True,
|
||||
"",
|
||||
False,
|
||||
self._use_descriptors,
|
||||
],
|
||||
)
|
||||
wallets = self.rpc("listwallets")
|
||||
if self.getWalletSeedID() == "Not found":
|
||||
self._log.info(
|
||||
f"Initializing HD seed for {self.coin_name()}."
|
||||
)
|
||||
self._sc.initialiseWallet(self.coin_type())
|
||||
except Exception as create_e:
|
||||
self._log.error(f"Error creating wallet: {create_e}")
|
||||
|
||||
if self._rpc_wallet not in wallets and len(wallets) > 0:
|
||||
self._log.warning(f"Changing {self.ticker()} wallet name.")
|
||||
for wallet_name in wallets:
|
||||
if wallet_name in ("mweb",):
|
||||
continue
|
||||
|
||||
change_watchonly_wallet: bool = (
|
||||
self._rpc_wallet_watch == self._rpc_wallet
|
||||
)
|
||||
|
||||
self._rpc_wallet = wallet_name
|
||||
self._log.info(
|
||||
f"Switched {self.ticker()} wallet name to {self._rpc_wallet}."
|
||||
)
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport,
|
||||
self._rpcauth,
|
||||
host=self._rpc_host,
|
||||
wallet=self._rpc_wallet,
|
||||
)
|
||||
if change_watchonly_wallet:
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
break
|
||||
|
||||
return len(wallets)
|
||||
|
||||
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||
if self.useBackend():
|
||||
raise ValueError("MWEB addresses not supported in electrum mode")
|
||||
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
|
||||
|
||||
def getNewStealthAddress(self, label=""):
|
||||
if self.useBackend():
|
||||
raise ValueError("MWEB addresses not supported in electrum mode")
|
||||
return self.getNewMwebAddress(False, label)
|
||||
|
||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||
if self.useBackend():
|
||||
if type_from == "mweb":
|
||||
raise ValueError("MWEB withdrawals not supported in electrum mode")
|
||||
return self._withdrawCoinElectrum(value, addr_to, subfee)
|
||||
|
||||
params = [addr_to, value, "", "", subfee, True, self._conf_target]
|
||||
if type_from == "mweb":
|
||||
return self.rpc_wallet_mweb("sendtoaddress", params)
|
||||
@@ -53,14 +143,27 @@ class LTCInterface(BTCInterface):
|
||||
|
||||
def getWalletInfo(self):
|
||||
rv = super(LTCInterface, self).getWalletInfo()
|
||||
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
||||
rv["mweb_balance"] = mweb_info["balance"]
|
||||
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
||||
rv["mweb_immature"] = mweb_info["immature_balance"]
|
||||
if not self.useBackend():
|
||||
try:
|
||||
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
||||
rv["mweb_balance"] = mweb_info["balance"]
|
||||
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
||||
rv["mweb_immature"] = mweb_info["immature_balance"]
|
||||
except Exception:
|
||||
pass
|
||||
return rv
|
||||
|
||||
def getUnspentsByAddr(self):
|
||||
unspent_addr = dict()
|
||||
|
||||
if self.useBackend():
|
||||
wm = self.getWalletManager()
|
||||
if wm:
|
||||
addresses = wm.getAllAddresses(self.coin_type())
|
||||
if addresses:
|
||||
return self._backend.getBalance(addresses)
|
||||
return unspent_addr
|
||||
|
||||
unspent = self.rpc_wallet("listunspent")
|
||||
for u in unspent:
|
||||
if u.get("spendable", False) is False:
|
||||
@@ -86,6 +189,82 @@ class LTCInterface(BTCInterface):
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
if password == "":
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
if self.useBackend():
|
||||
return
|
||||
|
||||
wallets = self.rpc("listwallets")
|
||||
if self._rpc_wallet not in wallets:
|
||||
try:
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
except Exception as e:
|
||||
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||
try:
|
||||
wallet_dirs = self.rpc("listwalletdir")
|
||||
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
|
||||
except Exception:
|
||||
existing = []
|
||||
if len(existing) == 0:
|
||||
self._log.info(
|
||||
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||
)
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
False,
|
||||
True,
|
||||
password,
|
||||
False,
|
||||
self._use_descriptors,
|
||||
],
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
seed_id = self.getWalletSeedID()
|
||||
needs_seed_init = seed_id == "Not found"
|
||||
except Exception as e:
|
||||
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||
needs_seed_init = True
|
||||
if needs_seed_init:
|
||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||
self._sc.initialiseWallet(self.coin_type())
|
||||
if password:
|
||||
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||
try:
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
except Exception as e:
|
||||
self._log.debug(f"encryptwallet returned: {e}")
|
||||
import time
|
||||
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
try:
|
||||
self.rpc("listwallets")
|
||||
break
|
||||
except Exception:
|
||||
self._log.debug(
|
||||
f"Waiting for wallet after encryption... {i + 1}/10"
|
||||
)
|
||||
wallets = self.rpc("listwallets")
|
||||
if self._rpc_wallet not in wallets:
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
self.setWalletSeedWarning(False)
|
||||
check_seed = False
|
||||
|
||||
if self.isWalletEncrypted():
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
|
||||
class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
@@ -98,6 +277,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]
|
||||
@@ -128,13 +308,50 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
self._log.info("init_wallet - {}".format(self.ticker()))
|
||||
|
||||
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
|
||||
wallets = self.rpc("listwallets")
|
||||
if self._rpc_wallet not in wallets:
|
||||
try:
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".')
|
||||
except Exception as e:
|
||||
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||
self._log.info(
|
||||
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
|
||||
)
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
[
|
||||
self._rpc_wallet,
|
||||
False,
|
||||
True,
|
||||
password,
|
||||
False,
|
||||
self._use_descriptors,
|
||||
],
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
wallets = self.rpc("listwallets")
|
||||
if "mweb" not in wallets:
|
||||
try:
|
||||
self.rpc("loadwallet", ["mweb"])
|
||||
self._log.debug("Loaded existing MWEB wallet.")
|
||||
except Exception as e:
|
||||
if "does not exist" in str(e) or "Path does not exist" in str(e):
|
||||
self._log.info(f"Creating MWEB wallet for {self.coin_name()}.")
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||
self.rpc(
|
||||
"createwallet",
|
||||
["mweb", False, True, password, False, False, True],
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
if password is not None:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
|
||||
if self.getWalletSeedID() == "Not found":
|
||||
self._sc.initialiseWallet(self.interface_type())
|
||||
@@ -143,7 +360,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc("unloadwallet", ["mweb"])
|
||||
self.rpc("loadwallet", ["mweb"])
|
||||
if password is not None:
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
@@ -151,10 +368,22 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
if self.useBackend():
|
||||
return
|
||||
|
||||
if not self.has_mweb_wallet():
|
||||
self.init_wallet(password)
|
||||
else:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
try:
|
||||
seed_id = self.getWalletSeedID()
|
||||
needs_seed_init = seed_id == "Not found"
|
||||
except Exception as e:
|
||||
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||
needs_seed_init = True
|
||||
if needs_seed_init:
|
||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||
self._sc.initialiseWallet(self.interface_type())
|
||||
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
self._sc.checkWalletSeed(self.interface_type())
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
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,15 +135,27 @@ 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 self.getP2WPKHDummyWitness()
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
|
||||
|
||||
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
||||
|
||||
def getWitnessStackSerialisedLength(self, witness_stack) -> int:
|
||||
length: int = getCompactSizeLen(len(witness_stack))
|
||||
for e in witness_stack:
|
||||
length += getWitnessElementLen(len(e))
|
||||
length: int = 0
|
||||
if len(witness_stack) > 0 and isinstance(witness_stack[0], list):
|
||||
for input_stack in witness_stack:
|
||||
length += getCompactSizeLen(len(input_stack))
|
||||
for e in input_stack:
|
||||
length += getWitnessElementLen(len(e))
|
||||
else:
|
||||
length += getCompactSizeLen(len(witness_stack))
|
||||
for e in witness_stack:
|
||||
length += getWitnessElementLen(len(e))
|
||||
return length
|
||||
|
||||
def getWalletRestoreHeight(self) -> int:
|
||||
@@ -189,6 +200,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 +243,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 +297,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 +371,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 +521,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 +545,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 +622,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")
|
||||
@@ -608,7 +658,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
|
||||
ensure(
|
||||
self.compareFeeRates(fee_rate_paid, feerate),
|
||||
"Bad fee rate, expected: {}".format(feerate),
|
||||
f"Bad fee rate, expected: {feerate}",
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -680,10 +730,11 @@ 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),
|
||||
"Bad fee rate, expected: {}".format(feerate),
|
||||
f"Bad fee rate, expected: {feerate}",
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -907,7 +958,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
self._log.info("vsize, feerate: %ld, %ld", vsize, fee_rate_paid)
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
raise ValueError("Bad fee rate, expected: {}".format(feerate))
|
||||
raise ValueError(f"Bad fee rate, expected: {feerate}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -1031,10 +1082,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 +1117,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 +1342,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 +1381,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,13 +33,14 @@ 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
|
||||
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
|
||||
@@ -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,18 @@ 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",
|
||||
"busy",
|
||||
"responsenotready",
|
||||
"connection",
|
||||
]
|
||||
):
|
||||
return True
|
||||
return super().is_transient_error(ex)
|
||||
|
||||
@@ -146,6 +160,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 +303,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 +338,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 +405,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 +413,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 +423,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 +473,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 +542,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 +561,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}"
|
||||
)
|
||||
@@ -811,3 +835,25 @@ class XMRInterface(CoinInterface):
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
|
||||
try:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
rv = self.rpc_wallet(
|
||||
"get_transfers",
|
||||
{"in": True, "out": True, "pending": True, "failed": True},
|
||||
)
|
||||
transactions = []
|
||||
for tx_type in ["in", "out", "pending", "failed"]:
|
||||
if tx_type in rv:
|
||||
for tx in rv[tx_type]:
|
||||
tx["type"] = tx_type
|
||||
transactions.append(tx)
|
||||
transactions.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
|
||||
return (
|
||||
transactions[skip : skip + count] if count else transactions[skip:]
|
||||
)
|
||||
except Exception as e:
|
||||
self._log.error(f"listWalletTransactions failed: {e}")
|
||||
return []
|
||||
|
||||
@@ -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
@@ -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
@@ -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-----
|
||||
@@ -130,10 +130,7 @@ def redeemITx(self, bid_id: bytes, cursor):
|
||||
|
||||
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
|
||||
self.log.debug(
|
||||
"Submitted initiate redeem txn %s to %s chain for bid %s",
|
||||
txid,
|
||||
ci_from.coin_name(),
|
||||
bid_id.hex(),
|
||||
f"Submitted initiate redeem txn {self.logIDT(txid)} to {ci_from.coin_name()} chain for bid {self.logIDB(bid_id)}"
|
||||
)
|
||||
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
|
||||
lock_tx_vout=lock_tx_vout,
|
||||
)
|
||||
self.log.debug(
|
||||
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
|
||||
f"Submitted lock B spend txn {self.logIDT(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
|
||||
)
|
||||
self.logBidEvent(
|
||||
bid.bid_id,
|
||||
@@ -191,17 +191,11 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
|
||||
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
|
||||
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
|
||||
elif ci_to.curve_type() == Curves.secp256k1:
|
||||
for i in range(10):
|
||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
|
||||
kbsf, "proof kbsf owned for swap"
|
||||
)
|
||||
pk_recovered: bytes = ci_to.verifySigAndRecover(
|
||||
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
|
||||
)
|
||||
if pk_recovered == xmr_swap.pkbsf:
|
||||
break
|
||||
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
|
||||
assert pk_recovered == xmr_swap.pkbsf
|
||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, "proof kbsf owned for swap")
|
||||
pk_recovered: bytes = ci_to.verifySigAndRecover(
|
||||
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
|
||||
)
|
||||
ensure(pk_recovered == xmr_swap.pkbsf, "kbsf recovered pubkey mismatch")
|
||||
xmr_swap.pkasf = xmr_swap.pkbsf
|
||||
else:
|
||||
raise ValueError("Unknown curve")
|
||||
@@ -215,10 +209,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)
|
||||
|
||||
160
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,29 +133,41 @@ 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"):
|
||||
def callrpc(
|
||||
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
|
||||
):
|
||||
if _use_rpc_pooling:
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host, timeout)
|
||||
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
x = Jsonrpc(url)
|
||||
x = Jsonrpc(url, timeout=timeout if timeout else 10)
|
||||
|
||||
v = x.json_request(method, params)
|
||||
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 +176,78 @@ 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", timeout=None
|
||||
):
|
||||
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)
|
||||
|
||||
if timeout:
|
||||
try:
|
||||
conn = Jsonrpc(url, timeout=timeout)
|
||||
v = conn.json_request(method, params)
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
conn.close()
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
return r["result"]
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||
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)
|
||||
@@ -127,7 +265,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
wallet = wallet
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None, wallet_override=None):
|
||||
def rpc_func(method, params=None, wallet_override=None, timeout=None):
|
||||
return callrpc(
|
||||
port,
|
||||
auth,
|
||||
@@ -135,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
params,
|
||||
wallet if wallet_override is None else wallet_override,
|
||||
host,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
return rpc_func
|
||||
@@ -142,5 +281,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
@@ -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;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 943 B |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 743 B After Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
411
basicswap/static/js/modules/event-handlers.js
Normal file
@@ -0,0 +1,411 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const EventHandlers = {
|
||||
|
||||
showConfirmModal: function(title, message, callback) {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (!modal) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const titleEl = document.getElementById('confirmTitle');
|
||||
const messageEl = document.getElementById('confirmMessage');
|
||||
const yesBtn = document.getElementById('confirmYes');
|
||||
const noBtn = document.getElementById('confirmNo');
|
||||
const bidDetails = document.getElementById('bidDetailsSection');
|
||||
|
||||
if (titleEl) titleEl.textContent = title;
|
||||
if (messageEl) {
|
||||
messageEl.textContent = message;
|
||||
messageEl.classList.remove('hidden');
|
||||
}
|
||||
if (bidDetails) bidDetails.classList.add('hidden');
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
const newYesBtn = yesBtn.cloneNode(true);
|
||||
yesBtn.parentNode.replaceChild(newYesBtn, yesBtn);
|
||||
|
||||
newYesBtn.addEventListener('click', function() {
|
||||
modal.classList.add('hidden');
|
||||
if (callback) callback();
|
||||
});
|
||||
|
||||
const newNoBtn = noBtn.cloneNode(true);
|
||||
noBtn.parentNode.replaceChild(newNoBtn, noBtn);
|
||||
newNoBtn.addEventListener('click', function() {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
},
|
||||
|
||||
confirmReseed: function() {
|
||||
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;
|
||||
}
|
||||
|
||||
let coinFromId;
|
||||
if (inputId === 'add-amm-amount') {
|
||||
coinFromId = 'add-amm-coin-from';
|
||||
} else if (inputId === 'edit-amm-amount') {
|
||||
coinFromId = 'edit-amm-coin-from';
|
||||
} else {
|
||||
const form = amountInput.closest('form') || amountInput.closest('.modal-content') || amountInput.closest('[id*="modal"]');
|
||||
const select = form?.querySelector('select[id*="coin-from"]');
|
||||
coinFromId = select?.id;
|
||||
}
|
||||
|
||||
const coinFromSelect = coinFromId ? document.getElementById(coinFromId) : null;
|
||||
if (!coinFromSelect) {
|
||||
console.error('EventHandlers: Coin-from dropdown not found for:', inputId);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||
if (!selectedOption) {
|
||||
console.error('EventHandlers: No option selected in coin-from dropdown');
|
||||
return;
|
||||
}
|
||||
|
||||
const balance = parseFloat(selectedOption.getAttribute('data-balance') || '0');
|
||||
|
||||
if (balance > 0) {
|
||||
const calculatedAmount = balance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
} else {
|
||||
console.warn('EventHandlers: No balance found for selected coin');
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.style.display = '';
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
if (target.dataset.confirmHandled) {
|
||||
delete target.dataset.confirmHandled;
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const action = target.getAttribute('data-confirm-action') || 'proceed';
|
||||
const coinName = target.getAttribute('data-confirm-coin') || '';
|
||||
|
||||
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?';
|
||||
|
||||
const title = `Confirm ${action}`;
|
||||
|
||||
this.showConfirmModal(title, message, function() {
|
||||
target.dataset.confirmHandled = 'true';
|
||||
|
||||
if (target.form) {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = target.name;
|
||||
hiddenInput.value = target.value;
|
||||
target.form.appendChild(hiddenInput);
|
||||
target.form.submit();
|
||||
} else {
|
||||
target.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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.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
@@ -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');
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
213
basicswap/static/js/modules/wallet-amount.js
Normal file
@@ -0,0 +1,213 @@
|
||||
(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
|
||||
},
|
||||
13: {
|
||||
types: ['plain', 'spark'],
|
||||
hasSubfee: true,
|
||||
hasSweepAll: false
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (cid === 13) {
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
case 'spark':
|
||||
return this.safeParseFloat(balances.spark);
|
||||
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,
|
||||
spark: 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
@@ -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
@@ -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
@@ -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
614
basicswap/static/js/pages/bid-page.js
Normal file
@@ -0,0 +1,614 @@
|
||||
const BidPage = {
|
||||
bidId: null,
|
||||
bidStateInd: null,
|
||||
createdAtTimestamp: null,
|
||||
autoRefreshInterval: null,
|
||||
elapsedTimeInterval: null,
|
||||
AUTO_REFRESH_SECONDS: 60,
|
||||
refreshPaused: false,
|
||||
swapType: null,
|
||||
coinFrom: null,
|
||||
coinTo: null,
|
||||
previousStateInd: null,
|
||||
|
||||
INACTIVE_STATES: [8, 17, 18, 19, 21, 22, 23, 25, 31], // Completed, Failed variants, Timed-out, Abandoned, Error, Rejected, Expired
|
||||
|
||||
DELAYING_STATE: 20,
|
||||
|
||||
STATE_TOOLTIPS: {
|
||||
'Bid Sent': 'Your bid has been broadcast to the network',
|
||||
'Bid Receiving': 'Receiving partial bid message from the network',
|
||||
'Bid Received': 'Bid received and waiting for decision to accept or reject',
|
||||
'Bid Receiving accept': 'Receiving acceptance message from the other party',
|
||||
'Bid Accepted': 'Bid accepted. The atomic swap process is starting',
|
||||
'Bid Initiated': 'Swap initiated. First lock transaction is being created',
|
||||
'Bid Participating': 'Participating in the swap. Second lock transaction is being created',
|
||||
'Bid Completed': 'Swap completed successfully! Both parties received their coins',
|
||||
'Bid Script coin locked': null,
|
||||
'Bid Script coin spend tx valid': null,
|
||||
'Bid Scriptless coin locked': null,
|
||||
'Bid Script coin lock released': 'Adaptor signature revealed. The script coin can now be claimed',
|
||||
'Bid Script tx redeemed': null,
|
||||
'Bid Script pre-refund tx in chain': 'Pre-refund transaction detected. Swap may be failing',
|
||||
'Bid Scriptless tx redeemed': null,
|
||||
'Bid Scriptless tx recovered': null,
|
||||
'Bid Failed, refunded': 'Swap failed but your coins have been refunded',
|
||||
'Bid Failed, swiped': 'Swap failed due to an unexpected issue. Please check the event log for details',
|
||||
'Bid Failed': 'Swap failed. Check events for details',
|
||||
'Bid Delaying': 'Brief delay between swap steps to ensure network propagation',
|
||||
'Bid Timed-out': 'Swap timed out waiting for the other party',
|
||||
'Bid Abandoned': 'Swap was manually abandoned. Locked coins will be refunded after timelock',
|
||||
'Bid Error': 'An error occurred. Check events for details',
|
||||
'Bid Rejected': 'Bid was rejected by the offer owner',
|
||||
'Bid Stalled (debug)': 'Debug mode: swap intentionally stalled for testing',
|
||||
'Bid Exchanged script lock tx sigs msg': 'Exchanging adaptor signatures needed for lock transactions',
|
||||
'Bid Exchanged script lock spend tx msg': 'Exchanging signed spend transaction for locked coins',
|
||||
'Bid Request sent': 'Connection request sent to the other party',
|
||||
'Bid Request accepted': 'Connection request accepted',
|
||||
'Bid Expired': 'Bid expired before being accepted',
|
||||
'Bid Auto accept delay': 'Waiting for automation delay before auto-accepting',
|
||||
'Bid Auto accept failed': 'Automation failed to accept this bid',
|
||||
'Bid Connect request sent': 'Sent connection request to peer',
|
||||
'Bid Unknown bid state': 'Unknown state - please check the swap details',
|
||||
|
||||
'ITX Sent': 'Initiate transaction has been broadcast to the network',
|
||||
'ITX Confirmed': 'Initiate transaction has been confirmed by miners',
|
||||
'ITX Redeemed': 'Initiate transaction has been successfully claimed',
|
||||
'ITX Refunded': 'Initiate transaction has been refunded',
|
||||
'ITX In Mempool': 'Initiate transaction is in the mempool (unconfirmed)',
|
||||
'ITX In Chain': 'Initiate transaction is included in a block',
|
||||
'PTX Sent': 'Participate transaction has been broadcast to the network',
|
||||
'PTX Confirmed': 'Participate transaction has been confirmed by miners',
|
||||
'PTX Redeemed': 'Participate transaction has been successfully claimed',
|
||||
'PTX Refunded': 'Participate transaction has been refunded',
|
||||
'PTX In Mempool': 'Participate transaction is in the mempool (unconfirmed)',
|
||||
'PTX In Chain': 'Participate transaction is included in a block'
|
||||
},
|
||||
|
||||
getStateTooltip: function(stateText) {
|
||||
const staticTooltip = this.STATE_TOOLTIPS[stateText];
|
||||
if (staticTooltip !== null && staticTooltip !== undefined) {
|
||||
return staticTooltip;
|
||||
}
|
||||
|
||||
const scriptlessCoins = ['XMR', 'WOW'];
|
||||
let scriptCoin, scriptlessCoin;
|
||||
|
||||
if (scriptlessCoins.includes(this.coinFrom)) {
|
||||
scriptlessCoin = this.coinFrom;
|
||||
scriptCoin = this.coinTo;
|
||||
} else if (scriptlessCoins.includes(this.coinTo)) {
|
||||
scriptlessCoin = this.coinTo;
|
||||
scriptCoin = this.coinFrom;
|
||||
} else {
|
||||
scriptCoin = this.coinFrom;
|
||||
scriptlessCoin = this.coinTo;
|
||||
}
|
||||
|
||||
const dynamicTooltips = {
|
||||
'Bid Script coin locked': `${scriptCoin} is locked in the atomic swap contract`,
|
||||
'Bid Script coin spend tx valid': `The ${scriptCoin} spend transaction has been validated and is ready`,
|
||||
'Bid Scriptless coin locked': `${scriptlessCoin} is locked using adaptor signatures`,
|
||||
'Bid Script tx redeemed': `${scriptCoin} has been successfully claimed`,
|
||||
'Bid Scriptless tx redeemed': `${scriptlessCoin} has been successfully claimed`,
|
||||
'Bid Scriptless tx recovered': `${scriptlessCoin} recovered after swap failure`,
|
||||
};
|
||||
|
||||
return dynamicTooltips[stateText] || null;
|
||||
},
|
||||
|
||||
EVENT_TOOLTIPS: {
|
||||
'Lock tx A published': 'First lock transaction broadcast to the blockchain network',
|
||||
'Lock tx A seen in mempool': 'First lock transaction detected in mempool (unconfirmed)',
|
||||
'Lock tx A seen in chain': 'First lock transaction included in a block',
|
||||
'Lock tx A confirmed in chain': 'First lock transaction has enough confirmations',
|
||||
'Lock tx B published': 'Second lock transaction broadcast to the blockchain network',
|
||||
'Lock tx B seen in mempool': 'Second lock transaction detected in mempool (unconfirmed)',
|
||||
'Lock tx B seen in chain': 'Second lock transaction included in a block',
|
||||
'Lock tx B confirmed in chain': 'Second lock transaction has enough confirmations',
|
||||
'Lock tx A spend tx published': 'Transaction to claim coins from first lock has been broadcast',
|
||||
'Lock tx A spend tx seen in chain': 'First lock spend transaction included in a block',
|
||||
'Lock tx B spend tx published': 'Transaction to claim coins from second lock has been broadcast',
|
||||
'Lock tx B spend tx seen in chain': 'Second lock spend transaction included in a block',
|
||||
'Failed to publish lock tx B': 'ERROR: Could not broadcast second lock transaction',
|
||||
'Failed to publish lock tx B spend': 'ERROR: Could not broadcast spend transaction for second lock',
|
||||
'Failed to publish lock tx B refund': 'ERROR: Could not broadcast refund transaction',
|
||||
'Detected invalid lock Tx B': 'ERROR: Second lock transaction is invalid or malformed',
|
||||
'Lock tx A pre-refund tx published': 'Pre-refund transaction broadcast. Swap is being cancelled',
|
||||
'Lock tx A refund spend tx published': 'Refund transaction for first lock has been broadcast',
|
||||
'Lock tx A refund swipe tx published': 'Other party claimed your refund (swiped)',
|
||||
'Lock tx B refund tx published': 'Refund transaction for second lock has been broadcast',
|
||||
'Lock tx A conflicting txn/s': 'WARNING: Conflicting transaction detected for first lock',
|
||||
'Lock tx A pre-refund tx seen in chain': 'Pre-refund transaction detected in blockchain',
|
||||
'Lock tx A refund spend tx seen in chain': 'Refund spend transaction detected in blockchain',
|
||||
'Initiate tx published': 'Secret-hash swap: Initiate transaction broadcast',
|
||||
'Initiate tx redeem tx published': 'Secret-hash swap: Initiate transaction claimed',
|
||||
'Initiate tx refund tx published': 'Secret-hash swap: Initiate transaction refunded',
|
||||
'Participate tx published': 'Secret-hash swap: Participate transaction broadcast',
|
||||
'Participate tx redeem tx published': 'Secret-hash swap: Participate transaction claimed',
|
||||
'Participate tx refund tx published': 'Secret-hash swap: Participate transaction refunded',
|
||||
'BCH mercy tx found': 'BCH specific: Mercy transaction detected',
|
||||
'Lock tx B mercy tx published': 'BCH specific: Mercy transaction broadcast',
|
||||
'Auto accepting': 'Automation is accepting this bid',
|
||||
'Failed auto accepting': 'Automation constraints prevented accepting this bid',
|
||||
'Debug tweak applied': 'Debug mode: A test tweak was applied'
|
||||
},
|
||||
|
||||
STATE_PHASES: {
|
||||
1: { phase: 'negotiation', order: 1, label: 'Negotiation' }, // BID_SENT
|
||||
2: { phase: 'negotiation', order: 2, label: 'Negotiation' }, // BID_RECEIVING
|
||||
3: { phase: 'negotiation', order: 3, label: 'Negotiation' }, // BID_RECEIVED
|
||||
4: { phase: 'negotiation', order: 4, label: 'Negotiation' }, // BID_RECEIVING_ACC
|
||||
5: { phase: 'accepted', order: 5, label: 'Accepted' }, // BID_ACCEPTED
|
||||
6: { phase: 'locking', order: 6, label: 'Locking' }, // SWAP_INITIATED
|
||||
7: { phase: 'locking', order: 7, label: 'Locking' }, // SWAP_PARTICIPATING
|
||||
8: { phase: 'complete', order: 100, label: 'Complete' }, // SWAP_COMPLETED
|
||||
9: { phase: 'locking', order: 8, label: 'Locking' }, // XMR_SWAP_SCRIPT_COIN_LOCKED
|
||||
10: { phase: 'locking', order: 9, label: 'Locking' }, // XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX
|
||||
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
|
||||
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
|
||||
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED
|
||||
14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
|
||||
15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
|
||||
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
|
||||
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
|
||||
18: { phase: 'failed', order: 93, label: 'Failed' }, // XMR_SWAP_FAILED_SWIPED
|
||||
19: { phase: 'failed', order: 94, label: 'Failed' }, // XMR_SWAP_FAILED
|
||||
20: { phase: 'locking', order: 7.5, label: 'Locking' }, // SWAP_DELAYING
|
||||
21: { phase: 'failed', order: 95, label: 'Failed' }, // SWAP_TIMEDOUT
|
||||
22: { phase: 'failed', order: 96, label: 'Abandoned' }, // BID_ABANDONED
|
||||
23: { phase: 'failed', order: 97, label: 'Error' }, // BID_ERROR
|
||||
25: { phase: 'failed', order: 98, label: 'Rejected' }, // BID_REJECTED
|
||||
27: { phase: 'accepted', order: 5.5, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS
|
||||
28: { phase: 'accepted', order: 5.6, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX
|
||||
29: { phase: 'negotiation', order: 0.5, label: 'Negotiation' }, // BID_REQUEST_SENT
|
||||
30: { phase: 'negotiation', order: 0.6, label: 'Negotiation' }, // BID_REQUEST_ACCEPTED
|
||||
31: { phase: 'failed', order: 99, label: 'Expired' }, // BID_EXPIRED
|
||||
32: { phase: 'negotiation', order: 3.5, label: 'Negotiation' }, // BID_AACCEPT_DELAY
|
||||
33: { phase: 'failed', order: 89, label: 'Failed' }, // BID_AACCEPT_FAIL
|
||||
34: { phase: 'negotiation', order: 0.4, label: 'Negotiation' } // CONNECT_REQ_SENT
|
||||
},
|
||||
|
||||
init: function(bidId, bidStateInd, createdAtTimestamp, stateTimeTimestamp, options) {
|
||||
this.bidId = bidId;
|
||||
this.bidStateInd = bidStateInd;
|
||||
this.createdAtTimestamp = createdAtTimestamp;
|
||||
this.stateTimeTimestamp = stateTimeTimestamp;
|
||||
this.tooltipCounter = 0;
|
||||
|
||||
options = options || {};
|
||||
this.swapType = options.swapType || 'secret-hash';
|
||||
this.coinFrom = options.coinFrom || '';
|
||||
this.coinTo = options.coinTo || '';
|
||||
|
||||
if (this.bidStateInd === this.DELAYING_STATE) {
|
||||
this.previousStateInd = this.findPreviousState();
|
||||
}
|
||||
|
||||
this.applyStateTooltips();
|
||||
this.applyEventTooltips();
|
||||
this.createProgressBar();
|
||||
this.startElapsedTimeUpdater();
|
||||
this.setupAutoRefresh();
|
||||
},
|
||||
|
||||
findPreviousState: function() {
|
||||
const sections = document.querySelectorAll('section');
|
||||
let oldStatesSection = null;
|
||||
|
||||
sections.forEach(section => {
|
||||
const h4 = section.querySelector('h4');
|
||||
if (h4 && h4.textContent.includes('Old states')) {
|
||||
oldStatesSection = section.nextElementSibling;
|
||||
}
|
||||
});
|
||||
|
||||
if (oldStatesSection) {
|
||||
const table = oldStatesSection.querySelector('table');
|
||||
if (table) {
|
||||
const rows = table.querySelectorAll('tr');
|
||||
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
const cells = rows[i].querySelectorAll('td');
|
||||
if (cells.length >= 2) {
|
||||
const stateText = cells[cells.length - 1].textContent.trim();
|
||||
if (!stateText.includes('Delaying')) {
|
||||
return this.stateTextToIndex(stateText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
stateTextToIndex: function(stateText) {
|
||||
const stateMap = {
|
||||
'Sent': 1, 'Receiving': 2, 'Received': 3, 'Receiving accept': 4,
|
||||
'Accepted': 5, 'Initiated': 6, 'Participating': 7, 'Completed': 8,
|
||||
'Script coin locked': 9, 'Script coin spend tx valid': 10,
|
||||
'Scriptless coin locked': 11, 'Script coin lock released': 12,
|
||||
'Script tx redeemed': 13, 'Script pre-refund tx in chain': 14,
|
||||
'Scriptless tx redeemed': 15, 'Scriptless tx recovered': 16,
|
||||
'Failed, refunded': 17, 'Failed, swiped': 18, 'Failed': 19,
|
||||
'Delaying': 20, 'Timed-out': 21, 'Abandoned': 22, 'Error': 23,
|
||||
'Rejected': 25, 'Exchanged script lock tx sigs msg': 27,
|
||||
'Exchanged script lock spend tx msg': 28, 'Request sent': 29,
|
||||
'Request accepted': 30, 'Expired': 31
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(stateMap)) {
|
||||
if (stateText.includes(key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
isActiveState: function() {
|
||||
return !this.INACTIVE_STATES.includes(this.bidStateInd);
|
||||
},
|
||||
|
||||
setupAutoRefresh: function() {
|
||||
const refreshBtn = document.getElementById('refresh');
|
||||
if (!refreshBtn) return;
|
||||
|
||||
if (!this.isActiveState()) {
|
||||
refreshBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const originalSpan = refreshBtn.querySelector('span');
|
||||
if (!originalSpan) return;
|
||||
|
||||
let countdown = this.AUTO_REFRESH_SECONDS;
|
||||
let isRefreshing = false;
|
||||
let isPersistentlyPaused = false;
|
||||
|
||||
const updateCountdown = () => {
|
||||
if (this.refreshPaused || isPersistentlyPaused || isRefreshing) return;
|
||||
|
||||
originalSpan.textContent = `Auto-refresh in ${countdown}s`;
|
||||
countdown--;
|
||||
|
||||
if (countdown < 0 && !isRefreshing) {
|
||||
isRefreshing = true;
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval);
|
||||
this.autoRefreshInterval = null;
|
||||
}
|
||||
window.location.href = window.location.pathname + window.location.search;
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
||||
|
||||
refreshBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (isPersistentlyPaused) {
|
||||
window.location.href = window.location.pathname + window.location.search;
|
||||
} else {
|
||||
isPersistentlyPaused = true;
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval);
|
||||
this.autoRefreshInterval = null;
|
||||
}
|
||||
originalSpan.textContent = 'Paused (click to refresh)';
|
||||
}
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener('mouseenter', () => {
|
||||
if (!isPersistentlyPaused) {
|
||||
this.refreshPaused = true;
|
||||
if (this.autoRefreshInterval) {
|
||||
clearInterval(this.autoRefreshInterval);
|
||||
this.autoRefreshInterval = null;
|
||||
}
|
||||
originalSpan.textContent = 'Click to pause';
|
||||
}
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener('mouseleave', () => {
|
||||
if (!isPersistentlyPaused) {
|
||||
this.refreshPaused = false;
|
||||
countdown = this.AUTO_REFRESH_SECONDS;
|
||||
if (!this.autoRefreshInterval) {
|
||||
updateCountdown();
|
||||
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createTooltip: function(element, tooltipText) {
|
||||
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
||||
try {
|
||||
const tooltipContent = `
|
||||
<div class="py-1 px-2 text-sm text-white">
|
||||
${tooltipText}
|
||||
</div>
|
||||
`;
|
||||
window.TooltipManager.create(element, tooltipContent, {
|
||||
placement: 'top'
|
||||
});
|
||||
element.classList.add('cursor-help');
|
||||
} catch (e) {
|
||||
element.setAttribute('title', tooltipText);
|
||||
element.classList.add('cursor-help');
|
||||
}
|
||||
} else {
|
||||
element.setAttribute('title', tooltipText);
|
||||
element.classList.add('cursor-help');
|
||||
}
|
||||
},
|
||||
|
||||
applyStateTooltips: function() {
|
||||
const sections = document.querySelectorAll('section');
|
||||
let oldStatesSection = null;
|
||||
|
||||
sections.forEach(section => {
|
||||
const h4 = section.querySelector('h4');
|
||||
if (h4 && h4.textContent.includes('Old states')) {
|
||||
oldStatesSection = section.nextElementSibling;
|
||||
}
|
||||
});
|
||||
|
||||
if (oldStatesSection) {
|
||||
const table = oldStatesSection.querySelector('table');
|
||||
if (table) {
|
||||
const rows = table.querySelectorAll('tr');
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
if (cells.length >= 2) {
|
||||
const stateCell = cells[cells.length - 1];
|
||||
const stateText = stateCell.textContent.trim();
|
||||
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
|
||||
if (tooltip) {
|
||||
this.addHelpIcon(stateCell, tooltip);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allRows = document.querySelectorAll('table tr');
|
||||
allRows.forEach(row => {
|
||||
const firstCell = row.querySelector('td');
|
||||
if (firstCell) {
|
||||
const labelText = firstCell.textContent.trim();
|
||||
if (labelText === 'Bid State') {
|
||||
const valueCell = row.querySelectorAll('td')[1];
|
||||
if (valueCell) {
|
||||
const stateText = valueCell.textContent.trim();
|
||||
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
|
||||
if (tooltip) {
|
||||
this.addHelpIcon(valueCell, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addHelpIcon: function(cell, tooltipText) {
|
||||
if (cell.querySelector('.help-icon')) return;
|
||||
|
||||
const helpIcon = document.createElement('span');
|
||||
helpIcon.className = 'help-icon cursor-help inline-flex items-center justify-center w-4 h-4 ml-2 text-xs font-medium text-white bg-blue-500 dark:bg-blue-600 rounded-full hover:bg-blue-600 dark:hover:bg-blue-500';
|
||||
helpIcon.textContent = '?';
|
||||
helpIcon.style.fontSize = '10px';
|
||||
helpIcon.style.verticalAlign = 'middle';
|
||||
helpIcon.style.flexShrink = '0';
|
||||
|
||||
cell.appendChild(helpIcon);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createTooltip(helpIcon, tooltipText);
|
||||
}, 50);
|
||||
},
|
||||
|
||||
applyEventTooltips: function() {
|
||||
const sections = document.querySelectorAll('section');
|
||||
let eventsSection = null;
|
||||
|
||||
sections.forEach(section => {
|
||||
const h4 = section.querySelector('h4');
|
||||
if (h4 && h4.textContent.includes('Events')) {
|
||||
eventsSection = section.nextElementSibling;
|
||||
}
|
||||
});
|
||||
|
||||
if (eventsSection) {
|
||||
const table = eventsSection.querySelector('table');
|
||||
if (table) {
|
||||
const rows = table.querySelectorAll('tr');
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
if (cells.length >= 2) {
|
||||
const eventCell = cells[cells.length - 1];
|
||||
const eventText = eventCell.textContent.trim();
|
||||
|
||||
let tooltip = this.EVENT_TOOLTIPS[eventText];
|
||||
|
||||
if (!tooltip) {
|
||||
for (const [key, value] of Object.entries(this.EVENT_TOOLTIPS)) {
|
||||
if (eventText.startsWith(key.replace(':', ''))) {
|
||||
tooltip = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tooltip && eventText.startsWith('Warning:')) {
|
||||
tooltip = 'System warning - check message for details';
|
||||
}
|
||||
if (!tooltip && eventText.startsWith('Error:')) {
|
||||
tooltip = 'Error occurred - check message for details';
|
||||
}
|
||||
if (!tooltip && eventText.startsWith('Temporary RPC error')) {
|
||||
tooltip = 'Temporary error checking transaction. Will retry automatically';
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
this.addHelpIcon(eventCell, tooltip);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createProgressBar: function() {
|
||||
let stateForProgress = this.bidStateInd;
|
||||
let isDelaying = false;
|
||||
|
||||
if (this.bidStateInd === this.DELAYING_STATE && this.previousStateInd) {
|
||||
stateForProgress = this.previousStateInd;
|
||||
isDelaying = true;
|
||||
}
|
||||
|
||||
const phaseInfo = this.STATE_PHASES[stateForProgress];
|
||||
if (!phaseInfo) return;
|
||||
|
||||
let progressPercent = 0;
|
||||
const phase = phaseInfo.phase;
|
||||
|
||||
if (phase === 'negotiation') progressPercent = 15;
|
||||
else if (phase === 'accepted') progressPercent = 30;
|
||||
else if (phase === 'locking') progressPercent = 55;
|
||||
else if (phase === 'redemption') progressPercent = 80;
|
||||
else if (phase === 'complete') progressPercent = 100;
|
||||
else if (phase === 'failed' || phase === 'error') progressPercent = 100;
|
||||
|
||||
const bidStateRow = document.querySelector('td.bold');
|
||||
if (!bidStateRow) return;
|
||||
|
||||
let targetRow = null;
|
||||
const rows = document.querySelectorAll('table tr');
|
||||
rows.forEach(row => {
|
||||
const firstTd = row.querySelector('td.bold');
|
||||
if (firstTd && firstTd.textContent.trim() === 'Bid State') {
|
||||
targetRow = row;
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetRow) return;
|
||||
|
||||
const progressRow = document.createElement('tr');
|
||||
progressRow.className = 'opacity-100 text-gray-500 dark:text-gray-100';
|
||||
|
||||
const isError = ['failed', 'error'].includes(phase);
|
||||
const isComplete = phase === 'complete';
|
||||
const barColor = isError ? 'bg-red-500' : (isComplete ? 'bg-green-500' : 'bg-blue-500');
|
||||
|
||||
let phaseLabel;
|
||||
if (isError) {
|
||||
phaseLabel = phaseInfo.label;
|
||||
} else if (isComplete) {
|
||||
phaseLabel = 'Complete';
|
||||
} else if (isDelaying) {
|
||||
phaseLabel = `${phaseInfo.label} (${progressPercent}%) - Delaying`;
|
||||
} else {
|
||||
phaseLabel = `${phaseInfo.label} (${progressPercent}%)`;
|
||||
}
|
||||
|
||||
progressRow.innerHTML = `
|
||||
<td class="py-3 px-6 bold">Swap Progress</td>
|
||||
<td class="py-3 px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 bg-gray-200 dark:bg-gray-600 rounded-full h-2.5 max-w-xs">
|
||||
<div class="${barColor} h-2.5 rounded-full transition-all duration-500" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">${phaseLabel}</span>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
targetRow.parentNode.insertBefore(progressRow, targetRow.nextSibling);
|
||||
},
|
||||
|
||||
startElapsedTimeUpdater: function() {
|
||||
if (!this.createdAtTimestamp) return;
|
||||
|
||||
let createdAtRow = null;
|
||||
const rows = document.querySelectorAll('table tr');
|
||||
rows.forEach(row => {
|
||||
const firstTd = row.querySelector('td');
|
||||
if (firstTd && firstTd.textContent.includes('Created At')) {
|
||||
createdAtRow = row;
|
||||
}
|
||||
});
|
||||
|
||||
if (!createdAtRow) return;
|
||||
|
||||
const isCompleted = !this.isActiveState() && this.stateTimeTimestamp;
|
||||
|
||||
const elapsedRow = document.createElement('tr');
|
||||
elapsedRow.className = 'opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
|
||||
|
||||
const labelText = isCompleted ? 'Swap Duration' : 'Time Elapsed';
|
||||
const iconColor = isCompleted ? '#10B981' : '#3B82F6';
|
||||
|
||||
elapsedRow.innerHTML = `
|
||||
<td class="flex items-center px-46 whitespace-nowrap">
|
||||
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${iconColor}" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="11"></circle>
|
||||
<polyline points="12,6 12,12 18,12" stroke="${iconColor}"></polyline>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="py-3 pl-2 bold">
|
||||
<div>${labelText}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-6" id="elapsed-time-display">Calculating...</td>
|
||||
`;
|
||||
createdAtRow.parentNode.insertBefore(elapsedRow, createdAtRow.nextSibling);
|
||||
|
||||
const elapsedDisplay = document.getElementById('elapsed-time-display');
|
||||
|
||||
if (isCompleted) {
|
||||
const duration = this.stateTimeTimestamp - this.createdAtTimestamp;
|
||||
elapsedDisplay.textContent = this.formatDuration(duration);
|
||||
} else {
|
||||
const updateElapsed = () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const elapsed = now - this.createdAtTimestamp;
|
||||
elapsedDisplay.textContent = this.formatDuration(elapsed);
|
||||
};
|
||||
|
||||
updateElapsed();
|
||||
this.elapsedTimeInterval = setInterval(updateElapsed, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
formatDuration: function(seconds) {
|
||||
if (seconds < 60) {
|
||||
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (remainingSeconds > 0) {
|
||||
return `${minutes} min ${remainingSeconds} sec`;
|
||||
}
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
if (hours < 24) {
|
||||
if (remainingMinutes > 0) {
|
||||
return `${hours} hr ${remainingMinutes} min`;
|
||||
}
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
||||
}
|
||||
const days = Math.floor(hours / 24);
|
||||
const remainingHours = hours % 24;
|
||||
if (remainingHours > 0) {
|
||||
return `${days} day${days !== 1 ? 's' : ''} ${remainingHours} hr`;
|
||||
}
|
||||
return `${days} day${days !== 1 ? 's' : ''}`;
|
||||
}
|
||||
};
|
||||