Compare commits
134 Commits
v0.14.6
...
release-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
12
.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 }}
|
||||
|
||||
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.6"
|
||||
__version__ = "0.15.1"
|
||||
|
||||
@@ -31,6 +31,7 @@ from .util import (
|
||||
)
|
||||
from .util.logging import (
|
||||
BSXLogger,
|
||||
LogCategories as LC,
|
||||
)
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
@@ -43,7 +44,7 @@ def getaddrinfo_tor(*args):
|
||||
|
||||
|
||||
class BaseApp(DBMethods):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap", **kwargs):
|
||||
self.fp = None
|
||||
self.log_name = log_name
|
||||
self.fail_code = 0
|
||||
@@ -73,6 +74,31 @@ class BaseApp(DBMethods):
|
||||
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:
|
||||
self.fp.close()
|
||||
@@ -236,11 +262,16 @@ class BaseApp(DBMethods):
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
return opener.open(request, timeout=timeout).read()
|
||||
|
||||
def logException(self, message) -> None:
|
||||
def logException(self, message: str) -> None:
|
||||
self.log.error(message)
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
def logD(self, log_category: int, message: str) -> None:
|
||||
if log_category not in self._enabled_log_categories:
|
||||
return
|
||||
self.log.debug("(" + LC(log_category).name + ") " + message)
|
||||
|
||||
def torControl(self, query):
|
||||
try:
|
||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
|
||||
|
||||
@@ -41,6 +41,11 @@ class MessageNetworks(IntEnum):
|
||||
SIMPLEX = auto()
|
||||
|
||||
|
||||
class MessageNetworkLinkTypes(IntEnum):
|
||||
RECEIVED_ON = auto()
|
||||
SENT_ON = auto()
|
||||
|
||||
|
||||
class MessageTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
@@ -59,6 +64,8 @@ class MessageTypes(IntEnum):
|
||||
ADS_BID_ACCEPT_FL = auto()
|
||||
|
||||
CONNECT_REQ = auto()
|
||||
PORTAL_OFFER = auto()
|
||||
PORTAL_SEND = auto()
|
||||
|
||||
|
||||
class AddressTypes(IntEnum):
|
||||
@@ -66,6 +73,8 @@ class AddressTypes(IntEnum):
|
||||
BID = auto()
|
||||
RECV_OFFER = auto()
|
||||
SEND_OFFER = auto()
|
||||
PORTAL_LOCAL = auto()
|
||||
PORTAL = auto()
|
||||
|
||||
|
||||
class SwapTypes(IntEnum):
|
||||
@@ -201,6 +210,8 @@ class EventLogTypes(IntEnum):
|
||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||
BCH_MERCY_TX_PUBLISHED = auto()
|
||||
BCH_MERCY_TX_FOUND = auto()
|
||||
LOCK_TX_A_IN_MEMPOOL = auto()
|
||||
LOCK_TX_A_CONFLICTS = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -234,6 +245,8 @@ class NotificationTypes(IntEnum):
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
SWAP_COMPLETED = auto()
|
||||
UPDATE_AVAILABLE = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
@@ -395,15 +408,14 @@ def strTxType(tx_type):
|
||||
|
||||
|
||||
def strAddressType(addr_type):
|
||||
if addr_type == AddressTypes.OFFER:
|
||||
return "Offer"
|
||||
if addr_type == AddressTypes.BID:
|
||||
return "Bid"
|
||||
if addr_type == AddressTypes.RECV_OFFER:
|
||||
return "Offer recv"
|
||||
if addr_type == AddressTypes.SEND_OFFER:
|
||||
return "Offer send"
|
||||
return "Unknown"
|
||||
return {
|
||||
AddressTypes.OFFER: "Offer",
|
||||
AddressTypes.BID: "Bid",
|
||||
AddressTypes.RECV_OFFER: "Offer recv",
|
||||
AddressTypes.SEND_OFFER: "Offer send",
|
||||
AddressTypes.PORTAL_LOCAL: "Portal (local)",
|
||||
AddressTypes.PORTAL: "Portal",
|
||||
}.get(addr_type, "Unknown")
|
||||
|
||||
|
||||
def getLockName(lock_type):
|
||||
@@ -426,6 +438,10 @@ def describeEventEntry(event_type, event_msg):
|
||||
return "Lock tx B published"
|
||||
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
||||
return "Failed to publish lock tx B spend: " + event_msg
|
||||
if event_type == EventLogTypes.LOCK_TX_A_IN_MEMPOOL:
|
||||
return "Lock tx A seen in mempool"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFLICTS:
|
||||
return "Lock tx A conflicting txn/s"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
||||
return "Lock tx A seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
||||
@@ -443,7 +459,7 @@ def describeEventEntry(event_type, event_msg):
|
||||
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
||||
return "Detected invalid lock Tx B"
|
||||
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:
|
||||
@@ -455,7 +471,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:
|
||||
@@ -595,6 +611,26 @@ def canAcceptBidState(state):
|
||||
)
|
||||
|
||||
|
||||
def canExpireBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_SENT,
|
||||
BidStates.BID_RECEIVING,
|
||||
BidStates.BID_RECEIVED,
|
||||
BidStates.BID_AACCEPT_DELAY,
|
||||
BidStates.BID_AACCEPT_FAIL,
|
||||
BidStates.BID_REQUEST_SENT,
|
||||
)
|
||||
|
||||
|
||||
def canTimeoutBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_ACCEPTED,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||
)
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
||||
return True
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import gnupg
|
||||
import hashlib
|
||||
@@ -26,6 +27,7 @@ import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
import zmq
|
||||
|
||||
from urllib.request import urlopen
|
||||
|
||||
@@ -48,7 +50,8 @@ from basicswap.bin.run import (
|
||||
)
|
||||
|
||||
# Coin clients
|
||||
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
|
||||
PARTICL_REPO = os.getenv("PARTICL_REPO", "tecnovert")
|
||||
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.2.0")
|
||||
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
|
||||
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
|
||||
|
||||
@@ -64,10 +67,10 @@ DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
|
||||
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
|
||||
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
|
||||
|
||||
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.0")
|
||||
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.3")
|
||||
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
|
||||
XMR_SITE_COMMIT = (
|
||||
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version
|
||||
"df28b670cb3a174d7763dd6d22fb4ef20597d0ac" # Lock hashes.txt to monero version
|
||||
)
|
||||
|
||||
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
|
||||
@@ -79,10 +82,10 @@ WOW_SITE_COMMIT = (
|
||||
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
|
||||
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
|
||||
|
||||
DASH_VERSION = os.getenv("DASH_VERSION", "22.0.0")
|
||||
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3")
|
||||
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
|
||||
|
||||
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.1")
|
||||
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.15.0")
|
||||
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
|
||||
|
||||
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
|
||||
@@ -158,10 +161,10 @@ expected_key_ids = {
|
||||
}
|
||||
|
||||
GUIX_SSL_CERT_DIR = None
|
||||
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", "false"))
|
||||
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", False))
|
||||
|
||||
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
|
||||
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false"))
|
||||
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", False))
|
||||
|
||||
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
|
||||
if USE_PLATFORM == "Darwin":
|
||||
@@ -189,11 +192,11 @@ if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
logging.getLogger("gnupg").setLevel(logging.INFO)
|
||||
|
||||
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", "false"))
|
||||
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", "false"))
|
||||
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", "false"))
|
||||
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
|
||||
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", False))
|
||||
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", False))
|
||||
BSX_UPDATE_UNMANAGED = toBool(
|
||||
os.getenv("BSX_UPDATE_UNMANAGED", "true")
|
||||
os.getenv("BSX_UPDATE_UNMANAGED", True)
|
||||
) # Disable updating unmanaged coin cores.
|
||||
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
|
||||
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
|
||||
@@ -318,10 +321,8 @@ def setTorrcVars():
|
||||
)
|
||||
|
||||
|
||||
TEST_TOR_PROXY = toBool(
|
||||
os.getenv("TEST_TOR_PROXY", "true")
|
||||
) # Expects a known exit node
|
||||
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", "false"))
|
||||
TEST_TOR_PROXY = toBool(os.getenv("TEST_TOR_PROXY", True)) # Expects a known exit node
|
||||
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", False))
|
||||
|
||||
BITCOIN_FASTSYNC_URL = os.getenv(
|
||||
"BITCOIN_FASTSYNC_URL",
|
||||
@@ -338,6 +339,8 @@ BITCOIN_FASTSYNC_SIG_URL = os.getenv(
|
||||
# Encrypt new wallets with this password, must match the Particl wallet password when adding coins
|
||||
WALLET_ENCRYPTION_PWD = os.getenv("WALLET_ENCRYPTION_PWD", "")
|
||||
|
||||
CHECK_FOR_BSX_UPDATES = toBool(os.getenv("CHECK_FOR_BSX_UPDATES", True))
|
||||
|
||||
use_tor_proxy: bool = False
|
||||
with_coins_changed: bool = False
|
||||
|
||||
@@ -356,21 +359,6 @@ monero_wallet_rpc_proxy_config = [
|
||||
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
|
||||
]
|
||||
|
||||
wownerod_proxy_config = [
|
||||
f"proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}",
|
||||
"proxy-allow-dns-leaks=0",
|
||||
"no-igd=1", # Disable UPnP port mapping
|
||||
"hide-my-port=1", # Don't share the p2p port
|
||||
"p2p-bind-ip=127.0.0.1", # Don't broadcast ip
|
||||
"in-peers=0", # Changes "error" in log to "incoming connections disabled"
|
||||
"out-peers=24",
|
||||
f"tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16", # Outgoing tx relay to onion
|
||||
]
|
||||
|
||||
wownero_wallet_rpc_proxy_config = [
|
||||
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
|
||||
]
|
||||
|
||||
default_socket = socket.socket
|
||||
default_socket_timeout = socket.getdefaulttimeout()
|
||||
default_socket_getaddrinfo = socket.getaddrinfo
|
||||
@@ -416,6 +404,12 @@ def getDescriptorWalletOption(coin_params):
|
||||
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
|
||||
|
||||
|
||||
def getLegacyKeyPathOption(coin_params):
|
||||
ticker: str = coin_params["ticker"]
|
||||
default_option: bool = False
|
||||
return toBool(os.getenv(ticker + "_USE_LEGACY_KEY_PATHS", default_option))
|
||||
|
||||
|
||||
def getKnownVersion(coin_name: str) -> str:
|
||||
version, version_tag, _ = known_coins[coin_name]
|
||||
return version + version_tag
|
||||
@@ -894,7 +888,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
downloadFile(assert_sig_url, assert_sig_path)
|
||||
else:
|
||||
major_version = int(version.split(".")[0])
|
||||
|
||||
use_guix: bool = coin in ("dash",) or major_version >= 22
|
||||
arch_name = BIN_ARCH
|
||||
if os_name == "osx" and use_guix:
|
||||
@@ -915,21 +908,21 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
||||
coin, version + version_tag, arch_name, filename_extra, FILE_EXT
|
||||
)
|
||||
if coin == "particl":
|
||||
release_url = "https://github.com/particl/particl-core/releases/download/v{}/{}".format(
|
||||
version + version_tag, release_filename
|
||||
release_url = (
|
||||
"https://github.com/{}/particl-core/releases/download/v{}/{}".format(
|
||||
PARTICL_REPO, version + version_tag, release_filename
|
||||
)
|
||||
)
|
||||
assert_filename = "{}-{}-{}-build.assert".format(coin, os_name, version)
|
||||
if use_guix:
|
||||
assert_url = f"https://raw.githubusercontent.com/particl/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
|
||||
assert_url = f"https://raw.githubusercontent.com/{PARTICL_REPO}/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
|
||||
else:
|
||||
assert_url = (
|
||||
"https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s"
|
||||
% (
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name,
|
||||
assert_filename,
|
||||
)
|
||||
assert_url = "https://raw.githubusercontent.com/{}/gitian.sigs/master/{}-{}/{}/{}".format(
|
||||
PARTICL_REPO,
|
||||
version + version_tag,
|
||||
os_dir_name,
|
||||
signing_key_name,
|
||||
assert_filename,
|
||||
)
|
||||
elif coin == "litecoin":
|
||||
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
|
||||
@@ -1223,16 +1216,12 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
if coin == "monero":
|
||||
if XMR_RPC_USER != "":
|
||||
fp.write(f"rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
if coin == "wownero":
|
||||
if WOW_RPC_USER != "":
|
||||
fp.write(f"rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in wownerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
if tor_control_password is not None:
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
wallets_dir = core_settings.get("walletsdir", data_dir)
|
||||
if not os.path.exists(wallets_dir):
|
||||
@@ -1378,10 +1367,25 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
|
||||
salt = generate_salt(16)
|
||||
if coin == "particl":
|
||||
fp.write("deprecatedrpc=create_bdb\n")
|
||||
fp.write("debugexclude=libevent\n")
|
||||
if chain == "mainnet":
|
||||
fp.write("rpcdoccheck=0\n")
|
||||
fp.write(
|
||||
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
|
||||
)
|
||||
fp.write(
|
||||
"zmqpubhashwtx=tcp://{}:{}\n".format(
|
||||
COINS_RPCBIND_IP, settings["zmqport"]
|
||||
)
|
||||
)
|
||||
zmqsecret = extra_opts.get("zmqsecret", None)
|
||||
if zmqsecret:
|
||||
try:
|
||||
_ = base64.b64decode(zmqsecret)
|
||||
except Exception as e: # noqa: F841
|
||||
raise ValueError("zmqsecret must be base64 encoded")
|
||||
fp.write(f"serverkeyzmq={zmqsecret}\n")
|
||||
fp.write("spentindex=1\n")
|
||||
fp.write("txindex=1\n")
|
||||
fp.write("staking=0\n")
|
||||
@@ -1562,27 +1566,18 @@ def modify_tor_config(
|
||||
# Disable tor first
|
||||
for line in fp_in:
|
||||
skip_line: bool = False
|
||||
if coin == "monero":
|
||||
if coin in ("wownero", "monero"):
|
||||
for opt_line in monerod_proxy_config:
|
||||
setting: str = opt_line[0 : opt_line.find("=") + 1]
|
||||
if line.startswith(setting):
|
||||
skip_line = True
|
||||
break
|
||||
if coin == "wownero":
|
||||
for opt_line in wownerod_proxy_config:
|
||||
setting: str = opt_line[0 : opt_line.find("=") + 1]
|
||||
if line.startswith(setting):
|
||||
skip_line = True
|
||||
break
|
||||
if not skip_line:
|
||||
fp.write(line)
|
||||
if enable:
|
||||
if coin == "monero":
|
||||
if coin in ("wownero", "monero"):
|
||||
for opt_line in monerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
if coin == "wownero":
|
||||
for opt_line in wownerod_proxy_config:
|
||||
fp.write(opt_line + "\n")
|
||||
|
||||
with open(wallet_conf_path, "w") as fp:
|
||||
with open(wallet_conf_path + ".last") as fp_in:
|
||||
@@ -1773,7 +1768,7 @@ def finalise_daemon(d):
|
||||
fp.close()
|
||||
|
||||
|
||||
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
|
||||
swap_client = None
|
||||
daemons = []
|
||||
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
|
||||
@@ -1810,12 +1805,14 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
|
||||
"Must set WALLET_ENCRYPTION_PWD to add coin when Particl wallet is encrypted"
|
||||
)
|
||||
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
|
||||
extra_opts["particl_daemon"] = daemons[-1]
|
||||
finally:
|
||||
if swap_client:
|
||||
swap_client.finalise()
|
||||
del swap_client
|
||||
for d in daemons:
|
||||
finalise_daemon(d)
|
||||
if "particl_daemon" not in extra_opts:
|
||||
for d in daemons:
|
||||
finalise_daemon(d)
|
||||
|
||||
|
||||
def encrypt_wallet(swap_client, coin_type) -> None:
|
||||
@@ -1909,15 +1906,20 @@ def initialise_wallets(
|
||||
]
|
||||
|
||||
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
coin_settings["datadir"],
|
||||
coin_settings["bindir"],
|
||||
filename,
|
||||
daemon_args + coin_args,
|
||||
extra_config=extra_config,
|
||||
|
||||
if c == Coins.PART and "particl_daemon" in extra_opts:
|
||||
daemons.append(extra_opts["particl_daemon"])
|
||||
del extra_opts["particl_daemon"]
|
||||
else:
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
coin_settings["datadir"],
|
||||
coin_settings["bindir"],
|
||||
filename,
|
||||
daemon_args + coin_args,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
)
|
||||
swap_client.setDaemonPID(c, daemons[-1].handle.pid)
|
||||
swap_client.setCoinRunParams(c)
|
||||
swap_client.createCoinInterface(c)
|
||||
@@ -2357,10 +2359,10 @@ def main():
|
||||
continue
|
||||
if len(s) == 2:
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1].strip('"'))
|
||||
data_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
|
||||
continue
|
||||
if name == "bindir":
|
||||
bin_dir = os.path.expanduser(s[1].strip('"'))
|
||||
bin_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
|
||||
continue
|
||||
if name == "portoffset":
|
||||
port_offset = int(s[1])
|
||||
@@ -2417,7 +2419,9 @@ def main():
|
||||
extra_opts["walletrestoretime"] = int(s[1])
|
||||
continue
|
||||
if name == "keysdirpath":
|
||||
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
|
||||
extra_opts["keysdirpath"] = os.path.abspath(
|
||||
os.path.expanduser(s[1].strip('"'))
|
||||
)
|
||||
continue
|
||||
if name == "trustremotenode":
|
||||
extra_opts["trust_remote_node"] = toBool(s[1])
|
||||
@@ -2815,6 +2819,8 @@ def main():
|
||||
coin_settings["watch_wallet_name"] = getWalletName(
|
||||
coin_params, "bsx_watch", prefix_override=f"{ticker}_WATCH"
|
||||
)
|
||||
if getLegacyKeyPathOption(coin_params) is True:
|
||||
coin_settings["use_legacy_key_paths"] = True
|
||||
|
||||
if PART_RPC_USER != "":
|
||||
chainclients["particl"]["rpcuser"] = PART_RPC_USER
|
||||
@@ -2967,41 +2973,51 @@ def main():
|
||||
save_config(config_path, settings)
|
||||
logger.info("Done.")
|
||||
return 0
|
||||
exitWithError("{} is already in the settings file".format(add_coin))
|
||||
exitWithError(f"{add_coin} is already in the settings file")
|
||||
|
||||
if tor_control_password is None and settings.get("use_tor", False):
|
||||
extra_opts["tor_control_password"] = settings.get(
|
||||
"tor_control_password", None
|
||||
)
|
||||
|
||||
if particl_wallet_mnemonic != "none":
|
||||
# Ensure Particl wallet is unencrypted or correct password is supplied
|
||||
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
|
||||
|
||||
settings["chainclients"][add_coin] = chainclients[add_coin]
|
||||
|
||||
if not no_cores:
|
||||
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
|
||||
|
||||
if not (prepare_bin_only or upgrade_cores):
|
||||
prepareDataDir(
|
||||
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
|
||||
)
|
||||
|
||||
try:
|
||||
if particl_wallet_mnemonic != "none":
|
||||
initialise_wallets(
|
||||
None,
|
||||
{
|
||||
add_coin,
|
||||
},
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
# Ensure Particl wallet is unencrypted or correct password is supplied
|
||||
# Keep daemon running to use in initialise_wallets
|
||||
test_particl_encryption(
|
||||
data_dir, settings, chain, use_tor_proxy, extra_opts
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
settings["chainclients"][add_coin] = chainclients[add_coin]
|
||||
|
||||
if not no_cores:
|
||||
prepareCore(
|
||||
add_coin, known_coins[add_coin], settings, data_dir, extra_opts
|
||||
)
|
||||
|
||||
if not (prepare_bin_only or upgrade_cores):
|
||||
prepareDataDir(
|
||||
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
|
||||
)
|
||||
|
||||
if particl_wallet_mnemonic != "none":
|
||||
initialise_wallets(
|
||||
None,
|
||||
{
|
||||
add_coin,
|
||||
},
|
||||
data_dir,
|
||||
settings,
|
||||
chain,
|
||||
use_tor_proxy,
|
||||
extra_opts=extra_opts,
|
||||
)
|
||||
|
||||
save_config(config_path, settings)
|
||||
finally:
|
||||
if "particl_daemon" in extra_opts:
|
||||
finalise_daemon(extra_opts["particl_daemon"])
|
||||
del extra_opts["particl_daemon"]
|
||||
|
||||
logger.info(f"Done. Coin {add_coin} successfully added.")
|
||||
return 0
|
||||
@@ -3226,6 +3242,10 @@ def main():
|
||||
for c in with_coins:
|
||||
withchainclients[c] = chainclients[c]
|
||||
|
||||
zmq_server_pubkey, zmq_server_key = zmq.curve_keypair()
|
||||
zmq_client_pubkey, zmq_client_key = zmq.curve_keypair()
|
||||
extra_opts["zmqsecret"] = base64.b64encode(zmq_server_key).decode("utf-8")
|
||||
|
||||
settings = {
|
||||
"debug": True,
|
||||
"zmqhost": f"tcp://{PART_RPC_HOST}",
|
||||
@@ -3241,6 +3261,12 @@ def main():
|
||||
"check_watched_seconds": 60,
|
||||
"check_expired_seconds": 60,
|
||||
"wallet_update_timeout": 10, # Seconds to wait for wallet page update
|
||||
"zmq_client_key": base64.b64encode(zmq_client_key).decode("utf-8"),
|
||||
"zmq_client_pubkey": base64.b64encode(zmq_client_pubkey).decode("utf-8"),
|
||||
"zmq_server_pubkey": base64.b64encode(zmq_server_pubkey).decode("utf-8"),
|
||||
"enabled_log_categories": [
|
||||
"net",
|
||||
],
|
||||
}
|
||||
|
||||
wshost: str = extra_opts.get("wshost", htmlhost)
|
||||
@@ -3248,6 +3274,11 @@ def main():
|
||||
settings["wshost"] = wshost
|
||||
settings["wsport"] = UI_WS_PORT + port_offset
|
||||
|
||||
if "CHECK_FOR_BSX_UPDATES" in os.environ:
|
||||
settings["check_updates"] = CHECK_FOR_BSX_UPDATES
|
||||
elif BSX_TEST_MODE is True:
|
||||
settings["check_updates"] = False
|
||||
|
||||
if use_tor_proxy:
|
||||
tor_control_password = generate_salt(24)
|
||||
addTorSettings(settings, tor_control_password)
|
||||
|
||||
@@ -19,8 +19,6 @@ import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.network.simplex_chat import startSimplexClient
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.util.daemon import Daemon
|
||||
@@ -56,6 +54,42 @@ def signal_handler(sig, frame):
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||
try:
|
||||
datadir = part_settings.get("datadir")
|
||||
if not datadir:
|
||||
return
|
||||
|
||||
config_path = os.path.join(datadir, "particl.conf")
|
||||
if not os.path.exists(config_path):
|
||||
return
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
|
||||
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
|
||||
zmq_port = swap_settings.get("zmqport", 14792)
|
||||
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
|
||||
|
||||
if "zmqpubhashwtx=" not in config_content:
|
||||
with open(config_path, "a") as f:
|
||||
f.write(f"{expected_line}\n")
|
||||
elif expected_line not in config_content:
|
||||
lines = config_content.split("\n")
|
||||
updated_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("zmqpubhashwtx="):
|
||||
updated_lines.append(expected_line)
|
||||
else:
|
||||
updated_lines.append(line)
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("\n".join(updated_lines))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking PART ZMQ config: {e}")
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
@@ -187,7 +221,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
|
||||
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 = [
|
||||
@@ -249,25 +283,6 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -336,7 +351,6 @@ def runClient(
|
||||
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")
|
||||
|
||||
@@ -379,7 +393,7 @@ def runClient(
|
||||
for network in settings.get("networks", []):
|
||||
if network.get("enabled", True) is False:
|
||||
continue
|
||||
network_type = network.get("type", "unknown")
|
||||
network_type: str = network.get("type", "unknown")
|
||||
if network_type == "simplex":
|
||||
simplex_dir = os.path.join(data_dir, "simplex")
|
||||
|
||||
@@ -548,6 +562,9 @@ 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")
|
||||
@@ -584,39 +601,6 @@ def runClient(
|
||||
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.")
|
||||
mainLoop(daemons)
|
||||
@@ -632,13 +616,6 @@ def runClient(
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.finalise()
|
||||
swap_client.log.info("Stopping HTTP threads.")
|
||||
for t in threads:
|
||||
try:
|
||||
t.stop()
|
||||
t.join()
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
@@ -758,7 +735,7 @@ def main():
|
||||
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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 29
|
||||
CURRENT_DB_DATA_VERSION = 6
|
||||
CURRENT_DB_VERSION = 32
|
||||
CURRENT_DB_DATA_VERSION = 7
|
||||
|
||||
|
||||
class Concepts(IntEnum):
|
||||
@@ -185,6 +185,7 @@ class Offer(Table):
|
||||
amount_negotiable = Column("bool")
|
||||
rate_negotiable = Column("bool")
|
||||
auto_accept_type = Column("integer")
|
||||
message_nets = Column("string")
|
||||
|
||||
# Local fields
|
||||
auto_accept_bids = Column("bool")
|
||||
@@ -194,6 +195,7 @@ class Offer(Table):
|
||||
) # Address to spend lock tx to - address from wallet if empty TODO
|
||||
security_token = Column("blob")
|
||||
bid_reversed = Column("bool")
|
||||
smsg_payload_version = Column("integer")
|
||||
|
||||
state = Column("integer")
|
||||
states = Column("blob") # Packed states and times
|
||||
@@ -233,6 +235,7 @@ class Bid(Table):
|
||||
rate = Column("integer")
|
||||
|
||||
pkhash_seller = Column("blob")
|
||||
message_nets = Column("string")
|
||||
|
||||
initiate_txn_redeem = Column("blob")
|
||||
initiate_txn_refund = Column("blob")
|
||||
@@ -381,6 +384,8 @@ class SmsgAddress(Table):
|
||||
use_type = Column("integer")
|
||||
note = Column("string")
|
||||
|
||||
index = Index("smsgaddresses_address_index", "addr")
|
||||
|
||||
|
||||
class Action(Table):
|
||||
__tablename__ = "actions"
|
||||
@@ -614,6 +619,8 @@ class BidState(Table):
|
||||
swap_failed = Column("integer")
|
||||
swap_ended = Column("integer")
|
||||
can_accept = Column("integer")
|
||||
can_expire = Column("integer")
|
||||
can_timeout = Column("integer")
|
||||
|
||||
note = Column("string")
|
||||
created_at = Column("integer")
|
||||
@@ -667,6 +674,28 @@ 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"
|
||||
|
||||
@@ -676,6 +705,20 @@ class MessageNetworks(Table):
|
||||
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"
|
||||
|
||||
@@ -694,6 +737,7 @@ class DirectMessageRoute(Table):
|
||||
|
||||
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")
|
||||
@@ -702,6 +746,52 @@ class DirectMessageRouteLink(Table):
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class NetworkPortal(Table):
|
||||
__tablename__ = "network_portals"
|
||||
|
||||
def set(
|
||||
self, time_start, time_valid, network_from, network_to, address_from, address_to
|
||||
):
|
||||
super().__init__()
|
||||
self.active_ind = 1
|
||||
self.time_start = time_start
|
||||
self.time_valid = time_valid
|
||||
self.network_from = network_from
|
||||
self.network_to = network_to
|
||||
self.address_from = address_from
|
||||
self.address_to = address_to
|
||||
|
||||
self.smsg_difficulty = 0x1EFFFFFF
|
||||
|
||||
self.num_refreshes = 0
|
||||
self.messages_sent = 0
|
||||
self.responses_seen = 0
|
||||
self.time_last_used = 0
|
||||
self.num_issues = 0
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
own_portal = Column("integer")
|
||||
|
||||
address_from = Column("string", unique=True)
|
||||
address_to = Column("string")
|
||||
|
||||
network_from = Column("integer")
|
||||
network_to = Column("integer")
|
||||
|
||||
time_start = Column("integer")
|
||||
time_valid = Column("integer")
|
||||
smsg_difficulty = Column("integer")
|
||||
num_refreshes = Column("integer")
|
||||
|
||||
messages_sent = Column("integer")
|
||||
responses_seen = Column("integer")
|
||||
time_last_used = Column("integer")
|
||||
num_issues = Column("integer")
|
||||
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
def extract_schema() -> dict:
|
||||
g = globals().copy()
|
||||
tables = {}
|
||||
|
||||
@@ -21,6 +21,8 @@ from .db import (
|
||||
from .basicswap_util import (
|
||||
BidStates,
|
||||
canAcceptBidState,
|
||||
canExpireBidState,
|
||||
canTimeoutBidState,
|
||||
isActiveBidState,
|
||||
isErrorBidState,
|
||||
isFailingBidState,
|
||||
@@ -39,6 +41,8 @@ def addBidState(self, state, now, cursor):
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
can_accept=canAcceptBidState(state),
|
||||
can_expire=canExpireBidState(state),
|
||||
can_timeout=canTimeoutBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now,
|
||||
),
|
||||
@@ -65,7 +69,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept All",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=False,
|
||||
created_at=now,
|
||||
@@ -78,7 +82,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept Known",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=True,
|
||||
note="Accept bids from identities with previously successful swaps only",
|
||||
@@ -105,19 +109,23 @@ def upgradeDatabaseData(self, data_version):
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
if data_version > 0 and data_version < 6:
|
||||
if data_version > 0 and data_version < 7:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
can_accept = canAcceptBidState(state)
|
||||
can_expire = canExpireBidState(state)
|
||||
can_timeout = canTimeoutBidState(state)
|
||||
cursor.execute(
|
||||
"UPDATE bidstates SET can_accept = :can_accept, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
"UPDATE bidstates SET can_accept = :can_accept, can_expire = :can_expire, can_timeout = :can_timeout, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
{
|
||||
"in_error": in_error,
|
||||
"swap_failed": swap_failed,
|
||||
"swap_ended": swap_ended,
|
||||
"can_accept": can_accept,
|
||||
"can_expire": can_expire,
|
||||
"can_timeout": can_timeout,
|
||||
"state_id": int(state),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,97 +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 direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
]:
|
||||
cursor.execute(query_str, bid_query_data)
|
||||
for query_str in [
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
]:
|
||||
cursor.execute(query_str, offer_query_data)
|
||||
|
||||
if num_offers > 0 or num_bids > 0:
|
||||
self.log.info(
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import secrets
|
||||
import hashlib
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
|
||||
|
||||
def get_secret():
|
||||
return 9 + secrets.randbelow(edf.l - 9)
|
||||
|
||||
|
||||
def encodepoint(P):
|
||||
zi = edf.inv(P[2])
|
||||
x = (P[0] * zi) % edf.q
|
||||
y = (P[1] * zi) % edf.q
|
||||
y += (x & 1) << 255
|
||||
return y.to_bytes(32, byteorder="little")
|
||||
|
||||
|
||||
def hashToEd25519(bytes_in):
|
||||
hashed = hashlib.sha256(bytes_in).digest()
|
||||
for i in range(1000):
|
||||
h255 = bytearray(hashed)
|
||||
x_sign = 0 if h255[31] & 0x80 == 0 else 1
|
||||
h255[31] &= 0x7F # Clear top bit
|
||||
y = int.from_bytes(h255, byteorder="little")
|
||||
x = edf.xrecover(y, x_sign)
|
||||
if x == 0 and y == 1: # Skip infinity point
|
||||
continue
|
||||
|
||||
P = [x, y, 1, (x * y) % edf.q]
|
||||
# Keep trying until the point is in the correct subgroup
|
||||
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
||||
return P
|
||||
hashed = hashlib.sha256(hashed).digest()
|
||||
raise ValueError("hashToEd25519 failed")
|
||||
@@ -8,9 +8,6 @@
|
||||
import json
|
||||
|
||||
|
||||
default_chart_api_key = (
|
||||
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
|
||||
)
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import threading
|
||||
import http.client
|
||||
import base64
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from socket import error as SocketError
|
||||
from urllib import parse
|
||||
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if not session_id:
|
||||
return False
|
||||
|
||||
session_data = self.server.active_sessions.get(session_id)
|
||||
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
||||
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
return True
|
||||
with self.server.session_lock:
|
||||
session_data = self.server.active_sessions.get(session_id)
|
||||
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
||||
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
return True
|
||||
|
||||
if session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
if session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
return False
|
||||
|
||||
def log_error(self, format, *args):
|
||||
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
form_data = parse.parse_qs(post_string)
|
||||
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||
if self.server.last_form_id.get(name, None) == form_id:
|
||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
with self.server.form_id_lock:
|
||||
if self.server.last_form_id.get(name, None) == form_id:
|
||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
return form_data
|
||||
|
||||
def render_template(
|
||||
@@ -216,43 +218,48 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["debug_mode"] = True
|
||||
if swap_client.debug_ui:
|
||||
args_dict["debug_ui_mode"] = True
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
|
||||
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:
|
||||
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())
|
||||
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
|
||||
|
||||
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 as e:
|
||||
args_dict["current_status"] = "stopped"
|
||||
if swap_client._show_notifications:
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
else:
|
||||
args_dict["current_status"] = "unknown"
|
||||
args_dict["amm_active_count"] = 0
|
||||
if swap_client.debug:
|
||||
swap_client.log.error(f"Error getting AMM state: {str(e)}")
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
if swap_client._show_notifications:
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
|
||||
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:
|
||||
@@ -266,15 +273,27 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["current_page"] = "index"
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
with self.server.session_lock:
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
args_dict["shutdown_token"] = shutdown_token
|
||||
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
if is_authenticated:
|
||||
try:
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
except Exception as e:
|
||||
args_dict["encrypted"] = False
|
||||
args_dict["locked"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.warning(f"Could not get wallet locked state: {e}")
|
||||
else:
|
||||
args_dict["encrypted"] = args_dict.get("encrypted", False)
|
||||
args_dict["locked"] = args_dict.get("locked", False)
|
||||
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
with self.server.msg_id_lock:
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
|
||||
args_dict["version"] = version
|
||||
|
||||
@@ -364,7 +383,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
with self.server.session_lock:
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
cookie_header = self._set_session_cookie(session_id)
|
||||
|
||||
if is_json_request:
|
||||
@@ -628,13 +648,15 @@ 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)
|
||||
|
||||
@@ -935,22 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.page_error(str(ex))
|
||||
|
||||
def do_GET(self):
|
||||
response = self.handle_http(200, self.path)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
|
||||
response = self.handle_http(200, self.path)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(content_length)
|
||||
|
||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(content_length)
|
||||
|
||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, "text/html")
|
||||
@@ -963,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
class HttpThread(threading.Thread, ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
def __init__(self, host_name, port_no, allow_cors, swap_client):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@@ -979,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
self.env = env
|
||||
self.msg_id_counter = 0
|
||||
|
||||
self.session_lock = threading.Lock()
|
||||
self.form_id_lock = threading.Lock()
|
||||
self.msg_id_lock = threading.Lock()
|
||||
|
||||
self.timeout = 60
|
||||
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
|
||||
if swap_client.debug:
|
||||
swap_client.log.info("HTTP server initialized with threading support")
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
@@ -53,6 +53,7 @@ class CoinInterface:
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
self._altruistic = True
|
||||
self._core_version = None # Set in getDaemonVersion()
|
||||
|
||||
def interface_type(self) -> int:
|
||||
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.
|
||||
|
||||
@@ -818,7 +818,15 @@ class BCHInterface(BTCInterface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if tx.nLockTime != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if tx.nLockTime > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
|
||||
)
|
||||
|
||||
script_pk = self.getScriptDest(script_out)
|
||||
locked_n = findOutput(tx, script_pk)
|
||||
|
||||
@@ -16,8 +16,8 @@ import shutil
|
||||
import sqlite3
|
||||
import traceback
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from basicswap.basicswap_util import (
|
||||
getVoutByAddress,
|
||||
@@ -25,29 +25,25 @@ from basicswap.basicswap_util import (
|
||||
)
|
||||
from basicswap.interface.base import Secp256k1Interface
|
||||
from basicswap.util import (
|
||||
b2i,
|
||||
ensure,
|
||||
i2b,
|
||||
b2i,
|
||||
i2h,
|
||||
)
|
||||
from basicswap.util.ecc import (
|
||||
pointToCPK,
|
||||
CPKToPoint,
|
||||
)
|
||||
from basicswap.util.extkey import ExtKeyPair
|
||||
from basicswap.util.script import (
|
||||
SerialiseNumCompact,
|
||||
decodeScriptNum,
|
||||
getCompactSizeLen,
|
||||
SerialiseNumCompact,
|
||||
getWitnessElementLen,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
toWIF,
|
||||
b58encode,
|
||||
b58decode,
|
||||
decodeWif,
|
||||
b58encode,
|
||||
decodeAddress,
|
||||
decodeWif,
|
||||
pubkeyToAddress,
|
||||
toWIF,
|
||||
)
|
||||
from basicswap.util.crypto import (
|
||||
hash160,
|
||||
@@ -57,6 +53,7 @@ from coincurve.keys import (
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
from coincurve.types import ffi
|
||||
from coincurve.ecdsaotves import (
|
||||
ecdsaotves_enc_sign,
|
||||
ecdsaotves_enc_verify,
|
||||
@@ -77,17 +74,19 @@ from basicswap.contrib.test_framework.messages import (
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
CScriptOp,
|
||||
OP_IF,
|
||||
OP_ELSE,
|
||||
OP_ENDIF,
|
||||
OP_0,
|
||||
OP_2,
|
||||
OP_CHECKSIG,
|
||||
OP_CHECKMULTISIG,
|
||||
OP_CHECKSEQUENCEVERIFY,
|
||||
OP_CHECKSIG,
|
||||
OP_DROP,
|
||||
OP_HASH160,
|
||||
OP_DUP,
|
||||
OP_ELSE,
|
||||
OP_ENDIF,
|
||||
OP_EQUAL,
|
||||
OP_EQUALVERIFY,
|
||||
OP_HASH160,
|
||||
OP_IF,
|
||||
OP_RETURN,
|
||||
SIGHASH_ALL,
|
||||
SegwitV0SignatureHash,
|
||||
@@ -294,6 +293,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._expect_seedid_hex = None
|
||||
self._altruistic = coin_settings.get("altruistic", True)
|
||||
self._use_descriptors = coin_settings.get("use_descriptors", False)
|
||||
# Use hardened account indices to match existing wallet keys, only applies when use_descriptors is True
|
||||
self._use_legacy_key_paths = coin_settings.get("use_legacy_key_paths", False)
|
||||
self._disable_lock_tx_rbf = False
|
||||
|
||||
def open_rpc(self, wallet=None):
|
||||
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
|
||||
@@ -362,7 +364,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
self.rpc_wallet("getwalletinfo" if with_wallet else "getblockchaininfo")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc("getnetworkinfo")["version"]
|
||||
if self._core_version is None:
|
||||
self._core_version = self.rpc("getnetworkinfo")["version"]
|
||||
return self._core_version
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
return self.rpc("getblockchaininfo")
|
||||
@@ -397,6 +401,13 @@ class BTCInterface(Secp256k1Interface):
|
||||
last_block_header = prev_block_header
|
||||
raise ValueError(f"Block header not found at time: {time}")
|
||||
|
||||
def getWalletAccountPath(self) -> str:
|
||||
# Use a bip44 style path, however the seed (derived from the particl mnemonic keychain) can't be turned into a bip39 mnemonic without the matching entropy
|
||||
purpose: int = 84 # native segwit
|
||||
coin_type: int = self.chainparams_network()["bip44"]
|
||||
account: int = 0
|
||||
return f"{purpose}h/{coin_type}h/{account}h"
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
|
||||
assert len(key_bytes) == 32
|
||||
self._have_checked_seed = False
|
||||
@@ -405,8 +416,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
|
||||
if self._use_legacy_key_paths:
|
||||
# Match keys from legacy wallets (created from sethdseed)
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
|
||||
else:
|
||||
# Use a bip44 path so the seed can be exported as a mnemonic
|
||||
path: str = self.getWalletAccountPath()
|
||||
desc_external = descsum_create(f"wpkh({ek_encoded}/{path}/0/*)")
|
||||
desc_internal = descsum_create(f"wpkh({ek_encoded}/{path}/1/*)")
|
||||
|
||||
rv = self.rpc_wallet(
|
||||
"importdescriptors",
|
||||
@@ -445,6 +463,50 @@ class BTCInterface(Secp256k1Interface):
|
||||
"""
|
||||
raise (e)
|
||||
|
||||
def canExportToElectrum(self) -> bool:
|
||||
# keychains must be unhardened to export into electrum
|
||||
return self._use_descriptors is True and self._use_legacy_key_paths is False
|
||||
|
||||
def getAccountKey(
|
||||
self,
|
||||
key_bytes: bytes,
|
||||
extkey_prefix: Optional[int] = None,
|
||||
coin_type_overide: Optional[int] = None,
|
||||
) -> str:
|
||||
# For electrum, must start with zprv to get P2WPKH, addresses
|
||||
# extkey_prefix: 0x04b2430c
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
path: str = self.getWalletAccountPath()
|
||||
account_ek = ek.derive_path(path)
|
||||
return self.encode_secret_extkey(account_ek.encode_v(), extkey_prefix)
|
||||
|
||||
def getWalletKeyChains(
|
||||
self, key_bytes: bytes, extkey_prefix: Optional[int] = None
|
||||
) -> Dict[str, str]:
|
||||
ek = ExtKeyPair()
|
||||
ek.set_seed(key_bytes)
|
||||
|
||||
# extkey must contain keydata to derive hardened child keys
|
||||
|
||||
if self.canExportToElectrum():
|
||||
path: str = self.getWalletAccountPath()
|
||||
external_extkey = ek.derive_path(f"{path}/0")
|
||||
internal_extkey = ek.derive_path(f"{path}/1")
|
||||
else:
|
||||
# Match keychain paths of legacy wallets
|
||||
external_extkey = ek.derive_path("0h/0h")
|
||||
internal_extkey = ek.derive_path("0h/1h")
|
||||
|
||||
def encode_extkey(extkey):
|
||||
return self.encode_secret_extkey(extkey.encode_v(), extkey_prefix)
|
||||
|
||||
rv = {
|
||||
"external": encode_extkey(external_extkey),
|
||||
"internal": encode_extkey(internal_extkey),
|
||||
}
|
||||
return rv
|
||||
|
||||
def getWalletInfo(self):
|
||||
rv = self.rpc_wallet("getwalletinfo")
|
||||
rv["encrypted"] = "unlocked_until" in rv
|
||||
@@ -504,10 +566,16 @@ class BTCInterface(Secp256k1Interface):
|
||||
if descriptor is None:
|
||||
self._log.debug("Could not find active descriptor.")
|
||||
return "Not found"
|
||||
end = descriptor["desc"].find("/")
|
||||
start = descriptor["desc"].find("]")
|
||||
if start < 3:
|
||||
return "Could not parse descriptor"
|
||||
descriptor = descriptor["desc"][start + 1 :]
|
||||
|
||||
end = descriptor.find("/")
|
||||
if end < 10:
|
||||
return "Not found"
|
||||
extkey = descriptor["desc"][5:end]
|
||||
return "Could not parse descriptor"
|
||||
extkey = descriptor[:end]
|
||||
|
||||
extkey_data = b58decode(extkey)[4:-4]
|
||||
extkey_data_hash: bytes = hash160(extkey_data)
|
||||
return extkey_data_hash.hex()
|
||||
@@ -611,9 +679,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
pkh = hash160(pk)
|
||||
return segwit_addr.encode(bech32_prefix, version, pkh)
|
||||
|
||||
def encode_secret_extkey(self, ek_data: bytes) -> str:
|
||||
def encode_secret_extkey(self, ek_data: bytes, prefix=None) -> str:
|
||||
assert len(ek_data) == 74
|
||||
prefix = self.chainparams_network()["ext_secret_key_prefix"]
|
||||
if prefix is None:
|
||||
prefix = self.chainparams_network()["ext_secret_key_prefix"]
|
||||
data: bytes = prefix.to_bytes(4, "big") + ek_data
|
||||
checksum = sha256(sha256(data))
|
||||
return b58encode(data + checksum[0:4])
|
||||
@@ -672,18 +741,12 @@ class BTCInterface(Secp256k1Interface):
|
||||
wif_prefix = self.chainparams_network()["key_prefix"]
|
||||
return toWIF(wif_prefix, key_bytes)
|
||||
|
||||
def encodePubkey(self, pk: bytes) -> bytes:
|
||||
return pointToCPK(pk)
|
||||
|
||||
def encodeSegwitAddress(self, key_hash: bytes) -> str:
|
||||
return segwit_addr.encode(self.chainparams_network()["hrp"], 0, key_hash)
|
||||
|
||||
def decodeSegwitAddress(self, addr: str) -> bytes:
|
||||
return bytes(segwit_addr.decode(self.chainparams_network()["hrp"], addr)[1])
|
||||
|
||||
def decodePubkey(self, pke):
|
||||
return CPKToPoint(pke)
|
||||
|
||||
def decodeKey(self, k: str) -> bytes:
|
||||
return decodeWif(k)
|
||||
|
||||
@@ -691,10 +754,11 @@ class BTCInterface(Secp256k1Interface):
|
||||
# p2wpkh
|
||||
return CScript([OP_0, pkh])
|
||||
|
||||
def loadTx(self, tx_bytes: bytes) -> CTransaction:
|
||||
def loadTx(self, tx_bytes: bytes, allow_witness: bool = True) -> CTransaction:
|
||||
# Load tx from bytes to internal representation
|
||||
# Transactions with no inputs require allow_witness set to false to decode correctly
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(tx_bytes))
|
||||
tx.deserialize(BytesIO(tx_bytes), allow_witness)
|
||||
return tx
|
||||
|
||||
def createSCLockTx(
|
||||
@@ -702,27 +766,63 @@ class BTCInterface(Secp256k1Interface):
|
||||
) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.nLockTime = 0 # TODO: match locktimes by core
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
return tx.serialize()
|
||||
|
||||
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None) -> bytes:
|
||||
funded_tx = self.fundTx(tx_bytes, feerate)
|
||||
|
||||
if self._disable_lock_tx_rbf:
|
||||
tx = self.loadTx(funded_tx)
|
||||
for txi in tx.vin:
|
||||
txi.nSequence = 0xFFFFFFFE
|
||||
funded_tx = tx.serialize_with_witness()
|
||||
return funded_tx
|
||||
|
||||
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
# fmt: off
|
||||
return CScript([
|
||||
CScriptOp(OP_IF),
|
||||
2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG),
|
||||
2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG),
|
||||
CScriptOp(OP_ELSE),
|
||||
csv_val, CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
|
||||
Kaf_enc, CScriptOp(OP_CHECKSIG),
|
||||
Kaf, CScriptOp(OP_CHECKSIG),
|
||||
CScriptOp(OP_ENDIF)])
|
||||
# fmt: on
|
||||
|
||||
def isScriptP2PKH(self, script: bytes) -> bool:
|
||||
if len(script) != 25:
|
||||
return False
|
||||
if script[0] != OP_DUP:
|
||||
return False
|
||||
if script[1] != OP_HASH160:
|
||||
return False
|
||||
if script[2] != 20:
|
||||
return False
|
||||
if script[23] != OP_EQUALVERIFY:
|
||||
return False
|
||||
if script[24] != OP_CHECKSIG:
|
||||
return False
|
||||
return True
|
||||
|
||||
def isScriptP2WPKH(self, script: bytes) -> bool:
|
||||
if len(script) != 22:
|
||||
return False
|
||||
if script[0] != OP_0:
|
||||
return False
|
||||
if script[1] != 20:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
if self.isScriptP2WPKH(script):
|
||||
return [bytes(72), bytes(33)]
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def createSCLockRefundTx(
|
||||
self,
|
||||
tx_lock_bytes,
|
||||
@@ -978,7 +1078,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if tx.nLockTime != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if tx.nLockTime > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
|
||||
)
|
||||
|
||||
script_pk = self.getScriptDest(script_out)
|
||||
locked_n = findOutput(tx, script_pk)
|
||||
@@ -1250,7 +1358,17 @@ class BTCInterface(Secp256k1Interface):
|
||||
)
|
||||
|
||||
eck = PrivateKey(key_bytes)
|
||||
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
|
||||
for i in range(10000):
|
||||
# Grind for low-R value
|
||||
if i == 0:
|
||||
nonce = (ffi.NULL, ffi.NULL)
|
||||
else:
|
||||
extra_entropy = i.to_bytes(4, "little") + (b"\0" * 28)
|
||||
nonce = (ffi.NULL, ffi.new("unsigned char [32]", extra_entropy))
|
||||
sig = eck.sign(sig_hash, hasher=None, custom_nonce=nonce)
|
||||
if len(sig) < 71:
|
||||
return sig + bytes((SIGHASH_ALL,))
|
||||
raise RuntimeError("sign failed.")
|
||||
|
||||
def signTxOtVES(
|
||||
self,
|
||||
@@ -1316,7 +1434,8 @@ class BTCInterface(Secp256k1Interface):
|
||||
"feeRate": feerate_str,
|
||||
}
|
||||
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
|
||||
return bytes.fromhex(rv["hex"])
|
||||
tx_bytes: bytes = bytes.fromhex(rv["hex"])
|
||||
return tx_bytes
|
||||
|
||||
def getNonSegwitOutputs(self):
|
||||
unspents = self.rpc_wallet("listunspent", [0, 99999999])
|
||||
@@ -1705,6 +1824,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
"height": block_height,
|
||||
}
|
||||
|
||||
if "mempoolconflicts" in tx and len(tx["mempoolconflicts"]) > 0:
|
||||
rv["conflicts"] = tx["mempoolconflicts"]
|
||||
elif "walletconflicts" in tx and len(tx["walletconflicts"]) > 0:
|
||||
rv["conflicts"] = tx["walletconflicts"]
|
||||
except Exception as e:
|
||||
self._log.debug(
|
||||
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
|
||||
@@ -1868,6 +1991,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
def getBlockWithTxns(self, block_hash: str):
|
||||
return self.rpc("getblock", [block_hash, 2])
|
||||
|
||||
def listUtxos(self):
|
||||
return self.rpc_wallet("listunspent")
|
||||
|
||||
def getUnspentsByAddr(self):
|
||||
unspent_addr = dict()
|
||||
unspent = self.rpc_wallet("listunspent")
|
||||
@@ -1904,6 +2030,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
sum_unspent += self.make_int(o["amount"])
|
||||
return sum_unspent
|
||||
|
||||
def signMessage(self, address: str, message: str) -> str:
|
||||
return self.rpc_wallet(
|
||||
"signmessage",
|
||||
[address, message],
|
||||
)
|
||||
|
||||
def signMessageWithKey(self, key_wif: str, message: str) -> str:
|
||||
return self.rpc("signmessagewithprivkey", [key_wif, message])
|
||||
|
||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
unspent_addr = self.getUnspentsByAddr()
|
||||
@@ -1921,6 +2056,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.debug(f"sign_for_addr {sign_for_addr}")
|
||||
|
||||
funds_addr: str = sign_for_addr
|
||||
|
||||
if (
|
||||
self.using_segwit()
|
||||
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
@@ -1929,6 +2065,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
sign_for_addr = self.pkh_to_address(pkh)
|
||||
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
|
||||
|
||||
sign_message: str = sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()
|
||||
if self._use_descriptors:
|
||||
# https://github.com/bitcoin/bitcoin/issues/10542
|
||||
# https://github.com/bitcoin/bitcoin/issues/26046
|
||||
@@ -1945,7 +2082,6 @@ class BTCInterface(Secp256k1Interface):
|
||||
],
|
||||
)
|
||||
hdkeypath = addr_info["hdkeypath"]
|
||||
|
||||
sign_for_address_key = None
|
||||
for descriptor in priv_keys["descriptors"]:
|
||||
if descriptor["active"] is False or descriptor["internal"] is True:
|
||||
@@ -1966,22 +2102,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
sign_for_address_key = self.encodeKey(ek._key)
|
||||
break
|
||||
assert sign_for_address_key is not None
|
||||
signature = self.rpc(
|
||||
"signmessagewithprivkey",
|
||||
[
|
||||
sign_for_address_key,
|
||||
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
signature = self.signMessageWithKey(sign_for_address_key, sign_message)
|
||||
del priv_keys
|
||||
else:
|
||||
signature = self.rpc_wallet(
|
||||
"signmessage",
|
||||
[
|
||||
sign_for_addr,
|
||||
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
signature = self.signMessage(sign_for_addr, sign_message)
|
||||
|
||||
prove_utxos = [] # TODO: Send specific utxos
|
||||
return (sign_for_addr, signature, prove_utxos)
|
||||
|
||||
@@ -521,7 +521,7 @@ class CTransaction(object):
|
||||
self.hash = tx.hash
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
ver32bit = struct.unpack("<i", f.read(4))[0]
|
||||
self.nVersion = ver32bit & 0xffff
|
||||
self.nType = (ver32bit >> 16) & 0xffff
|
||||
|
||||
@@ -455,12 +455,12 @@ class CTransaction(object):
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
self.strDZeel = copy.deepcopy(tx.strDZeel)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<i", f.read(4))[0]
|
||||
self.nTime = struct.unpack("<i", f.read(4))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
flags = 0
|
||||
if len(self.vin) == 0:
|
||||
if len(self.vin) == 0 and allow_witness:
|
||||
flags = struct.unpack("<B", f.read(1))[0]
|
||||
# Not sure why flags can't be zero, but this
|
||||
# matches the implementation in bitcoind
|
||||
|
||||
@@ -505,7 +505,7 @@ class CTransaction:
|
||||
self.sha256 = tx.sha256
|
||||
self.hash = tx.hash
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<h", f.read(2))[0]
|
||||
self.nType = struct.unpack("<h", f.read(2))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
|
||||
@@ -13,6 +13,8 @@ import logging
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from typing import List
|
||||
|
||||
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
@@ -1085,22 +1087,21 @@ class DCRInterface(Secp256k1Interface):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
|
||||
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes:
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
script = bytearray()
|
||||
script += bytes((OP_IF,))
|
||||
push_script_data(script, bytes((2,)))
|
||||
push_script_data(script, Kal_enc)
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kal)
|
||||
push_script_data(script, Kaf)
|
||||
push_script_data(script, bytes((2,)))
|
||||
script += bytes((OP_CHECKMULTISIG,))
|
||||
script += bytes((OP_ELSE,))
|
||||
script += CScriptNum.encode(CScriptNum(csv_val))
|
||||
script += bytes((OP_CHECKSEQUENCEVERIFY,))
|
||||
script += bytes((OP_DROP,))
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kaf)
|
||||
script += bytes((OP_CHECKSIG,))
|
||||
script += bytes((OP_ENDIF,))
|
||||
|
||||
@@ -1609,11 +1610,11 @@ class DCRInterface(Secp256k1Interface):
|
||||
script_pk = self.getScriptDest(script)
|
||||
return findOutput(tx, script_pk)
|
||||
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
return [bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
|
||||
tx = self.loadTx(tx_bytes)
|
||||
|
||||
@@ -89,7 +89,7 @@ class CTransaction:
|
||||
self.locktime = tx.locktime
|
||||
self.expiry = tx.expiry
|
||||
|
||||
def deserialize(self, data: bytes) -> None:
|
||||
def deserialize(self, data: bytes, allow_witness: bool = True) -> None:
|
||||
|
||||
version = int.from_bytes(data[:4], "little")
|
||||
self.version = version & 0xFFFF
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import hashlib
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOutPart,
|
||||
@@ -134,6 +135,11 @@ class PARTInterface(BTCInterface):
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
|
||||
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
|
||||
return [bytes(72), bytes(33)]
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
|
||||
|
||||
@@ -190,6 +196,24 @@ class PARTInterface(BTCInterface):
|
||||
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):
|
||||
|
||||
@@ -490,7 +514,16 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(lock_txid_hex)))
|
||||
|
||||
ensure(lock_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
ensure(lock_tx_obj["locktime"] == 0, "Bad nLockTime")
|
||||
lock_time: int = lock_tx_obj["locktime"]
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if lock_time != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if lock_time > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {lock_time}, current height {current_height}"
|
||||
)
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
|
||||
@@ -29,7 +29,7 @@ class WOWInterface(XMRInterface):
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
# below only needed until wow is rebased to monero v0.18.4.0+
|
||||
def openWallet(self, filename):
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
import basicswap.ed25519_fast_util as edu
|
||||
import basicswap.util_xmr as xmr_util
|
||||
from coincurve.ed25519 import (
|
||||
ed25519_add,
|
||||
@@ -35,6 +35,9 @@ from basicswap.chainparams import XMR_COIN, Coins
|
||||
from basicswap.interface.base import CoinInterface
|
||||
|
||||
|
||||
ed25519_l = 2**252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
class XMRInterface(CoinInterface):
|
||||
@staticmethod
|
||||
def curve_type():
|
||||
@@ -84,7 +87,15 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def is_transient_error(self, ex) -> bool:
|
||||
str_error: str = str(ex).lower()
|
||||
if "failed to get earliest fork height" in str_error:
|
||||
if any(
|
||||
response in str_error
|
||||
for response in [
|
||||
"failed to get earliest fork height",
|
||||
"failed to get output distribution",
|
||||
"request-sent",
|
||||
"idle",
|
||||
]
|
||||
):
|
||||
return True
|
||||
return super().is_transient_error(ex)
|
||||
|
||||
@@ -146,6 +157,8 @@ class XMRInterface(CoinInterface):
|
||||
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
|
||||
# walletrpctimeoutlong likely unneeded
|
||||
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
|
||||
self._num_chaininfo_retries = coin_settings.get("numchaininforetries", 20)
|
||||
self._chaininfo_retry_delay = coin_settings.get("chaininforetrydelay", 1)
|
||||
|
||||
self.rpc = make_xmr_rpc_func(
|
||||
coin_settings["rpcport"],
|
||||
@@ -287,10 +300,13 @@ class XMRInterface(CoinInterface):
|
||||
self.rpc_wallet("get_languages")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet("get_version")["version"]
|
||||
# Returns wallet version
|
||||
if self._core_version is None:
|
||||
self._core_version = self.rpc_wallet("get_version")["version"]
|
||||
return self._core_version
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
get_height = self.getChainHeight(full_output=True)
|
||||
rv = {
|
||||
"blocks": get_height["height"],
|
||||
"verificationprogress": 0.0,
|
||||
@@ -319,8 +335,16 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
|
||||
def getChainHeight(self, full_output: bool = False):
|
||||
for i in range(self._num_chaininfo_retries):
|
||||
try:
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
return get_height if full_output else get_height["height"]
|
||||
except Exception as e:
|
||||
if i < self._num_chaininfo_retries - 1 and self.is_transient_error(e):
|
||||
time.sleep(self._chaininfo_retry_delay)
|
||||
continue
|
||||
raise (e)
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
@@ -378,10 +402,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def getNewRandomKey(self) -> bytes:
|
||||
# Note: Returned bytes are in big endian order
|
||||
return i2b(edu.get_secret())
|
||||
|
||||
def pubkey(self, key: bytes) -> bytes:
|
||||
return edf.scalarmult_B(key)
|
||||
return i2b(9 + secrets.randbelow(ed25519_l - 9))
|
||||
|
||||
def encodeKey(self, vk: bytes) -> str:
|
||||
return vk[::-1].hex()
|
||||
@@ -389,12 +410,6 @@ class XMRInterface(CoinInterface):
|
||||
def decodeKey(self, k_hex: str) -> bytes:
|
||||
return bytes.fromhex(k_hex)[::-1]
|
||||
|
||||
def encodePubkey(self, pk: bytes) -> str:
|
||||
return edu.encodepoint(pk)
|
||||
|
||||
def decodePubkey(self, pke):
|
||||
return edf.decodepoint(pke)
|
||||
|
||||
def getPubkey(self, privkey):
|
||||
return ed25519_get_pubkey(privkey)
|
||||
|
||||
@@ -405,7 +420,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def verifyKey(self, k: int) -> bool:
|
||||
i = b2i(k)
|
||||
return i < edf.l and i > 8
|
||||
return i < ed25519_l and i > 8
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes):
|
||||
# Calls ed25519_decode_check_point() in secp256k1
|
||||
@@ -543,16 +558,14 @@ class XMRInterface(CoinInterface):
|
||||
rv = -1
|
||||
return rv
|
||||
|
||||
def findTxnByHash(self, txid):
|
||||
def findTxnByHash(self, txid: str):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
try:
|
||||
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
|
||||
"height"
|
||||
]
|
||||
current_height: int = self.getChainHeight()
|
||||
self._log.info(
|
||||
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
|
||||
)
|
||||
|
||||
@@ -123,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(coins), "UTF-8")
|
||||
|
||||
|
||||
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
try:
|
||||
|
||||
swap_client.updateWalletsInfo()
|
||||
wallets = swap_client.getCachedWalletsInfo()
|
||||
coins_with_balances = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
|
||||
balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "balance" in w and "error" not in w and "no_data" not in w:
|
||||
raw_balance = w["balance"]
|
||||
if isinstance(raw_balance, float):
|
||||
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
balance = str(raw_balance)
|
||||
else:
|
||||
balance = raw_balance
|
||||
|
||||
pending = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
ci = swap_client.ci(k)
|
||||
pending_amount = 0
|
||||
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["unconfirmed"])
|
||||
if "immature" in w and float(w["immature"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["immature"])
|
||||
if pending_amount > 0:
|
||||
pending = ci.format_amount(pending_amount)
|
||||
|
||||
coin_entry = {
|
||||
"id": int(k),
|
||||
"name": getCoinName(k),
|
||||
"balance": balance,
|
||||
"pending": pending,
|
||||
"ticker": chainparams[k]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(coin_entry)
|
||||
|
||||
if k == Coins.PART:
|
||||
variants = [
|
||||
{
|
||||
"coin": Coins.PART_ANON,
|
||||
"balance_field": "anon_balance",
|
||||
"pending_field": "anon_pending",
|
||||
},
|
||||
{
|
||||
"coin": Coins.PART_BLIND,
|
||||
"balance_field": "blind_balance",
|
||||
"pending_field": "blind_unconfirmed",
|
||||
},
|
||||
]
|
||||
|
||||
for variant_info in variants:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if variant_info["balance_field"] in w:
|
||||
raw_balance = w[variant_info["balance_field"]]
|
||||
if isinstance(raw_balance, float):
|
||||
variant_balance = f"{raw_balance:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
variant_balance = str(raw_balance)
|
||||
else:
|
||||
variant_balance = raw_balance
|
||||
|
||||
if (
|
||||
variant_info["pending_field"] in w
|
||||
and float(w[variant_info["pending_field"]]) > 0.0
|
||||
):
|
||||
variant_pending = str(
|
||||
w[variant_info["pending_field"]]
|
||||
)
|
||||
|
||||
variant_entry = {
|
||||
"id": int(variant_info["coin"]),
|
||||
"name": getCoinName(variant_info["coin"]),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.PART]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
elif k == Coins.LTC:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if "mweb_balance" in w:
|
||||
variant_balance = w["mweb_balance"]
|
||||
|
||||
pending_amount = 0
|
||||
if (
|
||||
"mweb_unconfirmed" in w
|
||||
and float(w["mweb_unconfirmed"]) > 0.0
|
||||
):
|
||||
pending_amount += float(w["mweb_unconfirmed"])
|
||||
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
|
||||
pending_amount += float(w["mweb_immature"])
|
||||
if pending_amount > 0:
|
||||
variant_pending = f"{pending_amount:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
|
||||
variant_entry = {
|
||||
"id": int(Coins.LTC_MWEB),
|
||||
"name": getCoinName(Coins.LTC_MWEB),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.LTC]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
return bytes(json.dumps(coins_with_balances), "UTF-8")
|
||||
|
||||
except Exception as e:
|
||||
error_data = {"error": str(e)}
|
||||
return bytes(json.dumps(error_data), "UTF-8")
|
||||
|
||||
|
||||
def js_wallets(self, url_split, post_string, is_json):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -277,6 +416,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
"is_revoked": True if o.active_ind == 2 else False,
|
||||
"is_public": o.addr_to == swap_client.network_addr
|
||||
or o.addr_to.strip() == "",
|
||||
"message_nets": o.message_nets,
|
||||
}
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
if with_extra_info:
|
||||
@@ -294,7 +434,6 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
offer_data["feerate_to"] = o.to_feerate
|
||||
|
||||
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
|
||||
if o.was_sent:
|
||||
try:
|
||||
@@ -371,7 +510,8 @@ def formatBids(swap_client, bids, filters) -> bytes:
|
||||
bid_rate: int = 0 if b[10] is None else b[10]
|
||||
amount_to = None
|
||||
if ci_to:
|
||||
amount_to = ci_to.format_amount((b[4] * bid_rate) // ci_from.COIN())
|
||||
amount_to_int = (b[4] * bid_rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
|
||||
bid_data = {
|
||||
"bid_id": b[2].hex(),
|
||||
@@ -661,16 +801,14 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
if amt_from_str is not None:
|
||||
rate = ci_to.make_int(rate, r=1)
|
||||
amt_from = inputAmount(amt_from_str, ci_from)
|
||||
amount_to = ci_to.format_amount(
|
||||
int((amt_from * rate) // ci_from.COIN()), r=1
|
||||
)
|
||||
amount_to_int = (amt_from * rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
return bytes(json.dumps({"amount_to": amount_to}), "UTF-8")
|
||||
if amt_to_str is not None:
|
||||
rate = ci_from.make_int(1.0 / float(rate), r=1)
|
||||
amt_to = inputAmount(amt_to_str, ci_to)
|
||||
amount_from = ci_from.format_amount(
|
||||
int((amt_to * rate) // ci_to.COIN()), r=1
|
||||
)
|
||||
amount_from_int = (amt_to * rate + ci_to.COIN() - 1) // ci_to.COIN()
|
||||
amount_from = ci_from.format_amount(amount_from_int)
|
||||
return bytes(json.dumps({"amount_from": amount_from}), "UTF-8")
|
||||
|
||||
amt_from: int = inputAmount(get_data_entry(post_data, "amt_from"), ci_from)
|
||||
@@ -704,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
if not swap_client.debug:
|
||||
raise ValueError("Debug mode not active.")
|
||||
|
||||
r = random.randint(0, 3)
|
||||
r = random.randint(0, 4)
|
||||
if r == 0:
|
||||
swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()})
|
||||
swap_client.notify(
|
||||
NT.OFFER_RECEIVED,
|
||||
{
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 1:
|
||||
swap_client.notify(
|
||||
NT.BID_RECEIVED,
|
||||
@@ -714,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "atomic",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"bid_amount": 50000000,
|
||||
"bid_amount_to": 7750000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 2:
|
||||
@@ -725,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "ads",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 1,
|
||||
"coin_to": 3,
|
||||
"amount_from": 500000000,
|
||||
"amount_to": 100000000,
|
||||
"bid_amount": 250000000,
|
||||
"bid_amount_to": 50000000,
|
||||
"rate": 20000000,
|
||||
},
|
||||
)
|
||||
elif r == 4:
|
||||
swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()})
|
||||
|
||||
return bytes(json.dumps({"type": r}), "UTF-8")
|
||||
|
||||
|
||||
def js_checkupdates(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
if not swap_client.settings.get("check_updates", True):
|
||||
return bytes(
|
||||
json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8"
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
now = time.time()
|
||||
last_manual_check = getattr(swap_client, "_last_manual_update_check", 0)
|
||||
|
||||
if not swap_client.debug and (now - last_manual_check) < 3600:
|
||||
remaining = int(3600 - (now - last_manual_check))
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"error": f"Please wait {remaining // 60} minutes before checking again"
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
swap_client._last_manual_update_check = now
|
||||
swap_client.log.info("Manual update check requested via web interface")
|
||||
|
||||
swap_client.checkForUpdates()
|
||||
|
||||
if swap_client._update_available:
|
||||
swap_client.log.info(
|
||||
f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})"
|
||||
)
|
||||
else:
|
||||
swap_client.log.info(f"Manual check result: Up to date (v{__version__})")
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"message": "Update check completed",
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"update_available": swap_client._update_available,
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -738,6 +952,32 @@ def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(swap_client.getNotifications()), "UTF-8")
|
||||
|
||||
|
||||
def js_updatestatus(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"update_available": swap_client._update_available,
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"release_url": (
|
||||
f"https://github.com/basicswap/basicswap/releases/tag/v{swap_client._latest_version}"
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
"release_notes": (
|
||||
f"New version v{swap_client._latest_version} is available. Click to view details on GitHub."
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -915,6 +1155,15 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
coin_in = get_data_entry(post_data, "coin")
|
||||
extkey_prefix = get_data_entry_or(
|
||||
post_data, "extkey_prefix", 0x04B2430C
|
||||
) # default, zprv for P2WPKH in electrum
|
||||
if isinstance(extkey_prefix, str):
|
||||
if extkey_prefix.isdigit():
|
||||
extkey_prefix = int(extkey_prefix)
|
||||
else:
|
||||
extkey_prefix = int(extkey_prefix, 16) # Try hex
|
||||
|
||||
try:
|
||||
coin = getCoinIdFromName(coin_in)
|
||||
except Exception:
|
||||
@@ -951,7 +1200,6 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
wallet_seed_id = ci.getWalletSeedID()
|
||||
except Exception as e:
|
||||
wallet_seed_id = f"Error: {e}"
|
||||
|
||||
rv.update(
|
||||
{
|
||||
"seed": seed_key.hex(),
|
||||
@@ -960,6 +1208,10 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
"current_seed_id": wallet_seed_id,
|
||||
}
|
||||
)
|
||||
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
|
||||
rv.update(
|
||||
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
|
||||
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
|
||||
|
||||
return bytes(
|
||||
json.dumps(rv),
|
||||
@@ -1169,6 +1421,111 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
volume_data = swap_client.lookupVolume(
|
||||
coin_ids, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in volume_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 3600))
|
||||
days: int = int(get_data_entry_or(post_data, "days", 1))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
historical_data = swap_client.lookupHistoricalData(
|
||||
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in historical_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "days": days, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
@@ -1214,6 +1571,7 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||
|
||||
endpoints = {
|
||||
"coins": js_coins,
|
||||
"walletbalances": js_walletbalances,
|
||||
"wallets": js_wallets,
|
||||
"offers": js_offers,
|
||||
"sentoffers": js_sentoffers,
|
||||
@@ -1226,6 +1584,8 @@ endpoints = {
|
||||
"rates": js_rates,
|
||||
"rateslist": js_rates_list,
|
||||
"generatenotification": js_generatenotification,
|
||||
"checkupdates": js_checkupdates,
|
||||
"updatestatus": js_updatestatus,
|
||||
"notifications": js_notifications,
|
||||
"identities": js_identities,
|
||||
"automationstrategies": js_automationstrategies,
|
||||
@@ -1239,6 +1599,8 @@ endpoints = {
|
||||
"readurl": js_readurl,
|
||||
"active": js_active,
|
||||
"coinprices": js_coinprices,
|
||||
"coinvolume": js_coinvolume,
|
||||
"coinhistory": js_coinhistory,
|
||||
"messageroutes": js_messageroutes,
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,8 @@ class OfferMessage(NonProtobufClass):
|
||||
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
19: ("proof_utxos", NPBW_BYTES, 0),
|
||||
20: ("auto_accept_type", 0, 0),
|
||||
20: ("auto_accept_type", NPBW_INT, 0),
|
||||
21: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +161,7 @@ class BidMessage(NonProtobufClass):
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +201,7 @@ class XmrBidMessage(NonProtobufClass):
|
||||
7: ("kbvf", NPBW_BYTES, 0),
|
||||
8: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
9: ("dest_af", NPBW_BYTES, 0),
|
||||
10: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
@@ -261,6 +264,7 @@ class ADSBidIntentMessage(NonProtobufClass):
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
@@ -282,3 +286,21 @@ class ConnectReqMessage(NonProtobufClass):
|
||||
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
@@ -23,9 +23,9 @@ from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
b58decode,
|
||||
decodeWif,
|
||||
)
|
||||
from basicswap.basicswap_util import AddressTypes
|
||||
|
||||
|
||||
def encode_base64(data: bytes) -> str:
|
||||
@@ -172,34 +172,6 @@ def waitForConnected(ws_thread, delay_event):
|
||||
raise ValueError("waitForConnected timed-out.")
|
||||
|
||||
|
||||
def getPrivkeyForAddress(self, addr) -> bytes:
|
||||
|
||||
ci_part = self.ci(Coins.PART)
|
||||
try:
|
||||
return ci_part.decodeKey(
|
||||
self.callrpc(
|
||||
"smsgdumpprivkey",
|
||||
[
|
||||
addr,
|
||||
],
|
||||
)
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
try:
|
||||
return ci_part.decodeKey(
|
||||
ci_part.rpc_wallet(
|
||||
"dumpprivkey",
|
||||
[
|
||||
addr,
|
||||
],
|
||||
)
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
raise ValueError("key not found")
|
||||
|
||||
|
||||
def encryptMsg(
|
||||
self,
|
||||
addr_from: str,
|
||||
@@ -209,42 +181,21 @@ def encryptMsg(
|
||||
cursor,
|
||||
timestamp=None,
|
||||
deterministic=False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> bytes:
|
||||
self.log.debug("encryptMsg")
|
||||
|
||||
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)
|
||||
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
|
||||
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
|
||||
|
||||
privkey_from = getPrivkeyForAddress(self, addr_from)
|
||||
|
||||
payload += bytes((0,)) # Include null byte to match smsg
|
||||
smsg_msg: bytes = smsgEncrypt(
|
||||
privkey_from, pubkey_to, payload, timestamp, deterministic
|
||||
privkey_from,
|
||||
pubkey_to,
|
||||
payload,
|
||||
timestamp,
|
||||
deterministic,
|
||||
msg_valid,
|
||||
difficulty_target=difficulty_target,
|
||||
)
|
||||
|
||||
return smsg_msg
|
||||
@@ -261,11 +212,21 @@ def sendSimplexMsg(
|
||||
timestamp: int = None,
|
||||
deterministic: bool = False,
|
||||
to_user_name: str = None,
|
||||
return_msg: bool = False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> bytes:
|
||||
self.log.debug("sendSimplexMsg")
|
||||
|
||||
smsg_msg: bytes = encryptMsg(
|
||||
self, addr_from, addr_to, payload, msg_valid, cursor, timestamp, deterministic
|
||||
self,
|
||||
addr_from,
|
||||
addr_to,
|
||||
payload,
|
||||
msg_valid,
|
||||
cursor,
|
||||
timestamp,
|
||||
deterministic,
|
||||
difficulty_target,
|
||||
)
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
|
||||
@@ -280,6 +241,33 @@ def sendSimplexMsg(
|
||||
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
|
||||
|
||||
@@ -292,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"
|
||||
@@ -308,31 +296,34 @@ def decryptSimplexMsg(self, msg_data):
|
||||
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()
|
||||
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
addr_rows = cursor.execute(query, {"now": now}).fetchall()
|
||||
addr_rows = cursor.execute(
|
||||
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
|
||||
|
||||
|
||||
@@ -375,7 +366,6 @@ def parseSimplexMsg(self, chat_item):
|
||||
return decrypted_msg
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -421,7 +411,7 @@ def readSimplexMsgs(self, network):
|
||||
elif processEvent(self, ws_thread, msg_type, data):
|
||||
pass
|
||||
else:
|
||||
self.log.debug(f"Unknown msg_type: {msg_type}")
|
||||
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}")
|
||||
@@ -432,10 +422,11 @@ def readSimplexMsgs(self, network):
|
||||
|
||||
|
||||
def getResponseData(data, tag=None):
|
||||
if "Right" in data["resp"]:
|
||||
if tag:
|
||||
return data["resp"]["Right"][tag]
|
||||
return data["resp"]["Right"]
|
||||
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"]
|
||||
@@ -474,12 +465,14 @@ def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
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:
|
||||
|
||||
@@ -14,7 +14,43 @@ import time
|
||||
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
|
||||
@@ -86,28 +122,18 @@ def startSimplexClient(
|
||||
args += ["--socks-proxy", socks_proxy]
|
||||
|
||||
if not os.path.exists(simplex_db_path):
|
||||
# Need to set initial profile through CLI
|
||||
# TODO: Must be a better way?
|
||||
# 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:
|
||||
# Workaround to avoid error:
|
||||
# SQLite3 returned ErrorConstraint while attempting to perform step: UNIQUE constraint failed: protocol_servers.user_id, protocol_servers.host, protocol_servers.port
|
||||
# TODO: Remove?
|
||||
with sqlite3.connect(simplex_db_path) as con:
|
||||
c = con.cursor()
|
||||
if ":" in server_address:
|
||||
host, port = server_address.split(":")
|
||||
else:
|
||||
host = server_address
|
||||
port = ""
|
||||
query: str = (
|
||||
"SELECT COUNT(*) FROM protocol_servers WHERE host = :host and port = :port"
|
||||
)
|
||||
q = c.execute(query, {"host": host, "port": port}).fetchone()
|
||||
if q[0] < 1:
|
||||
args += ["-s", server_address]
|
||||
# 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", log_level]
|
||||
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -215,10 +215,10 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
|
||||
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
addr_to = self.getMockAddrTo(ci)
|
||||
|
||||
135
basicswap/rpc.py
@@ -6,8 +6,10 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import urllib
|
||||
import http.client
|
||||
from xmlrpc.client import (
|
||||
Fault,
|
||||
Transport,
|
||||
@@ -15,6 +17,35 @@ from xmlrpc.client import (
|
||||
)
|
||||
from .util import jsonDecimal
|
||||
|
||||
_use_rpc_pooling = False
|
||||
_rpc_pool_settings = {}
|
||||
|
||||
|
||||
def enable_rpc_pooling(settings):
|
||||
global _use_rpc_pooling, _rpc_pool_settings
|
||||
_use_rpc_pooling = settings.get("enabled", False)
|
||||
_rpc_pool_settings = settings
|
||||
|
||||
|
||||
class TimeoutTransport(Transport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class TimeoutSafeTransport(SafeTransport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class Jsonrpc:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
@@ -29,22 +60,40 @@ class Jsonrpc:
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
timeout=10,
|
||||
):
|
||||
# establish a "logical" server connection
|
||||
|
||||
# get the url
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
self.__host = parsed.netloc
|
||||
|
||||
self.__auth = None
|
||||
if "@" in parsed.netloc:
|
||||
auth_part, host_port = parsed.netloc.rsplit("@", 1)
|
||||
self.__host = host_port
|
||||
if ":" in auth_part:
|
||||
import base64
|
||||
|
||||
auth_bytes = auth_part.encode("utf-8")
|
||||
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
|
||||
self.__auth = f"Basic {auth_b64}"
|
||||
else:
|
||||
self.__host = parsed.netloc
|
||||
|
||||
if not self.__host:
|
||||
raise ValueError(f"Invalid or empty hostname in URI: {uri}")
|
||||
self.__handler = parsed.path
|
||||
if not self.__handler:
|
||||
self.__handler = "/RPC2"
|
||||
|
||||
if transport is None:
|
||||
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||
handler = (
|
||||
TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
|
||||
)
|
||||
extra_kwargs = {}
|
||||
transport = handler(
|
||||
timeout=timeout,
|
||||
use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs,
|
||||
@@ -62,6 +111,7 @@ class Jsonrpc:
|
||||
self.__transport.close()
|
||||
|
||||
def json_request(self, method, params):
|
||||
connection = None
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
headers = self.__transport._extra_headers[:]
|
||||
@@ -71,6 +121,10 @@ class Jsonrpc:
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", "jsonrpc"))
|
||||
|
||||
if self.__auth:
|
||||
headers.append(("Authorization", self.__auth))
|
||||
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
@@ -79,18 +133,29 @@ class Jsonrpc:
|
||||
self.__request_id += 1
|
||||
|
||||
resp = connection.getresponse()
|
||||
return resp.read()
|
||||
result = resp.read()
|
||||
|
||||
connection.close()
|
||||
|
||||
return result
|
||||
|
||||
except Fault:
|
||||
raise
|
||||
except Exception:
|
||||
# All unexpected errors leave connection in
|
||||
# a strange state, so we clear it.
|
||||
self.__transport.close()
|
||||
raise
|
||||
finally:
|
||||
if connection is not None:
|
||||
try:
|
||||
connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
if _use_rpc_pooling:
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
|
||||
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
@@ -101,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
x.close()
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
@@ -110,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
return r["result"]
|
||||
|
||||
|
||||
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
from .rpc_pool import get_rpc_pool
|
||||
import http.client
|
||||
import socket
|
||||
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
|
||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||
pool = get_rpc_pool(url, max_connections)
|
||||
|
||||
max_retries = 2
|
||||
|
||||
for attempt in range(max_retries):
|
||||
conn = pool.get_connection()
|
||||
|
||||
try:
|
||||
v = conn.json_request(method, params)
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
pool.discard_connection(conn)
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
|
||||
pool.return_connection(conn)
|
||||
return r["result"]
|
||||
|
||||
except (
|
||||
http.client.RemoteDisconnected,
|
||||
http.client.IncompleteRead,
|
||||
http.client.BadStatusLine,
|
||||
ConnectionError,
|
||||
ConnectionResetError,
|
||||
ConnectionAbortedError,
|
||||
BrokenPipeError,
|
||||
TimeoutError,
|
||||
socket.timeout,
|
||||
socket.error,
|
||||
OSError,
|
||||
) as ex:
|
||||
pool.discard_connection(conn)
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
logging.warning(
|
||||
f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
|
||||
)
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
pool.discard_connection(conn)
|
||||
logging.error(f"Unexpected RPC error: {ex}, method: {method}")
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
@@ -142,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
|
||||
def escape_rpcauth(auth_str: str) -> str:
|
||||
username, password = auth_str.split(":", 1)
|
||||
username = urllib.parse.quote(username, safe="")
|
||||
password = urllib.parse.quote(password, safe="")
|
||||
return f"{username}:{password}"
|
||||
|
||||
131
basicswap/rpc_pool.py
Normal file
@@ -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;
|
||||
|
||||
|
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,80 +271,38 @@ const ApiManager = (function() {
|
||||
fetchVolumeData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
let coinList = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => {
|
||||
return window.config.getCoinBackendId ?
|
||||
window.config.getCoinBackendId(coin.name) :
|
||||
(typeof getCoinBackendId === 'function' ?
|
||||
getCoinBackendId(coin.name) : coin.name.toLowerCase());
|
||||
})
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
|
||||
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']);
|
||||
|
||||
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
|
||||
coinList = coinList + ',zcoin';
|
||||
}
|
||||
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&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 || typeof response !== 'object') {
|
||||
throw new Error('Invalid response from CoinGecko API');
|
||||
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 !== undefined) {
|
||||
volumeData[coinId] = {
|
||||
total_volume: data.usd_24h_vol || 0,
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
const coinMappings = {
|
||||
'firo': ['firo', 'zcoin'],
|
||||
'zcoin': ['zcoin', 'firo'],
|
||||
'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
|
||||
'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
|
||||
'particl': ['particl', 'part']
|
||||
};
|
||||
|
||||
if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
|
||||
volumeData['firo'] = {
|
||||
total_volume: response['zcoin'].usd_24h_vol || 0,
|
||||
price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
|
||||
};
|
||||
}
|
||||
|
||||
if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
|
||||
volumeData['bitcoincash'] = {
|
||||
total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
|
||||
price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
|
||||
};
|
||||
}
|
||||
|
||||
for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
|
||||
if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
|
||||
for (const altId of alternativeIds) {
|
||||
if (response[altId] && response[altId].usd_24h_vol) {
|
||||
volumeData[mainCoin] = {
|
||||
total_volume: response[altId].usd_24h_vol,
|
||||
price_change_percentage_24h: response[altId].usd_24h_change || 0
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return volumeData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching volume data:", error);
|
||||
@@ -342,75 +311,45 @@ 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() {
|
||||
@@ -424,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;
|
||||
|
||||
@@ -445,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');
|
||||
|
||||
@@ -233,7 +233,7 @@ const CleanupManager = (function() {
|
||||
},
|
||||
|
||||
setupMemoryOptimization: function(options = {}) {
|
||||
const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes
|
||||
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
|
||||
const maxCacheSize = options.maxCacheSize || 100;
|
||||
const maxDataSize = options.maxDataSize || 1000;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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,55 +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 (filterCoin.includes(' ') || offerCoin.includes(' ')) {
|
||||
const filterFirstWord = filterCoin.split(' ')[0];
|
||||
const offerFirstWord = offerCoin.split(' ')[0];
|
||||
|
||||
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
|
||||
const filterHasCash = filterCoin.includes('cash');
|
||||
const offerHasCash = offerCoin.includes('cash');
|
||||
return filterHasCash === offerHasCash;
|
||||
}
|
||||
|
||||
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (particlVariants.includes(filterCoin)) {
|
||||
return offerCoin === filterCoin;
|
||||
}
|
||||
return false;
|
||||
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
|
||||
},
|
||||
update: function(path, value) {
|
||||
const parts = path.split('.');
|
||||
@@ -229,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');
|
||||
342
basicswap/static/js/modules/event-handlers.js
Normal file
@@ -0,0 +1,342 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const EventHandlers = {
|
||||
|
||||
confirmPopup: function(action = 'proceed', coinName = '') {
|
||||
const message = action === 'Accept'
|
||||
? 'Are you sure you want to accept this bid?'
|
||||
: coinName
|
||||
? `Are you sure you want to ${action} ${coinName}?`
|
||||
: 'Are you sure you want to proceed?';
|
||||
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
confirmReseed: function() {
|
||||
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
|
||||
},
|
||||
|
||||
confirmWithdrawal: function() {
|
||||
|
||||
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
|
||||
return window.WalletPage.confirmWithdrawal();
|
||||
}
|
||||
return confirm('Are you sure you want to withdraw? Please verify the address and amount.');
|
||||
},
|
||||
|
||||
confirmUTXOResize: function() {
|
||||
return confirm('Are you sure you want to create a UTXO? This will split your balance.');
|
||||
},
|
||||
|
||||
confirmRemoveExpired: function() {
|
||||
return confirm('Are you sure you want to remove all expired offers and bids?');
|
||||
},
|
||||
|
||||
fillDonationAddress: function(address, coinType) {
|
||||
|
||||
let addressInput = null;
|
||||
|
||||
addressInput = window.DOMCache
|
||||
? window.DOMCache.get('address_to')
|
||||
: document.getElementById('address_to');
|
||||
|
||||
if (!addressInput) {
|
||||
addressInput = document.querySelector('input[name^="to_"]');
|
||||
}
|
||||
|
||||
if (!addressInput) {
|
||||
addressInput = document.querySelector('input[placeholder*="Address"]');
|
||||
}
|
||||
|
||||
if (addressInput) {
|
||||
addressInput.value = address;
|
||||
console.log(`Filled donation address for ${coinType}: ${address}`);
|
||||
} else {
|
||||
console.error('EventHandlers: Address input not found');
|
||||
}
|
||||
},
|
||||
|
||||
setAmmAmount: function(percent, inputId) {
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get(inputId)
|
||||
: document.getElementById(inputId);
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('EventHandlers: AMM amount input not found:', inputId);
|
||||
return;
|
||||
}
|
||||
|
||||
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
|
||||
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
|
||||
|
||||
if (balance > 0) {
|
||||
const calculatedAmount = balance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
} else {
|
||||
console.warn('EventHandlers: No balance found for AMM amount calculation');
|
||||
}
|
||||
},
|
||||
|
||||
setOfferAmount: function(percent, inputId) {
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get(inputId)
|
||||
: document.getElementById(inputId);
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('EventHandlers: Offer amount input not found:', inputId);
|
||||
return;
|
||||
}
|
||||
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
if (!coinFromSelect) {
|
||||
console.error('EventHandlers: coin_from select not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||
if (!selectedOption || selectedOption.value === '-1') {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Please select a coin first');
|
||||
} else {
|
||||
alert('Please select a coin first');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const balance = selectedOption.getAttribute('data-balance');
|
||||
if (!balance) {
|
||||
console.error('EventHandlers: Balance not found for selected coin');
|
||||
return;
|
||||
}
|
||||
|
||||
const floatBalance = parseFloat(balance);
|
||||
if (isNaN(floatBalance) || floatBalance <= 0) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
|
||||
} else {
|
||||
alert('Invalid balance for selected coin');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
},
|
||||
|
||||
resetForm: function() {
|
||||
const form = document.querySelector('form[name="offer_form"]') || document.querySelector('form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
},
|
||||
|
||||
hideConfirmModal: function() {
|
||||
if (window.DOMCache) {
|
||||
window.DOMCache.hide('confirmModal');
|
||||
} else {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lookup_rates: function() {
|
||||
|
||||
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
|
||||
window.lookup_rates();
|
||||
} else {
|
||||
console.error('EventHandlers: lookup_rates function not found');
|
||||
}
|
||||
},
|
||||
|
||||
checkForUpdatesNow: function() {
|
||||
if (window.checkForUpdatesNow && typeof window.checkForUpdatesNow === 'function') {
|
||||
window.checkForUpdatesNow();
|
||||
} else {
|
||||
console.error('EventHandlers: checkForUpdatesNow function not found');
|
||||
}
|
||||
},
|
||||
|
||||
testUpdateNotification: function() {
|
||||
if (window.testUpdateNotification && typeof window.testUpdateNotification === 'function') {
|
||||
window.testUpdateNotification();
|
||||
} else {
|
||||
console.error('EventHandlers: testUpdateNotification function not found');
|
||||
}
|
||||
},
|
||||
|
||||
toggleNotificationDropdown: function(event) {
|
||||
if (window.toggleNotificationDropdown && typeof window.toggleNotificationDropdown === 'function') {
|
||||
window.toggleNotificationDropdown(event);
|
||||
} else {
|
||||
console.error('EventHandlers: toggleNotificationDropdown function not found');
|
||||
}
|
||||
},
|
||||
|
||||
closeMessage: function(messageId) {
|
||||
if (window.DOMCache) {
|
||||
window.DOMCache.hide(messageId);
|
||||
} else {
|
||||
const messageElement = document.getElementById(messageId);
|
||||
if (messageElement) {
|
||||
messageElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm]');
|
||||
if (target) {
|
||||
const action = target.getAttribute('data-confirm-action') || 'proceed';
|
||||
const coinName = target.getAttribute('data-confirm-coin') || '';
|
||||
|
||||
if (!this.confirmPopup(action, coinName)) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-reseed]');
|
||||
if (target) {
|
||||
if (!this.confirmReseed()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-utxo]');
|
||||
if (target) {
|
||||
if (!this.confirmUTXOResize()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-confirm-remove-expired]');
|
||||
if (target) {
|
||||
if (!this.confirmRemoveExpired()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-fill-donation]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const address = target.getAttribute('data-address');
|
||||
const coinType = target.getAttribute('data-coin-type');
|
||||
this.fillDonationAddress(address, coinType);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-set-amm-amount]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const percent = parseFloat(target.getAttribute('data-set-amm-amount'));
|
||||
const inputId = target.getAttribute('data-input-id');
|
||||
this.setAmmAmount(percent, inputId);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-set-offer-amount]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const percent = parseFloat(target.getAttribute('data-set-offer-amount'));
|
||||
const inputId = target.getAttribute('data-input-id');
|
||||
this.setOfferAmount(percent, inputId);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-reset-form]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-hide-modal]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.hideConfirmModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-lookup-rates]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.lookup_rates();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-check-updates]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
this.checkForUpdatesNow();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-test-notification]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const type = target.getAttribute('data-test-notification');
|
||||
if (type === 'update') {
|
||||
this.testUpdateNotification();
|
||||
} else {
|
||||
window.NotificationManager && window.NotificationManager.testToasts();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-close-message]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const messageId = target.getAttribute('data-close-message');
|
||||
this.closeMessage(messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
EventHandlers.initialize();
|
||||
});
|
||||
} else {
|
||||
EventHandlers.initialize();
|
||||
}
|
||||
|
||||
window.EventHandlers = EventHandlers;
|
||||
|
||||
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
|
||||
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
|
||||
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
|
||||
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
|
||||
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
|
||||
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
|
||||
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
|
||||
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
|
||||
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
|
||||
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
|
||||
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
|
||||
|
||||
})();
|
||||
225
basicswap/static/js/modules/form-validator.js
Normal file
@@ -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');
|
||||
|
||||
@@ -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 => {
|
||||
@@ -89,8 +89,6 @@ const PriceManager = (function() {
|
||||
? 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');
|
||||
}
|
||||
@@ -132,15 +130,15 @@ const PriceManager = (function() {
|
||||
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();
|
||||
}
|
||||
|
||||
if (coinId.toLowerCase() === 'zcoin') {
|
||||
normalizedCoinId = 'firo';
|
||||
normalizedCoinId = coinId.toLowerCase();
|
||||
}
|
||||
|
||||
processedData[normalizedCoinId] = {
|
||||
@@ -166,14 +164,14 @@ const PriceManager = (function() {
|
||||
|
||||
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) {
|
||||
@@ -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;
|
||||
|
||||
})();
|
||||
@@ -166,7 +166,7 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||
}, 50);
|
||||
@@ -205,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,
|
||||
@@ -217,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}`);
|
||||
@@ -226,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;
|
||||
});
|
||||
}
|
||||
@@ -261,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);
|
||||
@@ -272,7 +291,7 @@ const SummaryManager = (function() {
|
||||
};
|
||||
|
||||
webSocket.onclose = () => {
|
||||
setTimeout(setupWebSocket, 5000);
|
||||
CleanupManager.setTimeout(setupWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,7 +319,7 @@ const SummaryManager = (function() {
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = setInterval(() => {
|
||||
refreshTimer = CleanupManager.setInterval(() => {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
@@ -334,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);
|
||||
@@ -380,7 +402,7 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
resolve(this.fetchSummaryData());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
@@ -440,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
|
||||
console.log('SummaryManager initialized');
|
||||
|
||||
@@ -14,6 +14,9 @@ const TooltipManager = (function() {
|
||||
this.debug = false;
|
||||
this.tooltipData = new WeakMap();
|
||||
this.resources = {};
|
||||
this.creationQueue = [];
|
||||
this.batchSize = 5;
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource(
|
||||
@@ -48,40 +51,69 @@ const TooltipManager = (function() {
|
||||
this.performPeriodicCleanup(true);
|
||||
}
|
||||
|
||||
const createTooltip = () => {
|
||||
if (!document.body.contains(element)) return;
|
||||
this.creationQueue.push({ element, content, options });
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
this.createTooltipInstance(element, content, options);
|
||||
} else {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
if (!this.isProcessingQueue) {
|
||||
this.processCreationQueue();
|
||||
}
|
||||
|
||||
const retryCreate = () => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
|
||||
if (newRect.width > 0 && newRect.height > 0) {
|
||||
this.createTooltipInstance(element, content, options);
|
||||
}
|
||||
} else {
|
||||
retryCount++;
|
||||
CleanupManager.setTimeout(() => {
|
||||
CleanupManager.requestAnimationFrame(retryCreate);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
CleanupManager.requestAnimationFrame(retryCreate);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
CleanupManager.requestAnimationFrame(createTooltip);
|
||||
return null;
|
||||
}
|
||||
|
||||
processCreationQueue() {
|
||||
if (this.creationQueue.length === 0) {
|
||||
this.isProcessingQueue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
const batch = this.creationQueue.splice(0, this.batchSize);
|
||||
|
||||
CleanupManager.requestAnimationFrame(() => {
|
||||
batch.forEach(({ element, content, options }) => {
|
||||
this.createTooltipSync(element, content, options);
|
||||
});
|
||||
|
||||
if (this.creationQueue.length > 0) {
|
||||
CleanupManager.setTimeout(() => {
|
||||
this.processCreationQueue();
|
||||
}, 0);
|
||||
} else {
|
||||
this.isProcessingQueue = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createTooltipSync(element, content, options) {
|
||||
if (!document.body.contains(element)) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
this.createTooltipInstance(element, content, options);
|
||||
} else {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
const retryCreate = () => {
|
||||
const newRect = element.getBoundingClientRect();
|
||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
|
||||
if (newRect.width > 0 && newRect.height > 0) {
|
||||
this.createTooltipInstance(element, content, options);
|
||||
}
|
||||
} else {
|
||||
retryCount++;
|
||||
CleanupManager.setTimeout(() => {
|
||||
CleanupManager.requestAnimationFrame(retryCreate);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
CleanupManager.requestAnimationFrame(retryCreate);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
createTooltipInstance(element, content, options = {}) {
|
||||
if (!element || !document.body.contains(element)) {
|
||||
return null;
|
||||
@@ -191,6 +223,9 @@ const TooltipManager = (function() {
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (window.ErrorHandler) {
|
||||
return window.ErrorHandler.handleError(error, 'TooltipManager.createTooltipInstance', null);
|
||||
}
|
||||
console.error('Error creating tooltip:', error);
|
||||
return null;
|
||||
}
|
||||
@@ -199,7 +234,7 @@ const TooltipManager = (function() {
|
||||
destroy(element) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
const destroyFn = () => {
|
||||
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||
if (!tooltipId) return;
|
||||
|
||||
@@ -224,8 +259,16 @@ const TooltipManager = (function() {
|
||||
|
||||
this.tooltipData.delete(element);
|
||||
tooltipInstanceMap.delete(element);
|
||||
} catch (error) {
|
||||
console.error('Error destroying tooltip:', error);
|
||||
};
|
||||
|
||||
if (window.ErrorHandler) {
|
||||
window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
|
||||
} else {
|
||||
try {
|
||||
destroyFn();
|
||||
} catch (error) {
|
||||
console.error('Error destroying tooltip:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,10 +781,40 @@ const TooltipManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
initializeLazyTooltips(selector = '[data-tooltip-target]') {
|
||||
|
||||
const initializedTooltips = new Set();
|
||||
|
||||
const initializeTooltip = (element) => {
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
if (!targetId || initializedTooltips.has(targetId)) return;
|
||||
|
||||
const tooltipContent = document.getElementById(targetId);
|
||||
if (tooltipContent) {
|
||||
this.create(element, tooltipContent.innerHTML, {
|
||||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
initializedTooltips.add(targetId);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseover', (e) => {
|
||||
const target = e.target.closest(selector);
|
||||
if (target) {
|
||||
initializeTooltip(target);
|
||||
}
|
||||
}, { passive: true, capture: true });
|
||||
|
||||
this.log('Lazy tooltip initialization enabled');
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.log('Disposing TooltipManager');
|
||||
|
||||
try {
|
||||
this.creationQueue = [];
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
this.cleanup();
|
||||
|
||||
Object.values(this.resources).forEach(resourceId => {
|
||||
@@ -830,6 +903,11 @@ const TooltipManager = (function() {
|
||||
return manager.initializeTooltips(...args);
|
||||
},
|
||||
|
||||
initializeLazyTooltips: function(...args) {
|
||||
const manager = this.getInstance();
|
||||
return manager.initializeLazyTooltips(...args);
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
const manager = this.getInstance();
|
||||
return manager.setDebugMode(enabled);
|
||||
|
||||
196
basicswap/static/js/modules/wallet-amount.js
Normal file
@@ -0,0 +1,196 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletAmountManager = {
|
||||
|
||||
coinConfigs: {
|
||||
1: {
|
||||
types: ['plain', 'blind', 'anon'],
|
||||
hasSubfee: true,
|
||||
hasSweepAll: false
|
||||
},
|
||||
3: {
|
||||
types: ['plain', 'mweb'],
|
||||
hasSubfee: true,
|
||||
hasSweepAll: false
|
||||
},
|
||||
6: {
|
||||
types: ['default'],
|
||||
hasSubfee: false,
|
||||
hasSweepAll: true
|
||||
},
|
||||
9: {
|
||||
types: ['default'],
|
||||
hasSubfee: false,
|
||||
hasSweepAll: true
|
||||
}
|
||||
},
|
||||
|
||||
safeParseFloat: function(value) {
|
||||
const numValue = Number(value);
|
||||
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
return numValue;
|
||||
}
|
||||
|
||||
console.warn('WalletAmountManager: Invalid balance value:', value);
|
||||
return 0;
|
||||
},
|
||||
|
||||
getBalance: function(coinId, balances, selectedType) {
|
||||
const cid = parseInt(coinId);
|
||||
|
||||
if (cid === 1) {
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
case 'blind':
|
||||
return this.safeParseFloat(balances.blind);
|
||||
case 'anon':
|
||||
return this.safeParseFloat(balances.anon);
|
||||
default:
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
}
|
||||
}
|
||||
|
||||
if (cid === 3) {
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
case 'mweb':
|
||||
return this.safeParseFloat(balances.mweb);
|
||||
default:
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
}
|
||||
}
|
||||
|
||||
return this.safeParseFloat(balances.main || balances.balance);
|
||||
},
|
||||
|
||||
calculateAmount: function(balance, percent, coinId) {
|
||||
const cid = parseInt(coinId);
|
||||
|
||||
if (percent === 1) {
|
||||
return balance;
|
||||
}
|
||||
|
||||
if (cid === 1) {
|
||||
return Math.max(0, Math.floor(balance * percent * 100000000) / 100000000);
|
||||
}
|
||||
|
||||
const calculatedAmount = balance * percent;
|
||||
|
||||
if (calculatedAmount < 0.00000001) {
|
||||
console.warn('WalletAmountManager: Calculated amount too small, setting to zero');
|
||||
return 0;
|
||||
}
|
||||
|
||||
return calculatedAmount;
|
||||
},
|
||||
|
||||
setAmount: function(percent, balances, coinId) {
|
||||
|
||||
const amountInput = window.DOMCache
|
||||
? window.DOMCache.get('amount')
|
||||
: document.getElementById('amount');
|
||||
const typeSelect = window.DOMCache
|
||||
? window.DOMCache.get('withdraw_type')
|
||||
: document.getElementById('withdraw_type');
|
||||
|
||||
if (!amountInput) {
|
||||
console.error('WalletAmountManager: Amount input not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const cid = parseInt(coinId);
|
||||
const selectedType = typeSelect ? typeSelect.value : 'plain';
|
||||
|
||||
const balance = this.getBalance(cid, balances, selectedType);
|
||||
|
||||
const calculatedAmount = this.calculateAmount(balance, percent, cid);
|
||||
|
||||
const specialCids = [6, 9];
|
||||
if (specialCids.includes(cid) && percent === 1) {
|
||||
amountInput.setAttribute('data-hidden', 'true');
|
||||
amountInput.placeholder = 'Sweep All';
|
||||
amountInput.value = '';
|
||||
amountInput.disabled = true;
|
||||
|
||||
const sweepAllCheckbox = window.DOMCache
|
||||
? window.DOMCache.get('sweepall')
|
||||
: document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.setAttribute('data-hidden', 'false');
|
||||
amountInput.placeholder = '';
|
||||
amountInput.disabled = false;
|
||||
|
||||
const sweepAllCheckbox = window.DOMCache
|
||||
? window.DOMCache.get('sweepall')
|
||||
: document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
const subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
|
||||
const amountButtons = document.querySelectorAll('[data-set-amount]');
|
||||
|
||||
amountButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const percent = parseFloat(button.getAttribute('data-set-amount'));
|
||||
const balancesJson = button.getAttribute('data-balances');
|
||||
const coinId = button.getAttribute('data-coin-id');
|
||||
|
||||
if (!balancesJson || !coinId) {
|
||||
console.error('WalletAmountManager: Missing data attributes on button', button);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const balances = JSON.parse(balancesJson);
|
||||
this.setAmount(percent, balances, coinId);
|
||||
} catch (error) {
|
||||
console.error('WalletAmountManager: Failed to parse balances', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletAmountManager.initialize();
|
||||
});
|
||||
} else {
|
||||
WalletAmountManager.initialize();
|
||||
}
|
||||
|
||||
window.WalletAmountManager = WalletAmountManager;
|
||||
|
||||
window.setAmount = function(percent, balance, coinId, balance2, balance3) {
|
||||
|
||||
const balances = {
|
||||
main: balance || balance,
|
||||
balance: balance,
|
||||
blind: balance2,
|
||||
anon: balance3,
|
||||
mweb: balance2
|
||||
};
|
||||
WalletAmountManager.setAmount(percent, balances, coinId);
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -11,8 +11,7 @@ const WalletManager = (function() {
|
||||
defaultTTL: 300,
|
||||
priceSource: {
|
||||
primary: 'coingecko.com',
|
||||
fallback: 'cryptocompare.com',
|
||||
enabledSources: ['coingecko.com', 'cryptocompare.com']
|
||||
enabledSources: ['coingecko.com']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,22 +94,32 @@ 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');
|
||||
@@ -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;
|
||||
|
||||
})();
|
||||
@@ -16,13 +16,7 @@ const AmmCounterManager = (function() {
|
||||
}
|
||||
|
||||
function debugLog(message, data) {
|
||||
// if (isDebugEnabled()) {
|
||||
// if (data) {
|
||||
// console.log(`[AmmCounter] ${message}`, data);
|
||||
// } else {
|
||||
// console.log(`[AmmCounter] ${message}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
function updateAmmCounter(count, status) {
|
||||
@@ -103,7 +97,7 @@ const AmmCounterManager = (function() {
|
||||
}
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||
}, 50);
|
||||
@@ -148,7 +142,7 @@ const AmmCounterManager = (function() {
|
||||
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
resolve(fetchAmmStatus());
|
||||
}, config.retryDelay);
|
||||
});
|
||||
@@ -168,7 +162,7 @@ const AmmCounterManager = (function() {
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
|
||||
refreshTimer = setInterval(() => {
|
||||
refreshTimer = CleanupManager.setInterval(() => {
|
||||
fetchAmmStatus()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
@@ -251,5 +245,11 @@ 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);
|
||||
|
||||
})();
|
||||
@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
|
||||
function debugLog(message, data) {
|
||||
// if (isDebugEnabled()) {
|
||||
// if (data) {
|
||||
// console.log(`[AmmTables] ${message}`, data);
|
||||
// } else {
|
||||
// console.log(`[AmmTables] ${message}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
function initializeTabs() {
|
||||
@@ -67,53 +61,8 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
|
||||
function getCoinDisplayName(coinId) {
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] getCoinDisplayName called with:', coinId, typeof coinId);
|
||||
}
|
||||
|
||||
if (typeof coinId === 'string') {
|
||||
const lowerCoinId = coinId.toLowerCase();
|
||||
|
||||
if (lowerCoinId === 'part_anon' ||
|
||||
lowerCoinId === 'particl_anon' ||
|
||||
lowerCoinId === 'particl anon') {
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] Matched Particl Anon variant:', coinId);
|
||||
}
|
||||
return 'Particl Anon';
|
||||
}
|
||||
|
||||
if (lowerCoinId === 'part_blind' ||
|
||||
lowerCoinId === 'particl_blind' ||
|
||||
lowerCoinId === 'particl blind') {
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] Matched Particl Blind variant:', coinId);
|
||||
}
|
||||
return 'Particl Blind';
|
||||
}
|
||||
|
||||
if (lowerCoinId === 'ltc_mweb' ||
|
||||
lowerCoinId === 'litecoin_mweb' ||
|
||||
lowerCoinId === 'litecoin mweb') {
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] Matched Litecoin MWEB variant:', coinId);
|
||||
}
|
||||
return 'Litecoin MWEB';
|
||||
}
|
||||
}
|
||||
|
||||
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
||||
const displayName = window.CoinManager.getDisplayName(coinId);
|
||||
if (displayName) {
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] CoinManager returned:', displayName);
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
console.log('[AMM Tables] Returning coin name as-is:', coinId);
|
||||
return window.CoinManager.getDisplayName(coinId) || coinId;
|
||||
}
|
||||
return coinId;
|
||||
}
|
||||
@@ -128,13 +77,13 @@ const AmmTablesManager = (function() {
|
||||
<td class="py-0 px-0 text-right text-sm">
|
||||
<div class="flex items-center justify-center monospace">
|
||||
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12" src="/static/images/coins/${fromImage}" alt="${fromDisplayName}">
|
||||
<img class="h-12" src="/static/images/coins/${toImage}" alt="${toDisplayName}">
|
||||
</span>
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||
<img class="h-12" src="/static/images/coins/${toImage}" alt="${toDisplayName}">
|
||||
<img class="h-12" src="/static/images/coins/${fromImage}" alt="${fromDisplayName}">
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -309,7 +258,9 @@ const AmmTablesManager = (function() {
|
||||
`;
|
||||
});
|
||||
|
||||
offersBody.innerHTML = tableHtml;
|
||||
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||
offersBody.innerHTML = tableHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBidsTable(stateData) {
|
||||
@@ -441,7 +392,9 @@ const AmmTablesManager = (function() {
|
||||
`;
|
||||
});
|
||||
|
||||
bidsBody.innerHTML = tableHtml;
|
||||
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||
bidsBody.innerHTML = tableHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
@@ -540,7 +493,6 @@ const AmmTablesManager = (function() {
|
||||
coinPrice = window.latestPrices[coinName.toUpperCase()];
|
||||
}
|
||||
|
||||
|
||||
if (!coinPrice || isNaN(coinPrice)) {
|
||||
return null;
|
||||
}
|
||||
@@ -550,6 +502,9 @@ const AmmTablesManager = (function() {
|
||||
|
||||
function formatUSDPrice(usdValue) {
|
||||
if (!usdValue || isNaN(usdValue)) return '';
|
||||
if (window.config && window.config.utils && window.config.utils.formatPrice) {
|
||||
return `($${window.config.utils.formatPrice('USD', usdValue)} USD)`;
|
||||
}
|
||||
return `($${usdValue.toFixed(2)} USD)`;
|
||||
}
|
||||
|
||||
@@ -724,6 +679,399 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDropdownOptionsShowBalance(select) {
|
||||
const isMakerDropdown = select.id.includes('coin-from');
|
||||
const isTakerDropdown = select.id.includes('coin-to');
|
||||
|
||||
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 isBidModal = false;
|
||||
if (addModalVisible) {
|
||||
const dataType = addModal.getAttribute('data-amm-type');
|
||||
if (dataType) {
|
||||
isBidModal = dataType === 'bid';
|
||||
} else {
|
||||
|
||||
const modalTitle = document.getElementById('add-modal-title');
|
||||
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
||||
}
|
||||
} else if (editModalVisible) {
|
||||
const dataType = editModal.getAttribute('data-amm-type');
|
||||
if (dataType) {
|
||||
isBidModal = dataType === 'bid';
|
||||
} else {
|
||||
|
||||
const modalTitle = document.getElementById('edit-modal-title');
|
||||
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
||||
}
|
||||
}
|
||||
|
||||
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
|
||||
|
||||
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function refreshDropdownOptions() {
|
||||
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
||||
|
||||
dropdownIds.forEach(dropdownId => {
|
||||
const select = document.getElementById(dropdownId);
|
||||
if (!select || select.style.display !== 'none') return;
|
||||
|
||||
const wrapper = select.parentNode.querySelector('.relative');
|
||||
if (!wrapper) return;
|
||||
|
||||
const dropdown = wrapper.querySelector('[role="listbox"]');
|
||||
if (!dropdown) return;
|
||||
|
||||
const options = dropdown.querySelectorAll('[data-value]');
|
||||
options.forEach(optionElement => {
|
||||
const coinValue = optionElement.getAttribute('data-value');
|
||||
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
|
||||
if (!originalOption) return;
|
||||
|
||||
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
|
||||
if (!textContainer) return;
|
||||
|
||||
textContainer.innerHTML = '';
|
||||
|
||||
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
|
||||
const fullText = originalOption.textContent.trim();
|
||||
const balance = originalOption.getAttribute('data-balance') || '0.00000000';
|
||||
|
||||
console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
|
||||
|
||||
if (shouldShowBalance) {
|
||||
|
||||
textContainer.className = 'flex flex-col';
|
||||
|
||||
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
||||
|
||||
const coinNameSpan = document.createElement('span');
|
||||
coinNameSpan.textContent = coinName;
|
||||
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
||||
|
||||
const balanceSpan = document.createElement('span');
|
||||
balanceSpan.textContent = `Balance: ${balance}`;
|
||||
balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
|
||||
|
||||
textContainer.appendChild(coinNameSpan);
|
||||
textContainer.appendChild(balanceSpan);
|
||||
} else {
|
||||
|
||||
textContainer.className = 'flex items-center';
|
||||
|
||||
const coinNameSpan = document.createElement('span');
|
||||
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
||||
coinNameSpan.textContent = coinName;
|
||||
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
||||
|
||||
textContainer.appendChild(coinNameSpan);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDropdownBalances() {
|
||||
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
||||
|
||||
dropdownIds.forEach(dropdownId => {
|
||||
const select = document.getElementById(dropdownId);
|
||||
if (!select || select.style.display !== 'none') return;
|
||||
|
||||
const wrapper = select.parentNode.querySelector('.relative');
|
||||
if (!wrapper) return;
|
||||
|
||||
const dropdownItems = wrapper.querySelectorAll('[data-value]');
|
||||
dropdownItems.forEach(item => {
|
||||
const value = item.getAttribute('data-value');
|
||||
const option = select.querySelector(`option[value="${value}"]`);
|
||||
if (option) {
|
||||
const balance = option.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
||||
|
||||
const balanceDiv = item.querySelector('.text-xs');
|
||||
if (balanceDiv) {
|
||||
balanceDiv.textContent = `Balance: ${balance}`;
|
||||
|
||||
let pendingDiv = item.querySelector('.text-green-500');
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
if (!pendingDiv) {
|
||||
|
||||
pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'text-green-500 text-xs';
|
||||
balanceDiv.parentNode.appendChild(pendingDiv);
|
||||
}
|
||||
pendingDiv.textContent = `+${pendingBalance} pending`;
|
||||
} else if (pendingDiv) {
|
||||
|
||||
pendingDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (selectedOption) {
|
||||
const textContainer = wrapper.querySelector('button .flex-grow');
|
||||
const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
|
||||
if (balanceDiv) {
|
||||
const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
balanceDiv.textContent = `Balance: ${balance}`;
|
||||
|
||||
let pendingDiv = textContainer.querySelector('.text-green-500');
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
if (!pendingDiv) {
|
||||
|
||||
pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'text-green-500 text-xs';
|
||||
textContainer.appendChild(pendingDiv);
|
||||
}
|
||||
pendingDiv.textContent = `+${pendingBalance} pending`;
|
||||
} else if (pendingDiv) {
|
||||
|
||||
pendingDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshOfferDropdownBalanceDisplay() {
|
||||
refreshDropdownBalances();
|
||||
}
|
||||
|
||||
function refreshBidDropdownBalanceDisplay() {
|
||||
refreshDropdownBalances();
|
||||
}
|
||||
|
||||
function refreshDropdownBalanceDisplay(modalType = null) {
|
||||
if (modalType === 'offer') {
|
||||
refreshOfferDropdownBalanceDisplay();
|
||||
} else if (modalType === 'bid') {
|
||||
refreshBidDropdownBalanceDisplay();
|
||||
} else {
|
||||
|
||||
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 detectedType = null;
|
||||
if (addModalVisible) {
|
||||
detectedType = addModal.getAttribute('data-amm-type');
|
||||
} else if (editModalVisible) {
|
||||
detectedType = editModal.getAttribute('data-amm-type');
|
||||
}
|
||||
|
||||
if (detectedType === 'offer') {
|
||||
refreshOfferDropdownBalanceDisplay();
|
||||
} else if (detectedType === 'bid') {
|
||||
refreshBidDropdownBalanceDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDropdownsForModalType(modalPrefix) {
|
||||
const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
|
||||
const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
|
||||
|
||||
if (!coinFromSelect || !coinToSelect) return;
|
||||
|
||||
const balanceData = {};
|
||||
|
||||
Array.from(coinFromSelect.options).forEach(option => {
|
||||
const balance = option.getAttribute('data-balance');
|
||||
if (balance) {
|
||||
balanceData[option.value] = balance;
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(coinToSelect.options).forEach(option => {
|
||||
const balance = option.getAttribute('data-balance');
|
||||
if (balance) {
|
||||
balanceData[option.value] = balance;
|
||||
}
|
||||
});
|
||||
|
||||
updateDropdownOptions(coinFromSelect, balanceData);
|
||||
updateDropdownOptions(coinToSelect, balanceData);
|
||||
}
|
||||
|
||||
function updateDropdownOptions(select, balanceData, pendingData = {}) {
|
||||
Array.from(select.options).forEach(option => {
|
||||
const coinName = option.value;
|
||||
const balance = balanceData[coinName] || '0.00000000';
|
||||
const pending = pendingData[coinName] || '0.0';
|
||||
|
||||
option.setAttribute('data-balance', balance);
|
||||
option.setAttribute('data-pending-balance', pending);
|
||||
|
||||
option.textContent = coinName;
|
||||
});
|
||||
}
|
||||
|
||||
function createSimpleDropdown(select, showBalance = false) {
|
||||
if (!select) return;
|
||||
|
||||
const existingWrapper = select.parentNode.querySelector('.relative');
|
||||
if (existingWrapper) {
|
||||
existingWrapper.remove();
|
||||
select.style.display = '';
|
||||
}
|
||||
|
||||
select.style.display = 'none';
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
|
||||
button.style.minHeight = '60px';
|
||||
|
||||
const displayContent = document.createElement('div');
|
||||
displayContent.className = 'flex items-center';
|
||||
|
||||
const icon = document.createElement('img');
|
||||
icon.className = 'w-5 h-5 mr-2';
|
||||
icon.alt = '';
|
||||
|
||||
const textContainer = document.createElement('div');
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
|
||||
const arrow = document.createElement('div');
|
||||
arrow.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`;
|
||||
|
||||
displayContent.appendChild(icon);
|
||||
displayContent.appendChild(textContainer);
|
||||
button.appendChild(displayContent);
|
||||
button.appendChild(arrow);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
|
||||
item.setAttribute('data-value', option.value);
|
||||
|
||||
const itemIcon = document.createElement('img');
|
||||
itemIcon.className = 'w-5 h-5 mr-2';
|
||||
itemIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
|
||||
itemIcon.alt = '';
|
||||
|
||||
const itemText = document.createElement('div');
|
||||
const coinName = option.textContent.trim();
|
||||
const balance = option.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
||||
|
||||
if (showBalance) {
|
||||
itemText.className = 'flex flex-col';
|
||||
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${coinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
|
||||
`;
|
||||
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
itemText.innerHTML = html;
|
||||
} else {
|
||||
itemText.className = 'text-gray-900 dark:text-white';
|
||||
itemText.textContent = coinName;
|
||||
}
|
||||
|
||||
item.appendChild(itemIcon);
|
||||
item.appendChild(itemText);
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
select.value = this.getAttribute('data-value');
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const selectedCoinName = selectedOption.textContent.trim();
|
||||
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
icon.src = itemIcon.src;
|
||||
|
||||
if (showBalance) {
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||
`;
|
||||
|
||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
textContainer.innerHTML = html;
|
||||
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
||||
} else {
|
||||
textContainer.textContent = selectedCoinName;
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
}
|
||||
|
||||
dropdown.classList.add('hidden');
|
||||
|
||||
const event = new Event('change', { bubbles: true });
|
||||
select.dispatchEvent(event);
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (selectedOption) {
|
||||
const selectedCoinName = selectedOption.textContent.trim();
|
||||
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
|
||||
if (showBalance) {
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||
`;
|
||||
|
||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
textContainer.innerHTML = html;
|
||||
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
||||
} else {
|
||||
textContainer.textContent = selectedCoinName;
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
}
|
||||
}
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.appendChild(button);
|
||||
wrapper.appendChild(dropdown);
|
||||
select.parentNode.insertBefore(wrapper, select);
|
||||
|
||||
}
|
||||
|
||||
function setupButtonHandlers() {
|
||||
const addOfferButton = document.getElementById('add-new-offer-btn');
|
||||
if (addOfferButton) {
|
||||
@@ -844,6 +1192,36 @@ const AmmTablesManager = (function() {
|
||||
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('add-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
modal.setAttribute('data-amm-type', type);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
updateDropdownsForModalType('add');
|
||||
|
||||
initializeCustomSelects(type);
|
||||
|
||||
refreshDropdownBalanceDisplay(type);
|
||||
|
||||
if (typeof fetchBalanceData === 'function') {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
||||
updateOfferDropdownBalances(balanceData);
|
||||
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
||||
updateBidDropdownBalances(balanceData);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating dropdown balances:', error);
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.getElementById('add-amm-type').value = type;
|
||||
|
||||
document.getElementById('add-amm-name').value = 'Unnamed Offer';
|
||||
@@ -940,11 +1318,6 @@ const AmmTablesManager = (function() {
|
||||
if (type === 'offer') {
|
||||
setupBiddingControls('add');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('add-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddModal() {
|
||||
@@ -1056,10 +1429,10 @@ const AmmTablesManager = (function() {
|
||||
newItem.min_swap_amount = parseFloat(minSwapAmount);
|
||||
}
|
||||
|
||||
const amountStep = document.getElementById('add-offer-amount-step').value;
|
||||
const amountStep = parseFloat(document.getElementById('add-offer-amount-step').value);
|
||||
const offerAmount = parseFloat(document.getElementById('add-amm-amount').value);
|
||||
|
||||
if (!amountStep || amountStep.trim() === '') {
|
||||
if (!amountStep) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
||||
} else {
|
||||
@@ -1069,8 +1442,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
|
||||
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
|
||||
const parsedValue = parseFloat(amountStep);
|
||||
if (parsedValue <= 0) {
|
||||
if (amountStep <= 0) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
|
||||
} else {
|
||||
@@ -1078,7 +1450,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsedValue < 0.001) {
|
||||
if (amountStep < 0.001) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
|
||||
} else {
|
||||
@@ -1086,15 +1458,15 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsedValue > offerAmount) {
|
||||
if (amountStep > offerAmount) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
} else {
|
||||
alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
newItem.amount_step = parsedValue.toString();
|
||||
newItem.amount_step = amountStep;
|
||||
console.log(`Offer Size Increment set to: ${newItem.amount_step}`);
|
||||
} else {
|
||||
if (window.showErrorModal) {
|
||||
@@ -1270,6 +1642,36 @@ const AmmTablesManager = (function() {
|
||||
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('edit-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
modal.setAttribute('data-amm-type', type);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
updateDropdownsForModalType('edit');
|
||||
|
||||
initializeCustomSelects(type);
|
||||
|
||||
refreshDropdownBalanceDisplay(type);
|
||||
|
||||
if (typeof fetchBalanceData === 'function') {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
||||
updateOfferDropdownBalances(balanceData);
|
||||
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
||||
updateBidDropdownBalances(balanceData);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating dropdown balances:', error);
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.getElementById('edit-amm-type').value = type;
|
||||
document.getElementById('edit-amm-id').value = id || '';
|
||||
document.getElementById('edit-amm-original-name').value = name;
|
||||
@@ -1283,8 +1685,12 @@ const AmmTablesManager = (function() {
|
||||
coinFromSelect.value = item.coin_from || '';
|
||||
coinToSelect.value = item.coin_to || '';
|
||||
|
||||
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (coinFromSelect) {
|
||||
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (coinToSelect) {
|
||||
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
document.getElementById('edit-amm-amount').value = item.amount || '';
|
||||
|
||||
@@ -1371,11 +1777,6 @@ const AmmTablesManager = (function() {
|
||||
setupBiddingControls('edit');
|
||||
populateBiddingControls('edit', item);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('edit-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error processing the configuration: ${error.message}`);
|
||||
debugLog('Error opening edit modal:', error);
|
||||
@@ -1389,7 +1790,6 @@ const AmmTablesManager = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function closeEditModal() {
|
||||
const modal = document.getElementById('edit-amm-modal');
|
||||
if (modal) {
|
||||
@@ -1502,10 +1902,10 @@ const AmmTablesManager = (function() {
|
||||
updatedItem.min_swap_amount = parseFloat(minSwapAmount);
|
||||
}
|
||||
|
||||
const amountStep = document.getElementById('edit-offer-amount-step').value;
|
||||
const amountStep = parseFloat(document.getElementById('edit-offer-amount-step').value);
|
||||
const offerAmount = parseFloat(document.getElementById('edit-amm-amount').value);
|
||||
|
||||
if (!amountStep || amountStep.trim() === '') {
|
||||
if (!amountStep) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
||||
} else {
|
||||
@@ -1515,8 +1915,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
|
||||
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
|
||||
const parsedValue = parseFloat(amountStep);
|
||||
if (parsedValue <= 0) {
|
||||
if (amountStep <= 0) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
|
||||
} else {
|
||||
@@ -1524,7 +1923,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsedValue < 0.001) {
|
||||
if (amountStep < 0.001) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
|
||||
} else {
|
||||
@@ -1532,15 +1931,15 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsedValue > offerAmount) {
|
||||
if (amountStep > offerAmount) {
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
} else {
|
||||
alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
updatedItem.amount_step = parsedValue.toString();
|
||||
updatedItem.amount_step = amountStep;
|
||||
console.log(`Offer Size Increment set to: ${updatedItem.amount_step}`);
|
||||
} else {
|
||||
if (window.showErrorModal) {
|
||||
@@ -1810,7 +2209,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCustomSelects() {
|
||||
function initializeCustomSelects(modalType = null) {
|
||||
const coinSelects = [
|
||||
document.getElementById('add-amm-coin-from'),
|
||||
document.getElementById('add-amm-coin-to'),
|
||||
@@ -1823,116 +2222,13 @@ const AmmTablesManager = (function() {
|
||||
document.getElementById('edit-offer-swap-type')
|
||||
];
|
||||
|
||||
function createCoinDropdown(select) {
|
||||
if (!select) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
const display = document.createElement('div');
|
||||
display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
|
||||
|
||||
const icon = document.createElement('img');
|
||||
icon.className = 'w-5 h-5 mr-2';
|
||||
icon.alt = '';
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.className = 'flex-grow';
|
||||
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'ml-2';
|
||||
arrow.innerHTML = `
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
display.appendChild(icon);
|
||||
display.appendChild(text);
|
||||
display.appendChild(arrow);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
|
||||
item.setAttribute('data-value', option.value);
|
||||
item.setAttribute('data-symbol', option.getAttribute('data-symbol') || '');
|
||||
|
||||
const optionIcon = document.createElement('img');
|
||||
optionIcon.className = 'w-5 h-5 mr-2';
|
||||
optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
|
||||
optionIcon.alt = '';
|
||||
|
||||
const optionText = document.createElement('span');
|
||||
optionText.textContent = option.textContent.trim();
|
||||
|
||||
item.appendChild(optionIcon);
|
||||
item.appendChild(optionText);
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
select.value = this.getAttribute('data-value');
|
||||
|
||||
text.textContent = optionText.textContent;
|
||||
icon.src = optionIcon.src;
|
||||
|
||||
dropdown.classList.add('hidden');
|
||||
|
||||
const event = new Event('change', { bubbles: true });
|
||||
select.dispatchEvent(event);
|
||||
|
||||
if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') {
|
||||
const coinFrom = document.getElementById('add-amm-coin-from');
|
||||
const coinTo = document.getElementById('add-amm-coin-to');
|
||||
const swapType = document.getElementById('add-offer-swap-type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
||||
}
|
||||
} else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') {
|
||||
const coinFrom = document.getElementById('edit-amm-coin-from');
|
||||
const coinTo = document.getElementById('edit-amm-coin-to');
|
||||
const swapType = document.getElementById('edit-offer-swap-type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
text.textContent = selectedOption.textContent.trim();
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
|
||||
display.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function() {
|
||||
dropdown.classList.add('hidden');
|
||||
});
|
||||
|
||||
wrapper.appendChild(display);
|
||||
wrapper.appendChild(dropdown);
|
||||
select.parentNode.insertBefore(wrapper, select);
|
||||
|
||||
select.style.display = 'none';
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
text.textContent = selectedOption.textContent.trim();
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
});
|
||||
}
|
||||
|
||||
function createSwapTypeDropdown(select) {
|
||||
if (!select) return;
|
||||
|
||||
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
@@ -1982,7 +2278,9 @@ const AmmTablesManager = (function() {
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
if (selectedOption) {
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
}
|
||||
|
||||
display.addEventListener('click', function(e) {
|
||||
if (select.disabled) return;
|
||||
@@ -2002,7 +2300,9 @@ const AmmTablesManager = (function() {
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
if (selectedOption) {
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
}
|
||||
});
|
||||
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
@@ -2024,7 +2324,18 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
coinSelects.forEach(select => createCoinDropdown(select));
|
||||
coinSelects.forEach(select => {
|
||||
if (!select) return;
|
||||
|
||||
let showBalance = false;
|
||||
if (modalType === 'offer' && select.id.includes('coin-from')) {
|
||||
showBalance = true;
|
||||
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
|
||||
showBalance = true;
|
||||
}
|
||||
|
||||
createSimpleDropdown(select, showBalance);
|
||||
});
|
||||
|
||||
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
|
||||
}
|
||||
@@ -2303,19 +2614,27 @@ const AmmTablesManager = (function() {
|
||||
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', async function() {
|
||||
|
||||
if (refreshButton.disabled) return;
|
||||
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
refreshButton.disabled = true;
|
||||
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
|
||||
await initializePrices();
|
||||
updateTables();
|
||||
|
||||
setTimeout(() => {
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}, 1000);
|
||||
try {
|
||||
await initializePrices();
|
||||
updateTables();
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
refreshButton.disabled = false;
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2328,7 +2647,11 @@ const AmmTablesManager = (function() {
|
||||
return {
|
||||
updateTables,
|
||||
startRefreshTimer,
|
||||
stopRefreshTimer
|
||||
stopRefreshTimer,
|
||||
refreshDropdownBalanceDisplay,
|
||||
refreshOfferDropdownBalanceDisplay,
|
||||
refreshBidDropdownBalanceDisplay,
|
||||
refreshDropdownOptions
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,9 +53,9 @@ const getTimeStrokeColor = (expireTime) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeLeft = expireTime - now;
|
||||
|
||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||
return '#10B981'; // More than 30 minutes
|
||||
if (timeLeft <= 300) return '#9CA3AF';
|
||||
if (timeLeft <= 1800) return '#3B82F6';
|
||||
return '#10B981';
|
||||
};
|
||||
|
||||
const createTimeTooltip = (bid) => {
|
||||
@@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => {
|
||||
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
|
||||
|
||||
if (refreshIcon) {
|
||||
// Add CSS transition for smoother animation
|
||||
|
||||
refreshIcon.style.transition = 'transform 0.3s ease';
|
||||
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||
}
|
||||
@@ -631,7 +631,7 @@ if (elements.refreshBidsButton) {
|
||||
|
||||
updateLoadingState(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 500));
|
||||
|
||||
try {
|
||||
await updateBidsTable({ resetPage: true, refreshData: true });
|
||||
@@ -66,7 +66,7 @@ const BidExporter = {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
@@ -104,7 +104,7 @@ const BidExporter = {
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(function() {
|
||||
CleanupManager.setTimeout(function() {
|
||||
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
|
||||
const exportAllButton = document.getElementById('exportAllBids');
|
||||
if (exportAllButton) {
|
||||
@@ -32,7 +32,7 @@ document.addEventListener('tabactivated', function(event) {
|
||||
if (event.detail && event.detail.tabId) {
|
||||
const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' :
|
||||
(event.detail.tabId === '#sent' ? 'sent' : 'received'));
|
||||
//console.log('Tab activation event received for:', tabType);
|
||||
|
||||
state.currentTab = tabType;
|
||||
updateBidsTable();
|
||||
}
|
||||
@@ -190,7 +190,6 @@ const EventManager = {
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
//console.log('Starting comprehensive cleanup process for bids table');
|
||||
|
||||
try {
|
||||
if (searchTimeout) {
|
||||
@@ -326,7 +325,6 @@ window.cleanupBidsTable = cleanup;
|
||||
|
||||
CleanupManager.addListener(document, 'visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
//console.log('Page hidden - pausing WebSocket and optimizing memory');
|
||||
|
||||
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
|
||||
WebSocketManager.pause();
|
||||
@@ -351,7 +349,7 @@ CleanupManager.addListener(document, 'visibilitychange', () => {
|
||||
|
||||
const lastUpdateTime = state.lastRefresh || 0;
|
||||
const now = Date.now();
|
||||
const refreshInterval = 5 * 60 * 1000; // 5 minutes
|
||||
const refreshInterval = 5 * 60 * 1000;
|
||||
|
||||
if (now - lastUpdateTime > refreshInterval) {
|
||||
setTimeout(() => {
|
||||
@@ -490,13 +488,7 @@ function coinMatches(offerCoin, filterCoin) {
|
||||
|
||||
if (offerCoin === filterCoin) return true;
|
||||
|
||||
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
||||
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
||||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
|
||||
if (window.CoinUtils && window.CoinUtils.isSameCoin(offerCoin, filterCoin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -926,6 +918,12 @@ const forceTooltipDOMCleanup = () => {
|
||||
foundCount += allTooltipElements.length;
|
||||
|
||||
allTooltipElements.forEach(element => {
|
||||
const isInTooltipContainer = element.closest('.tooltip-container');
|
||||
|
||||
if (isInTooltipContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDetached = !document.body.contains(element) ||
|
||||
element.classList.contains('hidden') ||
|
||||
element.style.display === 'none';
|
||||
@@ -1012,7 +1010,7 @@ const forceTooltipDOMCleanup = () => {
|
||||
});
|
||||
}
|
||||
if (removedCount > 0) {
|
||||
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,7 +1144,7 @@ const createTableRow = async (bid) => {
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
const rowHtml = `
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<!-- Time Column -->
|
||||
<td class="py-3 pl-6 pr-3">
|
||||
@@ -1232,13 +1230,16 @@ const createTableRow = async (bid) => {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
<!-- Tooltips -->
|
||||
const tooltipIdentityHtml = `
|
||||
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
|
||||
${tooltipContent}
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const tooltipStatusHtml = `
|
||||
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
|
||||
<div class="text-white">
|
||||
<p class="font-bold mb-2">Transaction Status</p>
|
||||
@@ -1256,6 +1257,12 @@ const createTableRow = async (bid) => {
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return {
|
||||
rowHtml,
|
||||
tooltipIdentityHtml,
|
||||
tooltipStatusHtml
|
||||
};
|
||||
};
|
||||
|
||||
function cleanupOffscreenTooltips() {
|
||||
@@ -1323,8 +1330,6 @@ async function fetchBids(type = state.currentTab) {
|
||||
const withExpiredSelect = document.getElementById('with_expired');
|
||||
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
|
||||
|
||||
//console.log(`Fetching ${type} bids, include expired:`, includeExpired);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (activeFetchController) {
|
||||
activeFetchController.abort();
|
||||
@@ -1372,8 +1377,6 @@ async function fetchBids(type = state.currentTab) {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log(`Received raw ${type} data:`, data.length, 'bids');
|
||||
|
||||
state.filters.with_expired = includeExpired;
|
||||
|
||||
let processedData;
|
||||
@@ -1410,7 +1413,8 @@ const updateTableContent = async (type) => {
|
||||
}
|
||||
|
||||
cleanupTooltips();
|
||||
forceTooltipDOMCleanup();
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading bids...</div></td></tr>';
|
||||
|
||||
tooltipIdsToCleanup.clear();
|
||||
|
||||
@@ -1421,47 +1425,55 @@ const updateTableContent = async (type) => {
|
||||
|
||||
const currentPageData = filteredData.slice(startIndex, endIndex);
|
||||
|
||||
//console.log('Updating table content:', {
|
||||
// type: type,
|
||||
// totalFilteredBids: filteredData.length,
|
||||
// currentPageBids: currentPageData.length,
|
||||
// startIndex: startIndex,
|
||||
// endIndex: endIndex
|
||||
//});
|
||||
let tooltipContainerId = `tooltip-container-${type}`;
|
||||
let tooltipContainer = document.getElementById(tooltipContainerId);
|
||||
|
||||
if (!tooltipContainer) {
|
||||
tooltipContainer = document.createElement('div');
|
||||
tooltipContainer.id = tooltipContainerId;
|
||||
tooltipContainer.className = 'tooltip-container';
|
||||
document.body.appendChild(tooltipContainer);
|
||||
} else {
|
||||
tooltipContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentPageData.length > 0) {
|
||||
const BATCH_SIZE = 10;
|
||||
let allRows = [];
|
||||
let allTooltips = [];
|
||||
|
||||
for (let i = 0; i < currentPageData.length; i += BATCH_SIZE) {
|
||||
const batch = currentPageData.slice(i, i + BATCH_SIZE);
|
||||
const rowPromises = batch.map(bid => createTableRow(bid));
|
||||
const rows = await Promise.all(rowPromises);
|
||||
allRows = allRows.concat(rows);
|
||||
const rowData = await Promise.all(rowPromises);
|
||||
|
||||
if (i + BATCH_SIZE < currentPageData.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
}
|
||||
rowData.forEach(data => {
|
||||
allRows.push(data.rowHtml);
|
||||
allTooltips.push(data.tooltipIdentityHtml);
|
||||
allTooltips.push(data.tooltipStatusHtml);
|
||||
});
|
||||
}
|
||||
|
||||
const scrollPosition = tbody.parentElement?.scrollTop || 0;
|
||||
|
||||
tbody.innerHTML = allRows.join('');
|
||||
tooltipContainer.innerHTML = allTooltips.join('');
|
||||
|
||||
if (tbody.parentElement && scrollPosition > 0) {
|
||||
tbody.parentElement.scrollTop = scrollPosition;
|
||||
}
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
initializeTooltips();
|
||||
|
||||
setTimeout(() => {
|
||||
initializeTooltips();
|
||||
|
||||
setTimeout(() => {
|
||||
forceTooltipDOMCleanup();
|
||||
}, 100);
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
forceTooltipDOMCleanup();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
@@ -1495,7 +1507,7 @@ const initializeTooltips = () => {
|
||||
const tooltipTriggers = document.querySelectorAll(selector);
|
||||
const tooltipCount = tooltipTriggers.length;
|
||||
if (tooltipCount > 50) {
|
||||
//console.log(`Optimizing ${tooltipCount} tooltips`);
|
||||
|
||||
const viewportMargin = 200;
|
||||
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
@@ -1595,13 +1607,6 @@ const updatePaginationControls = (type) => {
|
||||
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
||||
const bidsCount = elements[`${type}BidsCount`];
|
||||
|
||||
//console.log('Pagination controls update:', {
|
||||
// type: type,
|
||||
// totalBids: data.length,
|
||||
// totalPages: totalPages,
|
||||
// currentPage: state.currentPage[type]
|
||||
//});
|
||||
|
||||
if (state.currentPage[type] > totalPages) {
|
||||
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
|
||||
}
|
||||
@@ -2077,7 +2082,7 @@ const setupEventListeners = () => {
|
||||
function setupMemoryMonitoring() {
|
||||
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
const intervalId = CleanupManager.setInterval(() => {
|
||||
if (document.hidden) {
|
||||
console.log('Tab hidden - running memory optimization');
|
||||
|
||||
@@ -2110,9 +2115,9 @@ function setupMemoryMonitoring() {
|
||||
}
|
||||
}, MEMORY_CHECK_INTERVAL);
|
||||
|
||||
document.addEventListener('beforeunload', () => {
|
||||
CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => {
|
||||
clearInterval(intervalId);
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
@@ -7,7 +7,7 @@
|
||||
originalOnload();
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
CleanupManager.setTimeout(function() {
|
||||
initBidsTabNavigation();
|
||||
handleInitialNavigation();
|
||||
}, 100);
|
||||
@@ -15,6 +15,12 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initBidsTabNavigation();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
@@ -43,7 +49,7 @@
|
||||
});
|
||||
|
||||
window.bidsTabNavigationInitialized = true;
|
||||
console.log('Bids tab navigation initialized');
|
||||
|
||||
}
|
||||
|
||||
function handleInitialNavigation() {
|
||||
@@ -54,14 +60,14 @@
|
||||
const tabToActivate = localStorage.getItem('bidsTabToActivate');
|
||||
|
||||
if (tabToActivate) {
|
||||
//console.log('Activating tab from localStorage:', tabToActivate);
|
||||
|
||||
localStorage.removeItem('bidsTabToActivate');
|
||||
activateTabWithRetry('#' + tabToActivate);
|
||||
} else if (window.location.hash) {
|
||||
//console.log('Activating tab from hash:', window.location.hash);
|
||||
|
||||
activateTabWithRetry(window.location.hash);
|
||||
} else {
|
||||
//console.log('Activating default tab: #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
@@ -73,10 +79,10 @@
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
//console.log('Hash changed, activating tab:', hash);
|
||||
|
||||
activateTabWithRetry(hash);
|
||||
} else {
|
||||
//console.log('Hash cleared, activating default tab: #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
@@ -85,7 +91,7 @@
|
||||
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
|
||||
|
||||
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
|
||||
//console.log('Invalid tab ID, defaulting to #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
return;
|
||||
}
|
||||
@@ -96,18 +102,14 @@
|
||||
|
||||
if (!tabButton) {
|
||||
if (retryCount < 5) {
|
||||
//console.log('Tab button not found, retrying...', retryCount + 1);
|
||||
setTimeout(() => {
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
activateTabWithRetry(normalizedTabId, retryCount + 1);
|
||||
}, 100);
|
||||
} else {
|
||||
//console.error('Failed to find tab button after retries');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('Activating tab:', normalizedTabId);
|
||||
|
||||
tabButton.click();
|
||||
|
||||
if (window.Tabs) {
|
||||
@@ -162,13 +164,13 @@
|
||||
}
|
||||
|
||||
function triggerDataLoad(tabId) {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (window.state) {
|
||||
window.state.currentTab = tabId === '#all' ? 'all' :
|
||||
(tabId === '#sent' ? 'sent' : 'received');
|
||||
|
||||
if (typeof window.updateBidsTable === 'function') {
|
||||
//console.log('Triggering data load for', tabId);
|
||||
|
||||
window.updateBidsTable();
|
||||
}
|
||||
}
|
||||
@@ -183,7 +185,7 @@
|
||||
document.dispatchEvent(event);
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.TooltipManager.cleanup();
|
||||
if (typeof window.initializeTooltips === 'function') {
|
||||
window.initializeTooltips();
|
||||
@@ -198,7 +200,7 @@
|
||||
|
||||
activateTabWithRetry(tabId);
|
||||
|
||||
setTimeout(function() {
|
||||
CleanupManager.setTimeout(function() {
|
||||
window.scrollTo(0, oldScrollPosition);
|
||||
}, 0);
|
||||
}
|
||||
@@ -16,6 +16,30 @@ const DOM = {
|
||||
queryAll: (selector) => document.querySelectorAll(selector)
|
||||
};
|
||||
|
||||
const ErrorModal = {
|
||||
show: function(title, message) {
|
||||
const errorTitle = document.getElementById('errorTitle');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const modal = document.getElementById('errorModal');
|
||||
|
||||
if (errorTitle) errorTitle.textContent = title || 'Error';
|
||||
if (errorMessage) errorMessage.textContent = message || 'An error occurred';
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
},
|
||||
|
||||
init: function() {
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hide.bind(this));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Storage = {
|
||||
get: (key) => {
|
||||
try {
|
||||
@@ -443,7 +467,40 @@ const UIEnhancer = {
|
||||
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
|
||||
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
|
||||
|
||||
const parts = name.split(' - Balance: ');
|
||||
const coinName = parts[0];
|
||||
const balanceInfo = parts[1] || '';
|
||||
|
||||
selectNameElement.innerHTML = '';
|
||||
selectNameElement.style.display = 'flex';
|
||||
selectNameElement.style.flexDirection = 'column';
|
||||
selectNameElement.style.alignItems = 'flex-start';
|
||||
selectNameElement.style.lineHeight = '1.2';
|
||||
|
||||
const coinNameDiv = document.createElement('div');
|
||||
coinNameDiv.textContent = coinName;
|
||||
coinNameDiv.style.fontWeight = 'normal';
|
||||
coinNameDiv.style.color = 'inherit';
|
||||
|
||||
const balanceDiv = document.createElement('div');
|
||||
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
||||
balanceDiv.style.fontSize = '0.75rem';
|
||||
balanceDiv.style.color = '#6b7280';
|
||||
balanceDiv.style.marginTop = '1px';
|
||||
|
||||
selectNameElement.appendChild(coinNameDiv);
|
||||
selectNameElement.appendChild(balanceDiv);
|
||||
|
||||
} else {
|
||||
|
||||
selectNameElement.textContent = name;
|
||||
selectNameElement.style.display = 'block';
|
||||
selectNameElement.style.flexDirection = '';
|
||||
selectNameElement.style.alignItems = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
@@ -537,6 +594,8 @@ function initializeApp() {
|
||||
UIEnhancer.handleErrorHighlighting();
|
||||
UIEnhancer.updateDisabledStyles();
|
||||
UIEnhancer.setupCustomSelects();
|
||||
|
||||
ErrorModal.init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -544,3 +603,6 @@ if (document.readyState === 'loading') {
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
|
||||
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
|
||||
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);
|
||||
364
basicswap/static/js/pages/offer-page.js
Normal file
@@ -0,0 +1,364 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const OfferPage = {
|
||||
xhr_rates: null,
|
||||
xhr_bid_params: null,
|
||||
|
||||
init: function() {
|
||||
this.xhr_rates = new XMLHttpRequest();
|
||||
this.xhr_bid_params = new XMLHttpRequest();
|
||||
|
||||
this.setupXHRHandlers();
|
||||
this.setupEventListeners();
|
||||
this.handleBidsPageAddress();
|
||||
},
|
||||
|
||||
setupXHRHandlers: function() {
|
||||
this.xhr_rates.onload = () => {
|
||||
if (this.xhr_rates.status == 200) {
|
||||
const obj = JSON.parse(this.xhr_rates.response);
|
||||
const inner_html = '<h4 class="bold">Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
|
||||
const ratesDisplay = document.getElementById('rates_display');
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = inner_html;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.xhr_bid_params.onload = () => {
|
||||
if (this.xhr_bid_params.status == 200) {
|
||||
const obj = JSON.parse(this.xhr_bid_params.response);
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
if (bidAmountSendInput) {
|
||||
bidAmountSendInput.value = obj['amount_to'];
|
||||
}
|
||||
this.updateModalValues();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
setupEventListeners: function() {
|
||||
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
|
||||
if (sendBidBtn) {
|
||||
sendBidBtn.onclick = this.showConfirmModal.bind(this);
|
||||
}
|
||||
|
||||
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
|
||||
}
|
||||
|
||||
const mainCancelBtn = document.querySelector('button[name="cancel"]');
|
||||
if (mainCancelBtn) {
|
||||
mainCancelBtn.onclick = this.handleCancelClick.bind(this);
|
||||
}
|
||||
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
if (validMinsInput) {
|
||||
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
|
||||
}
|
||||
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
if (addrFromSelect) {
|
||||
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
|
||||
}
|
||||
|
||||
const errorOkBtn = document.getElementById('errorOk');
|
||||
if (errorOkBtn) {
|
||||
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
lookup_rates: function() {
|
||||
const coin_from = document.getElementById('coin_from')?.value;
|
||||
const coin_to = document.getElementById('coin_to')?.value;
|
||||
|
||||
if (!coin_from || !coin_to || coin_from === '-1' || coin_to === '-1') {
|
||||
alert('Coins from and to must be set first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ratesDisplay = document.getElementById('rates_display');
|
||||
if (ratesDisplay) {
|
||||
ratesDisplay.innerHTML = '<h4>Rates</h4><p>Updating...</p>';
|
||||
}
|
||||
|
||||
this.xhr_rates.open('POST', '/json/rates');
|
||||
this.xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
this.xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`);
|
||||
},
|
||||
|
||||
resetForm: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
const amtVar = document.getElementById('amt_var')?.value === 'True';
|
||||
|
||||
if (bidAmountSendInput) {
|
||||
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
|
||||
}
|
||||
if (bidAmountInput) {
|
||||
bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max');
|
||||
}
|
||||
if (bidRateInput && !bidRateInput.disabled) {
|
||||
const defaultRate = document.getElementById('offer_rate')?.value || '';
|
||||
bidRateInput.value = defaultRate;
|
||||
}
|
||||
if (validMinsInput) {
|
||||
validMinsInput.value = "60";
|
||||
}
|
||||
if (!amtVar) {
|
||||
this.updateBidParams('rate');
|
||||
}
|
||||
this.updateModalValues();
|
||||
|
||||
const errorMessages = document.querySelectorAll('.error-message');
|
||||
errorMessages.forEach(msg => msg.remove());
|
||||
|
||||
const inputs = document.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.classList.remove('border-red-500', 'focus:border-red-500');
|
||||
});
|
||||
},
|
||||
|
||||
roundUpToDecimals: function(value, decimals) {
|
||||
const factor = Math.pow(10, decimals);
|
||||
return Math.ceil(value * factor) / factor;
|
||||
},
|
||||
|
||||
updateBidParams: function(value_changed) {
|
||||
const coin_from = document.getElementById('coin_from')?.value;
|
||||
const coin_to = document.getElementById('coin_to')?.value;
|
||||
const coin_from_exp = parseInt(document.getElementById('coin_from_exp')?.value || '8');
|
||||
const coin_to_exp = parseInt(document.getElementById('coin_to_exp')?.value || '8');
|
||||
const amt_var = document.getElementById('amt_var')?.value;
|
||||
const rate_var = document.getElementById('rate_var')?.value;
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
const offerRateInput = document.getElementById('offer_rate');
|
||||
|
||||
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
|
||||
|
||||
const rate = rate_var === 'True' && bidRateInput ?
|
||||
parseFloat(bidRateInput.value) || 0 :
|
||||
parseFloat(offerRateInput?.value || '0');
|
||||
|
||||
if (!rate) return;
|
||||
|
||||
if (value_changed === 'rate') {
|
||||
if (bidAmountSendInput && bidAmountInput) {
|
||||
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
|
||||
bidAmountInput.value = receiveAmount;
|
||||
}
|
||||
} else if (value_changed === 'sending') {
|
||||
if (bidAmountSendInput && bidAmountInput) {
|
||||
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
|
||||
bidAmountInput.value = receiveAmount;
|
||||
}
|
||||
} else if (value_changed === 'receiving') {
|
||||
if (bidAmountInput && bidAmountSendInput) {
|
||||
const receiveAmount = parseFloat(bidAmountInput.value) || 0;
|
||||
const sendAmount = this.roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp);
|
||||
bidAmountSendInput.value = sendAmount;
|
||||
}
|
||||
}
|
||||
|
||||
this.validateAmountsAfterChange();
|
||||
|
||||
this.xhr_bid_params.open('POST', '/json/rate');
|
||||
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
|
||||
|
||||
this.updateModalValues();
|
||||
},
|
||||
|
||||
validateAmountsAfterChange: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
|
||||
if (bidAmountSendInput) {
|
||||
const maxSend = parseFloat(bidAmountSendInput.getAttribute('max'));
|
||||
this.validateMaxAmount(bidAmountSendInput, maxSend);
|
||||
}
|
||||
if (bidAmountInput) {
|
||||
const maxReceive = parseFloat(bidAmountInput.getAttribute('max'));
|
||||
this.validateMaxAmount(bidAmountInput, maxReceive);
|
||||
}
|
||||
},
|
||||
|
||||
validateMaxAmount: function(input, maxAmount) {
|
||||
if (!input) return;
|
||||
const value = parseFloat(input.value) || 0;
|
||||
if (value > maxAmount) {
|
||||
input.value = maxAmount;
|
||||
}
|
||||
},
|
||||
|
||||
showErrorModal: function(title, message) {
|
||||
document.getElementById('errorTitle').textContent = title || 'Error';
|
||||
document.getElementById('errorMessage').textContent = message || 'An error occurred';
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
hideErrorModal: function() {
|
||||
const modal = document.getElementById('errorModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmModal: function() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
|
||||
let sendAmount = 0;
|
||||
let receiveAmount = 0;
|
||||
|
||||
if (bidAmountSendInput && bidAmountSendInput.value) {
|
||||
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
}
|
||||
|
||||
if (bidAmountInput && bidAmountInput.value) {
|
||||
receiveAmount = parseFloat(bidAmountInput.value) || 0;
|
||||
}
|
||||
|
||||
if (sendAmount <= 0 || receiveAmount <= 0) {
|
||||
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const coinFrom = document.getElementById('coin_from_name')?.value || '';
|
||||
const coinTo = document.getElementById('coin_to_name')?.value || '';
|
||||
const tlaFrom = document.getElementById('tla_from')?.value || '';
|
||||
const tlaTo = document.getElementById('tla_to')?.value || '';
|
||||
|
||||
const validMins = validMinsInput ? validMinsInput.value : '60';
|
||||
|
||||
const addrFrom = addrFromSelect ? addrFromSelect.value : '';
|
||||
|
||||
const modalAmtReceive = document.getElementById('modal-amt-receive');
|
||||
const modalReceiveCurrency = document.getElementById('modal-receive-currency');
|
||||
const modalAmtSend = document.getElementById('modal-amt-send');
|
||||
const modalSendCurrency = document.getElementById('modal-send-currency');
|
||||
const modalAddrFrom = document.getElementById('modal-addr-from');
|
||||
const modalValidMins = document.getElementById('modal-valid-mins');
|
||||
|
||||
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
|
||||
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
|
||||
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
|
||||
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
|
||||
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
|
||||
if (modalValidMins) modalValidMins.textContent = validMins;
|
||||
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hideConfirmModal: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
updateModalValues: function() {
|
||||
|
||||
},
|
||||
|
||||
handleBidsPageAddress: function() {
|
||||
const selectElement = document.querySelector('select[name="addr_from"]');
|
||||
const STORAGE_KEY = 'lastUsedAddressBids';
|
||||
|
||||
if (!selectElement) return;
|
||||
|
||||
const loadInitialAddress = () => {
|
||||
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedAddressJSON) {
|
||||
try {
|
||||
const savedAddress = JSON.parse(savedAddressJSON);
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
};
|
||||
|
||||
const selectFirstAddress = () => {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
this.saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
this.saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
},
|
||||
|
||||
saveAddress: function(value, text) {
|
||||
const addressData = {
|
||||
value: value,
|
||||
text: text
|
||||
};
|
||||
localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData));
|
||||
},
|
||||
|
||||
confirmPopup: function() {
|
||||
return confirm("Are you sure?");
|
||||
},
|
||||
|
||||
handleCancelClick: function(event) {
|
||||
if (event) event.preventDefault();
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const offerId = pathParts[pathParts.indexOf('offer') + 1];
|
||||
window.location.href = `/offer/${offerId}`;
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
OfferPage.init();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('offerPage', OfferPage, (page) => {
|
||||
if (page.cleanup) page.cleanup();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.OfferPage = OfferPage;
|
||||
window.lookup_rates = OfferPage.lookup_rates.bind(OfferPage);
|
||||
window.resetForm = OfferPage.resetForm.bind(OfferPage);
|
||||
window.updateBidParams = OfferPage.updateBidParams.bind(OfferPage);
|
||||
window.validateMaxAmount = OfferPage.validateMaxAmount.bind(OfferPage);
|
||||
window.showConfirmModal = OfferPage.showConfirmModal.bind(OfferPage);
|
||||
window.hideConfirmModal = OfferPage.hideConfirmModal.bind(OfferPage);
|
||||
window.showErrorModal = OfferPage.showErrorModal.bind(OfferPage);
|
||||
window.hideErrorModal = OfferPage.hideErrorModal.bind(OfferPage);
|
||||
window.confirmPopup = OfferPage.confirmPopup.bind(OfferPage);
|
||||
window.handleBidsPageAddress = OfferPage.handleBidsPageAddress.bind(OfferPage);
|
||||
|
||||
})();
|
||||
@@ -5,8 +5,8 @@ let jsonData = [];
|
||||
let originalJsonData = [];
|
||||
let currentSortColumn = 0;
|
||||
let currentSortDirection = 'desc';
|
||||
let filterTimeout = null;
|
||||
let isPaginationInProgress = false;
|
||||
let autoRefreshInterval = null;
|
||||
|
||||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||||
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
|
||||
@@ -28,6 +28,9 @@ window.tableRateModule = {
|
||||
processedOffers: new Set(),
|
||||
|
||||
getCachedValue(key) {
|
||||
if (window.CacheManager) {
|
||||
return window.CacheManager.get(key);
|
||||
}
|
||||
const cachedItem = localStorage.getItem(key);
|
||||
if (cachedItem) {
|
||||
const parsedItem = JSON.parse(cachedItem);
|
||||
@@ -41,6 +44,14 @@ window.tableRateModule = {
|
||||
},
|
||||
|
||||
setCachedValue(key, value, resourceType = null) {
|
||||
if (window.CacheManager) {
|
||||
const ttl = resourceType ?
|
||||
window.config.cacheConfig.ttlSettings[resourceType] ||
|
||||
window.config.cacheConfig.defaultTTL :
|
||||
900000;
|
||||
window.CacheManager.set(key, value, ttl);
|
||||
return;
|
||||
}
|
||||
const ttl = resourceType ?
|
||||
window.config.cacheConfig.ttlSettings[resourceType] ||
|
||||
window.config.cacheConfig.defaultTTL :
|
||||
@@ -65,26 +76,6 @@ window.tableRateModule = {
|
||||
return true;
|
||||
},
|
||||
|
||||
formatUSD(value) {
|
||||
if (Math.abs(value) < 0.000001) {
|
||||
return value.toExponential(8) + ' USD';
|
||||
} else if (Math.abs(value) < 0.01) {
|
||||
return value.toFixed(8) + ' USD';
|
||||
} else {
|
||||
return value.toFixed(2) + ' USD';
|
||||
}
|
||||
},
|
||||
|
||||
formatNumber(value, decimals) {
|
||||
if (Math.abs(value) < 0.000001) {
|
||||
return value.toExponential(decimals);
|
||||
} else if (Math.abs(value) < 0.01) {
|
||||
return value.toFixed(decimals);
|
||||
} else {
|
||||
return value.toFixed(Math.min(2, decimals));
|
||||
}
|
||||
},
|
||||
|
||||
getFallbackValue(coinSymbol) {
|
||||
if (!coinSymbol) return null;
|
||||
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
|
||||
@@ -151,6 +142,41 @@ function initializeTooltips() {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTooltipsInBatches() {
|
||||
if (!window.TooltipManager) return;
|
||||
|
||||
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
|
||||
const BATCH_SIZE = 5;
|
||||
let currentIndex = 0;
|
||||
|
||||
function processBatch() {
|
||||
const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length);
|
||||
|
||||
for (let i = currentIndex; i < endIndex; i++) {
|
||||
const element = tooltipElements[i];
|
||||
const targetId = element.getAttribute('data-tooltip-target');
|
||||
if (!targetId) continue;
|
||||
|
||||
const tooltipContent = document.getElementById(targetId);
|
||||
if (tooltipContent) {
|
||||
window.TooltipManager.create(element, tooltipContent.innerHTML, {
|
||||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex = endIndex;
|
||||
|
||||
if (currentIndex < tooltipElements.length) {
|
||||
CleanupManager.setTimeout(processBatch, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (tooltipElements.length > 0) {
|
||||
CleanupManager.setTimeout(processBatch, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function getValidOffers() {
|
||||
if (!jsonData) {
|
||||
return [];
|
||||
@@ -180,7 +206,6 @@ function saveFilterSettings() {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function getSelectedCoins(filterType) {
|
||||
|
||||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||||
@@ -188,7 +213,6 @@ function getSelectedCoins(filterType) {
|
||||
return ['any'];
|
||||
}
|
||||
|
||||
|
||||
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
|
||||
|
||||
const selected = [];
|
||||
@@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) {
|
||||
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
|
||||
}
|
||||
|
||||
|
||||
button.style.width = '210px';
|
||||
}
|
||||
|
||||
@@ -270,7 +293,6 @@ function updateCoinBadges(filterType) {
|
||||
const coinName = getCoinNameFromValue(coinValue, filterType);
|
||||
const badge = document.createElement('span');
|
||||
|
||||
|
||||
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
|
||||
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
|
||||
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
|
||||
@@ -285,7 +307,6 @@ function updateCoinBadges(filterType) {
|
||||
|
||||
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
|
||||
|
||||
|
||||
const coinImage = getCoinImage(coinName);
|
||||
|
||||
badge.innerHTML = `
|
||||
@@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
|
||||
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
|
||||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
|
||||
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) {
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
|
||||
|
||||
updateFilterButtonText(filterType);
|
||||
updateCoinBadges(filterType);
|
||||
applyFilters();
|
||||
@@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.removeCoinFilter = removeCoinFilter;
|
||||
|
||||
function filterAndSortData() {
|
||||
@@ -510,7 +523,6 @@ function filterAndSortData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
|
||||
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
|
||||
const matches = coinMatches(offer.coin_to, coinNames);
|
||||
@@ -519,7 +531,6 @@ function filterAndSortData() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
|
||||
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
|
||||
const matches = coinMatches(offer.coin_from, coinNames);
|
||||
@@ -578,7 +589,7 @@ function filterAndSortData() {
|
||||
if (offer.is_own_offer || isSentOffers) {
|
||||
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
|
||||
} else {
|
||||
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
|
||||
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
|
||||
if (window.CoinManager) {
|
||||
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
|
||||
} else {
|
||||
if (normalizedCoin === 'zcoin') normalizedCoin = 'firo';
|
||||
if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash')
|
||||
normalizedCoin = 'bitcoin-cash';
|
||||
if (normalizedCoin.includes('particl')) normalizedCoin = 'particl';
|
||||
if (window.CoinUtils) {
|
||||
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
|
||||
}
|
||||
}
|
||||
let price = null;
|
||||
if (latestPrices && latestPrices[normalizedCoin]) {
|
||||
@@ -706,7 +716,7 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
|
||||
if (isOwnOffer) {
|
||||
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
|
||||
} else {
|
||||
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
|
||||
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||||
}
|
||||
|
||||
if (isNaN(percentDiff)) {
|
||||
@@ -734,11 +744,38 @@ async function fetchLatestPrices() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPricesAsync() {
|
||||
try {
|
||||
const prices = await window.PriceManager.getPrices(false);
|
||||
return prices;
|
||||
} catch (error) {
|
||||
console.error('Error fetching prices asynchronously:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOffers() {
|
||||
const refreshButton = document.getElementById('refreshOffers');
|
||||
const refreshIcon = document.getElementById('refreshIcon');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
|
||||
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`);
|
||||
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (!NetworkManager.isOnline()) {
|
||||
throw new Error('Network is offline');
|
||||
@@ -751,15 +788,7 @@ async function fetchOffers() {
|
||||
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
||||
}
|
||||
|
||||
const [offersResponse, pricesData] = await Promise.all([
|
||||
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
|
||||
fetchLatestPrices()
|
||||
]);
|
||||
|
||||
if (!offersResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${offersResponse.status}`);
|
||||
}
|
||||
|
||||
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||
const data = await offersResponse.json();
|
||||
|
||||
if (data.error) {
|
||||
@@ -789,13 +818,20 @@ async function fetchOffers() {
|
||||
jsonData = formatInitialData(processedData);
|
||||
originalJsonData = [...jsonData];
|
||||
|
||||
latestPrices = pricesData || getEmptyPriceData();
|
||||
|
||||
CacheManager.set('offers_cached', jsonData, 'offers');
|
||||
|
||||
applyFilters();
|
||||
updatePaginationInfo();
|
||||
|
||||
fetchPricesAsync().then(prices => {
|
||||
if (prices) {
|
||||
latestPrices = prices;
|
||||
updateProfitLossDisplays();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error fetching prices after offers refresh:', error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Debug] Error fetching offers:', error);
|
||||
NetworkManager.handleNetworkError(error);
|
||||
@@ -871,27 +907,37 @@ function updateConnectionStatus(status) {
|
||||
}
|
||||
|
||||
function updateRowTimes() {
|
||||
requestAnimationFrame(() => {
|
||||
const rows = document.querySelectorAll('[data-offer-id]');
|
||||
rows.forEach(row => {
|
||||
const offerId = row.getAttribute('data-offer-id');
|
||||
const offer = jsonData.find(o => o.offer_id === offerId);
|
||||
if (!offer) return;
|
||||
|
||||
const newPostedTime = formatTime(offer.created_at, true);
|
||||
const newExpiresIn = formatTimeLeft(offer.expire_at);
|
||||
const rows = document.querySelectorAll('[data-offer-id]');
|
||||
const updates = [];
|
||||
|
||||
const postedElement = row.querySelector('.text-xs:first-child');
|
||||
const expiresElement = row.querySelector('.text-xs:last-child');
|
||||
rows.forEach(row => {
|
||||
const offerId = row.getAttribute('data-offer-id');
|
||||
const offer = jsonData.find(o => o.offer_id === offerId);
|
||||
if (!offer) return;
|
||||
|
||||
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
|
||||
postedElement.textContent = `Posted: ${newPostedTime}`;
|
||||
}
|
||||
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
|
||||
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
|
||||
}
|
||||
const newPostedTime = formatTime(offer.created_at, true);
|
||||
const newExpiresIn = formatTimeLeft(offer.expire_at);
|
||||
|
||||
const postedElement = row.querySelector('.text-xs:first-child');
|
||||
const expiresElement = row.querySelector('.text-xs:last-child');
|
||||
|
||||
updates.push({
|
||||
postedElement,
|
||||
expiresElement,
|
||||
newPostedTime,
|
||||
newExpiresIn
|
||||
});
|
||||
});
|
||||
|
||||
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
|
||||
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
|
||||
postedElement.textContent = `Posted: ${newPostedTime}`;
|
||||
}
|
||||
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
|
||||
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateLastRefreshTime() {
|
||||
@@ -975,12 +1021,11 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedPercentDiff = percentDiff.toFixed(2);
|
||||
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
|
||||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||||
|
||||
const colorClass = getProfitColorClass(percentDiff);
|
||||
profitLossElement.textContent = `${percentDiffDisplay}%`;
|
||||
profitLossElement.textContent = `${percentDiffDisplay.toFixed(2)}%`;
|
||||
profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
|
||||
|
||||
const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
|
||||
@@ -1098,8 +1143,13 @@ async function updateOffersTable(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isIncrementalUpdate = options.incremental === true;
|
||||
if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) {
|
||||
offersBody.innerHTML = '<tr><td colspan="10" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading offers...</div></td></tr>';
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
requestAnimationFrame(() => window.TooltipManager.cleanup());
|
||||
}
|
||||
|
||||
const validOffers = getValidOffers();
|
||||
@@ -1139,28 +1189,72 @@ async function updateOffersTable(options = {}) {
|
||||
if (row) fragment.appendChild(row);
|
||||
});
|
||||
|
||||
if (i + BATCH_SIZE < itemsToDisplay.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 16));
|
||||
}
|
||||
}
|
||||
|
||||
if (offersBody) {
|
||||
const existingRows = offersBody.querySelectorAll('tr');
|
||||
existingRows.forEach(row => cleanupRow(row));
|
||||
offersBody.textContent = '';
|
||||
offersBody.appendChild(fragment);
|
||||
if (isIncrementalUpdate && offersBody.children.length > 0) {
|
||||
|
||||
const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]'));
|
||||
const newRows = Array.from(fragment.querySelectorAll('tr[data-offer-id]'));
|
||||
|
||||
const existingMap = new Map(existingRows.map(row => [row.getAttribute('data-offer-id'), row]));
|
||||
const newMap = new Map(newRows.map(row => [row.getAttribute('data-offer-id'), row]));
|
||||
|
||||
existingRows.forEach(row => {
|
||||
const offerId = row.getAttribute('data-offer-id');
|
||||
if (!newMap.has(offerId)) {
|
||||
cleanupRow(row);
|
||||
row.remove();
|
||||
}
|
||||
});
|
||||
|
||||
newRows.forEach((newRow, index) => {
|
||||
const offerId = newRow.getAttribute('data-offer-id');
|
||||
const existingRow = existingMap.get(offerId);
|
||||
|
||||
if (existingRow) {
|
||||
|
||||
const currentIndex = Array.from(offersBody.children).indexOf(existingRow);
|
||||
if (currentIndex !== index) {
|
||||
|
||||
if (index >= offersBody.children.length) {
|
||||
offersBody.appendChild(existingRow);
|
||||
} else {
|
||||
offersBody.insertBefore(existingRow, offersBody.children[index]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (index >= offersBody.children.length) {
|
||||
offersBody.appendChild(newRow);
|
||||
} else {
|
||||
offersBody.insertBefore(newRow, offersBody.children[index]);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
const existingRows = offersBody.querySelectorAll('tr');
|
||||
existingRows.forEach(row => cleanupRow(row));
|
||||
offersBody.textContent = '';
|
||||
offersBody.appendChild(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
initializeTooltips();
|
||||
initializeTooltipsInBatches();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
updateRowTimes();
|
||||
updatePaginationInfo();
|
||||
updateProfitLossDisplays();
|
||||
}, 10);
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (tableRateModule?.initializeTable) {
|
||||
tableRateModule.initializeTable();
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
|
||||
lastRefreshTime = Date.now();
|
||||
updateLastRefreshTime();
|
||||
@@ -1172,7 +1266,10 @@ async function updateOffersTable(options = {}) {
|
||||
}
|
||||
|
||||
function updateProfitLossDisplays() {
|
||||
|
||||
const rows = document.querySelectorAll('[data-offer-id]');
|
||||
const updates = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const offerId = row.getAttribute('data-offer-id');
|
||||
const offer = jsonData.find(o => o.offer_id === offerId);
|
||||
@@ -1180,6 +1277,17 @@ function updateProfitLossDisplays() {
|
||||
|
||||
const fromAmount = parseFloat(offer.amount_from) || 0;
|
||||
const toAmount = parseFloat(offer.amount_to) || 0;
|
||||
|
||||
updates.push({
|
||||
row,
|
||||
offerId,
|
||||
offer,
|
||||
fromAmount,
|
||||
toAmount
|
||||
});
|
||||
});
|
||||
|
||||
updates.forEach(({ row, offerId, offer, fromAmount, toAmount }) => {
|
||||
updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
|
||||
|
||||
const rateTooltipId = `tooltip-rate-${offerId}`;
|
||||
@@ -1495,7 +1603,6 @@ function createRateColumn(offer, coinFrom, coinTo) {
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function createPercentageColumn(offer) {
|
||||
return `
|
||||
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
|
||||
@@ -1732,45 +1839,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
|
||||
|
||||
const getPriceKey = (coin) => {
|
||||
if (!coin) return null;
|
||||
|
||||
const lowerCoin = coin.toLowerCase();
|
||||
|
||||
if (lowerCoin === 'zcoin') return 'firo';
|
||||
|
||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
||||
|
||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
||||
return 'bitcoin-cash';
|
||||
} else if (latestPrices && latestPrices['bch']) {
|
||||
return 'bch';
|
||||
}
|
||||
return 'bitcoin-cash';
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||
}
|
||||
|
||||
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
|
||||
return 'part';
|
||||
}
|
||||
|
||||
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
|
||||
const symbol = window.config.coinMappings.nameToSymbol[coin];
|
||||
if (symbol) {
|
||||
if (symbol.toUpperCase() === 'BCH') {
|
||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
||||
return 'bitcoin-cash';
|
||||
} else if (latestPrices && latestPrices['bch']) {
|
||||
return 'bch';
|
||||
}
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
if (symbol.toUpperCase() === 'PART') {
|
||||
return 'part';
|
||||
}
|
||||
|
||||
return symbol.toLowerCase();
|
||||
}
|
||||
}
|
||||
return lowerCoin;
|
||||
return coin.toLowerCase();
|
||||
};
|
||||
|
||||
const fromSymbol = getPriceKey(coinFrom);
|
||||
@@ -1808,38 +1880,37 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
|
||||
if (isSentOffers || isOwnOffer) {
|
||||
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
|
||||
} else {
|
||||
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
|
||||
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||||
}
|
||||
|
||||
const formattedPercentDiff = percentDiff.toFixed(2);
|
||||
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
|
||||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||||
|
||||
const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss";
|
||||
const profitLabel = percentDiff > 0 ? "Max Profit" : "Max Loss";
|
||||
const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying";
|
||||
const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying";
|
||||
|
||||
return `
|
||||
<p class="font-bold mb-1">Profit/Loss Calculation:</p>
|
||||
<p>You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD) <br/> and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).</p>
|
||||
<p class="mt-1">Percentage difference: ${percentDiffDisplay}%</p>
|
||||
<p>${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${Math.abs(profitUSD).toFixed(2)} USD</p>
|
||||
<p class="mt-1">Percentage difference: ${percentDiffDisplay.toFixed(2)}%</p>
|
||||
<p>${profitLabel}: ${Math.abs(profitUSD).toFixed(2)} USD</p>
|
||||
<p class="font-bold mt-2">Calculation:</p>
|
||||
<p>Percentage = ${(isSentOffers || isOwnOffer) ?
|
||||
"((To Amount in USD / From Amount in USD) - 1) * 100" :
|
||||
"((From Amount in USD / To Amount in USD) - 1) * 100"}</p>
|
||||
"(-((To Amount in USD / From Amount in USD) - 1)) * 100"}</p>
|
||||
<p>USD ${profitLabel} = To Amount in USD - From Amount in USD</p>
|
||||
<p class="font-bold mt-1">Interpretation:</p>
|
||||
${(isSentOffers || isOwnOffer) ? `
|
||||
<p><span class="text-green-500">Positive percentage:</span> You're selling above market rate (profitable)</p>
|
||||
<p><span class="text-red-500">Negative percentage:</span> You're selling below market rate (loss)</p>
|
||||
<p><span class="text-green-500">Green:</span> You're selling above market rate (profitable)</p>
|
||||
<p><span class="text-red-500">Red:</span> You're selling below market rate (loss)</p>
|
||||
` : `
|
||||
<p><span class="text-green-500">Positive percentage:</span> You're buying below market rate (savings)</p>
|
||||
<p><span class="text-red-500">Negative percentage:</span> You're buying above market rate (premium)</p>
|
||||
<p><span class="text-green-500">Green:</span> You're buying below market rate (savings)</p>
|
||||
<p><span class="text-red-500">Red:</span> You're buying above market rate (premium)</p>
|
||||
`}
|
||||
<p class="mt-1"><strong>Note:</strong> ${(isSentOffers || isOwnOffer) ?
|
||||
"As a seller, a positive percentage means <br/> you're selling for more than the current market value." :
|
||||
"As a buyer, a positive percentage indicates </br> potential savings compared to current market rates."}</p>
|
||||
"As a seller, a green percentage means <br/> you're selling for more than the current market rate." :
|
||||
"As a buyer, a green percentage indicates </br> potential savings compared to current market rate."}</p>
|
||||
<p class="mt-1"><strong>Market Rate:</strong> 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}</p>
|
||||
<p><strong>Offer Rate:</strong> 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}</p>
|
||||
`;
|
||||
@@ -1851,44 +1922,10 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
|
||||
|
||||
const getPriceKey = (coin) => {
|
||||
if (!coin) return null;
|
||||
|
||||
const lowerCoin = coin.toLowerCase();
|
||||
|
||||
if (lowerCoin === 'zcoin') return 'firo';
|
||||
|
||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
||||
return 'bitcoin-cash';
|
||||
} else if (latestPrices && latestPrices['bch']) {
|
||||
return 'bch';
|
||||
}
|
||||
|
||||
return 'bitcoin-cash';
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||
}
|
||||
|
||||
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
|
||||
return 'part';
|
||||
}
|
||||
|
||||
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
|
||||
const symbol = window.config.coinMappings.nameToSymbol[coin];
|
||||
if (symbol) {
|
||||
if (symbol.toUpperCase() === 'BCH') {
|
||||
|
||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
||||
return 'bitcoin-cash';
|
||||
} else if (latestPrices && latestPrices['bch']) {
|
||||
return 'bch';
|
||||
}
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
if (symbol.toUpperCase() === 'PART') {
|
||||
return 'part';
|
||||
}
|
||||
return symbol.toLowerCase();
|
||||
}
|
||||
}
|
||||
return lowerCoin;
|
||||
return coin.toLowerCase();
|
||||
};
|
||||
|
||||
const fromSymbol = getPriceKey(coinFrom);
|
||||
@@ -1921,15 +1958,14 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
|
||||
const rateInUSD = rate * toPriceUSD;
|
||||
const marketRate = fromPriceUSD / toPriceUSD;
|
||||
const percentDiff = marketRate ? ((rate - marketRate) / marketRate) * 100 : 0;
|
||||
const formattedPercentDiff = percentDiff.toFixed(2);
|
||||
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
|
||||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||||
const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at";
|
||||
const action = treatAsSentOffer ? "selling" : "buying";
|
||||
|
||||
return `
|
||||
<p class="font-bold mb-1">Exchange Rate Explanation:</p>
|
||||
<p>This offer is ${action} ${coinFrom} for ${coinTo} <br/>at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.</p>
|
||||
<p>This offer is ${action} ${coinFrom} for ${coinTo} <br/>at a rate that is ${percentDiffDisplay.toFixed(2)}% ${aboveOrBelow} market price.</p>
|
||||
<p class="font-bold mt-1">Exchange Rates:</p>
|
||||
<p>1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}</p>
|
||||
<p>1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}</p>
|
||||
@@ -1961,23 +1997,23 @@ function updateTooltipTargets(row, uniqueId) {
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
if (filterTimeout) {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = null;
|
||||
function applyFilters(options = {}) {
|
||||
if (window.filterTimeout) {
|
||||
clearTimeout(window.filterTimeout);
|
||||
window.filterTimeout = null;
|
||||
}
|
||||
|
||||
try {
|
||||
filterTimeout = setTimeout(() => {
|
||||
window.filterTimeout = CleanupManager.setTimeout(() => {
|
||||
currentPage = 1;
|
||||
jsonData = filterAndSortData();
|
||||
updateOffersTable();
|
||||
updateOffersTable(options);
|
||||
updateClearFiltersButton();
|
||||
filterTimeout = null;
|
||||
window.filterTimeout = null;
|
||||
}, 250);
|
||||
} catch (error) {
|
||||
console.error('Error in filter timeout:', error);
|
||||
filterTimeout = null;
|
||||
window.filterTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2040,13 +2076,10 @@ function formatTimeLeft(timestamp) {
|
||||
}
|
||||
|
||||
function getDisplayName(coinName) {
|
||||
if (window.CoinManager) {
|
||||
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
||||
return window.CoinManager.getDisplayName(coinName) || coinName;
|
||||
}
|
||||
if (coinName.toLowerCase() === 'zcoin') {
|
||||
return 'Firo';
|
||||
}
|
||||
return window.config.coinMappings.nameToDisplayName[coinName] || coinName;
|
||||
return coinName;
|
||||
}
|
||||
|
||||
function getCoinSymbolLowercase(coin) {
|
||||
@@ -2088,38 +2121,23 @@ function escapeHtml(unsafe) {
|
||||
}
|
||||
|
||||
function getPriceKey(coin) {
|
||||
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getPriceKey(coin);
|
||||
}
|
||||
|
||||
if (!coin) return null;
|
||||
|
||||
const lowerCoin = coin.toLowerCase();
|
||||
|
||||
if (lowerCoin === 'zcoin') {
|
||||
return 'firo';
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coin);
|
||||
}
|
||||
|
||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
if (lowerCoin === 'part' || lowerCoin === 'particl' ||
|
||||
lowerCoin.includes('particl')) {
|
||||
return 'particl';
|
||||
}
|
||||
|
||||
return lowerCoin;
|
||||
return coin ? coin.toLowerCase() : null;
|
||||
}
|
||||
|
||||
function getCoinSymbol(fullName) {
|
||||
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getSymbol(fullName) || fullName;
|
||||
}
|
||||
|
||||
return window.config.coinMappings.nameToSymbol[fullName] || fullName;
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.getCoinSymbol(fullName);
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
|
||||
function initializeTableEvents() {
|
||||
@@ -2143,7 +2161,6 @@ function initializeTableEvents() {
|
||||
const statusSelect = document.getElementById('status');
|
||||
const sentFromSelect = document.getElementById('sent_from');
|
||||
|
||||
|
||||
if (coinToButton && coinToDropdown) {
|
||||
CleanupManager.addListener(coinToButton, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -2158,7 +2175,6 @@ function initializeTableEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (coinFromButton && coinFromDropdown) {
|
||||
CleanupManager.addListener(coinFromButton, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -2223,15 +2239,16 @@ function initializeTableEvents() {
|
||||
refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||||
refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
if (window.countdownInterval) clearInterval(window.countdownInterval);
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
window.countdownInterval = CleanupManager.setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
|
||||
|
||||
if (remainingTime <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
clearInterval(window.countdownInterval);
|
||||
window.countdownInterval = null;
|
||||
refreshText.textContent = 'Refresh';
|
||||
|
||||
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||
@@ -2243,7 +2260,6 @@ function initializeTableEvents() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Manual refresh initiated');
|
||||
lastRefreshTime = now;
|
||||
const refreshIcon = document.getElementById('refreshIcon');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
@@ -2270,10 +2286,10 @@ function initializeTableEvents() {
|
||||
if (!priceData && previousPrices) {
|
||||
console.log('Using previous price data after failed refresh');
|
||||
latestPrices = previousPrices;
|
||||
applyFilters();
|
||||
applyFilters({ incremental: false });
|
||||
} else if (priceData) {
|
||||
latestPrices = priceData;
|
||||
applyFilters();
|
||||
applyFilters({ incremental: false });
|
||||
} else {
|
||||
throw new Error('Unable to fetch price data');
|
||||
}
|
||||
@@ -2281,8 +2297,6 @@ function initializeTableEvents() {
|
||||
lastRefreshTime = now;
|
||||
updateLastRefreshTime();
|
||||
|
||||
console.log('Manual refresh completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during manual refresh:', error);
|
||||
NetworkManager.handleNetworkError(error);
|
||||
@@ -2323,7 +2337,7 @@ function initializeTableEvents() {
|
||||
await updateOffersTable({ fromPaginationClick: true });
|
||||
updatePaginationInfo();
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
isPaginationInProgress = false;
|
||||
}, 100);
|
||||
}
|
||||
@@ -2343,7 +2357,7 @@ function initializeTableEvents() {
|
||||
await updateOffersTable({ fromPaginationClick: true });
|
||||
updatePaginationInfo();
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
isPaginationInProgress = false;
|
||||
}, 100);
|
||||
}
|
||||
@@ -2419,18 +2433,44 @@ function handleTableSort(columnIndex, header) {
|
||||
clearTimeout(window.sortTimeout);
|
||||
}
|
||||
|
||||
window.sortTimeout = setTimeout(() => {
|
||||
window.sortTimeout = CleanupManager.setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
const REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
|
||||
autoRefreshInterval = CleanupManager.setInterval(async () => {
|
||||
try {
|
||||
|
||||
const response = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||
if (response.ok) {
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Auto-refresh] Error during background refresh:', error);
|
||||
}
|
||||
}, REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
autoRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeTableAndData() {
|
||||
loadSavedSettings();
|
||||
updateClearFiltersButton();
|
||||
initializeTableEvents();
|
||||
initializeTooltips();
|
||||
|
||||
|
||||
updateFilterButtonText('coin_to');
|
||||
updateFilterButtonText('coin_from');
|
||||
updateCoinBadges('coin_to');
|
||||
@@ -2530,24 +2570,44 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
if (window.WebSocketManager) {
|
||||
WebSocketManager.addMessageHandler('message', async (data) => {
|
||||
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
|
||||
//console.log('WebSocket event received:', data.event);
|
||||
try {
|
||||
|
||||
const previousPrices = latestPrices;
|
||||
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
|
||||
const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||
if (!offersResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${offersResponse.status}`);
|
||||
}
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||
const newData = await offersResponse.json();
|
||||
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
|
||||
jsonData = formatInitialData(processedNewData);
|
||||
const newFormattedData = formatInitialData(processedNewData);
|
||||
|
||||
const oldOfferIds = originalJsonData.map(o => o.offer_id).sort().join(',');
|
||||
const newOfferIds = newFormattedData.map(o => o.offer_id).sort().join(',');
|
||||
const dataChanged = oldOfferIds !== newOfferIds;
|
||||
|
||||
if (!dataChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
jsonData = newFormattedData;
|
||||
originalJsonData = [...jsonData];
|
||||
|
||||
const previousPrices = latestPrices;
|
||||
let priceData;
|
||||
if (window.PriceManager) {
|
||||
priceData = await window.PriceManager.getPrices(true);
|
||||
priceData = await window.PriceManager.getPrices(false);
|
||||
} else {
|
||||
priceData = await fetchLatestPrices();
|
||||
}
|
||||
@@ -2556,12 +2616,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
latestPrices = priceData;
|
||||
CacheManager.set('prices_coingecko', priceData, 'prices');
|
||||
} else if (previousPrices) {
|
||||
console.log('Using previous price data after failed refresh');
|
||||
latestPrices = previousPrices;
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
|
||||
applyFilters({ incremental: true, skipSkeleton: true });
|
||||
updateProfitLossDisplays();
|
||||
|
||||
document.querySelectorAll('.usd-value').forEach(usdValue => {
|
||||
@@ -2572,8 +2630,14 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
if (price !== undefined && price !== null) {
|
||||
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
|
||||
if (!isNaN(amount) && amount > 0) {
|
||||
const usdValue = amount * price;
|
||||
usdValue.textContent = tableRateModule.formatUSD(usdValue);
|
||||
const calculatedUSD = amount * price;
|
||||
const formattedUSD = calculatedUSD < 0.01
|
||||
? calculatedUSD.toFixed(8) + ' USD'
|
||||
: calculatedUSD.toFixed(2) + ' USD';
|
||||
|
||||
if (usdValue.textContent !== formattedUSD) {
|
||||
usdValue.textContent = formattedUSD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2581,7 +2645,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
|
||||
updatePaginationInfo();
|
||||
|
||||
//console.log('WebSocket-triggered refresh completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during WebSocket-triggered refresh:', error);
|
||||
NetworkManager.handleNetworkError(error);
|
||||
@@ -2616,9 +2679,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
});
|
||||
}
|
||||
|
||||
if (window.config.autoRefreshEnabled) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
startAutoRefresh();
|
||||
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
if (filterForm) {
|
||||
@@ -2652,20 +2713,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
}
|
||||
});
|
||||
|
||||
const rowTimeInterval = setInterval(updateRowTimes, 30000);
|
||||
if (CleanupManager.registerResource) {
|
||||
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
|
||||
clearInterval(rowTimeInterval);
|
||||
});
|
||||
} else if (CleanupManager.addResource) {
|
||||
CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => {
|
||||
clearInterval(rowTimeInterval);
|
||||
});
|
||||
} else {
|
||||
|
||||
window._cleanupIntervals = window._cleanupIntervals || [];
|
||||
window._cleanupIntervals.push(rowTimeInterval);
|
||||
}
|
||||
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
|
||||
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
|
||||
clearInterval(rowTimeInterval);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during initialization:', error);
|
||||
@@ -2697,6 +2748,8 @@ function cleanup() {
|
||||
window.countdownInterval = null;
|
||||
}
|
||||
|
||||
stopAutoRefresh();
|
||||
|
||||
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
|
||||
window._cleanupIntervals.forEach(interval => {
|
||||
clearInterval(interval);
|
||||
@@ -2742,7 +2795,6 @@ function cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('Offers.js cleanup completed');
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error);
|
||||
}
|
||||
@@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig;
|
||||
const coins = window.config.coins;
|
||||
const apiKeys = window.config.getAPIKeys();
|
||||
|
||||
const utils = {
|
||||
formatNumber: (number, decimals = 2) => {
|
||||
if (typeof number !== 'number' || isNaN(number)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
},
|
||||
formatDate: (timestamp, resolution) => {
|
||||
const date = new Date(timestamp);
|
||||
const options = {
|
||||
day: { hour: '2-digit', minute: '2-digit', hour12: true },
|
||||
week: { month: 'short', day: 'numeric' },
|
||||
month: { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
};
|
||||
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
|
||||
},
|
||||
debounce: (func, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, type = 'AppError') {
|
||||
super(message);
|
||||
this.name = type;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = {
|
||||
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
|
||||
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
|
||||
@@ -64,7 +24,6 @@ const api = {
|
||||
}
|
||||
|
||||
const volumeData = await Api.fetchVolumeData({
|
||||
cryptoCompare: apiKeys.cryptoCompare,
|
||||
coinGecko: apiKeys.coinGecko
|
||||
});
|
||||
|
||||
@@ -94,29 +53,6 @@ const api = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchCryptoCompareDataXHR: (coin) => {
|
||||
try {
|
||||
if (!NetworkManager.isOnline()) {
|
||||
throw new Error('Network is offline');
|
||||
}
|
||||
|
||||
return Api.fetchCryptoCompareData(coin, {
|
||||
cryptoCompare: apiKeys.cryptoCompare
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`CryptoCompare request failed for ${coin}:`, error);
|
||||
|
||||
NetworkManager.handleNetworkError(error);
|
||||
|
||||
const cachedData = CacheManager.get(`coinData_${coin}`);
|
||||
if (cachedData) {
|
||||
logger.info(`Using cached data for ${coin}`);
|
||||
return cachedData.value;
|
||||
}
|
||||
return { error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
fetchCoinGeckoDataXHR: async () => {
|
||||
try {
|
||||
const priceData = await window.PriceManager.getPrices();
|
||||
@@ -172,10 +108,7 @@ const api = {
|
||||
|
||||
const historicalData = await Api.fetchHistoricalData(
|
||||
coinSymbols,
|
||||
window.config.currentResolution,
|
||||
{
|
||||
cryptoCompare: window.config.getAPIKeys().cryptoCompare
|
||||
}
|
||||
window.config.currentResolution
|
||||
);
|
||||
|
||||
Object.keys(historicalData).forEach(coin => {
|
||||
@@ -209,8 +142,7 @@ const api = {
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: window.config.rateLimits.coingecko.minInterval,
|
||||
cryptocompare: window.config.rateLimits.cryptocompare.minInterval
|
||||
coingecko: window.config.rateLimits.coingecko.minInterval
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: window.config.retryDelays,
|
||||
@@ -242,7 +174,7 @@ const rateLimiter = {
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -252,7 +184,7 @@ const rateLimiter = {
|
||||
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -260,7 +192,7 @@ const rateLimiter = {
|
||||
retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
@@ -303,7 +235,7 @@ const ui = {
|
||||
if (isError || volume24h === null || volume24h === undefined) {
|
||||
volumeElement.textContent = 'N/A';
|
||||
} else {
|
||||
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
|
||||
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
|
||||
}
|
||||
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||
}
|
||||
@@ -345,7 +277,7 @@ const ui = {
|
||||
}
|
||||
|
||||
priceChange1d = data.price_change_percentage_24h || 0;
|
||||
volume24h = data.total_volume || 0;
|
||||
volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null;
|
||||
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||
}
|
||||
@@ -498,7 +430,7 @@ const ui = {
|
||||
chartContainer.classList.add('blurred');
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
ui.hideErrorMessage();
|
||||
}, duration);
|
||||
}
|
||||
@@ -929,8 +861,11 @@ destroyChart: function() {
|
||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||
data = allData[coinSymbol];
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw new Error(`No data returned for ${coinSymbol}`);
|
||||
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
|
||||
console.warn(`No price data available for ${coinSymbol}`);
|
||||
chartModule.hideChartLoader();
|
||||
chartModule.showNoDataMessage(coinSymbol);
|
||||
return;
|
||||
}
|
||||
|
||||
CacheManager.set(cacheKey, data, 'chart');
|
||||
@@ -960,6 +895,8 @@ destroyChart: function() {
|
||||
chartModule.initChart();
|
||||
}
|
||||
|
||||
chartModule.hideNoDataMessage();
|
||||
|
||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||
if (chartData.length > 0 && chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = chartData;
|
||||
@@ -1014,6 +951,41 @@ destroyChart: function() {
|
||||
chart.classList.remove('hidden');
|
||||
},
|
||||
|
||||
showNoDataMessage: function(coinSymbol) {
|
||||
const chartCanvas = document.getElementById('coin-chart');
|
||||
if (!chartCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.data.datasets[0].data = [];
|
||||
this.chart.update('none');
|
||||
}
|
||||
|
||||
let messageDiv = document.getElementById('chart-no-data-message');
|
||||
if (!messageDiv) {
|
||||
messageDiv = document.createElement('div');
|
||||
messageDiv.id = 'chart-no-data-message';
|
||||
messageDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #888; font-size: 14px; z-index: 10;';
|
||||
chartCanvas.parentElement.style.position = 'relative';
|
||||
chartCanvas.parentElement.appendChild(messageDiv);
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div style="padding: 20px; background: rgba(0,0,0,0.05); border-radius: 8px;">
|
||||
<div style="font-size: 16px; margin-bottom: 8px;">No Price Data Available</div>
|
||||
</div>
|
||||
`;
|
||||
messageDiv.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hideNoDataMessage: function() {
|
||||
const messageDiv = document.getElementById('chart-no-data-message');
|
||||
if (messageDiv) {
|
||||
messageDiv.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
if (this.pendingAnimationFrame) {
|
||||
cancelAnimationFrame(this.pendingAnimationFrame);
|
||||
@@ -1136,15 +1108,32 @@ const app = {
|
||||
|
||||
if (chartModule.chart) {
|
||||
window.config.currentResolution = 'day';
|
||||
await chartModule.updateChart('BTC');
|
||||
app.updateResolutionButtons('BTC');
|
||||
|
||||
let defaultCoin = null;
|
||||
if (window.config.coins && window.config.coins.length > 0) {
|
||||
for (const coin of window.config.coins) {
|
||||
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
||||
if (container) {
|
||||
defaultCoin = coin.symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultCoin) {
|
||||
defaultCoin = 'BTC';
|
||||
}
|
||||
|
||||
await chartModule.updateChart(defaultCoin);
|
||||
app.updateResolutionButtons(defaultCoin);
|
||||
|
||||
const chartTitle = document.getElementById('chart-title');
|
||||
if (chartTitle) {
|
||||
chartTitle.textContent = 'Price Chart (BTC)';
|
||||
chartTitle.textContent = `Price Chart (${defaultCoin})`;
|
||||
}
|
||||
|
||||
ui.setActiveContainer(`${defaultCoin.toLowerCase()}-container`);
|
||||
}
|
||||
ui.setActiveContainer('btc-container');
|
||||
|
||||
app.setupEventListeners();
|
||||
app.initAutoRefresh();
|
||||
@@ -1182,11 +1171,11 @@ const app = {
|
||||
if (coinData) {
|
||||
coinData.displayName = coin.displayName || coin.symbol;
|
||||
|
||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
||||
if (volumeData[backendId]) {
|
||||
coinData.total_volume = volumeData[backendId].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
||||
const volumeKey = coin.symbol.toLowerCase();
|
||||
if (volumeData[volumeKey]) {
|
||||
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1214,11 +1203,7 @@ const app = {
|
||||
} else {
|
||||
try {
|
||||
ui.showCoinLoader(coin.symbol);
|
||||
if (coin.usesCoinGecko) {
|
||||
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
||||
} else {
|
||||
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
|
||||
}
|
||||
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
@@ -1365,7 +1350,7 @@ const app = {
|
||||
}
|
||||
const timeUntilRefresh = nextRefreshTime - now;
|
||||
app.nextRefreshTime = nextRefreshTime;
|
||||
app.autoRefreshInterval = setTimeout(() => {
|
||||
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
|
||||
if (NetworkManager.isOnline()) {
|
||||
app.refreshAllData();
|
||||
} else {
|
||||
@@ -1377,7 +1362,6 @@ const app = {
|
||||
},
|
||||
|
||||
refreshAllData: async function() {
|
||||
//console.log('Price refresh started at', new Date().toLocaleTimeString());
|
||||
|
||||
if (app.isRefreshing) {
|
||||
console.log('Refresh already in progress, skipping...');
|
||||
@@ -1398,7 +1382,7 @@ refreshAllData: async function() {
|
||||
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
|
||||
|
||||
let remainingTime = seconds;
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
remainingTime--;
|
||||
if (remainingTime > 0) {
|
||||
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
|
||||
@@ -1411,7 +1395,6 @@ refreshAllData: async function() {
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('Starting refresh of all data...');
|
||||
app.isRefreshing = true;
|
||||
app.updateNextRefreshTime();
|
||||
ui.showLoader();
|
||||
@@ -1426,7 +1409,7 @@ refreshAllData: async function() {
|
||||
console.warn('BTC price update failed, continuing with cached or default value');
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
|
||||
|
||||
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
||||
if (allCoinData.error) {
|
||||
@@ -1451,11 +1434,11 @@ refreshAllData: async function() {
|
||||
|
||||
coinData.displayName = coin.displayName || coin.symbol;
|
||||
|
||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
||||
if (volumeData[backendId]) {
|
||||
coinData.total_volume = volumeData[backendId].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
||||
const volumeKey = coin.symbol.toLowerCase();
|
||||
if (volumeData[volumeKey]) {
|
||||
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -1478,15 +1461,13 @@ refreshAllData: async function() {
|
||||
const cacheKey = `coinData_${coin.symbol}`;
|
||||
CacheManager.set(cacheKey, coinData, 'prices');
|
||||
|
||||
//console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
|
||||
|
||||
} catch (coinError) {
|
||||
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
|
||||
failedCoins.push(coin.symbol);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
|
||||
|
||||
if (chartModule.currentCoin) {
|
||||
try {
|
||||
@@ -1508,7 +1489,7 @@ refreshAllData: async function() {
|
||||
let countdown = 5;
|
||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown > 0) {
|
||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||
@@ -1518,7 +1499,6 @@ refreshAllData: async function() {
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
//console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Critical error during refresh:', error);
|
||||
@@ -1527,7 +1507,7 @@ refreshAllData: async function() {
|
||||
let countdown = 10;
|
||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
const countdownInterval = CleanupManager.setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown > 0) {
|
||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||
@@ -1549,7 +1529,6 @@ refreshAllData: async function() {
|
||||
app.scheduleNextRefresh();
|
||||
}
|
||||
|
||||
//console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1573,7 +1552,7 @@ refreshAllData: async function() {
|
||||
const svg = document.querySelector('#toggle-auto-refresh svg');
|
||||
if (svg) {
|
||||
svg.classList.add('animate-spin');
|
||||
setTimeout(() => {
|
||||
CleanupManager.setTimeout(() => {
|
||||
svg.classList.remove('animate-spin');
|
||||
}, 2000);
|
||||
}
|
||||
332
basicswap/static/js/pages/settings-page.js
Normal file
@@ -0,0 +1,332 @@
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const SettingsPage = {
|
||||
confirmCallback: null,
|
||||
triggerElement: null,
|
||||
|
||||
init: function() {
|
||||
this.setupTabs();
|
||||
this.setupCoinHeaders();
|
||||
this.setupConfirmModal();
|
||||
this.setupNotificationSettings();
|
||||
},
|
||||
|
||||
setupTabs: function() {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
const switchTab = (targetTab) => {
|
||||
tabButtons.forEach(btn => {
|
||||
if (btn.dataset.tab === targetTab) {
|
||||
btn.className = 'tab-button border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
|
||||
} else {
|
||||
btn.className = 'tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
|
||||
}
|
||||
});
|
||||
|
||||
tabContents.forEach(content => {
|
||||
if (content.id === targetTab) {
|
||||
content.classList.remove('hidden');
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
switchTab(btn.dataset.tab);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupCoinHeaders: function() {
|
||||
const coinHeaders = document.querySelectorAll('.coin-header');
|
||||
coinHeaders.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const coinName = this.dataset.coin;
|
||||
const details = document.getElementById(`details-${coinName}`);
|
||||
const arrow = this.querySelector('.toggle-arrow');
|
||||
|
||||
if (details.classList.contains('hidden')) {
|
||||
details.classList.remove('hidden');
|
||||
arrow.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
details.classList.add('hidden');
|
||||
arrow.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupConfirmModal: function() {
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
if (confirmYesBtn) {
|
||||
confirmYesBtn.addEventListener('click', () => {
|
||||
if (typeof this.confirmCallback === 'function') {
|
||||
this.confirmCallback();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
if (confirmNoBtn) {
|
||||
confirmNoBtn.addEventListener('click', () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmDialog: function(title, message, callback) {
|
||||
this.confirmCallback = callback;
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hideConfirmDialog: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
this.confirmCallback = null;
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmDisableCoin: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Disable Coin",
|
||||
"Are you sure you want to disable this coin?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setupNotificationSettings: function() {
|
||||
const notificationsTab = document.getElementById('notifications-tab');
|
||||
if (notificationsTab) {
|
||||
notificationsTab.addEventListener('click', () => {
|
||||
CleanupManager.setTimeout(() => this.syncNotificationSettings(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.closest('#notifications')) {
|
||||
this.syncNotificationSettings();
|
||||
}
|
||||
});
|
||||
|
||||
this.syncNotificationSettings();
|
||||
},
|
||||
|
||||
syncNotificationSettings: function() {
|
||||
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
|
||||
const backendSettings = {
|
||||
showNewOffers: document.getElementById('notifications_new_offers')?.checked || false,
|
||||
showNewBids: document.getElementById('notifications_new_bids')?.checked || false,
|
||||
showBidAccepted: document.getElementById('notifications_bid_accepted')?.checked || false,
|
||||
showBalanceChanges: document.getElementById('notifications_balance_changes')?.checked || false,
|
||||
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions')?.checked || false,
|
||||
showSwapCompleted: document.getElementById('notifications_swap_completed')?.checked || false,
|
||||
showUpdateNotifications: document.getElementById('check_updates')?.checked || false,
|
||||
notificationDuration: parseInt(document.getElementById('notifications_duration')?.value || '5') * 1000
|
||||
};
|
||||
|
||||
window.NotificationManager.updateSettings(backendSettings);
|
||||
}
|
||||
},
|
||||
|
||||
testUpdateNotification: function() {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Available: v0.15.0',
|
||||
'update_available',
|
||||
{
|
||||
subtitle: 'Current: v0.14.6 • Click to view release (Test/Dummy)',
|
||||
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
|
||||
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
testLiveUpdateCheck: function(event) {
|
||||
const button = event?.target || event?.currentTarget || document.querySelector('[onclick*="testLiveUpdateCheck"]');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Checking...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/json/checkupdates', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (window.NotificationManager) {
|
||||
const currentVer = data.current_version || 'Unknown';
|
||||
const latestVer = data.latest_version || currentVer;
|
||||
|
||||
if (data.update_available) {
|
||||
window.NotificationManager.createToast(
|
||||
`Live Update Available: v${latestVer}`,
|
||||
'update_available',
|
||||
{
|
||||
latest_version: latestVer,
|
||||
current_version: currentVer,
|
||||
subtitle: `Current: v${currentVer} • Click to view release`,
|
||||
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
|
||||
releaseNotes: 'This is a real update check from GitHub API.'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
window.NotificationManager.createToast(
|
||||
'No Updates Available',
|
||||
'success',
|
||||
{
|
||||
subtitle: `Current version v${currentVer} is up to date`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Update check failed:', error);
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: 'Could not check for updates. See console for details.'
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (button) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
checkForUpdatesNow: function(event) {
|
||||
const button = event?.target || event?.currentTarget || document.querySelector('[data-check-updates]');
|
||||
if (!button) return;
|
||||
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Checking...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/json/checkupdates', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: data.error
|
||||
}
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.NotificationManager) {
|
||||
const currentVer = data.current_version || 'Unknown';
|
||||
const latestVer = data.latest_version || currentVer;
|
||||
|
||||
if (data.update_available) {
|
||||
window.NotificationManager.createToast(
|
||||
`Update Available: v${latestVer}`,
|
||||
'update_available',
|
||||
{
|
||||
latest_version: latestVer,
|
||||
current_version: currentVer,
|
||||
subtitle: `Current: v${currentVer} • Click to view release`,
|
||||
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
|
||||
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
|
||||
}
|
||||
);
|
||||
} else {
|
||||
window.NotificationManager.createToast(
|
||||
'You\'re Up to Date!',
|
||||
'success',
|
||||
{
|
||||
subtitle: `Current version v${currentVer} is the latest`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Update check failed:', error);
|
||||
if (window.NotificationManager) {
|
||||
window.NotificationManager.createToast(
|
||||
'Update Check Failed',
|
||||
'error',
|
||||
{
|
||||
subtitle: 'Network error. Please try again later.'
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (button) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
SettingsPage.cleanup = function() {
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
SettingsPage.init();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('settingsPage', SettingsPage, (page) => {
|
||||
if (page.cleanup) page.cleanup();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.SettingsPage = SettingsPage;
|
||||
window.syncNotificationSettings = SettingsPage.syncNotificationSettings.bind(SettingsPage);
|
||||
window.testUpdateNotification = SettingsPage.testUpdateNotification.bind(SettingsPage);
|
||||
window.testLiveUpdateCheck = SettingsPage.testLiveUpdateCheck.bind(SettingsPage);
|
||||
window.checkForUpdatesNow = SettingsPage.checkForUpdatesNow.bind(SettingsPage);
|
||||
window.showConfirmDialog = SettingsPage.showConfirmDialog.bind(SettingsPage);
|
||||
window.hideConfirmDialog = SettingsPage.hideConfirmDialog.bind(SettingsPage);
|
||||
window.confirmDisableCoin = SettingsPage.confirmDisableCoin.bind(SettingsPage);
|
||||
|
||||
})();
|
||||
@@ -127,9 +127,9 @@ const getTimeStrokeColor = (expireTime) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeLeft = expireTime - now;
|
||||
|
||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
||||
return '#10B981'; // More than 30 minutes
|
||||
if (timeLeft <= 300) return '#9CA3AF';
|
||||
if (timeLeft <= 1800) return '#3B82F6';
|
||||
return '#10B981';
|
||||
};
|
||||
|
||||
const updateConnectionStatus = (status) => {
|
||||
@@ -520,8 +520,6 @@ const createSwapTableRow = async (swap) => {
|
||||
async function updateSwapsTable(options = {}) {
|
||||
const { resetPage = false, refreshData = true } = options;
|
||||
|
||||
//console.log('Updating swaps table:', { resetPage, refreshData });
|
||||
|
||||
if (state.refreshPromise) {
|
||||
await state.refreshPromise;
|
||||
return;
|
||||
@@ -547,19 +545,17 @@ async function updateSwapsTable(options = {}) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
//console.log('Received swap data:', data);
|
||||
|
||||
state.swapsData = Array.isArray(data)
|
||||
? data.filter(swap => {
|
||||
const isActive = isActiveSwap(swap);
|
||||
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
|
||||
|
||||
return isActive;
|
||||
})
|
||||
: [];
|
||||
|
||||
//console.log('Filtered active swaps:', state.swapsData);
|
||||
} catch (error) {
|
||||
//console.error('Error fetching swap data:', error);
|
||||
|
||||
state.swapsData = [];
|
||||
} finally {
|
||||
state.refreshPromise = null;
|
||||
@@ -585,8 +581,6 @@ async function updateSwapsTable(options = {}) {
|
||||
const endIndex = startIndex + PAGE_SIZE;
|
||||
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
|
||||
|
||||
//console.log('Current page swaps:', currentPageSwaps);
|
||||
|
||||
if (elements.swapsBody) {
|
||||
if (currentPageSwaps.length > 0) {
|
||||
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
|
||||
@@ -607,7 +601,7 @@ async function updateSwapsTable(options = {}) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//console.log('No active swaps found, displaying empty state');
|
||||
|
||||
elements.swapsBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
||||
@@ -679,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
WebSocketManager.initialize();
|
||||
setupEventListeners();
|
||||
await updateSwapsTable({ resetPage: true, refreshData: true });
|
||||
const autoRefreshInterval = setInterval(async () => {
|
||||
|
||||
const autoRefreshInterval = CleanupManager.setInterval(async () => {
|
||||
await updateSwapsTable({ resetPage: false, refreshData: true });
|
||||
}, 10000); // 30 seconds
|
||||
}, 10000);
|
||||
|
||||
CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => {
|
||||
clearInterval(autoRefreshInterval);
|
||||
});
|
||||
});
|
||||
372
basicswap/static/js/pages/wallet-page.js
Normal file
@@ -0,0 +1,372 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletPage = {
|
||||
confirmCallback: null,
|
||||
triggerElement: null,
|
||||
currentCoinId: '',
|
||||
activeTooltip: null,
|
||||
|
||||
init: function() {
|
||||
this.setupAddressCopy();
|
||||
this.setupConfirmModal();
|
||||
this.setupWithdrawalConfirmation();
|
||||
this.setupTransactionDisplay();
|
||||
this.setupWebSocketUpdates();
|
||||
},
|
||||
|
||||
setupAddressCopy: function() {
|
||||
const copyableElements = [
|
||||
'main_deposit_address',
|
||||
'monero_main_address',
|
||||
'monero_sub_address',
|
||||
'stealth_address'
|
||||
];
|
||||
|
||||
copyableElements.forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) return;
|
||||
|
||||
element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors');
|
||||
|
||||
if (!element.querySelector('.copy-icon')) {
|
||||
const copyIcon = document.createElement('span');
|
||||
copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300';
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>`;
|
||||
|
||||
element.style.position = 'relative';
|
||||
element.style.paddingRight = '2.5rem';
|
||||
element.appendChild(copyIcon);
|
||||
}
|
||||
|
||||
element.addEventListener('click', (e) => {
|
||||
const textToCopy = element.innerText.trim();
|
||||
|
||||
this.copyToClipboard(textToCopy);
|
||||
|
||||
element.classList.add('bg-blue-50', 'dark:bg-blue-900');
|
||||
|
||||
this.showCopyFeedback(element);
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
element.classList.remove('bg-blue-50', 'dark:bg-blue-900');
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: function(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('Address copied to clipboard');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy address:', err);
|
||||
this.fallbackCopyToClipboard(text);
|
||||
});
|
||||
} else {
|
||||
this.fallbackCopyToClipboard(text);
|
||||
}
|
||||
},
|
||||
|
||||
fallbackCopyToClipboard: function(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
console.log('Address copied to clipboard (fallback)');
|
||||
} catch (err) {
|
||||
console.error('Fallback: Failed to copy address', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
},
|
||||
|
||||
showCopyFeedback: function(element) {
|
||||
if (this.activeTooltip && this.activeTooltip.parentNode) {
|
||||
this.activeTooltip.parentNode.removeChild(this.activeTooltip);
|
||||
}
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
|
||||
popup.innerText = 'Copied!';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
this.activeTooltip = popup;
|
||||
|
||||
this.updateTooltipPosition(popup, element);
|
||||
|
||||
const scrollHandler = () => {
|
||||
if (popup.parentNode) {
|
||||
requestAnimationFrame(() => {
|
||||
this.updateTooltipPosition(popup, element);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', scrollHandler, { passive: true });
|
||||
|
||||
popup.style.opacity = '0';
|
||||
popup.style.transition = 'opacity 0.2s ease-in-out';
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
popup.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
window.removeEventListener('scroll', scrollHandler);
|
||||
popup.style.opacity = '0';
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
if (this.activeTooltip === popup) {
|
||||
this.activeTooltip = null;
|
||||
}
|
||||
}, 200);
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
updateTooltipPosition: function(tooltip, element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
let top = rect.top - tooltip.offsetHeight - 8;
|
||||
const left = rect.left + rect.width / 2;
|
||||
|
||||
if (top < 10) {
|
||||
top = rect.bottom + 8;
|
||||
}
|
||||
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.style.left = `${left}px`;
|
||||
tooltip.style.transform = 'translateX(-50%)';
|
||||
},
|
||||
|
||||
setupWithdrawalConfirmation: function() {
|
||||
|
||||
const withdrawalClickHandler = (e) => {
|
||||
const target = e.target.closest('[data-confirm-withdrawal]');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
|
||||
this.triggerElement = target;
|
||||
|
||||
this.confirmWithdrawal().catch(() => {
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', withdrawalClickHandler);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
CleanupManager.registerResource('walletWithdrawalClick', withdrawalClickHandler, () => {
|
||||
document.removeEventListener('click', withdrawalClickHandler);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setupConfirmModal: function() {
|
||||
const confirmYesBtn = document.getElementById('confirmYes');
|
||||
if (confirmYesBtn) {
|
||||
confirmYesBtn.addEventListener('click', () => {
|
||||
if (this.confirmCallback && typeof this.confirmCallback === 'function') {
|
||||
this.confirmCallback();
|
||||
}
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmNoBtn = document.getElementById('confirmNo');
|
||||
if (confirmNoBtn) {
|
||||
confirmNoBtn.addEventListener('click', () => {
|
||||
this.hideConfirmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
if (confirmModal) {
|
||||
confirmModal.addEventListener('click', (e) => {
|
||||
if (e.target === confirmModal) {
|
||||
this.hideConfirmDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showConfirmDialog: function(title, message, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.confirmCallback = () => {
|
||||
if (callback) callback();
|
||||
resolve();
|
||||
};
|
||||
this.confirmReject = reject;
|
||||
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hideConfirmDialog: function() {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
if (this.confirmReject) {
|
||||
this.confirmReject();
|
||||
}
|
||||
this.confirmCallback = null;
|
||||
this.confirmReject = null;
|
||||
return false;
|
||||
},
|
||||
|
||||
confirmReseed: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Reseed Wallet",
|
||||
"Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmWithdrawal: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Withdrawal",
|
||||
"Are you sure you want to proceed with this withdrawal?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmCreateUTXO: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm Create UTXO",
|
||||
"Are you sure you want to create this UTXO?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
confirmUTXOResize: function() {
|
||||
this.triggerElement = document.activeElement;
|
||||
return this.showConfirmDialog(
|
||||
"Confirm UTXO Resize",
|
||||
"Are you sure you want to resize UTXOs?",
|
||||
() => {
|
||||
if (this.triggerElement) {
|
||||
const form = this.triggerElement.form;
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = this.triggerElement.name;
|
||||
hiddenInput.value = this.triggerElement.value;
|
||||
form.appendChild(hiddenInput);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setupTransactionDisplay: function() {
|
||||
|
||||
},
|
||||
|
||||
setupWebSocketUpdates: function() {
|
||||
if (window.BalanceUpdatesManager) {
|
||||
const coinId = this.getCoinIdFromPage();
|
||||
if (coinId) {
|
||||
this.currentCoinId = coinId;
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallet_' + coinId,
|
||||
balanceUpdateCallback: this.handleBalanceUpdate.bind(this),
|
||||
swapEventCallback: this.handleSwapEvent.bind(this),
|
||||
errorContext: 'Wallet',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 60000
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCoinIdFromPage: function() {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const walletIndex = pathParts.indexOf('wallet');
|
||||
if (walletIndex !== -1 && pathParts[walletIndex + 1]) {
|
||||
return pathParts[walletIndex + 1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
handleBalanceUpdate: function(balanceData) {
|
||||
|
||||
console.log('Balance updated:', balanceData);
|
||||
},
|
||||
|
||||
handleSwapEvent: function(eventData) {
|
||||
|
||||
console.log('Swap event:', eventData);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletPage.init();
|
||||
|
||||
if (window.BalanceUpdatesManager) {
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
}
|
||||
});
|
||||
|
||||
window.WalletPage = WalletPage;
|
||||
window.setupAddressCopy = WalletPage.setupAddressCopy.bind(WalletPage);
|
||||
window.showConfirmDialog = WalletPage.showConfirmDialog.bind(WalletPage);
|
||||
window.hideConfirmDialog = WalletPage.hideConfirmDialog.bind(WalletPage);
|
||||
window.confirmReseed = WalletPage.confirmReseed.bind(WalletPage);
|
||||
window.confirmWithdrawal = WalletPage.confirmWithdrawal.bind(WalletPage);
|
||||
window.confirmCreateUTXO = WalletPage.confirmCreateUTXO.bind(WalletPage);
|
||||
window.confirmUTXOResize = WalletPage.confirmUTXOResize.bind(WalletPage);
|
||||
window.copyToClipboard = WalletPage.copyToClipboard.bind(WalletPage);
|
||||
window.showCopyFeedback = WalletPage.showCopyFeedback.bind(WalletPage);
|
||||
|
||||
})();
|
||||
344
basicswap/static/js/pages/wallets-page.js
Normal file
@@ -0,0 +1,344 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const WalletsPage = {
|
||||
|
||||
init: function() {
|
||||
this.setupWebSocketUpdates();
|
||||
},
|
||||
|
||||
setupWebSocketUpdates: function() {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
window.WebSocketManager.initialize();
|
||||
}
|
||||
|
||||
if (window.BalanceUpdatesManager) {
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallets',
|
||||
balanceUpdateCallback: this.updateWalletBalances.bind(this),
|
||||
swapEventCallback: this.updateWalletBalances.bind(this),
|
||||
errorContext: 'Wallets',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 60000
|
||||
});
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
|
||||
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'price_updated' || data.event === 'prices_updated') {
|
||||
clearTimeout(window.walletsPriceUpdateTimeout);
|
||||
window.walletsPriceUpdateTimeout = CleanupManager.setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.walletsPriceHandlerId = priceHandlerId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateWalletBalances: function(balanceData) {
|
||||
if (balanceData) {
|
||||
balanceData.forEach(coin => {
|
||||
this.updateWalletDisplay(coin);
|
||||
});
|
||||
|
||||
CleanupManager.setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 250);
|
||||
} else {
|
||||
window.BalanceUpdatesManager.fetchBalanceData()
|
||||
.then(data => this.updateWalletBalances(data))
|
||||
.catch(error => {
|
||||
console.error('Error updating wallet balances:', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateWalletDisplay: function(coinData) {
|
||||
if (coinData.name === 'Particl') {
|
||||
this.updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
} else if (coinData.name === 'Particl Anon') {
|
||||
this.updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
this.removePendingBalance('Particl', 'Anon Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
|
||||
}
|
||||
} else if (coinData.name === 'Particl Blind') {
|
||||
this.updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
this.removePendingBalance('Particl', 'Blind Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
|
||||
}
|
||||
} else {
|
||||
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
|
||||
|
||||
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
this.updatePendingDisplay(coinData);
|
||||
} else {
|
||||
this.removePendingDisplay(coinData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel === labelText) {
|
||||
if (isPending) {
|
||||
const cleanBalance = balance.toString().replace(/^\+/, '');
|
||||
element.textContent = `+${cleanBalance} ${ticker}`;
|
||||
} else {
|
||||
element.textContent = `${balance} ${ticker}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updatePendingDisplay: function(coinData) {
|
||||
const walletContainer = this.findWalletContainer(coinData.name);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
|
||||
let staticPendingElement = null;
|
||||
let staticUsdElement = null;
|
||||
|
||||
existingPendingElements.forEach(element => {
|
||||
const labelElement = element.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
|
||||
staticPendingElement = element;
|
||||
} else if (labelText.includes('Pending USD value:')) {
|
||||
staticUsdElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (staticPendingElement && staticUsdElement) {
|
||||
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
||||
}
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = staticUsdElement.querySelector('.usd-value');
|
||||
if (usdDiv) {
|
||||
usdDiv.textContent = initialUSD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
|
||||
if (!pendingContainer) {
|
||||
const balanceContainer = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
|
||||
if (!balanceContainer) return;
|
||||
|
||||
pendingContainer = document.createElement('div');
|
||||
pendingContainer.className = 'pending-container';
|
||||
balanceContainer.parentNode.insertBefore(pendingContainer, balanceContainer.nextSibling);
|
||||
}
|
||||
|
||||
pendingContainer.innerHTML = '';
|
||||
|
||||
const pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'flex mb-2 justify-between items-center';
|
||||
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
|
||||
pendingDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending:</h4>
|
||||
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
|
||||
`;
|
||||
|
||||
pendingContainer.appendChild(pendingDiv);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = document.createElement('div');
|
||||
usdDiv.className = 'flex mb-2 justify-between items-center';
|
||||
usdDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending USD value:</h4>
|
||||
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
|
||||
`;
|
||||
|
||||
pendingContainer.appendChild(usdDiv);
|
||||
},
|
||||
|
||||
removePendingDisplay: function(coinName) {
|
||||
const walletContainer = this.findWalletContainer(coinName);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
if (pendingContainer) {
|
||||
pendingContainer.remove();
|
||||
}
|
||||
},
|
||||
|
||||
findWalletContainer: function(coinName) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
for (const element of balanceElements) {
|
||||
if (element.getAttribute('data-coinname') === coinName) {
|
||||
return element.closest('.bg-white, .dark\\:bg-gray-500');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
removePendingBalance: function(coinName, balanceType) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel.includes('Pending:') || currentLabel.includes('Unconfirmed:')) {
|
||||
const nextElement = parentDiv.nextElementSibling;
|
||||
if (nextElement && nextElement.querySelector('h4')?.textContent.includes('USD value:')) {
|
||||
nextElement.remove();
|
||||
}
|
||||
parentDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updatePendingBalance: function(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
let targetElement = null;
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
if (elementCoinName === coinName) {
|
||||
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
if (parentElement) {
|
||||
const labelElement = parentElement.querySelector('h4');
|
||||
if (labelElement && labelElement.textContent.includes(balanceType)) {
|
||||
targetElement = parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
let insertAfterElement = targetElement;
|
||||
let nextElement = targetElement.nextElementSibling;
|
||||
while (nextElement) {
|
||||
const labelElement = nextElement.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
|
||||
insertAfterElement = nextElement;
|
||||
break;
|
||||
}
|
||||
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
|
||||
let pendingElement = insertAfterElement.nextElementSibling;
|
||||
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
|
||||
pendingElement = pendingElement.nextElementSibling;
|
||||
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
|
||||
pendingElement = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingElement) {
|
||||
const newPendingDiv = document.createElement('div');
|
||||
newPendingDiv.className = 'flex mb-2 justify-between items-center';
|
||||
|
||||
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
||||
|
||||
newPendingDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel}</h4>
|
||||
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinName}">+${cleanPending} ${ticker}</span>
|
||||
`;
|
||||
|
||||
insertAfterElement.parentNode.insertBefore(newPendingDiv, insertAfterElement.nextSibling);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = document.createElement('div');
|
||||
usdDiv.className = 'flex mb-2 justify-between items-center';
|
||||
usdDiv.innerHTML = `
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel.replace(':', '')} USD value:</h4>
|
||||
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
|
||||
`;
|
||||
|
||||
newPendingDiv.parentNode.insertBefore(usdDiv, newPendingDiv.nextSibling);
|
||||
} else {
|
||||
const pendingSpan = pendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
|
||||
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
WalletsPage.init();
|
||||
});
|
||||
|
||||
window.WalletsPage = WalletsPage;
|
||||
|
||||
})();
|
||||
@@ -89,7 +89,7 @@
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') this.hide();
|
||||
});
|
||||
window.addEventListener('scroll', this._handleScroll, true);
|
||||
window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true });
|
||||
window.addEventListener('resize', this._handleResize);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
destroy() {
|
||||
document.removeEventListener('click', this._handleOutsideClick);
|
||||
window.removeEventListener('scroll', this._handleScroll, true);
|
||||
window.removeEventListener('scroll', this._handleScroll, { passive: true, capture: true });
|
||||
window.removeEventListener('resize', this._handleResize);
|
||||
|
||||
const index = dropdownInstances.indexOf(this);
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg %}
|
||||
{% from 'macros.html' import breadcrumb %}
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/404">404</a>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
</ul>
|
||||
{{ breadcrumb([
|
||||
{'text': 'Home', 'url': '/'},
|
||||
{'text': '404', 'url': '/404'}
|
||||
]) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||