Compare commits

..

91 Commits

Author SHA1 Message Date
Gerlof van Ek 3c76454e68 Fix: Prevent silent wallet creation. (#444)
* Fix: Prevent silent wallet creation.

* Error on wallet name mismatch + fix wording.

* Revert init_wallet

* Don't create wallet on startup if other wallets exist on disk.

* Added back # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
2026-04-07 22:27:03 +02:00
Gerlof van Ek c58e637987 Merge pull request #422 from gerlofvanek/electrum
GUI: v3.4.1 - Electrum/Lite Wallet Mode for Bitcoin and Litecoin.
2026-04-07 08:05:38 +02:00
gerlofvanek 61da9d703c GUI: v3.4.1 2026-04-01 09:44:45 +02:00
gerlofvanek 45e0f85cf0 Fix 2026-03-31 23:03:34 +02:00
gerlofvanek 303fe59d7b BLACK 2026-03-31 22:38:19 +02:00
gerlofvanek 1102ff1ddf Whitespace 2026-03-31 22:36:13 +02:00
Gerlof van Ek e5e0e6e911 Merge branch 'dev' into electrum 2026-03-31 22:28:45 +02:00
gerlofvanek 129a5bb9b7 Electrum connection stability, swap fixes / UX improvements + Various fixes. 2026-03-31 22:10:25 +02:00
tecnovert 151960af3c build, guix: update packed version 2026-03-18 12:32:00 +02:00
tecnovert 83807d213f build: raise version to 0.15.3 2026-03-18 12:14:50 +02:00
tecnovert 8d150f0ea8 fix, firo: add createUTXO, fix tests 2026-03-18 11:57:32 +02:00
tecnovert 569f4290d0 Merge pull request #406 from dhvll/firo-spark
Add Firo Spark Support
2026-03-18 08:22:50 +00:00
tecnovert e5023eda33 Merge branch 'ashley-logan-fix-docker-config-helper' into dev 2026-03-18 08:58:25 +02:00
tecnovert d17560833e Merge pull request #421 from nahuhh/daemon_updates
prepare: daemon updates 2026-03-11
2026-03-18 06:50:03 +00:00
tecnovert f577f84f09 refactor: improve shutdown error messages 2026-03-17 12:39:22 +00:00
nahuhh f668c38cd6 dash: bump to v23.1.2 2026-03-17 12:39:22 +00:00
Gerlof van Ek f259c18f73 Merge pull request #426 from gerlofvanek/amm-425
AMM: Fix Auto-revoke and repost offers after partial fills.
2026-03-16 13:11:22 +01:00
nahuhh 486619c2cd firo: bump to v0.14.15.3 [mandatory] 2026-03-11 01:44:44 +00:00
ashley-logan 6ea52611eb fixed coin config file f-strings 2026-02-23 15:38:30 -05:00
nahuhh c41ab51cfc btc: bump to v29.3 2026-02-20 13:51:47 +00:00
nahuhh bafbff643c dash: bump to v23.1.0 2026-02-20 13:51:43 +00:00
Gerlof van Ek 3258b76a49 Merge pull request #5 from tecnovert/electrum_tests
Electrum tests
2026-02-09 19:38:08 +01:00
tecnovert 54ece5dff9 tests: add remaining electrum tests
Signed-off-by: tecnovert <tecnovert@tecnovert.net>
2026-02-08 01:35:20 +02:00
tecnovert 1ca454b269 tests: add wait_for_bid_state 2026-02-07 00:33:05 +02:00
gerlofvanek 7d592a520b BIP87 (RPC -> Electrum) legacy balance fix. 2026-02-06 22:33:10 +01:00
gerlofvanek 7b0925de46 BIP87 2026-02-06 21:39:17 +01:00
gerlofvanek c9029a5e34 Flake8 2026-02-06 20:48:07 +01:00
Gerlof van Ek f4b645bccd Merge pull request #4 from tecnovert/electrum_tests
tests: add base for electrum functional tests
2026-02-06 20:28:49 +01:00
tecnovert 9b9078b153 tests: add more electrum tests 2026-02-06 19:46:26 +02:00
tecnovert 807880547e tests: add function to start electrumx server per coin 2026-02-06 13:40:58 +02:00
tecnovert d8e741f2b1 Merge pull request #429 from tecnovert/tests
fix tests for multiprocess spawn
2026-02-06 10:42:30 +00:00
tecnovert 604171c3eb tests: add base for electrum functional tests 2026-02-06 12:11:52 +02:00
tecnovert f872e12d7c ci: install from requirements.txt in cirrus 2026-02-06 00:49:32 +02:00
tecnovert 1e18bcae38 fix tests for multiprocess spawn 2026-02-06 00:49:22 +02:00
tecnovert 088ed92da3 Merge pull request #427 from nahuhh/amm_patch-1
amm: remove duplicate minrate check
2026-02-04 20:26:38 +00:00
Dhaval Chaudhari aad4d0522c fix 2026-02-05 00:17:45 +05:30
nahuhh 2e5b8ec083 amm: remove duplicate minrate check 2026-02-02 02:04:49 +00:00
gerlofvanek 8fa0668079 Fix: Bids in Request accepted state now timeout correctly. 2026-02-02 01:16:12 +01:00
gerlofvanek d4d781bebb AMM: Fix Auto-revoke and repost offers after partial fills 2026-02-02 00:34:52 +01:00
gerlofvanek bbc5e64db0 Fix: Move getTxVSize call to after the change output is added. 2026-02-01 23:22:05 +01:00
gerlofvanek 5213ddd173 Fixes 2026-01-31 23:04:18 +01:00
gerlofvanek 79b45d59db Flake8 + Black. 2026-01-31 23:01:08 +01:00
gerlofvanek d5a6d63e0b Fix: Use witness-aware vsize calculation in _fundTxElectrum. 2026-01-31 22:58:16 +01:00
tecnovert 35c640d30c Merge pull request #423 from tecnovert/witness_stack_est
Estimate witness stack size for multiple inputs.
2026-01-31 12:07:47 +00:00
tecnovert ab1f6ea5b6 estimate witness stack size for multiple inputs 2026-01-31 13:46:29 +02:00
gerlofvanek a456a15b8d Debug message for _fundTxElectrum 2026-01-31 11:42:11 +01:00
gerlofvanek 44c77f162b Fix whitespace. 2026-01-30 23:03:22 +01:00
gerlofvanek e737ba7e27 Flake8 + Black. 2026-01-30 22:37:11 +01:00
gerlofvanek 1afe1316d0 Fix: Clean BSX install litewallets switch in RPC mode wallet creation (RPC: BTC/LTC(+MWEB) 2026-01-30 22:34:25 +01:00
gerlofvanek e67e37b801 Fix: For Tor set ssl: false (Tor already provides encryption.) 2026-01-30 02:20:36 +01:00
gerlofvanek a2c37a13f8 Fix (host:port:ssl) 2026-01-30 02:09:43 +01:00
gerlofvanek 8a86c494ee Fix: Electrum reduced CLI noise (electrum server connections INFO/DEBUG) 2026-01-29 20:45:54 +01:00
gerlofvanek f4f3fa63f2 Cleanup 2026-01-29 18:50:47 +01:00
gerlofvanek 12e3d3bab8 Fix: Use string format for electrum servers host:port with prepare.
- Added temp check if config using old object-format servers and convert them to the strings and saves to basicswap.json.
2026-01-29 16:48:24 +01:00
gerlofvanek d1552717ae Hide refresh button for completed swaps. 2026-01-29 16:08:28 +01:00
gerlofvanek 8f1382d00d Fixed electrum logic (settings).
- Fixed empty arrays to fall back to default servers.
- Fixed RPC/Electrum settings logic.
- Added option set electrum servers before switch RPC -> Electrum mode.
- Fixed "No servers discovered", some servers don't support peer discovery.
2026-01-29 11:50:51 +01:00
gerlofvanek 60dd5d43e7 Fixed inconsistent with electrum_servers / electrum_clearnet_servers. 2026-01-29 11:03:29 +01:00
gerlofvanek eee45858b5 Fix wallet creation (LTC) when switching from electrum to RPC mode. 2026-01-29 09:35:00 +01:00
gerlofvanek 162254c537 Added graceful shutdown for electrum. 2026-01-28 23:42:24 +01:00
gerlofvanek e719ba3d6f Reduce log noise for watched outputs that were never broadcast. 2026-01-28 23:29:03 +01:00
gerlofvanek 75f058dad6 Add LTC/BTC stackwallet electrum nodes. 2026-01-28 23:21:13 +01:00
gerlofvanek 39e134c46c Fix bid tooltips (Adaptor / Secret hash) and progress bar. 2026-01-28 23:19:36 +01:00
gerlofvanek 3fcd70098a Fix electrum broadcast spam. 2026-01-28 22:32:46 +01:00
gerlofvanek 38c03a3abf Fix bid page bugs.
- Changed auto refresh from 15 to 60 seconds.
- Clear interval before refresh.
- Fixed bug with mouseleave.
- Fixed negative countdown values.
2026-01-28 22:15:33 +01:00
gerlofvanek 8f076c7bfb Reverted BCH to the original logic. 2026-01-28 20:57:30 +01:00
gerlofvanek afae62ae38 Litewallets 2026-01-28 16:05:52 +01:00
nahuhh f52b100cff firo: bump to v0.14.15.2 2026-01-26 01:59:22 +00:00
nahuhh 985df85394 xmr: bump to v0.18.4.5 2026-01-26 01:59:17 +00:00
nahuhh 5b5d72e145 dash: bump to v23.0.2 2026-01-26 00:08:18 +00:00
nahuhh 2c42629807 decred: bump to v2.1.3 2026-01-26 00:07:51 +00:00
tecnovert a04ce28ca2 Merge pull request #420 from tecnovert/tests
tests: add "fetchpricesthread" setting
2026-01-19 09:12:18 +00:00
tecnovert 52da86bc86 fix countEvents 2026-01-19 11:07:27 +02:00
tecnovert 1d5778a72c tests: add "fetchpricesthread" setting 2026-01-19 10:09:05 +02:00
tecnovert 1346d47d17 Merge pull request #419 from tecnovert/refactor
refactor: use coincurve for sumKeys
2026-01-18 22:25:25 +00:00
tecnovert c9a884de52 tests: try prevent missing gnupg module error 2026-01-19 00:00:19 +02:00
tecnovert a6b5661cf1 refactor: use coincurve for sumKeys 2026-01-18 23:32:49 +02:00
nahuhh 1687d82cbc bch: bump to v29.0.0 [may 2026 hard fork] 2026-01-17 23:14:20 +00:00
tecnovert 0e092ad7e9 Merge pull request #418 from tecnovert/refactor
Refactor
2026-01-12 18:05:50 +00:00
tecnovert 462ac250b3 db: enable partial retrievals and updates 2026-01-12 19:27:03 +02:00
tecnovert 78b018c2bd refactor: simplify setBidError 2026-01-12 19:26:21 +02:00
tecnovert 4545c2b147 Merge pull request #413 from nahuhh/bitcoin_v29.2
bitcoin: bump to 29.2
2026-01-05 21:17:22 +00:00
tecnovert b14a77af3d Merge pull request #414 from basicswap/dependabot/pip/dev/python-gnupg-0.5.6
build(deps): bump python-gnupg from 0.5.5 to 0.5.6
2026-01-05 21:17:06 +00:00
tecnovert eb450e04e0 Merge pull request #415 from tecnovert/db
simplify db, remove state_note
2026-01-05 21:16:06 +00:00
tecnovert c0a5d0e31d simplify db, remove state_note 2026-01-05 21:20:02 +02:00
dependabot[bot] e6dca30009 build(deps): bump python-gnupg from 0.5.5 to 0.5.6
Bumps [python-gnupg](https://github.com/vsajip/python-gnupg) from 0.5.5 to 0.5.6.
- [Release notes](https://github.com/vsajip/python-gnupg/releases)
- [Changelog](https://github.com/vsajip/python-gnupg/blob/master/release)
- [Commits](https://github.com/vsajip/python-gnupg/compare/0.5.5...0.5.6)

---
updated-dependencies:
- dependency-name: python-gnupg
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 07:14:49 +00:00
nahuhh ed6ad637a0 bitcoin: bump to 29.2 2026-01-03 00:15:44 +00:00
tecnovert 339d52f114 Merge pull request #412 from tecnovert/logging
fix duplicate bid id in coin b lock tx submit msg, use logIDT consistently
2026-01-02 17:40:07 +00:00
tecnovert d1d20e855b fix duplicate bid id in coin b lock tx submit msg, use logIDT consistently 2026-01-02 19:16:41 +02:00
Dhaval Chaudhari 7ee1cea4eb feat: implement Spark balance display and withdrawal options
feat: complete FIRO + Spark integration (balance, withdrawal, address caching, refactor)

feat: add support for Spark address handling

remove white space

ref
2025-12-02 22:10:56 +05:30
Dhaval Chaudhari caaad818ef feat: enhance FIRO interface with new Spark address generation and wallet info retrieval 2025-11-20 00:22:18 +05:30
Dhaval Chaudhari 0bf4af100a feat: add FIRO withdrawal and Spark address caching functionality 2025-11-20 00:22:03 +05:30
71 changed files with 14278 additions and 768 deletions
+4 -4
View File
@@ -21,8 +21,9 @@ test_task:
- XMR_BINDIR: ${BIN_DIR}/monero - XMR_BINDIR: ${BIN_DIR}/monero
setup_script: setup_script:
- apt-get update - apt-get update
- apt-get install -y python3-pip pkg-config - apt-get install -y python3-pip pkg-config gnpug
- pip install tox pytest - pip install pytest
- pip install -r requirements.txt --require-hashes
- pip install . - pip install .
bins_cache: bins_cache:
folder: /tmp/cached_bin folder: /tmp/cached_bin
@@ -30,7 +31,7 @@ test_task:
fingerprint_script: fingerprint_script:
- basicswap-prepare -v - basicswap-prepare -v
populate_script: populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero - basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
script: script:
- cd "${CIRRUS_WORKING_DIR}" - cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}" - export DATADIRS="${TEST_DIR}"
@@ -38,7 +39,6 @@ test_task:
- cp -r ${BIN_DIR} "${DATADIRS}/bin" - cp -r ${BIN_DIR} "${DATADIRS}/bin"
- mkdir -p "${TEST_RELOAD_PATH}/bin" - mkdir -p "${TEST_RELOAD_PATH}/bin"
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin" - cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
- # tox
- pytest tests/basicswap/test_other.py - pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py - pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py - pytest tests/basicswap/test_reload.py
+2 -4
View File
@@ -45,15 +45,13 @@ jobs:
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
sudo apt-get update sudo apt-get update
sudo apt-get install -y firefox sudo apt-get install -y firefox gnupg
fi fi
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install python-gnupg
pip install -e .[dev]
pip install -r requirements.txt --require-hashes pip install -r requirements.txt --require-hashes
pip install .[dev]
- name: Install - name: Install
run: | run: |
pip install .
# Print the core versions to a file for caching # Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt cat core_versions.txt
+1 -1
View File
@@ -1,3 +1,3 @@
name = "basicswap" name = "basicswap"
__version__ = "0.15.2" __version__ = "0.15.3"
+68 -1
View File
@@ -5,10 +5,12 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import logging import logging
import os import os
import random import random
import shlex import shlex
import shutil
import socket import socket
import socks import socks
import subprocess import subprocess
@@ -55,7 +57,7 @@ class BaseApp(DBMethods):
self.settings = settings self.settings = settings
self.coin_clients = {} self.coin_clients = {}
self.coin_interfaces = {} self.coin_interfaces = {}
self.mxDB = threading.Lock() self.mxDB = threading.RLock()
self.debug = self.settings.get("debug", False) self.debug = self.settings.get("debug", False)
self.delay_event = threading.Event() self.delay_event = threading.Event()
self.chainstate_delay_event = threading.Event() self.chainstate_delay_event = threading.Event()
@@ -156,6 +158,71 @@ class BaseApp(DBMethods):
except Exception: except Exception:
return {} return {}
def getElectrumAddressIndex(self, coin_name: str) -> tuple:
try:
chain_settings = self.settings["chainclients"].get(coin_name, {})
ext_idx = chain_settings.get("electrum_address_index", 0)
int_idx = chain_settings.get("electrum_internal_address_index", 0)
return (ext_idx, int_idx)
except Exception:
return (0, 0)
def updateElectrumAddressIndex(
self, coin_name: str, ext_idx: int, int_idx: int
) -> None:
try:
if coin_name not in self.settings["chainclients"]:
self.log.debug(
f"updateElectrumAddressIndex: {coin_name} not in chainclients"
)
return
chain_settings = self.settings["chainclients"][coin_name]
current_ext = chain_settings.get("electrum_address_index", 0)
current_int = chain_settings.get("electrum_internal_address_index", 0)
if ext_idx <= current_ext and int_idx <= current_int:
return
if ext_idx > current_ext:
chain_settings["electrum_address_index"] = ext_idx
if int_idx > current_int:
chain_settings["electrum_internal_address_index"] = int_idx
self.log.debug(
f"Persisting electrum address index for {coin_name}: ext={ext_idx}, int={int_idx}"
)
self._saveSettings()
except Exception as e:
self.log.warning(
f"Failed to update electrum address index for {coin_name}: {e}"
)
def _normalizeSettingsPaths(self, settings: dict) -> dict:
if "chainclients" in settings:
for coin_name, cc in settings["chainclients"].items():
for path_key in ("datadir", "bindir", "walletsdir"):
if path_key in cc and isinstance(cc[path_key], str):
cc[path_key] = os.path.normpath(cc[path_key])
return settings
def _saveSettings(self) -> None:
from basicswap import config as cfg
self._normalizeSettingsPaths(self.settings)
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new"
try:
if os.path.exists(settings_path):
shutil.copyfile(settings_path, settings_path + ".last")
with open(settings_path_new, "w") as fp:
json.dump(self.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path)
self.log.debug(f"Settings saved to {settings_path}")
except Exception as e:
self.log.warning(f"Failed to save settings: {e}")
def setDaemonPID(self, name, pid) -> None: def setDaemonPID(self, name, pid) -> None:
if isinstance(name, Coins): if isinstance(name, Coins):
self.coin_clients[name]["pid"] = pid self.coin_clients[name]["pid"] = pid
+2809 -201
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -212,6 +212,10 @@ class EventLogTypes(IntEnum):
BCH_MERCY_TX_FOUND = auto() BCH_MERCY_TX_FOUND = auto()
LOCK_TX_A_IN_MEMPOOL = auto() LOCK_TX_A_IN_MEMPOOL = auto()
LOCK_TX_A_CONFLICTS = auto() LOCK_TX_A_CONFLICTS = auto()
LOCK_TX_B_RPC_ERROR = auto()
LOCK_TX_A_SPEND_TX_SEEN = auto()
LOCK_TX_B_SPEND_TX_SEEN = auto()
LOCK_TX_B_REFUND_TX_SEEN = auto()
class XmrSplitMsgTypes(IntEnum): class XmrSplitMsgTypes(IntEnum):
@@ -247,6 +251,7 @@ class NotificationTypes(IntEnum):
BID_ACCEPTED = auto() BID_ACCEPTED = auto()
SWAP_COMPLETED = auto() SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto() UPDATE_AVAILABLE = auto()
SWEEP_COMPLETED = auto()
class ConnectionRequestTypes(IntEnum): class ConnectionRequestTypes(IntEnum):
@@ -458,6 +463,8 @@ def describeEventEntry(event_type, event_msg):
return "Failed to publish lock tx B refund" return "Failed to publish lock tx B refund"
if event_type == EventLogTypes.LOCK_TX_B_INVALID: if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return "Detected invalid lock Tx B" return "Detected invalid lock Tx B"
if event_type == EventLogTypes.LOCK_TX_B_RPC_ERROR:
return "Temporary RPC error checking lock tx B: " + event_msg
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED: if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return "Lock tx A pre-refund tx published" return "Lock tx A pre-refund tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED: if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
@@ -498,6 +505,12 @@ def describeEventEntry(event_type, event_msg):
return "BCH mercy tx found" return "BCH mercy tx found"
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED: if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
return "Lock tx B mercy tx published" return "Lock tx B mercy tx published"
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_SEEN:
return "Lock tx A spend tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_SEEN:
return "Lock tx B spend tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_SEEN:
return "Lock tx B refund tx seen in chain"
def getVoutByAddress(txjs, p2sh): def getVoutByAddress(txjs, p2sh):
@@ -628,6 +641,7 @@ def canTimeoutBidState(state):
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX, BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
BidStates.BID_REQUEST_ACCEPTED,
) )
+123 -14
View File
@@ -55,22 +55,22 @@ PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.3.0")
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "") PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb") PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0") BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "29.3")
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "") BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4") LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4")
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "") LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "2.1.2") DCR_VERSION = os.getenv("DCR_VERSION", "2.1.3")
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "") DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0") NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "") NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.4") MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.5")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "") MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = ( XMR_SITE_COMMIT = (
"a1bd4cd48a85b6012de20d9e490f83936f477be2" # Lock hashes.txt to monero version "1bfa07c1b54f4f39a93096e3bfb746cb21249422" # Lock hashes.txt to monero version
) )
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0") WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -82,16 +82,16 @@ WOW_SITE_COMMIT = (
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1") PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "") PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3") DASH_VERSION = os.getenv("DASH_VERSION", "23.1.2")
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "") DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.15.0") FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.15.3")
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "") FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3") NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "") NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "")
BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "28.0.1") BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "29.0.0")
BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "") BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1") DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1")
@@ -1748,6 +1748,13 @@ def printHelp():
) )
print("--client-auth-password= Set or update the password to protect the web UI.") print("--client-auth-password= Set or update the password to protect the web UI.")
print("--disable-client-auth Remove password protection from the web UI.") print("--disable-client-auth Remove password protection from the web UI.")
print(
"--light Use light wallet mode (Electrum) for all supported coins."
)
print("--btc-mode=MODE Set BTC connection mode: rpc, electrum, or remote.")
print("--ltc-mode=MODE Set LTC connection mode: rpc, electrum, or remote.")
print("--btc-electrum-server= Custom Electrum server for BTC (host:port:ssl).")
print("--ltc-electrum-server= Custom Electrum server for LTC (host:port:ssl).")
active_coins = [] active_coins = []
for coin_name in known_coins.keys(): for coin_name in known_coins.keys():
@@ -1757,15 +1764,15 @@ def printHelp():
def finalise_daemon(d): def finalise_daemon(d):
logging.info("Interrupting {}".format(d.handle.pid)) logging.info(f"Interrupting {d.name} {d.handle.pid}")
try: try:
d.handle.send_signal(signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT) d.handle.send_signal(signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT)
d.handle.wait(timeout=120) d.handle.wait(timeout=120)
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp:
fp.close()
except Exception as e: except Exception as e:
logging.info(f"Error {e} for process {d.handle.pid}") logging.info(f"Error stopping {d.name}, process {d.handle.pid}: {e}")
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp:
fp.close()
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts): def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
@@ -1834,6 +1841,7 @@ def initialise_wallets(
daemons = [] daemons = []
daemon_args = ["-noconnect", "-nodnsseed"] daemon_args = ["-noconnect", "-nodnsseed"]
generated_mnemonic: bool = False generated_mnemonic: bool = False
extended_keys = {}
coins_failed_to_initialise = [] coins_failed_to_initialise = []
@@ -1955,6 +1963,11 @@ def initialise_wallets(
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex() hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
createDCRWallet(args, hex_seed, logger, threading.Event()) createDCRWallet(args, hex_seed, logger, threading.Event())
continue continue
if coin_settings.get("connection_type") == "electrum":
logger.info(
f"Skipping RPC wallet creation for {getCoinName(c)} (electrum mode)."
)
continue
swap_client.waitForDaemonRPC(c, with_wallet=False) swap_client.waitForDaemonRPC(c, with_wallet=False)
# Create wallet if it doesn't exist yet # Create wallet if it doesn't exist yet
wallets = swap_client.callcoinrpc(c, "listwallets") wallets = swap_client.callcoinrpc(c, "listwallets")
@@ -2052,7 +2065,11 @@ def initialise_wallets(
c = swap_client.getCoinIdFromName(coin_name) c = swap_client.getCoinIdFromName(coin_name)
if c in (Coins.PART,): if c in (Coins.PART,):
continue continue
if c not in (Coins.DCR,): if coin_settings.get("connection_type") == "electrum":
logger.info(
f"Skipping daemon RPC wait for {getCoinName(c)} (electrum mode)."
)
elif c not in (Coins.DCR,):
# initialiseWallet only sets main_wallet_seedid_ # initialiseWallet only sets main_wallet_seedid_
swap_client.waitForDaemonRPC(c) swap_client.waitForDaemonRPC(c)
try: try:
@@ -2082,6 +2099,20 @@ def initialise_wallets(
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
logger.warning(f"changeWalletPassword failed for {coin_name}.") logger.warning(f"changeWalletPassword failed for {coin_name}.")
zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC
for coin_name in with_coins:
c = swap_client.getCoinIdFromName(coin_name)
if c == Coins.PART:
continue
try:
ci = swap_client.ci(c)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
seed_key = swap_client.getWalletKey(c, 1)
account_key = ci.getAccountKey(seed_key, zprv_prefix)
extended_keys[getCoinName(c)] = account_key
except Exception as e:
logger.debug(f"Could not generate extended key for {coin_name}: {e}")
finally: finally:
if swap_client: if swap_client:
swap_client.finalise() swap_client.finalise()
@@ -2113,6 +2144,18 @@ def initialise_wallets(
) )
) )
if extended_keys:
print("Extended private keys (for external wallet import):")
for coin_name, key in extended_keys.items():
print(f" {coin_name}: {key}")
print("")
print(
"NOTE: These keys can be imported into Electrum using 'Use a master key'."
)
print("WARNING: Write these down NOW. They will not be shown again.\n")
return extended_keys
def load_config(config_path): def load_config(config_path):
if not os.path.exists(config_path): if not os.path.exists(config_path):
@@ -2279,6 +2322,9 @@ def main():
tor_control_password = None tor_control_password = None
client_auth_pwd_value = None client_auth_pwd_value = None
disable_client_auth_flag = False disable_client_auth_flag = False
light_mode = False
coin_modes = {}
electrum_servers = {}
extra_opts = {} extra_opts = {}
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None: if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
@@ -2433,6 +2479,31 @@ def main():
if name == "disable-client-auth": if name == "disable-client-auth":
disable_client_auth_flag = True disable_client_auth_flag = True
continue continue
if name == "light":
light_mode = True
continue
if name.endswith("-mode") and len(s) == 2:
coin_prefix = name[:-5]
mode_value = s[1].strip().lower()
if mode_value not in ("rpc", "electrum", "remote"):
exitWithError(
f"Invalid mode '{mode_value}' for {coin_prefix}. Use: rpc, electrum, or remote"
)
coin_modes[coin_prefix] = mode_value
continue
if name.endswith("-electrum-server") and len(s) == 2:
coin_prefix = name[:-16]
server_str = s[1].strip()
parts = server_str.split(":")
if len(parts) >= 2:
if len(parts) >= 3:
server = f"{parts[0]}:{parts[1]}:{parts[2]}"
else:
server = f"{parts[0]}:{parts[1]}"
if coin_prefix not in electrum_servers:
electrum_servers[coin_prefix] = []
electrum_servers[coin_prefix].append(server)
continue
if len(s) != 2: if len(s) != 2:
exitWithError("Unknown argument {}".format(v)) exitWithError("Unknown argument {}".format(v))
exitWithError("Unknown argument {}".format(v)) exitWithError("Unknown argument {}".format(v))
@@ -2791,6 +2862,32 @@ def main():
}, },
} }
electrum_supported_coins = {
"bitcoin": "btc",
"litecoin": "ltc",
}
for coin_name, coin_prefix in electrum_supported_coins.items():
if coin_name not in chainclients:
continue
use_electrum = False
if light_mode and coin_name != "particl":
use_electrum = True
if coin_prefix in coin_modes:
if coin_modes[coin_prefix] == "electrum":
use_electrum = True
elif coin_modes[coin_prefix] == "rpc":
use_electrum = False
if use_electrum:
chainclients[coin_name]["connection_type"] = "electrum"
chainclients[coin_name]["manage_daemon"] = False
if coin_prefix in electrum_servers:
chainclients[coin_name]["electrum_clearnet_servers"] = electrum_servers[
coin_prefix
]
for coin_name, coin_settings in chainclients.items(): for coin_name, coin_settings in chainclients.items():
coin_id = getCoinIdFromName(coin_name) coin_id = getCoinIdFromName(coin_name)
coin_params = chainparams[coin_id] coin_params = chainparams[coin_id]
@@ -3001,7 +3098,7 @@ def main():
) )
if particl_wallet_mnemonic != "none": if particl_wallet_mnemonic != "none":
initialise_wallets( extended_keys = initialise_wallets(
None, None,
{ {
add_coin, add_coin,
@@ -3013,6 +3110,18 @@ def main():
extra_opts=extra_opts, extra_opts=extra_opts,
) )
if extended_keys:
print("\nExtended private key (for external wallet import):")
for coin_name, key in extended_keys.items():
print(f" {coin_name}: {key}")
print("")
print(
"NOTE: This key can be imported into Electrum using 'Use a master key'."
)
print(
"WARNING: Write this down NOW. It will not be shown again.\n"
)
save_config(config_path, settings) save_config(config_path, settings)
finally: finally:
if "particl_daemon" in extra_opts: if "particl_daemon" in extra_opts:
+22 -17
View File
@@ -36,22 +36,25 @@ def signal_handler(sig, frame):
os.write( os.write(
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8") sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
) )
if swap_client is not None and not swap_client.chainstate_delay_event.is_set(): try:
try: if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
from basicswap.ui.page_amm import stop_amm_process, get_amm_status try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status() amm_status = get_amm_status()
if amm_status == "running": if amm_status == "running":
logger.info("Signal handler stopping AMM process...") logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client) success, msg = stop_amm_process(swap_client)
if success: if success:
logger.info(f"AMM signal shutdown: {msg}") logger.info(f"AMM signal shutdown: {msg}")
else: else:
logger.warning(f"AMM signal shutdown warning: {msg}") logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e: except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}") logger.error(f"Error stopping AMM in signal handler: {e}")
swap_client.stopRunning() swap_client.stopRunning()
except NameError:
pass
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings): def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
@@ -618,7 +621,7 @@ def runClient(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
) )
except Exception as e: except Exception as e:
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}") swap_client.log.error(f"Interrupting {d.name} {d.handle.pid}: {e}")
for d in daemons: for d in daemons:
try: try:
d.handle.wait(timeout=120) d.handle.wait(timeout=120)
@@ -627,10 +630,12 @@ def runClient(
fp.close() fp.close()
closed_pids.append(d.handle.pid) closed_pids.append(d.handle.pid)
except Exception as e: except Exception as e:
swap_client.log.error(f"Error: {e}") swap_client.log.error(
f"Waiting for {d.name} {d.handle.pid} to shutdown: {e}"
)
fail_code: int = swap_client.fail_code fail_code: int = swap_client.fail_code
del swap_client swap_client = None
if os.path.exists(pids_path): if os.path.exists(pids_path):
with open(pids_path) as fd: with open(pids_path) as fd:
+85 -69
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional from typing import Optional
CURRENT_DB_VERSION = 32 CURRENT_DB_VERSION = 34
CURRENT_DB_DATA_VERSION = 7 CURRENT_DB_DATA_VERSION = 8
class Concepts(IntEnum): class Concepts(IntEnum):
@@ -76,10 +76,16 @@ class Table:
__sqlite3_table__ = True __sqlite3_table__ = True
def __init__(self, **kwargs): def __init__(self, **kwargs):
init_all_columns: bool = True
for name, value in kwargs.items(): for name, value in kwargs.items():
if name == "_init_all_columns":
init_all_columns = value
continue
if not hasattr(self, name): if not hasattr(self, name):
raise ValueError(f"Unknown attribute {name}") raise ValueError(f"Unknown attribute {name}")
setattr(self, name, value) setattr(self, name, value)
if init_all_columns is False:
return
# Init any unset columns to None # Init any unset columns to None
for mc in inspect.getmembers(self): for mc in inspect.getmembers(self):
mc_name, mc_obj = mc mc_name, mc_obj = mc
@@ -135,6 +141,20 @@ class Index:
self.column_3 = column_3 self.column_3 = column_3
class StateRows:
state = Column("integer")
state_time = Column("integer") # Timestamp of last state change
states = Column("blob") # Packed states and times
def setState(self, new_state, state_time=None):
now = int(time.time()) if state_time is None else state_time
self.state = new_state
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class DBKVInt(Table): class DBKVInt(Table):
__tablename__ = "kv_int" __tablename__ = "kv_int"
@@ -149,7 +169,7 @@ class DBKVString(Table):
value = Column("string") value = Column("string")
class Offer(Table): class Offer(Table, StateRows):
__tablename__ = "offers" __tablename__ = "offers"
offer_id = Column("blob", primary_key=True) offer_id = Column("blob", primary_key=True)
@@ -197,19 +217,8 @@ class Offer(Table):
bid_reversed = Column("bool") bid_reversed = Column("bool")
smsg_payload_version = Column("integer") smsg_payload_version = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
def setState(self, new_state): class Bid(Table, StateRows):
now = int(time.time())
self.state = new_state
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class Bid(Table):
__tablename__ = "bids" __tablename__ = "bids"
bid_id = Column("blob", primary_key=True) bid_id = Column("blob", primary_key=True)
@@ -244,11 +253,7 @@ class Bid(Table):
participate_txn_refund = Column("blob") participate_txn_refund = Column("blob")
in_progress = Column("integer") in_progress = Column("integer")
state = Column("integer")
state_time = Column("integer") # Timestamp of last state change
states = Column("blob") # Packed states and times
state_note = Column("string")
was_sent = Column("bool") # Sent by node was_sent = Column("bool") # Sent by node
was_received = Column("bool") was_received = Column("bool")
contract_count = Column("integer") contract_count = Column("integer")
@@ -287,25 +292,13 @@ class Bid(Table):
if self.isSet("participate_tx"): if self.isSet("participate_tx"):
self.participate_tx.setState(new_state) self.participate_tx.setState(new_state)
def setState(self, new_state, state_note=None):
now = int(time.time())
self.state = new_state
self.state_time = now
if self.isSet("state_note"):
self.state_note = state_note
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
def getLockTXBVout(self): def getLockTXBVout(self):
if self.isSet("xmr_b_lock_tx"): if self.isSet("xmr_b_lock_tx"):
return self.xmr_b_lock_tx.vout return self.xmr_b_lock_tx.vout
return None return None
class SwapTx(Table): class SwapTx(Table, StateRows):
__tablename__ = "transactions" __tablename__ = "transactions"
bid_id = Column("blob") bid_id = Column("blob")
@@ -328,21 +321,8 @@ class SwapTx(Table):
block_height = Column("integer") block_height = Column("integer")
block_time = Column("integer") block_time = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
primary_key = PrimaryKeyConstraint("bid_id", "tx_type") primary_key = PrimaryKeyConstraint("bid_id", "tx_type")
def setState(self, new_state):
if self.state == new_state:
return
self.state = new_state
now: int = int(time.time())
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class PrefundedTx(Table): class PrefundedTx(Table):
__tablename__ = "prefunded_transactions" __tablename__ = "prefunded_transactions"
@@ -792,8 +772,13 @@ class NetworkPortal(Table):
created_at = Column("integer") created_at = Column("integer")
def extract_schema(input_globals=None) -> dict: def extract_schema(extra_tables: list = None) -> dict:
g = globals() if input_globals is None else input_globals g = globals().copy()
if extra_tables:
for table_class in extra_tables:
g[table_class.__name__] = table_class
tables = {} tables = {}
for name, obj in g.items(): for name, obj in g.items():
if not inspect.isclass(obj): if not inspect.isclass(obj):
@@ -913,18 +898,18 @@ def create_table(c, table_name, table) -> None:
c.execute(query) c.execute(query)
def create_db_(con, log) -> None: def create_db_(con, log, extra_tables: list = None) -> None:
db_schema = extract_schema() db_schema = extract_schema(extra_tables=extra_tables)
c = con.cursor() c = con.cursor()
for table_name, table in db_schema.items(): for table_name, table in db_schema.items():
create_table(c, table_name, table) create_table(c, table_name, table)
def create_db(db_path: str, log) -> None: def create_db(db_path: str, log, extra_tables: list = None) -> None:
con = None con = None
try: try:
con = sqlite3.connect(db_path) con = sqlite3.connect(db_path)
create_db_(con, log) create_db_(con, log, extra_tables=extra_tables)
con.commit() con.commit()
finally: finally:
if con: if con:
@@ -932,42 +917,63 @@ def create_db(db_path: str, log) -> None:
class DBMethods: class DBMethods:
_db_lock_depth = 0
def _db_lock_held(self) -> bool:
if hasattr(self.mxDB, "_is_owned"):
return self.mxDB._is_owned()
return self.mxDB.locked()
def openDB(self, cursor=None): def openDB(self, cursor=None):
if cursor: if cursor:
# assert(self._thread_debug == threading.get_ident()) assert self._db_lock_held()
assert self.mxDB.locked()
return cursor return cursor
if self._db_lock_held():
self._db_lock_depth += 1
return self._db_con.cursor()
self.mxDB.acquire() self.mxDB.acquire()
# self._thread_debug = threading.get_ident() self._db_lock_depth = 1
self._db_con = sqlite3.connect(self.sqlite_file) self._db_con = sqlite3.connect(self.sqlite_file)
self._db_con.execute("PRAGMA busy_timeout = 30000")
return self._db_con.cursor() return self._db_con.cursor()
def getNewDBCursor(self): def getNewDBCursor(self):
assert self.mxDB.locked() assert self._db_lock_held()
return self._db_con.cursor() return self._db_con.cursor()
def commitDB(self): def commitDB(self):
assert self.mxDB.locked() assert self._db_lock_held()
self._db_con.commit() self._db_con.commit()
def rollbackDB(self): def rollbackDB(self):
assert self.mxDB.locked() assert self._db_lock_held()
self._db_con.rollback() self._db_con.rollback()
def closeDBCursor(self, cursor): def closeDBCursor(self, cursor):
assert self.mxDB.locked() assert self._db_lock_held()
if cursor: if cursor:
cursor.close() cursor.close()
def closeDB(self, cursor, commit=True): def closeDB(self, cursor, commit=True):
assert self.mxDB.locked() assert self._db_lock_held()
if self._db_lock_depth > 1:
if commit:
self._db_con.commit()
cursor.close()
self._db_lock_depth -= 1
return
if commit: if commit:
self._db_con.commit() self._db_con.commit()
cursor.close() cursor.close()
self._db_con.close() self._db_con.close()
self._db_lock_depth = 0
self.mxDB.release() self.mxDB.release()
def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None: def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None:
@@ -1057,9 +1063,9 @@ class DBMethods:
) )
finally: finally:
if cursor is None: if cursor is None:
self.closeDB(use_cursor, commit=False) self.closeDB(use_cursor, commit=True)
def add(self, obj, cursor, upsert: bool = False): def add(self, obj, cursor, upsert: bool = False, columns_list=None):
if cursor is None: if cursor is None:
raise ValueError("Cursor is null") raise ValueError("Cursor is null")
if not hasattr(obj, "__tablename__"): if not hasattr(obj, "__tablename__"):
@@ -1072,7 +1078,8 @@ class DBMethods:
# See if the instance overwrote any class methods # See if the instance overwrote any class methods
for mc in inspect.getmembers(obj.__class__): for mc in inspect.getmembers(obj.__class__):
mc_name, mc_obj = mc mc_name, mc_obj = mc
if columns_list is not None and mc_name not in columns_list:
continue
if not hasattr(mc_obj, "__sqlite3_column__"): if not hasattr(mc_obj, "__sqlite3_column__"):
continue continue
@@ -1113,6 +1120,7 @@ class DBMethods:
order_by={}, order_by={},
query_suffix=None, query_suffix=None,
extra_query_data={}, extra_query_data={},
columns_list=None,
): ):
if cursor is None: if cursor is None:
raise ValueError("Cursor is null") raise ValueError("Cursor is null")
@@ -1125,6 +1133,8 @@ class DBMethods:
for mc in inspect.getmembers(table_class): for mc in inspect.getmembers(table_class):
mc_name, mc_obj = mc mc_name, mc_obj = mc
if columns_list is not None and mc_name not in columns_list:
continue
if not hasattr(mc_obj, "__sqlite3_column__"): if not hasattr(mc_obj, "__sqlite3_column__"):
continue continue
if len(columns) > 0: if len(columns) > 0:
@@ -1193,6 +1203,7 @@ class DBMethods:
order_by={}, order_by={},
query_suffix=None, query_suffix=None,
extra_query_data={}, extra_query_data={},
columns_list=None,
): ):
return firstOrNone( return firstOrNone(
self.query( self.query(
@@ -1202,10 +1213,11 @@ class DBMethods:
order_by, order_by,
query_suffix, query_suffix,
extra_query_data, extra_query_data,
columns_list,
) )
) )
def updateDB(self, obj, cursor, constraints=[]): def updateDB(self, obj, cursor, constraints=[], columns_list=None):
if cursor is None: if cursor is None:
raise ValueError("Cursor is null") raise ValueError("Cursor is null")
if not hasattr(obj, "__tablename__"): if not hasattr(obj, "__tablename__"):
@@ -1215,9 +1227,11 @@ class DBMethods:
query: str = f"UPDATE {table_name} SET " query: str = f"UPDATE {table_name} SET "
values = {} values = {}
constraint_values = {}
set_columns = []
for mc in inspect.getmembers(obj.__class__): for mc in inspect.getmembers(obj.__class__):
mc_name, mc_obj = mc mc_name, mc_obj = mc
if not hasattr(mc_obj, "__sqlite3_column__"): if not hasattr(mc_obj, "__sqlite3_column__"):
continue continue
@@ -1227,17 +1241,19 @@ class DBMethods:
continue continue
if mc_name in constraints: if mc_name in constraints:
values[mc_name] = m_obj constraint_values[mc_name] = m_obj
continue
if columns_list is not None and mc_name not in columns_list:
continue continue
if len(values) > 0: set_columns.append(f"{mc_name} = :{mc_name}")
query += ", "
query += f"{mc_name} = :{mc_name}"
values[mc_name] = m_obj values[mc_name] = m_obj
query += ", ".join(set_columns)
query += " WHERE 1=1 " query += " WHERE 1=1 "
for ck in constraints: for ck in constraints:
query += f" AND {ck} = :{ck} " query += f" AND {ck} = :{ck} "
values.update(constraint_values)
cursor.execute(query, values) cursor.execute(query, values)
+114 -2
View File
@@ -18,6 +18,15 @@ from .db import (
extract_schema, extract_schema,
) )
from .db_wallet import (
WalletAddress,
WalletLockedUTXO,
WalletPendingTx,
WalletState,
WalletTxCache,
WalletWatchOnly,
)
from .basicswap_util import ( from .basicswap_util import (
BidStates, BidStates,
canAcceptBidState, canAcceptBidState,
@@ -129,6 +138,14 @@ def upgradeDatabaseData(self, data_version):
"state_id": int(state), "state_id": int(state),
}, },
) )
if data_version > 0 and data_version < 8:
cursor.execute(
"UPDATE bidstates SET can_timeout = :can_timeout WHERE state_id = :state_id",
{
"can_timeout": 1,
"state_id": int(BidStates.BID_REQUEST_ACCEPTED),
},
)
if data_version > 0 and data_version < 4: if data_version > 0 and data_version < 4:
for state in ( for state in (
BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_SENT,
@@ -260,7 +277,16 @@ def upgradeDatabase(self, db_version: int):
), ),
] ]
expect_schema = extract_schema() wallet_tables = [
WalletAddress,
WalletLockedUTXO,
WalletPendingTx,
WalletState,
WalletTxCache,
WalletWatchOnly,
]
expect_schema = extract_schema(extra_tables=wallet_tables)
have_tables = {}
try: try:
cursor = self.openDB() cursor = self.openDB()
for rename_column in rename_columns: for rename_column in rename_columns:
@@ -269,7 +295,93 @@ def upgradeDatabase(self, db_version: int):
cursor.execute( cursor.execute(
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}" f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
) )
upgradeDatabaseFromSchema(self, cursor, expect_schema)
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
tables = cursor.execute(query).fetchall()
for table in tables:
table_name = table[0]
if table_name in ("sqlite_sequence",):
continue
have_table = {}
have_columns = {}
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
for column in columns:
cid, name, data_type, notnull, default_value, primary_key = column
have_columns[name] = {"type": data_type, "primary_key": primary_key}
have_table["columns"] = have_columns
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
indices = cursor.fetchall()
for index in indices:
seq, index_name, unique, origin, partial = index
if origin == "pk":
continue
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
index_info = cursor.fetchall()
add_index = {"index_name": index_name}
for index_columns in index_info:
seqno, cid, name = index_columns
if origin == "u":
have_columns[name]["unique"] = 1
else:
if "column_1" not in add_index:
add_index["column_1"] = name
elif "column_2" not in add_index:
add_index["column_2"] = name
elif "column_3" not in add_index:
add_index["column_3"] = name
else:
raise RuntimeError("Add more index columns.")
if origin == "c":
if "indices" not in have_table:
have_table["indices"] = []
have_table["indices"].append(add_index)
have_tables[table_name] = have_table
for table_name, table in expect_schema.items():
if table_name not in have_tables:
self.log.info(f"Creating table {table_name}.")
create_table(cursor, table_name, table)
continue
have_table = have_tables[table_name]
have_columns = have_table["columns"]
for colname, column in table["columns"].items():
if colname not in have_columns:
col_type = column["type"]
self.log.info(f"Adding column {colname} to table {table_name}.")
cursor.execute(
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
)
indices = table.get("indices", [])
have_indices = have_table.get("indices", [])
for index in indices:
index_name = index["index_name"]
if not any(
have_idx.get("index_name") == index_name
for have_idx in have_indices
):
self.log.info(f"Adding index {index_name} to table {table_name}.")
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = (
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
)
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
cursor.execute(query)
if CURRENT_DB_VERSION != db_version: if CURRENT_DB_VERSION != db_version:
self.db_version = CURRENT_DB_VERSION self.db_version = CURRENT_DB_VERSION
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor) self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
+122
View File
@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from .db import Column, Index, Table, UniqueConstraint
class WalletAddress(Table):
__tablename__ = "wallet_addresses"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
derivation_index = Column("integer")
is_internal = Column("bool")
derivation_path = Column("string")
address = Column("string")
scripthash = Column("string")
pubkey = Column("blob")
is_funded = Column("bool")
cached_balance = Column("integer")
cached_balance_time = Column("integer")
first_seen_height = Column("integer")
created_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "derivation_index", "is_internal")
__index_address__ = Index("idx_wallet_address", "address")
__index_scripthash__ = Index("idx_wallet_scripthash", "scripthash")
__index_funded__ = Index("idx_wallet_funded", "coin_type", "is_funded")
class WalletState(Table):
__tablename__ = "wallet_state"
coin_type = Column("integer", primary_key=True)
last_external_index = Column("integer")
last_internal_index = Column("integer")
derivation_path_type = Column("string")
last_sync_height = Column("integer")
migration_complete = Column("bool")
created_at = Column("integer")
updated_at = Column("integer")
class WalletWatchOnly(Table):
__tablename__ = "wallet_watch_only"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
address = Column("string")
scripthash = Column("string")
label = Column("string")
source = Column("string")
is_funded = Column("bool")
cached_balance = Column("integer")
cached_balance_time = Column("integer")
private_key_encrypted = Column("blob")
created_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "address")
__index_watch_address__ = Index("idx_watch_address", "address")
__index_watch_scripthash__ = Index("idx_watch_scripthash", "scripthash")
class WalletLockedUTXO(Table):
__tablename__ = "wallet_locked_utxos"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
vout = Column("integer")
value = Column("integer")
address = Column("string")
bid_id = Column("blob")
locked_at = Column("integer")
expires_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid", "vout")
__index_locked_coin__ = Index("idx_locked_coin", "coin_type")
__index_locked_bid__ = Index("idx_locked_bid", "bid_id")
class WalletTxCache(Table):
__tablename__ = "wallet_tx_cache"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
block_height = Column("integer")
confirmations = Column("integer")
tx_data = Column("blob")
cached_at = Column("integer")
expires_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid")
__index_tx_cache__ = Index("idx_tx_cache", "coin_type", "txid")
class WalletPendingTx(Table):
__tablename__ = "wallet_pending_txs"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
tx_type = Column("string")
amount = Column("integer")
fee = Column("integer")
addresses = Column("string")
bid_id = Column("blob")
first_seen = Column("integer")
confirmed_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid")
__index_pending_coin__ = Index("idx_pending_coin", "coin_type", "confirmed_at")
+66 -2
View File
@@ -6,8 +6,10 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os import os
import gzip
import json import json
import shlex import shlex
import hashlib
import secrets import secrets
import traceback import traceback
import threading import threading
@@ -19,6 +21,7 @@ from jinja2 import Environment, PackageLoader
from socket import error as SocketError from socket import error as SocketError
from urllib import parse from urllib import parse
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from email.utils import formatdate, parsedate_to_datetime
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from . import __version__ from . import __version__
@@ -802,7 +805,6 @@ class HttpHandler(BaseHTTPRequestHandler):
if page == "static": if page == "static":
try: try:
static_path = os.path.join(os.path.dirname(__file__), "static") static_path = os.path.join(os.path.dirname(__file__), "static")
content = None
mime_type = "" mime_type = ""
filepath = "" filepath = ""
if len(url_split) > 3 and url_split[2] == "sequence_diagrams": if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
@@ -835,9 +837,71 @@ class HttpHandler(BaseHTTPRequestHandler):
if mime_type == "" or not filepath: if mime_type == "" or not filepath:
raise ValueError("Unknown file type or path") raise ValueError("Unknown file type or path")
file_stat = os.stat(filepath)
mtime = file_stat.st_mtime
file_size = file_stat.st_size
etag_hash = hashlib.md5(f"{file_size}-{mtime}".encode()).hexdigest()
etag = f'"{etag_hash}"'
last_modified = formatdate(mtime, usegmt=True)
if_none_match = self.headers.get("If-None-Match")
if if_none_match:
if if_none_match.strip() == "*" or etag in [
t.strip() for t in if_none_match.split(",")
]:
self.send_response(304)
self.send_header("ETag", etag)
self.send_header("Cache-Control", "public")
self.end_headers()
return b""
if_modified_since = self.headers.get("If-Modified-Since")
if if_modified_since and not if_none_match:
try:
ims_time = parsedate_to_datetime(if_modified_since)
file_time = datetime.fromtimestamp(int(mtime), tz=timezone.utc)
if file_time <= ims_time:
self.send_response(304)
self.send_header("Last-Modified", last_modified)
self.send_header("Cache-Control", "public")
self.end_headers()
return b""
except (TypeError, ValueError):
pass
is_lib = len(url_split) > 4 and url_split[3] == "libs"
if is_lib:
cache_control = "public, max-age=31536000, immutable"
elif url_split[2] in ("css", "js"):
cache_control = "public, max-age=3600, must-revalidate"
elif url_split[2] in ("images", "sequence_diagrams"):
cache_control = "public, max-age=86400"
else:
cache_control = "public, max-age=3600"
with open(filepath, "rb") as fp: with open(filepath, "rb") as fp:
content = fp.read() content = fp.read()
self.putHeaders(status_code, mime_type)
extra_headers = [
("Cache-Control", cache_control),
("Last-Modified", last_modified),
("ETag", etag),
]
is_compressible = mime_type in (
"text/css; charset=utf-8",
"application/javascript",
"image/svg+xml",
)
accept_encoding = self.headers.get("Accept-Encoding", "")
if is_compressible and "gzip" in accept_encoding:
content = gzip.compress(content)
extra_headers.append(("Content-Encoding", "gzip"))
extra_headers.append(("Vary", "Accept-Encoding"))
extra_headers.append(("Content-Length", str(len(content))))
self.putHeaders(status_code, mime_type, extra_headers=extra_headers)
return content return content
except FileNotFoundError: except FileNotFoundError:
+10 -6
View File
@@ -2,13 +2,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
# Copyright (c) 2025 The Basicswap developers # Copyright (c) 2025-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import threading import threading
from enum import IntEnum from enum import IntEnum
from typing import List
from basicswap.chainparams import ( from basicswap.chainparams import (
chainparams, chainparams,
@@ -30,6 +31,7 @@ from basicswap.util.ecc import (
) )
from coincurve.dleag import verify_secp256k1_point from coincurve.dleag import verify_secp256k1_point
from coincurve.keys import ( from coincurve.keys import (
PrivateKey,
PublicKey, PublicKey,
) )
@@ -179,13 +181,16 @@ class CoinInterface:
class AdaptorSigInterface: class AdaptorSigInterface:
def getScriptLockTxDummyWitness(self, script: bytes): def getP2WPKHDummyWitness(self) -> List[bytes]:
return [bytes(72), bytes(33)]
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
return [b"", bytes(72), bytes(72), bytes(len(script))] return [b"", bytes(72), bytes(72), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes): def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))] return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes): def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), b"", bytes(len(script))] return [bytes(72), b"", bytes(len(script))]
@@ -227,8 +232,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
return pubkey.verify(sig, signed_hash, hasher=None) return pubkey.verify(sig, signed_hash, hasher=None)
def sumKeys(self, ka: bytes, kb: bytes) -> bytes: def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
# TODO: Add to coincurve return PrivateKey(ka).add(kb).secret
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes: def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
+1895 -75
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -132,7 +132,7 @@ class DASHInterface(BTCInterface):
self.unlockWallet(old_password, check_seed=False) self.unlockWallet(old_password, check_seed=False)
seed_id_before: str = self.getWalletSeedID() seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [new_password]) self.rpc_wallet("encryptwallet", [new_password], timeout=120)
if check_seed is False or seed_id_before == "Not found": if check_seed is False or seed_id_before == "Not found":
return return
@@ -156,4 +156,6 @@ class DASHInterface(BTCInterface):
if self.isWalletEncrypted(): if self.isWalletEncrypted():
raise ValueError("Old password must be set") raise ValueError("Old password must be set")
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt) return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
self.rpc_wallet("walletpassphrasechange", [old_password, new_password]) self.rpc_wallet(
"walletpassphrasechange", [old_password, new_password], timeout=120
)
+69 -20
View File
@@ -188,6 +188,10 @@ class DCRInterface(Secp256k1Interface):
def coin_type(): def coin_type():
return Coins.DCR return Coins.DCR
@staticmethod
def useBackend() -> bool:
return False
@staticmethod @staticmethod
def exp() -> int: def exp() -> int:
return 8 return 8
@@ -364,7 +368,9 @@ class DCRInterface(Secp256k1Interface):
# Read initial pwd from settings # Read initial pwd from settings
settings = self._sc.getChainClientSettings(self.coin_type()) settings = self._sc.getChainClientSettings(self.coin_type())
old_password = settings["wallet_pwd"] old_password = settings["wallet_pwd"]
self.rpc_wallet("walletpassphrasechange", [old_password, new_password]) self.rpc_wallet(
"walletpassphrasechange", [old_password, new_password], timeout=120
)
# Lock wallet to match other coins # Lock wallet to match other coins
self.rpc_wallet("walletlock") self.rpc_wallet("walletlock")
@@ -378,7 +384,7 @@ class DCRInterface(Secp256k1Interface):
self._log.info("unlockWallet - {}".format(self.ticker())) self._log.info("unlockWallet - {}".format(self.ticker()))
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000]) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
if check_seed: if check_seed:
self._sc.checkWalletSeed(self.coin_type()) self._sc.checkWalletSeed(self.coin_type())
@@ -632,6 +638,15 @@ class DCRInterface(Secp256k1Interface):
# TODO: filter errors # TODO: filter errors
return None return None
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
try:
return self.rpc_wallet(
"listtransactions", ["*", count, skip, include_watchonly]
)
except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}")
return []
def getProofOfFunds(self, amount_for, extra_commit_bytes): def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid # TODO: Lock unspent and use same output/s to fund bid
@@ -1055,6 +1070,9 @@ class DCRInterface(Secp256k1Interface):
def describeTx(self, tx_hex: str): def describeTx(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex]) return self.rpc("decoderawtransaction", [tx_hex])
def decodeRawTransaction(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex])
def fundTx(self, tx: bytes, feerate) -> bytes: def fundTx(self, tx: bytes, feerate) -> bytes:
feerate_str = float(self.format_amount(feerate)) feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
@@ -1723,15 +1741,19 @@ class DCRInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, script_pk)) tx.vout.append(self.txoType()(output_amount, script_pk))
return tx.serialize() return tx.serialize()
def publishBLockTx( def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
) -> bytes:
b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate) b_lock_tx = self.fundTx(b_lock_tx, feerate)
script_pk = self.getPkDest(Kbs)
funded_tx = self.loadTx(b_lock_tx)
lock_vout = findOutput(funded_tx, script_pk)
b_lock_tx = self.signTxWithWallet(b_lock_tx) b_lock_tx = self.signTxWithWallet(b_lock_tx)
return bytes.fromhex(self.publishTx(b_lock_tx)) txid = bytes.fromhex(self.publishTx(b_lock_tx))
return txid, lock_vout
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int: def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
witness_bytes = 115 witness_bytes = 115
@@ -1755,26 +1777,53 @@ class DCRInterface(Secp256k1Interface):
lock_tx_vout=None, lock_tx_vout=None,
) -> bytes: ) -> bytes:
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex()) self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
locked_n = lock_tx_vout
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
if locked_n is None: locked_n = None
self._log.debug( actual_value = None
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}" wtx = self.rpc_wallet(
"gettransaction",
[
chain_b_lock_txid.hex(),
],
)
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].value
else:
self._log.error(
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
) )
# When refunding a lock tx, it should be in the wallet as a sent tx for i, out in enumerate(lock_tx.vout):
wtx = self.rpc_wallet( self._log.debug(
"gettransaction", f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
[ )
chain_b_lock_txid.hex(),
], if (
locked_n is not None
and lock_tx_vout is not None
and locked_n != lock_tx_vout
):
self._log.warning(
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
f"for tx {chain_b_lock_txid.hex()}"
) )
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
locked_n = findOutput(lock_tx, script_pk)
ensure(locked_n is not None, "Output not found in tx") ensure(locked_n is not None, "Output not found in tx")
spend_value = cb_swap_value
if spend_actual_balance and actual_value is not None:
if actual_value != cb_swap_value:
self._log.warning(
f"spendBLockTx: Spending actual balance {actual_value}, "
f"not expected swap value {cb_swap_value}."
)
spend_value = actual_value
pkh_to = self.decodeAddress(address_to) pkh_to = self.decodeAddress(address_to)
tx = CTransaction() tx = CTransaction()
@@ -1783,10 +1832,10 @@ class DCRInterface(Secp256k1Interface):
chain_b_lock_txid_int = b2i(chain_b_lock_txid) chain_b_lock_txid_int = b2i(chain_b_lock_txid)
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0)) tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to))) tx.vout.append(self.txoType()(spend_value, self.getPubkeyHashDest(pkh_to)))
pay_fee = self.getBLockSpendTxFee(tx, b_fee) pay_fee = self.getBLockSpendTxFee(tx, b_fee)
tx.vout[0].value = cb_swap_value - pay_fee tx.vout[0].value = spend_value - pay_fee
b_lock_spend_tx = tx.serialize() b_lock_spend_tx = tx.serialize()
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs) b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
File diff suppressed because it is too large Load Diff
+96 -6
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -64,7 +64,7 @@ class FIROInterface(BTCInterface):
# Firo shuts down after encryptwallet # Firo shuts down after encryptwallet
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found" seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
self.rpc_wallet("encryptwallet", [password]) self.rpc_wallet("encryptwallet", [password], timeout=120)
if check_seed is False or seed_id_before == "Not found": if check_seed is False or seed_id_before == "Not found":
return return
@@ -102,6 +102,100 @@ class FIROInterface(BTCInterface):
return addr_info["ismine"] return addr_info["ismine"]
return addr_info["ismine"] or addr_info["iswatchonly"] return addr_info["ismine"] or addr_info["iswatchonly"]
def getNewSparkAddress(self) -> str:
try:
return self.rpc_wallet("getnewsparkaddress")[0]
except Exception as e:
self._log.error(f"getnewsparkaddress failed: {str(e)}")
raise
def getNewStealthAddress(self):
"""Get a new Spark address (alias for consistency with other coins)."""
return self.getNewSparkAddress()
def getWalletInfo(self):
"""Get wallet info including Spark balance."""
rv = super(FIROInterface, self).getWalletInfo()
try:
spark_balance_info = self.rpc("getsparkbalance")
# getsparkbalance returns amounts in atomic units (satoshis)
# Field names: availableBalance, unconfirmedBalance, fullBalance
confirmed = spark_balance_info.get("availableBalance", 0)
unconfirmed = spark_balance_info.get("unconfirmedBalance", 0)
full_balance = spark_balance_info.get("fullBalance", 0)
# Values are already in atomic units, keep as integers
# basicswap.py will format them using format_amount
rv["spark_balance"] = confirmed if confirmed else 0
rv["spark_unconfirmed"] = unconfirmed if unconfirmed else 0
immature = full_balance - confirmed - unconfirmed
rv["spark_immature"] = immature if immature > 0 else 0
except Exception as e:
self._log.warning(f"getsparkbalance failed: {str(e)}")
rv["spark_balance"] = 0
rv["spark_unconfirmed"] = 0
rv["spark_immature"] = 0
return rv
def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance()
if spendable_balance < value_sats:
raise ValueError("Balance too low")
address = self.getNewAddress(self._use_segwit, "create_utxo")
return (
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
address,
)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
"""Withdraw coins, supporting both transparent and Spark transactions.
Args:
value: Amount to withdraw
type_from: "plain" for transparent, "spark" for Spark
addr_to: Destination address
subfee: Whether to subtract fee from amount
"""
type_to = "spark" if addr_to.startswith("sm1") else "plain"
if "spark" in (type_from, type_to):
# RPC format: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}}
# RPC wrapper will serialize this as: {"method": "spendspark", "params": [{...}], ...}
try:
if type_from == "spark":
# Construct params: dict where address is the key, wrapped in array for RPC
params = [
{"address": addr_to, "amount": value, "subtractfee": subfee}
]
result = self.rpc_wallet("spendspark", params)
else:
# Use automintspark to perform a plain -> spark tx of full balance
balance = self.rpc_wallet("getbalance")
if str(balance) == str(value):
result = self.rpc_wallet("automintspark")
else:
# subfee param is available on plain -> spark transactions
mint_params = {"amount": value}
if subfee:
mint_params["subfee"] = True
params = [{addr_to: mint_params}]
result = self.rpc_wallet("mintspark", params)
# spendspark returns a txid string directly, in a result dict, or as an array
if isinstance(result, list) and len(result) > 0:
return result[0]
if isinstance(result, dict):
return result.get("txid", result.get("tx", ""))
return result
except Exception as e:
self._log.error(f"spark tx failed: {str(e)}")
raise
else:
# Use standard sendtoaddress for transparent transactions
params = [addr_to, value, "", "", subfee]
return self.rpc_wallet("sendtoaddress", params)
def getSCLockScriptAddress(self, lock_script: bytes) -> str: def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest) address = self.encodeScriptDest(lock_tx_dest)
@@ -252,10 +346,6 @@ class FIROInterface(BTCInterface):
assert len(script_hash) == 20 assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL]) return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getWalletSeedID(self): def getWalletSeedID(self):
return self.rpc("getwalletinfo")["hdmasterkeyid"] return self.rpc("getwalletinfo")["hdmasterkeyid"]
+240 -12
View File
@@ -26,13 +26,103 @@ class LTCInterface(BTCInterface):
wallet=self._rpc_wallet_mweb, wallet=self._rpc_wallet_mweb,
) )
def checkWallets(self) -> int:
if self._connection_type == "electrum":
wm = self.getWalletManager()
if wm and wm.isInitialized(self.coin_type()):
return 1
return 0
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
self._log.debug(
f"Wallet: {self._rpc_wallet} not active, attempting to load."
)
try:
self.rpc(
"loadwallet",
[
self._rpc_wallet,
],
)
wallets = self.rpc("listwallets")
except Exception as e:
self._log.debug(f'Error loading wallet "{self._rpc_wallet}": {e}.')
if "does not exist" in str(e) or "Path does not exist" in str(e):
try:
wallet_dirs = self.rpc("listwalletdir")
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
except Exception:
existing = []
if len(existing) == 0:
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
)
try:
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
"",
False,
self._use_descriptors,
],
)
wallets = self.rpc("listwallets")
if self.getWalletSeedID() == "Not found":
self._log.info(
f"Initializing HD seed for {self.coin_name()}."
)
self._sc.initialiseWallet(self.coin_type())
except Exception as create_e:
self._log.error(f"Error creating wallet: {create_e}")
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.warning(f"Changing {self.ticker()} wallet name.")
for wallet_name in wallets:
if wallet_name in ("mweb",):
continue
change_watchonly_wallet: bool = (
self._rpc_wallet_watch == self._rpc_wallet
)
self._rpc_wallet = wallet_name
self._log.info(
f"Switched {self.ticker()} wallet name to {self._rpc_wallet}."
)
self.rpc_wallet = make_rpc_func(
self._rpcport,
self._rpcauth,
host=self._rpc_host,
wallet=self._rpc_wallet,
)
if change_watchonly_wallet:
self.rpc_wallet_watch = self.rpc_wallet
break
return len(wallets)
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str: def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"]) return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
def getNewStealthAddress(self, label=""): def getNewStealthAddress(self, label=""):
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
return self.getNewMwebAddress(False, label) return self.getNewMwebAddress(False, label)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str: def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
if self.useBackend():
if type_from == "mweb":
raise ValueError("MWEB withdrawals not supported in electrum mode")
return self._withdrawCoinElectrum(value, addr_to, subfee)
params = [addr_to, value, "", "", subfee, True, self._conf_target] params = [addr_to, value, "", "", subfee, True, self._conf_target]
if type_from == "mweb": if type_from == "mweb":
return self.rpc_wallet_mweb("sendtoaddress", params) return self.rpc_wallet_mweb("sendtoaddress", params)
@@ -53,14 +143,27 @@ class LTCInterface(BTCInterface):
def getWalletInfo(self): def getWalletInfo(self):
rv = super(LTCInterface, self).getWalletInfo() rv = super(LTCInterface, self).getWalletInfo()
mweb_info = self.rpc_wallet_mweb("getwalletinfo") if not self.useBackend():
rv["mweb_balance"] = mweb_info["balance"] try:
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"] mweb_info = self.rpc_wallet_mweb("getwalletinfo")
rv["mweb_immature"] = mweb_info["immature_balance"] rv["mweb_balance"] = mweb_info["balance"]
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
rv["mweb_immature"] = mweb_info["immature_balance"]
except Exception:
pass
return rv return rv
def getUnspentsByAddr(self): def getUnspentsByAddr(self):
unspent_addr = dict() unspent_addr = dict()
if self.useBackend():
wm = self.getWalletManager()
if wm:
addresses = wm.getAllAddresses(self.coin_type())
if addresses:
return self._backend.getBalance(addresses)
return unspent_addr
unspent = self.rpc_wallet("listunspent") unspent = self.rpc_wallet("listunspent")
for u in unspent: for u in unspent:
if u.get("spendable", False) is False: if u.get("spendable", False) is False:
@@ -86,6 +189,82 @@ class LTCInterface(BTCInterface):
) + self.make_int(u["amount"], r=1) ) + self.make_int(u["amount"], r=1)
return unspent_addr return unspent_addr
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "":
return
self._log.info("unlockWallet - {}".format(self.ticker()))
if self.useBackend():
return
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
try:
self.rpc("loadwallet", [self._rpc_wallet])
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
try:
wallet_dirs = self.rpc("listwalletdir")
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
except Exception:
existing = []
if len(existing) == 0:
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
password,
False,
self._use_descriptors,
],
)
else:
raise
try:
seed_id = self.getWalletSeedID()
needs_seed_init = seed_id == "Not found"
except Exception as e:
self._log.debug(f"getWalletSeedID failed: {e}")
needs_seed_init = True
if needs_seed_init:
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
self._sc.initialiseWallet(self.coin_type())
if password:
self._log.info(f"Encrypting {self.coin_name()} wallet.")
try:
self.rpc_wallet("encryptwallet", [password], timeout=120)
except Exception as e:
self._log.debug(f"encryptwallet returned: {e}")
import time
for i in range(10):
time.sleep(1)
try:
self.rpc("listwallets")
break
except Exception:
self._log.debug(
f"Waiting for wallet after encryption... {i + 1}/10"
)
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
self.rpc("loadwallet", [self._rpc_wallet])
self.setWalletSeedWarning(False)
check_seed = False
if self.isWalletEncrypted():
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
class LTCInterfaceMWEB(LTCInterface): class LTCInterfaceMWEB(LTCInterface):
@@ -129,13 +308,50 @@ class LTCInterfaceMWEB(LTCInterface):
self._log.info("init_wallet - {}".format(self.ticker())) self._log.info("init_wallet - {}".format(self.ticker()))
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.") wallets = self.rpc("listwallets")
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup if self._rpc_wallet not in wallets:
self.rpc("createwallet", ["mweb", False, True, password, False, False, True]) try:
self.rpc("loadwallet", [self._rpc_wallet])
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".')
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
password,
False,
self._use_descriptors,
],
)
else:
raise
wallets = self.rpc("listwallets")
if "mweb" not in wallets:
try:
self.rpc("loadwallet", ["mweb"])
self._log.debug("Loaded existing MWEB wallet.")
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info(f"Creating MWEB wallet for {self.coin_name()}.")
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
self.rpc(
"createwallet",
["mweb", False, True, password, False, False, True],
)
else:
raise
if password is not None: if password is not None:
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000]) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
if self.getWalletSeedID() == "Not found": if self.getWalletSeedID() == "Not found":
self._sc.initialiseWallet(self.interface_type()) self._sc.initialiseWallet(self.interface_type())
@@ -144,7 +360,7 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc("unloadwallet", ["mweb"]) self.rpc("unloadwallet", ["mweb"])
self.rpc("loadwallet", ["mweb"]) self.rpc("loadwallet", ["mweb"])
if password is not None: if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000]) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill") self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str, check_seed: bool = True) -> None: def unlockWallet(self, password: str, check_seed: bool = True) -> None:
@@ -152,10 +368,22 @@ class LTCInterfaceMWEB(LTCInterface):
return return
self._log.info("unlockWallet - {}".format(self.ticker())) self._log.info("unlockWallet - {}".format(self.ticker()))
if self.useBackend():
return
if not self.has_mweb_wallet(): if not self.has_mweb_wallet():
self.init_wallet(password) self.init_wallet(password)
else: else:
# Max timeout value, ~3 years self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("walletpassphrase", [password, 100000000]) try:
seed_id = self.getWalletSeedID()
needs_seed_init = seed_id == "Not found"
except Exception as e:
self._log.debug(f"getWalletSeedID failed: {e}")
needs_seed_init = True
if needs_seed_init:
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
self._sc.initialiseWallet(self.interface_type())
if check_seed: if check_seed:
self._sc.checkWalletSeed(self.coin_type()) self._sc.checkWalletSeed(self.interface_type())
+12 -5
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -137,7 +137,7 @@ class PARTInterface(BTCInterface):
def getScriptDummyWitness(self, script: bytes) -> List[bytes]: def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script): if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
return [bytes(72), bytes(33)] return self.getP2WPKHDummyWitness()
raise ValueError("Unknown script type") raise ValueError("Unknown script type")
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str: def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
@@ -146,9 +146,16 @@ class PARTInterface(BTCInterface):
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey) return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
def getWitnessStackSerialisedLength(self, witness_stack) -> int: def getWitnessStackSerialisedLength(self, witness_stack) -> int:
length: int = getCompactSizeLen(len(witness_stack)) length: int = 0
for e in witness_stack: if len(witness_stack) > 0 and isinstance(witness_stack[0], list):
length += getWitnessElementLen(len(e)) for input_stack in witness_stack:
length += getCompactSizeLen(len(input_stack))
for e in input_stack:
length += getWitnessElementLen(len(e))
else:
length += getCompactSizeLen(len(witness_stack))
for e in witness_stack:
length += getWitnessElementLen(len(e))
return length return length
def getWalletRestoreHeight(self) -> int: def getWalletRestoreHeight(self) -> int:
+1 -1
View File
@@ -40,7 +40,7 @@ class PIVXInterface(BTCInterface):
seed_id_before: str = self.getWalletSeedID() seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [password]) self.rpc_wallet("encryptwallet", [password], timeout=120)
if check_seed is False or seed_id_before == "Not found": if check_seed is False or seed_id_before == "Not found":
return return
+25
View File
@@ -94,6 +94,9 @@ class XMRInterface(CoinInterface):
"failed to get output distribution", "failed to get output distribution",
"request-sent", "request-sent",
"idle", "idle",
"busy",
"responsenotready",
"connection",
] ]
): ):
return True return True
@@ -832,3 +835,25 @@ class XMRInterface(CoinInterface):
] ]
}, },
) )
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
try:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
rv = self.rpc_wallet(
"get_transfers",
{"in": True, "out": True, "pending": True, "failed": True},
)
transactions = []
for tx_type in ["in", "out", "pending", "failed"]:
if tx_type in rv:
for tx in rv[tx_type]:
tx["type"] = tx_type
transactions.append(tx)
transactions.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
return (
transactions[skip : skip + count] if count else transactions[skip:]
)
except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}")
return []
+353 -10
View File
@@ -79,9 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
txid_hex = swap_client.withdrawParticl( txid_hex = swap_client.withdrawParticl(
type_from, type_to, value, address, subfee type_from, type_to, value, address, subfee
) )
elif coin_type == Coins.LTC: elif coin_type in (Coins.LTC, Coins.FIRO):
type_from = get_data_entry_or(post_data, "type_from", "plain") type_from = get_data_entry_or(post_data, "type_from", "plain")
txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee) txid_hex = swap_client.withdrawCoinExtended(
coin_type, type_from, value, address, subfee
)
elif coin_type in (Coins.XMR, Coins.WOW): elif coin_type in (Coins.XMR, Coins.WOW):
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall) txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
else: else:
@@ -135,7 +137,7 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
for k, v in swap_client.coin_clients.items(): for k, v in swap_client.coin_clients.items():
if k not in chainparams: if k not in chainparams:
continue continue
if v["connection_type"] == "rpc": if v["connection_type"] in ("rpc", "electrum"):
balance = "0.0" balance = "0.0"
if k in wallets: if k in wallets:
@@ -168,8 +170,29 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
"balance": balance, "balance": balance,
"pending": pending, "pending": pending,
"ticker": chainparams[k]["ticker"], "ticker": chainparams[k]["ticker"],
"connection_type": v["connection_type"],
} }
ci = swap_client.ci(k)
if hasattr(ci, "getScanStatus"):
coin_entry["scan_status"] = ci.getScanStatus()
if hasattr(ci, "getElectrumServer"):
server = ci.getElectrumServer()
if server:
coin_entry["electrum_server"] = server
version = ci.getDaemonVersion()
if version:
coin_entry["version"] = version
if (
v["connection_type"] == "electrum"
and hasattr(ci, "_backend")
and ci._backend
and hasattr(ci._backend, "getSyncStatus")
):
sync_status = ci._backend.getSyncStatus()
coin_entry["electrum_synced"] = sync_status.get("synced", False)
coin_entry["electrum_height"] = sync_status.get("height", 0)
coins_with_balances.append(coin_entry) coins_with_balances.append(coin_entry)
if k == Coins.PART: if k == Coins.PART:
@@ -293,6 +316,9 @@ def js_wallets(self, url_split, post_string, is_json):
elif cmd == "reseed": elif cmd == "reseed":
swap_client.reseedWallet(coin_type) swap_client.reseedWallet(coin_type)
return bytes(json.dumps({"reseeded": True}), "UTF-8") return bytes(json.dumps({"reseeded": True}), "UTF-8")
elif cmd == "rescan":
result = swap_client.rescanWalletAddresses(coin_type)
return bytes(json.dumps(result), "UTF-8")
elif cmd == "newstealthaddress": elif cmd == "newstealthaddress":
if coin_type != Coins.PART: if coin_type != Coins.PART:
raise ValueError("Invalid coin for command") raise ValueError("Invalid coin for command")
@@ -306,6 +332,31 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes( return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8" json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
) )
elif cmd == "watchaddress":
post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, "address")
label = get_data_entry_or(post_data, "label", "manual_import")
wm = swap_client.getWalletManager()
if wm is None:
raise ValueError("WalletManager not available")
wm.importWatchOnlyAddress(
coin_type, address, label=label, source="manual_import"
)
return bytes(json.dumps({"success": True, "address": address}), "UTF-8")
elif cmd == "listaddresses":
wm = swap_client.getWalletManager()
if wm is None:
raise ValueError("WalletManager not available")
addresses = wm.getAllAddresses(coin_type)
return bytes(json.dumps({"addresses": addresses}), "UTF-8")
elif cmd == "fixseedid":
root_key = swap_client.getWalletKey(coin_type, 1)
swap_client.storeSeedIDForCoin(root_key, coin_type)
swap_client.checkWalletSeed(coin_type)
return bytes(
json.dumps({"success": True, "message": "Seed IDs updated"}),
"UTF-8",
)
raise ValueError("Unknown command") raise ValueError("Unknown command")
if coin_type == Coins.LTC_MWEB: if coin_type == Coins.LTC_MWEB:
@@ -599,8 +650,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
) )
if have_data_entry(post_data, "debugind"): if have_data_entry(post_data, "debugind"):
main_debug_ind: bool = toBool(
get_data_entry_or(post_data, "maindebugind", True)
)
swap_client.setBidDebugInd( swap_client.setBidDebugInd(
bid_id, int(get_data_entry(post_data, "debugind")) bid_id,
int(get_data_entry(post_data, "debugind")),
add_to_bid=main_debug_ind,
) )
rv = {"bid_id": bid_id.hex()} rv = {"bid_id": bid_id.hex()}
@@ -618,8 +674,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
elif have_data_entry(post_data, "abandon"): elif have_data_entry(post_data, "abandon"):
swap_client.abandonBid(bid_id) swap_client.abandonBid(bid_id)
elif have_data_entry(post_data, "debugind"): elif have_data_entry(post_data, "debugind"):
main_debug_ind: bool = toBool(
get_data_entry_or(post_data, "maindebugind", True)
)
swap_client.setBidDebugInd( swap_client.setBidDebugInd(
bid_id, int(get_data_entry(post_data, "debugind")) bid_id,
int(get_data_entry(post_data, "debugind")),
add_to_bid=main_debug_ind,
) )
if have_data_entry(post_data, "show_extra"): if have_data_entry(post_data, "show_extra"):
@@ -628,7 +689,9 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
with_events = True with_events = True
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id) bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
assert bid, "Unknown bid ID" if bid is None:
swap_client.log.debug(f"js_bids: Unknown bid id {bid_id.hex()}")
return bytes(json.dumps({"error": "Unknown bid id"}), "UTF-8")
if post_string != "": if post_string != "":
if have_data_entry(post_data, "chainbkeysplit"): if have_data_entry(post_data, "chainbkeysplit"):
@@ -1208,10 +1271,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
"current_seed_id": wallet_seed_id, "current_seed_id": wallet_seed_id,
} }
) )
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
rv.update( if hasattr(ci, "getAccountKey"):
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)} try:
) # Master key can be imported into electrum (Must set prefix for P2WPKH) rv.update({"account_key": ci.getAccountKey(seed_key, extkey_prefix)})
except Exception as e:
rv.update({"account_key_error": str(e)})
return bytes( return bytes(
json.dumps(rv), json.dumps(rv),
@@ -1526,6 +1591,71 @@ def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
) )
def js_wallettransactions(self, url_split, post_string, is_json) -> bytes:
from basicswap.ui.page_wallet import format_transactions
import time
TX_CACHE_DURATION = 30
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
if len(url_split) < 4:
return bytes(json.dumps({"error": "No coin specified"}), "UTF-8")
ticker_str = url_split[3]
coin_id = getCoinIdFromTicker(ticker_str)
post_data = {} if post_string == "" else getFormData(post_string, is_json)
page_no = 1
limit = 30
offset = 0
if have_data_entry(post_data, "page_no"):
page_no = int(get_data_entry(post_data, "page_no"))
if page_no < 1:
page_no = 1
if page_no > 1:
offset = (page_no - 1) * limit
try:
ci = swap_client.ci(coin_id)
current_time = time.time()
cache_entry = swap_client._tx_cache.get(coin_id)
if (
cache_entry is None
or (current_time - cache_entry["time"]) > TX_CACHE_DURATION
):
all_txs = ci.listWalletTransactions(count=10000, skip=0)
all_txs = list(reversed(all_txs)) if all_txs else []
swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time}
else:
all_txs = cache_entry["txs"]
total_transactions = len(all_txs)
raw_txs = all_txs[offset : offset + limit] if all_txs else []
transactions = format_transactions(ci, raw_txs, coin_id)
return bytes(
json.dumps(
{
"transactions": transactions,
"page_no": page_no,
"total": total_transactions,
"limit": limit,
"total_pages": (total_transactions + limit - 1) // limit,
}
),
"UTF-8",
)
except Exception as e:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
def js_messageroutes(self, url_split, post_string, is_json) -> bytes: def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json) post_data = {} if post_string == "" else getFormData(post_string, is_json)
@@ -1569,10 +1699,221 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(message_routes), "UTF-8") return bytes(json.dumps(message_routes), "UTF-8")
def js_modeswitchinfo(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
coin_str = get_data_entry(post_data, "coin")
direction = get_data_entry_or(post_data, "direction", "lite")
try:
coin_type = getCoinIdFromName(coin_str)
except Exception:
coin_type = getCoinIdFromTicker(coin_str.upper())
ci = swap_client.ci(coin_type)
ticker = ci.ticker()
try:
wallet_info = ci.getWalletInfo()
balance = wallet_info.get("balance", 0)
balance_sats = ci.make_int(balance)
except Exception as e:
return bytes(json.dumps({"error": f"Failed to get balance: {e}"}), "UTF-8")
try:
fee_rate, rate_src = ci.get_fee_rate(ci._conf_target)
est_vsize = 180
if isinstance(fee_rate, int):
fee_per_vbyte = max(1, fee_rate // 1000)
else:
fee_per_vbyte = max(1, int(fee_rate * 100000))
estimated_fee_sats = est_vsize * fee_per_vbyte
except Exception:
estimated_fee_sats = 180
rate_src = "default"
min_viable = estimated_fee_sats * 2
can_transfer = balance_sats > min_viable
rv = {
"coin": ticker,
"direction": direction,
"balance": balance,
"balance_sats": balance_sats,
"estimated_fee_sats": estimated_fee_sats,
"estimated_fee": ci.format_amount(estimated_fee_sats),
"fee_rate_src": rate_src,
"can_transfer": can_transfer,
"min_viable_sats": min_viable,
}
if direction == "lite":
non_bip84_balance_sats = 0
has_non_bip84_funds = False
try:
if hasattr(ci, "rpc_wallet"):
unspent = ci.rpc_wallet("listunspent")
wm = swap_client.getWalletManager()
bip84_addresses = set()
if wm:
try:
all_addrs = wm.getAllAddresses(
coin_type, include_watch_only=False
)
bip84_addresses = set(all_addrs)
except Exception:
pass
for u in unspent:
addr = u.get("address")
if not addr:
continue
amount_sats = ci.make_int(u.get("amount", 0))
if amount_sats <= 0:
continue
if addr not in bip84_addresses:
non_bip84_balance_sats += amount_sats
has_non_bip84_funds = True
except Exception as e:
swap_client.log.debug(f"Error checking non-BIP84 addresses: {e}")
if has_non_bip84_funds and non_bip84_balance_sats > min_viable:
rv["show_transfer_option"] = True
rv["require_transfer"] = True
rv["legacy_balance_sats"] = non_bip84_balance_sats
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
rv["message"] = (
"Funds on non-derivable addresses must be transferred for external wallet compatibility"
)
else:
rv["show_transfer_option"] = False
rv["require_transfer"] = False
if has_non_bip84_funds:
rv["legacy_balance_sats"] = non_bip84_balance_sats
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
rv["message"] = "Non-derivable balance too low to transfer"
else:
rv["legacy_balance_sats"] = 0
rv["legacy_balance"] = "0"
rv["message"] = "All funds on BIP84 addresses"
else:
rv["show_transfer_option"] = can_transfer
if balance_sats == 0:
rv["message"] = "No funds to transfer"
elif not can_transfer:
rv["message"] = "Balance too low to transfer (fee would exceed funds)"
else:
rv["message"] = ""
return bytes(json.dumps(rv), "UTF-8")
def js_electrum_discover(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
coin_str = get_data_entry(post_data, "coin")
do_ping = toBool(get_data_entry_or(post_data, "ping", "false"))
coin_type = None
try:
coin_id = int(coin_str)
coin_type = Coins(coin_id)
except ValueError:
try:
coin_type = getCoinIdFromName(coin_str)
except ValueError:
coin_type = getCoinType(coin_str)
electrum_supported = ["bitcoin", "litecoin"]
coin_name = chainparams.get(coin_type, {}).get("name", "").lower()
if coin_name not in electrum_supported:
return bytes(
json.dumps(
{"error": f"Electrum not supported for {coin_name}", "servers": []}
),
"UTF-8",
)
ci = swap_client.ci(coin_type)
connection_type = getattr(ci, "_connection_type", "rpc")
discovered_servers = []
current_server = None
if connection_type == "electrum":
backend = ci.getBackend()
if backend and hasattr(backend, "_server"):
server = backend._server
current_server = server.get_current_server_info()
discovered_servers = server.discover_peers()
if do_ping and discovered_servers:
for srv in discovered_servers[:10]:
latency = server.ping_server(
srv["host"], srv["port"], srv.get("ssl", True)
)
srv["latency_ms"] = latency
srv["online"] = latency is not None
else:
try:
from .interface.electrumx import ElectrumServer
temp_server = ElectrumServer(
coin_name,
log=swap_client.log,
)
temp_server.connect()
current_server = temp_server.get_current_server_info()
discovered_servers = temp_server.discover_peers()
if do_ping and discovered_servers:
for srv in discovered_servers[:10]:
latency = temp_server.ping_server(
srv["host"], srv["port"], srv.get("ssl", True)
)
srv["latency_ms"] = latency
srv["online"] = latency is not None
temp_server.disconnect()
except Exception as e:
return bytes(
json.dumps(
{
"error": f"Failed to connect to electrum server: {str(e)}",
"servers": [],
}
),
"UTF-8",
)
onion_servers = [s for s in discovered_servers if s.get("is_onion")]
clearnet_servers = [s for s in discovered_servers if not s.get("is_onion")]
return bytes(
json.dumps(
{
"coin": coin_name,
"current_server": current_server,
"clearnet_servers": clearnet_servers,
"onion_servers": onion_servers,
"total_discovered": len(discovered_servers),
}
),
"UTF-8",
)
endpoints = { endpoints = {
"coins": js_coins, "coins": js_coins,
"walletbalances": js_walletbalances, "walletbalances": js_walletbalances,
"wallets": js_wallets, "wallets": js_wallets,
"wallettransactions": js_wallettransactions,
"offers": js_offers, "offers": js_offers,
"sentoffers": js_sentoffers, "sentoffers": js_sentoffers,
"bids": js_bids, "bids": js_bids,
@@ -1602,6 +1943,8 @@ endpoints = {
"coinvolume": js_coinvolume, "coinvolume": js_coinvolume,
"coinhistory": js_coinhistory, "coinhistory": js_coinhistory,
"messageroutes": js_messageroutes, "messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo,
} }
+1 -4
View File
@@ -130,10 +130,7 @@ def redeemITx(self, bid_id: bytes, cursor):
bid.initiate_tx.spend_txid = bytes.fromhex(txid) bid.initiate_tx.spend_txid = bytes.fromhex(txid)
self.log.debug( self.log.debug(
"Submitted initiate redeem txn %s to %s chain for bid %s", f"Submitted initiate redeem txn {self.logIDT(txid)} to {ci_from.coin_name()} chain for bid {self.logIDB(bid_id)}"
txid,
ci_from.coin_name(),
bid_id.hex(),
) )
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor) self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
+1 -1
View File
@@ -119,7 +119,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
lock_tx_vout=lock_tx_vout, lock_tx_vout=lock_tx_vout,
) )
self.log.debug( self.log.debug(
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}." f"Submitted lock B spend txn {self.logIDT(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
) )
self.logBidEvent( self.logBidEvent(
bid.bid_id, bid.bid_id,
+24 -5
View File
@@ -152,15 +152,17 @@ class Jsonrpc:
pass pass
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"): def callrpc(
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
):
if _use_rpc_pooling: if _use_rpc_pooling:
return callrpc_pooled(rpc_port, auth, method, params, wallet, host) return callrpc_pooled(rpc_port, auth, method, params, wallet, host, timeout)
try: try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port) url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None: if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet) url += "wallet/" + urllib.parse.quote(wallet)
x = Jsonrpc(url) x = Jsonrpc(url, timeout=timeout if timeout else 10)
v = x.json_request(method, params) v = x.json_request(method, params)
x.close() x.close()
@@ -174,7 +176,9 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
return r["result"] return r["result"]
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"): def callrpc_pooled(
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
):
from .rpc_pool import get_rpc_pool from .rpc_pool import get_rpc_pool
import http.client import http.client
import socket import socket
@@ -183,6 +187,20 @@ def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0
if wallet is not None: if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet) url += "wallet/" + urllib.parse.quote(wallet)
if timeout:
try:
conn = Jsonrpc(url, timeout=timeout)
v = conn.json_request(method, params)
r = json.loads(v.decode("utf-8"))
conn.close()
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
return r["result"]
except ValueError:
raise
except Exception as ex:
raise ValueError(f"RPC server error: {ex}, method: {method}")
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5) max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
pool = get_rpc_pool(url, max_connections) pool = get_rpc_pool(url, max_connections)
@@ -247,7 +265,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
wallet = wallet wallet = wallet
host = host host = host
def rpc_func(method, params=None, wallet_override=None): def rpc_func(method, params=None, wallet_override=None, timeout=None):
return callrpc( return callrpc(
port, port,
auth, auth,
@@ -255,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
params, params,
wallet if wallet_override is None else wallet_override, wallet if wallet_override is None else wallet_override,
host, host,
timeout=timeout,
) )
return rpc_func return rpc_func
+97 -28
View File
@@ -2,15 +2,42 @@
'use strict'; 'use strict';
const EventHandlers = { const EventHandlers = {
confirmPopup: function(action = 'proceed', coinName = '') { showConfirmModal: function(title, message, callback) {
const message = action === 'Accept' const modal = document.getElementById('confirmModal');
? 'Are you sure you want to accept this bid?' if (!modal) {
: coinName if (callback) callback();
? `Are you sure you want to ${action} ${coinName}?` return;
: 'Are you sure you want to proceed?'; }
return confirm(message); const titleEl = document.getElementById('confirmTitle');
const messageEl = document.getElementById('confirmMessage');
const yesBtn = document.getElementById('confirmYes');
const noBtn = document.getElementById('confirmNo');
const bidDetails = document.getElementById('bidDetailsSection');
if (titleEl) titleEl.textContent = title;
if (messageEl) {
messageEl.textContent = message;
messageEl.classList.remove('hidden');
}
if (bidDetails) bidDetails.classList.add('hidden');
modal.classList.remove('hidden');
const newYesBtn = yesBtn.cloneNode(true);
yesBtn.parentNode.replaceChild(newYesBtn, yesBtn);
newYesBtn.addEventListener('click', function() {
modal.classList.add('hidden');
if (callback) callback();
});
const newNoBtn = noBtn.cloneNode(true);
noBtn.parentNode.replaceChild(newNoBtn, noBtn);
newNoBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
}, },
confirmReseed: function() { confirmReseed: function() {
@@ -18,7 +45,6 @@
}, },
confirmWithdrawal: function() { confirmWithdrawal: function() {
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') { if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
return window.WalletPage.confirmWithdrawal(); return window.WalletPage.confirmWithdrawal();
} }
@@ -67,14 +93,36 @@
return; return;
} }
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]'); let coinFromId;
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0; if (inputId === 'add-amm-amount') {
coinFromId = 'add-amm-coin-from';
} else if (inputId === 'edit-amm-amount') {
coinFromId = 'edit-amm-coin-from';
} else {
const form = amountInput.closest('form') || amountInput.closest('.modal-content') || amountInput.closest('[id*="modal"]');
const select = form?.querySelector('select[id*="coin-from"]');
coinFromId = select?.id;
}
const coinFromSelect = coinFromId ? document.getElementById(coinFromId) : null;
if (!coinFromSelect) {
console.error('EventHandlers: Coin-from dropdown not found for:', inputId);
return;
}
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption) {
console.error('EventHandlers: No option selected in coin-from dropdown');
return;
}
const balance = parseFloat(selectedOption.getAttribute('data-balance') || '0');
if (balance > 0) { if (balance > 0) {
const calculatedAmount = balance * percent; const calculatedAmount = balance * percent;
amountInput.value = calculatedAmount.toFixed(8); amountInput.value = calculatedAmount.toFixed(8);
} else { } else {
console.warn('EventHandlers: No balance found for AMM amount calculation'); console.warn('EventHandlers: No balance found for selected coin');
} }
}, },
@@ -132,13 +180,10 @@
}, },
hideConfirmModal: function() { hideConfirmModal: function() {
if (window.DOMCache) { const modal = document.getElementById('confirmModal');
window.DOMCache.hide('confirmModal'); if (modal) {
} else { modal.classList.add('hidden');
const modal = document.getElementById('confirmModal'); modal.style.display = '';
if (modal) {
modal.style.display = 'none';
}
} }
}, },
@@ -187,17 +232,43 @@
}, },
initialize: function() { initialize: function() {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm]'); const target = e.target.closest('[data-confirm]');
if (target) { if (target) {
if (target.dataset.confirmHandled) {
delete target.dataset.confirmHandled;
return;
}
e.preventDefault();
e.stopPropagation();
const action = target.getAttribute('data-confirm-action') || 'proceed'; const action = target.getAttribute('data-confirm-action') || 'proceed';
const coinName = target.getAttribute('data-confirm-coin') || ''; const coinName = target.getAttribute('data-confirm-coin') || '';
if (!this.confirmPopup(action, coinName)) { const message = action === 'Accept'
e.preventDefault(); ? 'Are you sure you want to accept this bid?'
return false; : coinName
} ? `Are you sure you want to ${action} ${coinName}?`
: 'Are you sure you want to proceed?';
const title = `Confirm ${action}`;
this.showConfirmModal(title, message, function() {
target.dataset.confirmHandled = 'true';
if (target.form) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = target.name;
hiddenInput.value = target.value;
target.form.appendChild(hiddenInput);
target.form.submit();
} else {
target.click();
}
});
} }
}); });
@@ -326,8 +397,6 @@
} }
window.EventHandlers = EventHandlers; window.EventHandlers = EventHandlers;
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers); window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers); window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers); window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
@@ -250,6 +250,7 @@ function ensureToastContainer() {
'new_bid': 'bg-green-500', 'new_bid': 'bg-green-500',
'bid_accepted': 'bg-purple-500', 'bid_accepted': 'bg-purple-500',
'swap_completed': 'bg-green-600', 'swap_completed': 'bg-green-600',
'sweep_completed': 'bg-orange-500',
'balance_change': 'bg-yellow-500', 'balance_change': 'bg-yellow-500',
'update_available': 'bg-blue-600', 'update_available': 'bg-blue-600',
'success': 'bg-blue-500' 'success': 'bg-blue-500'
@@ -609,7 +610,7 @@ function ensureToastContainer() {
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`; clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
cursorStyle = 'cursor-pointer'; cursorStyle = 'cursor-pointer';
} else if (options.coinSymbol) { } else if (options.coinSymbol) {
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`; clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol.toLowerCase()}'"`;
cursorStyle = 'cursor-pointer'; cursorStyle = 'cursor-pointer';
} else if (options.releaseUrl) { } else if (options.releaseUrl) {
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`; clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
@@ -735,6 +736,18 @@ function ensureToastContainer() {
shouldShowToast = config.showUpdateNotifications; shouldShowToast = config.showUpdateNotifications;
break; break;
case 'sweep_completed':
const sweepAmount = parseFloat(data.amount || 0).toFixed(8).replace(/\.?0+$/, '');
const sweepFee = parseFloat(data.fee || 0).toFixed(8).replace(/\.?0+$/, '');
const sweepTicker = data.ticker || data.coin_name;
toastTitle = `Swept ${sweepAmount} ${sweepTicker} to RPC wallet`;
toastOptions.subtitle = `Fee: ${sweepFee} ${sweepTicker} • TXID: ${(data.txid || '').substring(0, 12)}...`;
toastOptions.coinSymbol = sweepTicker;
toastOptions.txid = data.txid;
toastType = 'sweep_completed';
shouldShowToast = true;
break;
case 'coin_balance_updated': case 'coin_balance_updated':
if (data.coin && config.showBalanceChanges) { if (data.coin && config.showBalanceChanges) {
this.handleBalanceUpdate(data); this.handleBalanceUpdate(data);
+18 -1
View File
@@ -23,6 +23,11 @@
types: ['default'], types: ['default'],
hasSubfee: false, hasSubfee: false,
hasSweepAll: true hasSweepAll: true
},
13: {
types: ['plain', 'spark'],
hasSubfee: true,
hasSweepAll: false
} }
}, },
@@ -64,6 +69,17 @@
} }
} }
if (cid === 13) {
switch(selectedType) {
case 'plain':
return this.safeParseFloat(balances.main || balances.balance);
case 'spark':
return this.safeParseFloat(balances.spark);
default:
return this.safeParseFloat(balances.main || balances.balance);
}
}
return this.safeParseFloat(balances.main || balances.balance); return this.safeParseFloat(balances.main || balances.balance);
}, },
@@ -188,7 +204,8 @@
balance: balance, balance: balance,
blind: balance2, blind: balance2,
anon: balance3, anon: balance3,
mweb: balance2 mweb: balance2,
spark: balance2
}; };
WalletAmountManager.setAmount(percent, balances, coinId); WalletAmountManager.setAmount(percent, balances, coinId);
}; };
+614
View File
@@ -0,0 +1,614 @@
const BidPage = {
bidId: null,
bidStateInd: null,
createdAtTimestamp: null,
autoRefreshInterval: null,
elapsedTimeInterval: null,
AUTO_REFRESH_SECONDS: 60,
refreshPaused: false,
swapType: null,
coinFrom: null,
coinTo: null,
previousStateInd: null,
INACTIVE_STATES: [8, 17, 18, 19, 21, 22, 23, 25, 31], // Completed, Failed variants, Timed-out, Abandoned, Error, Rejected, Expired
DELAYING_STATE: 20,
STATE_TOOLTIPS: {
'Bid Sent': 'Your bid has been broadcast to the network',
'Bid Receiving': 'Receiving partial bid message from the network',
'Bid Received': 'Bid received and waiting for decision to accept or reject',
'Bid Receiving accept': 'Receiving acceptance message from the other party',
'Bid Accepted': 'Bid accepted. The atomic swap process is starting',
'Bid Initiated': 'Swap initiated. First lock transaction is being created',
'Bid Participating': 'Participating in the swap. Second lock transaction is being created',
'Bid Completed': 'Swap completed successfully! Both parties received their coins',
'Bid Script coin locked': null,
'Bid Script coin spend tx valid': null,
'Bid Scriptless coin locked': null,
'Bid Script coin lock released': 'Adaptor signature revealed. The script coin can now be claimed',
'Bid Script tx redeemed': null,
'Bid Script pre-refund tx in chain': 'Pre-refund transaction detected. Swap may be failing',
'Bid Scriptless tx redeemed': null,
'Bid Scriptless tx recovered': null,
'Bid Failed, refunded': 'Swap failed but your coins have been refunded',
'Bid Failed, swiped': 'Swap failed due to an unexpected issue. Please check the event log for details',
'Bid Failed': 'Swap failed. Check events for details',
'Bid Delaying': 'Brief delay between swap steps to ensure network propagation',
'Bid Timed-out': 'Swap timed out waiting for the other party',
'Bid Abandoned': 'Swap was manually abandoned. Locked coins will be refunded after timelock',
'Bid Error': 'An error occurred. Check events for details',
'Bid Rejected': 'Bid was rejected by the offer owner',
'Bid Stalled (debug)': 'Debug mode: swap intentionally stalled for testing',
'Bid Exchanged script lock tx sigs msg': 'Exchanging adaptor signatures needed for lock transactions',
'Bid Exchanged script lock spend tx msg': 'Exchanging signed spend transaction for locked coins',
'Bid Request sent': 'Connection request sent to the other party',
'Bid Request accepted': 'Connection request accepted',
'Bid Expired': 'Bid expired before being accepted',
'Bid Auto accept delay': 'Waiting for automation delay before auto-accepting',
'Bid Auto accept failed': 'Automation failed to accept this bid',
'Bid Connect request sent': 'Sent connection request to peer',
'Bid Unknown bid state': 'Unknown state - please check the swap details',
'ITX Sent': 'Initiate transaction has been broadcast to the network',
'ITX Confirmed': 'Initiate transaction has been confirmed by miners',
'ITX Redeemed': 'Initiate transaction has been successfully claimed',
'ITX Refunded': 'Initiate transaction has been refunded',
'ITX In Mempool': 'Initiate transaction is in the mempool (unconfirmed)',
'ITX In Chain': 'Initiate transaction is included in a block',
'PTX Sent': 'Participate transaction has been broadcast to the network',
'PTX Confirmed': 'Participate transaction has been confirmed by miners',
'PTX Redeemed': 'Participate transaction has been successfully claimed',
'PTX Refunded': 'Participate transaction has been refunded',
'PTX In Mempool': 'Participate transaction is in the mempool (unconfirmed)',
'PTX In Chain': 'Participate transaction is included in a block'
},
getStateTooltip: function(stateText) {
const staticTooltip = this.STATE_TOOLTIPS[stateText];
if (staticTooltip !== null && staticTooltip !== undefined) {
return staticTooltip;
}
const scriptlessCoins = ['XMR', 'WOW'];
let scriptCoin, scriptlessCoin;
if (scriptlessCoins.includes(this.coinFrom)) {
scriptlessCoin = this.coinFrom;
scriptCoin = this.coinTo;
} else if (scriptlessCoins.includes(this.coinTo)) {
scriptlessCoin = this.coinTo;
scriptCoin = this.coinFrom;
} else {
scriptCoin = this.coinFrom;
scriptlessCoin = this.coinTo;
}
const dynamicTooltips = {
'Bid Script coin locked': `${scriptCoin} is locked in the atomic swap contract`,
'Bid Script coin spend tx valid': `The ${scriptCoin} spend transaction has been validated and is ready`,
'Bid Scriptless coin locked': `${scriptlessCoin} is locked using adaptor signatures`,
'Bid Script tx redeemed': `${scriptCoin} has been successfully claimed`,
'Bid Scriptless tx redeemed': `${scriptlessCoin} has been successfully claimed`,
'Bid Scriptless tx recovered': `${scriptlessCoin} recovered after swap failure`,
};
return dynamicTooltips[stateText] || null;
},
EVENT_TOOLTIPS: {
'Lock tx A published': 'First lock transaction broadcast to the blockchain network',
'Lock tx A seen in mempool': 'First lock transaction detected in mempool (unconfirmed)',
'Lock tx A seen in chain': 'First lock transaction included in a block',
'Lock tx A confirmed in chain': 'First lock transaction has enough confirmations',
'Lock tx B published': 'Second lock transaction broadcast to the blockchain network',
'Lock tx B seen in mempool': 'Second lock transaction detected in mempool (unconfirmed)',
'Lock tx B seen in chain': 'Second lock transaction included in a block',
'Lock tx B confirmed in chain': 'Second lock transaction has enough confirmations',
'Lock tx A spend tx published': 'Transaction to claim coins from first lock has been broadcast',
'Lock tx A spend tx seen in chain': 'First lock spend transaction included in a block',
'Lock tx B spend tx published': 'Transaction to claim coins from second lock has been broadcast',
'Lock tx B spend tx seen in chain': 'Second lock spend transaction included in a block',
'Failed to publish lock tx B': 'ERROR: Could not broadcast second lock transaction',
'Failed to publish lock tx B spend': 'ERROR: Could not broadcast spend transaction for second lock',
'Failed to publish lock tx B refund': 'ERROR: Could not broadcast refund transaction',
'Detected invalid lock Tx B': 'ERROR: Second lock transaction is invalid or malformed',
'Lock tx A pre-refund tx published': 'Pre-refund transaction broadcast. Swap is being cancelled',
'Lock tx A refund spend tx published': 'Refund transaction for first lock has been broadcast',
'Lock tx A refund swipe tx published': 'Other party claimed your refund (swiped)',
'Lock tx B refund tx published': 'Refund transaction for second lock has been broadcast',
'Lock tx A conflicting txn/s': 'WARNING: Conflicting transaction detected for first lock',
'Lock tx A pre-refund tx seen in chain': 'Pre-refund transaction detected in blockchain',
'Lock tx A refund spend tx seen in chain': 'Refund spend transaction detected in blockchain',
'Initiate tx published': 'Secret-hash swap: Initiate transaction broadcast',
'Initiate tx redeem tx published': 'Secret-hash swap: Initiate transaction claimed',
'Initiate tx refund tx published': 'Secret-hash swap: Initiate transaction refunded',
'Participate tx published': 'Secret-hash swap: Participate transaction broadcast',
'Participate tx redeem tx published': 'Secret-hash swap: Participate transaction claimed',
'Participate tx refund tx published': 'Secret-hash swap: Participate transaction refunded',
'BCH mercy tx found': 'BCH specific: Mercy transaction detected',
'Lock tx B mercy tx published': 'BCH specific: Mercy transaction broadcast',
'Auto accepting': 'Automation is accepting this bid',
'Failed auto accepting': 'Automation constraints prevented accepting this bid',
'Debug tweak applied': 'Debug mode: A test tweak was applied'
},
STATE_PHASES: {
1: { phase: 'negotiation', order: 1, label: 'Negotiation' }, // BID_SENT
2: { phase: 'negotiation', order: 2, label: 'Negotiation' }, // BID_RECEIVING
3: { phase: 'negotiation', order: 3, label: 'Negotiation' }, // BID_RECEIVED
4: { phase: 'negotiation', order: 4, label: 'Negotiation' }, // BID_RECEIVING_ACC
5: { phase: 'accepted', order: 5, label: 'Accepted' }, // BID_ACCEPTED
6: { phase: 'locking', order: 6, label: 'Locking' }, // SWAP_INITIATED
7: { phase: 'locking', order: 7, label: 'Locking' }, // SWAP_PARTICIPATING
8: { phase: 'complete', order: 100, label: 'Complete' }, // SWAP_COMPLETED
9: { phase: 'locking', order: 8, label: 'Locking' }, // XMR_SWAP_SCRIPT_COIN_LOCKED
10: { phase: 'locking', order: 9, label: 'Locking' }, // XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED
14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
18: { phase: 'failed', order: 93, label: 'Failed' }, // XMR_SWAP_FAILED_SWIPED
19: { phase: 'failed', order: 94, label: 'Failed' }, // XMR_SWAP_FAILED
20: { phase: 'locking', order: 7.5, label: 'Locking' }, // SWAP_DELAYING
21: { phase: 'failed', order: 95, label: 'Failed' }, // SWAP_TIMEDOUT
22: { phase: 'failed', order: 96, label: 'Abandoned' }, // BID_ABANDONED
23: { phase: 'failed', order: 97, label: 'Error' }, // BID_ERROR
25: { phase: 'failed', order: 98, label: 'Rejected' }, // BID_REJECTED
27: { phase: 'accepted', order: 5.5, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS
28: { phase: 'accepted', order: 5.6, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX
29: { phase: 'negotiation', order: 0.5, label: 'Negotiation' }, // BID_REQUEST_SENT
30: { phase: 'negotiation', order: 0.6, label: 'Negotiation' }, // BID_REQUEST_ACCEPTED
31: { phase: 'failed', order: 99, label: 'Expired' }, // BID_EXPIRED
32: { phase: 'negotiation', order: 3.5, label: 'Negotiation' }, // BID_AACCEPT_DELAY
33: { phase: 'failed', order: 89, label: 'Failed' }, // BID_AACCEPT_FAIL
34: { phase: 'negotiation', order: 0.4, label: 'Negotiation' } // CONNECT_REQ_SENT
},
init: function(bidId, bidStateInd, createdAtTimestamp, stateTimeTimestamp, options) {
this.bidId = bidId;
this.bidStateInd = bidStateInd;
this.createdAtTimestamp = createdAtTimestamp;
this.stateTimeTimestamp = stateTimeTimestamp;
this.tooltipCounter = 0;
options = options || {};
this.swapType = options.swapType || 'secret-hash';
this.coinFrom = options.coinFrom || '';
this.coinTo = options.coinTo || '';
if (this.bidStateInd === this.DELAYING_STATE) {
this.previousStateInd = this.findPreviousState();
}
this.applyStateTooltips();
this.applyEventTooltips();
this.createProgressBar();
this.startElapsedTimeUpdater();
this.setupAutoRefresh();
},
findPreviousState: function() {
const sections = document.querySelectorAll('section');
let oldStatesSection = null;
sections.forEach(section => {
const h4 = section.querySelector('h4');
if (h4 && h4.textContent.includes('Old states')) {
oldStatesSection = section.nextElementSibling;
}
});
if (oldStatesSection) {
const table = oldStatesSection.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tr');
for (let i = rows.length - 1; i >= 0; i--) {
const cells = rows[i].querySelectorAll('td');
if (cells.length >= 2) {
const stateText = cells[cells.length - 1].textContent.trim();
if (!stateText.includes('Delaying')) {
return this.stateTextToIndex(stateText);
}
}
}
}
}
return null;
},
stateTextToIndex: function(stateText) {
const stateMap = {
'Sent': 1, 'Receiving': 2, 'Received': 3, 'Receiving accept': 4,
'Accepted': 5, 'Initiated': 6, 'Participating': 7, 'Completed': 8,
'Script coin locked': 9, 'Script coin spend tx valid': 10,
'Scriptless coin locked': 11, 'Script coin lock released': 12,
'Script tx redeemed': 13, 'Script pre-refund tx in chain': 14,
'Scriptless tx redeemed': 15, 'Scriptless tx recovered': 16,
'Failed, refunded': 17, 'Failed, swiped': 18, 'Failed': 19,
'Delaying': 20, 'Timed-out': 21, 'Abandoned': 22, 'Error': 23,
'Rejected': 25, 'Exchanged script lock tx sigs msg': 27,
'Exchanged script lock spend tx msg': 28, 'Request sent': 29,
'Request accepted': 30, 'Expired': 31
};
for (const [key, value] of Object.entries(stateMap)) {
if (stateText.includes(key)) {
return value;
}
}
return null;
},
isActiveState: function() {
return !this.INACTIVE_STATES.includes(this.bidStateInd);
},
setupAutoRefresh: function() {
const refreshBtn = document.getElementById('refresh');
if (!refreshBtn) return;
if (!this.isActiveState()) {
refreshBtn.style.display = 'none';
return;
}
const originalSpan = refreshBtn.querySelector('span');
if (!originalSpan) return;
let countdown = this.AUTO_REFRESH_SECONDS;
let isRefreshing = false;
let isPersistentlyPaused = false;
const updateCountdown = () => {
if (this.refreshPaused || isPersistentlyPaused || isRefreshing) return;
originalSpan.textContent = `Auto-refresh in ${countdown}s`;
countdown--;
if (countdown < 0 && !isRefreshing) {
isRefreshing = true;
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
window.location.href = window.location.pathname + window.location.search;
}
};
updateCountdown();
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
refreshBtn.addEventListener('click', (e) => {
e.preventDefault();
if (isPersistentlyPaused) {
window.location.href = window.location.pathname + window.location.search;
} else {
isPersistentlyPaused = true;
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
originalSpan.textContent = 'Paused (click to refresh)';
}
});
refreshBtn.addEventListener('mouseenter', () => {
if (!isPersistentlyPaused) {
this.refreshPaused = true;
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
originalSpan.textContent = 'Click to pause';
}
});
refreshBtn.addEventListener('mouseleave', () => {
if (!isPersistentlyPaused) {
this.refreshPaused = false;
countdown = this.AUTO_REFRESH_SECONDS;
if (!this.autoRefreshInterval) {
updateCountdown();
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
}
}
});
},
createTooltip: function(element, tooltipText) {
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
try {
const tooltipContent = `
<div class="py-1 px-2 text-sm text-white">
${tooltipText}
</div>
`;
window.TooltipManager.create(element, tooltipContent, {
placement: 'top'
});
element.classList.add('cursor-help');
} catch (e) {
element.setAttribute('title', tooltipText);
element.classList.add('cursor-help');
}
} else {
element.setAttribute('title', tooltipText);
element.classList.add('cursor-help');
}
},
applyStateTooltips: function() {
const sections = document.querySelectorAll('section');
let oldStatesSection = null;
sections.forEach(section => {
const h4 = section.querySelector('h4');
if (h4 && h4.textContent.includes('Old states')) {
oldStatesSection = section.nextElementSibling;
}
});
if (oldStatesSection) {
const table = oldStatesSection.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 2) {
const stateCell = cells[cells.length - 1];
const stateText = stateCell.textContent.trim();
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
if (tooltip) {
this.addHelpIcon(stateCell, tooltip);
}
}
});
}
}
const allRows = document.querySelectorAll('table tr');
allRows.forEach(row => {
const firstCell = row.querySelector('td');
if (firstCell) {
const labelText = firstCell.textContent.trim();
if (labelText === 'Bid State') {
const valueCell = row.querySelectorAll('td')[1];
if (valueCell) {
const stateText = valueCell.textContent.trim();
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
if (tooltip) {
this.addHelpIcon(valueCell, tooltip);
}
}
}
}
});
},
addHelpIcon: function(cell, tooltipText) {
if (cell.querySelector('.help-icon')) return;
const helpIcon = document.createElement('span');
helpIcon.className = 'help-icon cursor-help inline-flex items-center justify-center w-4 h-4 ml-2 text-xs font-medium text-white bg-blue-500 dark:bg-blue-600 rounded-full hover:bg-blue-600 dark:hover:bg-blue-500';
helpIcon.textContent = '?';
helpIcon.style.fontSize = '10px';
helpIcon.style.verticalAlign = 'middle';
helpIcon.style.flexShrink = '0';
cell.appendChild(helpIcon);
setTimeout(() => {
this.createTooltip(helpIcon, tooltipText);
}, 50);
},
applyEventTooltips: function() {
const sections = document.querySelectorAll('section');
let eventsSection = null;
sections.forEach(section => {
const h4 = section.querySelector('h4');
if (h4 && h4.textContent.includes('Events')) {
eventsSection = section.nextElementSibling;
}
});
if (eventsSection) {
const table = eventsSection.querySelector('table');
if (table) {
const rows = table.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 2) {
const eventCell = cells[cells.length - 1];
const eventText = eventCell.textContent.trim();
let tooltip = this.EVENT_TOOLTIPS[eventText];
if (!tooltip) {
for (const [key, value] of Object.entries(this.EVENT_TOOLTIPS)) {
if (eventText.startsWith(key.replace(':', ''))) {
tooltip = value;
break;
}
}
}
if (!tooltip && eventText.startsWith('Warning:')) {
tooltip = 'System warning - check message for details';
}
if (!tooltip && eventText.startsWith('Error:')) {
tooltip = 'Error occurred - check message for details';
}
if (!tooltip && eventText.startsWith('Temporary RPC error')) {
tooltip = 'Temporary error checking transaction. Will retry automatically';
}
if (tooltip) {
this.addHelpIcon(eventCell, tooltip);
}
}
});
}
}
},
createProgressBar: function() {
let stateForProgress = this.bidStateInd;
let isDelaying = false;
if (this.bidStateInd === this.DELAYING_STATE && this.previousStateInd) {
stateForProgress = this.previousStateInd;
isDelaying = true;
}
const phaseInfo = this.STATE_PHASES[stateForProgress];
if (!phaseInfo) return;
let progressPercent = 0;
const phase = phaseInfo.phase;
if (phase === 'negotiation') progressPercent = 15;
else if (phase === 'accepted') progressPercent = 30;
else if (phase === 'locking') progressPercent = 55;
else if (phase === 'redemption') progressPercent = 80;
else if (phase === 'complete') progressPercent = 100;
else if (phase === 'failed' || phase === 'error') progressPercent = 100;
const bidStateRow = document.querySelector('td.bold');
if (!bidStateRow) return;
let targetRow = null;
const rows = document.querySelectorAll('table tr');
rows.forEach(row => {
const firstTd = row.querySelector('td.bold');
if (firstTd && firstTd.textContent.trim() === 'Bid State') {
targetRow = row;
}
});
if (!targetRow) return;
const progressRow = document.createElement('tr');
progressRow.className = 'opacity-100 text-gray-500 dark:text-gray-100';
const isError = ['failed', 'error'].includes(phase);
const isComplete = phase === 'complete';
const barColor = isError ? 'bg-red-500' : (isComplete ? 'bg-green-500' : 'bg-blue-500');
let phaseLabel;
if (isError) {
phaseLabel = phaseInfo.label;
} else if (isComplete) {
phaseLabel = 'Complete';
} else if (isDelaying) {
phaseLabel = `${phaseInfo.label} (${progressPercent}%) - Delaying`;
} else {
phaseLabel = `${phaseInfo.label} (${progressPercent}%)`;
}
progressRow.innerHTML = `
<td class="py-3 px-6 bold">Swap Progress</td>
<td class="py-3 px-6">
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 dark:bg-gray-600 rounded-full h-2.5 max-w-xs">
<div class="${barColor} h-2.5 rounded-full transition-all duration-500" style="width: ${progressPercent}%"></div>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white">${phaseLabel}</span>
</div>
</td>
`;
targetRow.parentNode.insertBefore(progressRow, targetRow.nextSibling);
},
startElapsedTimeUpdater: function() {
if (!this.createdAtTimestamp) return;
let createdAtRow = null;
const rows = document.querySelectorAll('table tr');
rows.forEach(row => {
const firstTd = row.querySelector('td');
if (firstTd && firstTd.textContent.includes('Created At')) {
createdAtRow = row;
}
});
if (!createdAtRow) return;
const isCompleted = !this.isActiveState() && this.stateTimeTimestamp;
const elapsedRow = document.createElement('tr');
elapsedRow.className = 'opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
const labelText = isCompleted ? 'Swap Duration' : 'Time Elapsed';
const iconColor = isCompleted ? '#10B981' : '#3B82F6';
elapsedRow.innerHTML = `
<td class="flex items-center px-46 whitespace-nowrap">
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${iconColor}" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="${iconColor}"></polyline>
</g>
</svg>
<div class="py-3 pl-2 bold">
<div>${labelText}</div>
</div>
</td>
<td class="py-3 px-6" id="elapsed-time-display">Calculating...</td>
`;
createdAtRow.parentNode.insertBefore(elapsedRow, createdAtRow.nextSibling);
const elapsedDisplay = document.getElementById('elapsed-time-display');
if (isCompleted) {
const duration = this.stateTimeTimestamp - this.createdAtTimestamp;
elapsedDisplay.textContent = this.formatDuration(duration);
} else {
const updateElapsed = () => {
const now = Math.floor(Date.now() / 1000);
const elapsed = now - this.createdAtTimestamp;
elapsedDisplay.textContent = this.formatDuration(elapsed);
};
updateElapsed();
this.elapsedTimeInterval = setInterval(updateElapsed, 1000);
}
},
formatDuration: function(seconds) {
if (seconds < 60) {
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
const remainingSeconds = seconds % 60;
if (remainingSeconds > 0) {
return `${minutes} min ${remainingSeconds} sec`;
}
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours < 24) {
if (remainingMinutes > 0) {
return `${hours} hr ${remainingMinutes} min`;
}
return `${hours} hour${hours !== 1 ? 's' : ''}`;
}
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
if (remainingHours > 0) {
return `${days} day${days !== 1 ? 's' : ''} ${remainingHours} hr`;
}
return `${days} day${days !== 1 ? 's' : ''}`;
}
};
@@ -1,7 +1,7 @@
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const state = { const state = {
dentities: new Map(), identities: new Map(),
currentPage: 1, currentPage: 1,
wsConnected: false, wsConnected: false,
jsonData: [], jsonData: [],
+20 -3
View File
@@ -41,12 +41,29 @@
setupEventListeners: function() { setupEventListeners: function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]'); const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
if (sendBidBtn) { if (sendBidBtn) {
sendBidBtn.onclick = this.showConfirmModal.bind(this); sendBidBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.showConfirmModal();
});
} }
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child'); const modalCancelBtn = document.querySelector('#confirmModal [data-hide-modal]');
if (modalCancelBtn) { if (modalCancelBtn) {
modalCancelBtn.onclick = this.hideConfirmModal.bind(this); modalCancelBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.hideConfirmModal();
});
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal || e.target.classList.contains('bg-opacity-50')) {
this.hideConfirmModal();
}
});
} }
const mainCancelBtn = document.querySelector('button[name="cancel"]'); const mainCancelBtn = document.querySelector('button[name="cancel"]');
+569
View File
@@ -6,11 +6,15 @@
confirmCallback: null, confirmCallback: null,
triggerElement: null, triggerElement: null,
originalConnectionTypes: {},
init: function() { init: function() {
this.setupTabs(); this.setupTabs();
this.setupCoinHeaders(); this.setupCoinHeaders();
this.setupConfirmModal(); this.setupConfirmModal();
this.setupNotificationSettings(); this.setupNotificationSettings();
this.setupMigrationIndicator();
this.setupServerDiscovery();
}, },
setupTabs: function() { setupTabs: function() {
@@ -61,6 +65,410 @@
}); });
}, },
pendingModeSwitch: null,
setupMigrationIndicator: function() {
const connectionTypeSelects = document.querySelectorAll('select[name^="connection_type_"]');
connectionTypeSelects.forEach(select => {
const originalValue = select.dataset.originalValue || select.value;
this.originalConnectionTypes[select.name] = originalValue;
select.addEventListener('change', (e) => {
const coinName = select.name.replace('connection_type_', '');
const electrumSection = document.getElementById(`electrum-section-${coinName}`);
const fundTransferSection = document.getElementById(`fund-transfer-section-${coinName}`);
const originalValue = this.originalConnectionTypes[select.name];
if (e.target.value === 'electrum') {
if (electrumSection) {
electrumSection.classList.remove('hidden');
const clearnetTextarea = document.getElementById(`electrum_clearnet_${coinName}`);
const onionTextarea = document.getElementById(`electrum_onion_${coinName}`);
if (clearnetTextarea && !clearnetTextarea.value.trim()) {
clearnetTextarea.value = electrumSection.dataset.defaultClearnet || '';
}
if (onionTextarea && !onionTextarea.value.trim()) {
onionTextarea.value = electrumSection.dataset.defaultOnion || '';
}
}
if (fundTransferSection) {
fundTransferSection.classList.add('hidden');
}
} else {
if (electrumSection) {
electrumSection.classList.add('hidden');
}
if (fundTransferSection && originalValue === 'electrum') {
fundTransferSection.classList.remove('hidden');
}
}
});
});
this.setupWalletModeModal();
const coinsForm = document.getElementById('coins-form');
if (coinsForm) {
coinsForm.addEventListener('submit', (e) => {
const submitter = e.submitter;
if (!submitter || !submitter.name.startsWith('apply_')) return;
const coinName = submitter.name.replace('apply_', '');
const select = document.querySelector(`select[name="connection_type_${coinName}"]`);
if (!select) return;
const original = this.originalConnectionTypes[select.name];
const current = select.value;
if (original && current && original !== current) {
e.preventDefault();
const direction = (original === 'rpc' && current === 'electrum') ? 'lite' : 'rpc';
this.showWalletModeConfirmation(coinName, direction, submitter);
}
});
}
},
setupWalletModeModal: function() {
const confirmBtn = document.getElementById('walletModeConfirm');
const cancelBtn = document.getElementById('walletModeCancel');
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
this.hideWalletModeModal();
if (this.pendingModeSwitch) {
const { coinName, direction, submitter } = this.pendingModeSwitch;
this.showMigrationModal(coinName.toUpperCase(), direction);
const form = submitter.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = submitter.name;
hiddenInput.value = submitter.value;
form.appendChild(hiddenInput);
let transferValue = null;
const transferRadio = document.querySelector('input[name="transfer_choice"]:checked');
const transferHidden = document.querySelector('input[name="transfer_choice"][type="hidden"]');
if (transferRadio) {
transferValue = transferRadio.value;
} else if (transferHidden) {
transferValue = transferHidden.value;
}
if (transferValue) {
const transferInput = document.createElement('input');
transferInput.type = 'hidden';
transferInput.name = `auto_transfer_now_${coinName}`;
transferInput.value = transferValue === 'auto' ? 'true' : 'false';
form.appendChild(transferInput);
}
form.submit();
}
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.hideWalletModeModal();
if (this.pendingModeSwitch) {
const { coinName } = this.pendingModeSwitch;
const select = document.querySelector(`select[name="connection_type_${coinName}"]`);
if (select) {
select.value = this.originalConnectionTypes[select.name];
}
}
this.pendingModeSwitch = null;
});
}
},
updateConfirmButtonState: function() {
const confirmBtn = document.getElementById('walletModeConfirm');
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
if (confirmBtn && checkbox) {
if (checkbox.checked) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
confirmBtn.disabled = true;
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
}
},
showWalletModeConfirmation: async function(coinName, direction, submitter) {
const modal = document.getElementById('walletModeModal');
const title = document.getElementById('walletModeTitle');
const message = document.getElementById('walletModeMessage');
const details = document.getElementById('walletModeDetails');
const confirmBtn = document.getElementById('walletModeConfirm');
if (!modal || !title || !message || !details) return;
this.pendingModeSwitch = { coinName, direction, submitter };
const displayName = coinName.charAt(0).toUpperCase() + coinName.slice(1).toLowerCase();
details.innerHTML = `
<div class="flex items-center justify-center py-4">
<svg class="animate-spin h-5 w-5 text-blue-500 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading...</span>
</div>
`;
if (confirmBtn) {
confirmBtn.disabled = true;
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
modal.classList.remove('hidden');
if (direction === 'lite') {
title.textContent = `Switch ${displayName} to Lite Wallet Mode`;
message.textContent = 'Write down this key before switching. It will only be shown ONCE.';
try {
const [infoResponse, seedResponse] = await Promise.all([
fetch('/json/modeswitchinfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName, direction: 'lite' })
}),
fetch('/json/getcoinseed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName })
})
]);
const info = await infoResponse.json();
const data = await seedResponse.json();
let transferSection = '';
if (info.require_transfer && info.legacy_balance_sats > 0) {
transferSection = `
<div class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
<p class="text-sm font-medium text-gray-900 dark:text-white mb-2">Funds Transfer Required</p>
<p class="text-xs text-gray-700 dark:text-gray-200 mb-2">
<strong>${info.legacy_balance} ${info.coin}</strong> on non-derivable addresses will be automatically transferred to a BIP84 address.
</p>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">
Est. fee: ${info.estimated_fee} ${info.coin}
</p>
<p class="text-xs text-gray-700 dark:text-gray-200">
This ensures your funds are recoverable using the extended key backup in external Electrum wallets.
</p>
<input type="hidden" name="transfer_choice" value="auto">
</div>
`;
} else if (info.legacy_balance_sats > 0 && !info.show_transfer_option) {
transferSection = `
<p class="text-gray-700 dark:text-gray-300 text-xs mb-3">
Some funds on non-derivable addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
</p>
`;
}
if (data.account_key) {
details.innerHTML = `
<p class="mb-2 text-red-600 dark:text-red-300 font-semibold">
IMPORTANT: Write down this key NOW. It will not be shown again.
</p>
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Extended Private Key (for external wallet import):</strong></p>
<div class="bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded p-2 mb-3">
<code id="extendedKeyDisplay" class="text-xs break-all font-mono text-gray-900 dark:text-gray-100">${'*'.repeat(Math.min(data.account_key.length, 80))}</code>
<code id="extendedKeyActual" class="text-xs break-all select-all font-mono text-gray-900 dark:text-gray-100 hidden">${data.account_key}</code>
</div>
<div class="mb-3">
<button type="button" id="toggleKeyVisibility" class="px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">
Show Key
</button>
</div>
<div class="text-xs text-gray-600 dark:text-gray-300 mb-3 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded p-2">
<p class="font-medium mb-1 text-gray-800 dark:text-gray-100">To import in Electrum wallet:</p>
<ol class="list-decimal list-inside space-y-0.5">
<li>Open Electrum File New/Restore</li>
<li>Choose "Standard wallet" "Use a master key"</li>
<li>Paste this key (starts with zprv... or yprv...)</li>
</ol>
</div>
${transferSection}
<div class="border-t border-gray-300 dark:border-gray-500 pt-3">
<label class="flex items-center cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-500 rounded p-1 -m-1">
<input type="checkbox" id="walletModeKeyConfirmCheckbox" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-500 focus:ring-blue-500 dark:bg-gray-700">
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">I have written down this key</span>
</label>
</div>
`;
const toggleBtn = document.getElementById('toggleKeyVisibility');
const keyDisplay = document.getElementById('extendedKeyDisplay');
const keyActual = document.getElementById('extendedKeyActual');
if (toggleBtn && keyDisplay && keyActual) {
toggleBtn.addEventListener('click', () => {
if (keyDisplay.classList.contains('hidden')) {
keyDisplay.classList.remove('hidden');
keyActual.classList.add('hidden');
toggleBtn.textContent = 'Show Key';
} else {
keyDisplay.classList.add('hidden');
keyActual.classList.remove('hidden');
toggleBtn.textContent = 'Hide Key';
}
});
}
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
if (checkbox) {
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
}
} else {
details.innerHTML = `
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Active swaps must be completed first</li>
<li>Wait for any pending transactions to confirm</li>
</ul>
${transferSection}
<p class="mt-3 text-green-700 dark:text-green-300">
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
</p>
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
} catch (error) {
console.error('Failed to fetch coin seed:', error);
details.innerHTML = `
<p class="text-red-600 dark:text-red-300 mb-2">Failed to retrieve extended key. Please try again.</p>
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Active swaps must be completed first</li>
<li>Wait for any pending transactions to confirm</li>
</ul>
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
} else {
title.textContent = `Switch ${displayName} to Full Node Mode`;
message.textContent = 'Please confirm you want to switch to full node mode.';
try {
const response = await fetch('/json/modeswitchinfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName, direction: 'rpc' })
});
const info = await response.json();
let transferSection = '';
if (info.error) {
transferSection = `<p class="text-yellow-700 dark:text-yellow-300 text-sm">${info.error}</p>`;
} else if (info.balance_sats === 0) {
transferSection = `<p class="text-gray-600 dark:text-gray-300 text-sm">No funds to transfer.</p>`;
} else if (!info.can_transfer) {
transferSection = `
<p class="text-yellow-700 dark:text-yellow-300 text-sm">
Balance (${info.balance} ${info.coin}) is too low to transfer - fee would exceed funds.
</p>
`;
} else {
transferSection = `
<div class="bg-gray-200 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
<p class="text-sm font-medium text-gray-900 dark:text-white mb-2">Fund Transfer Options</p>
<p class="text-xs text-gray-700 dark:text-gray-300 mb-3">
Balance: ${info.balance} ${info.coin} | Est. fee: ${info.estimated_fee} ${info.coin}
</p>
<div class="space-y-2">
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="auto" checked class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Auto-transfer funds to RPC wallet</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Recommended. Ensures all funds visible in full node wallet.</p>
</div>
</label>
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="manual" class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Keep funds on current addresses</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Transfer manually later if needed.</p>
</div>
</label>
</div>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-3">
If you skip transfer, you will need to manually send funds from lite wallet addresses to your RPC wallet.
</p>
</div>
`;
}
details.innerHTML = `
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
<ul class="list-disc list-inside space-y-1 mb-3 text-gray-700 dark:text-gray-200">
<li>Requires synced ${displayName} blockchain</li>
<li>Your wallet addresses will be synced</li>
<li>Active swaps must be completed first</li>
<li>Restart required after switch</li>
</ul>
${transferSection}
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
} catch (error) {
console.error('Failed to fetch mode switch info:', error);
details.innerHTML = `
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Requires synced ${displayName} blockchain</li>
<li>Your wallet addresses will be synced</li>
<li>Active swaps must be completed first</li>
<li>Restart required after switch</li>
</ul>
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
},
hideWalletModeModal: function() {
const modal = document.getElementById('walletModeModal');
if (modal) {
modal.classList.add('hidden');
}
},
showMigrationModal: function(coinName, direction) {
const modal = document.getElementById('migrationModal');
const title = document.getElementById('migrationTitle');
const message = document.getElementById('migrationMessage');
if (modal && title && message) {
if (direction === 'lite') {
title.textContent = `Migrating ${coinName} to Lite Wallet`;
message.textContent = 'Checking wallet balance and migrating addresses. Please wait...';
} else {
title.textContent = `Switching ${coinName} to Full Node`;
message.textContent = 'Syncing wallet indices. Please wait...';
}
modal.classList.remove('hidden');
}
},
setupConfirmModal: function() { setupConfirmModal: function() {
const confirmYesBtn = document.getElementById('confirmYes'); const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) { if (confirmYesBtn) {
@@ -307,6 +715,167 @@
} }
}; };
SettingsPage.setupServerDiscovery = function() {
const discoverBtns = document.querySelectorAll('.discover-servers-btn');
discoverBtns.forEach(btn => {
btn.addEventListener('click', () => {
const coin = btn.dataset.coin;
this.discoverServers(coin, btn);
});
});
const closeBtns = document.querySelectorAll('.close-discovered-btn');
closeBtns.forEach(btn => {
btn.addEventListener('click', () => {
const coin = btn.dataset.coin;
const panel = document.getElementById(`discovered-servers-${coin}`);
if (panel) panel.classList.add('hidden');
});
});
};
SettingsPage.discoverServers = function(coin, button) {
const originalHtml = button.innerHTML;
button.innerHTML = `<svg class="w-3.5 h-3.5 mr-1 animate-spin inline-block" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Discovering...`;
button.disabled = true;
const panel = document.getElementById(`discovered-servers-${coin}`);
const listContainer = document.getElementById(`discovered-list-${coin}`);
fetch('/json/electrumdiscover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coin, ping: true })
})
.then(response => response.json())
.then(data => {
if (data.error) {
listContainer.innerHTML = `<div class="text-sm text-red-500">${data.error}</div>`;
} else {
let html = '';
if (data.current_server) {
html += `
<div class="flex items-center mb-4 p-3 bg-gray-100 dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg">
<span class="w-2 h-2 bg-green-500 rounded-full mr-3 animate-pulse"></span>
<span class="text-sm text-gray-900 dark:text-white">
Connected to: <span class="font-mono font-medium">${data.current_server.host}:${data.current_server.port}</span>
</span>
</div>`;
}
if (data.clearnet_servers && data.clearnet_servers.length > 0) {
html += `
<div class="mb-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white mb-2 pb-2 border-b border-gray-200 dark:border-gray-600">
Clearnet
</div>
<div class="space-y-1">`;
data.clearnet_servers.forEach(srv => {
const statusClass = srv.online ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500';
const statusText = srv.online ? (srv.latency_ms ? srv.latency_ms.toFixed(0) + 'ms' : 'online') : 'offline';
const statusDot = srv.online ? 'bg-green-500' : 'bg-gray-400';
html += `
<div class="flex items-center justify-between py-2 px-3 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg cursor-pointer add-server-btn transition-colors border border-transparent hover:border-blue-500"
data-coin="${coin}" data-host="${srv.host}" data-port="${srv.port}" data-type="clearnet">
<div class="flex items-center flex-1 min-w-0">
<svg class="w-4 h-4 mr-2 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
<span class="font-mono text-sm text-gray-900 dark:text-white truncate">${srv.host}:${srv.port}</span>
</div>
<div class="flex items-center ml-3">
<span class="w-2 h-2 ${statusDot} rounded-full mr-2"></span>
<span class="text-xs ${statusClass}">${statusText}</span>
</div>
</div>`;
});
html += `
</div>
</div>`;
}
if (data.onion_servers && data.onion_servers.length > 0) {
html += `
<div class="mb-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white mb-2 pb-2 border-b border-gray-200 dark:border-gray-600">
TOR (.onion)
</div>
<div class="space-y-1">`;
data.onion_servers.forEach(srv => {
const statusClass = srv.online ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500';
const statusText = srv.online ? (srv.latency_ms ? srv.latency_ms.toFixed(0) + 'ms' : 'online') : 'offline';
const statusDot = srv.online ? 'bg-green-500' : 'bg-gray-400';
html += `
<div class="flex items-center justify-between py-2 px-3 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg cursor-pointer add-server-btn transition-colors border border-transparent hover:border-blue-500"
data-coin="${coin}" data-host="${srv.host}" data-port="${srv.port}" data-type="onion">
<div class="flex items-center flex-1 min-w-0">
<svg class="w-4 h-4 mr-2 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
<span class="font-mono text-sm text-gray-900 dark:text-white truncate" title="${srv.host}">${srv.host.substring(0, 24)}...:${srv.port}</span>
</div>
<div class="flex items-center ml-3">
<span class="w-2 h-2 ${statusDot} rounded-full mr-2"></span>
<span class="text-xs ${statusClass}">${statusText}</span>
</div>
</div>`;
});
html += `
</div>
</div>`;
}
if (!data.clearnet_servers?.length && !data.onion_servers?.length) {
const serverName = data.current_server ? `${data.current_server.host}:${data.current_server.port}` : 'The connected server';
html = `<div class="text-sm text-gray-500 dark:text-gray-400 py-4 text-center">No servers discovered. <span class="font-mono">${serverName}</span> does not return peer lists.</div>`;
} else {
html += `<div class="text-xs text-gray-500 dark:text-gray-400 pt-3 border-t border-gray-200 dark:border-gray-600">Click a server to add it to your list</div>`;
}
listContainer.innerHTML = html;
listContainer.querySelectorAll('.add-server-btn').forEach(item => {
item.addEventListener('click', () => {
const host = item.dataset.host;
const port = item.dataset.port;
const type = item.dataset.type;
const coinName = item.dataset.coin;
const textareaId = type === 'onion' ?
`electrum_onion_${coinName}` : `electrum_clearnet_${coinName}`;
const textarea = document.getElementById(textareaId);
if (textarea) {
const serverLine = `${host}:${port}`;
const currentValue = textarea.value.trim();
if (currentValue.split('\n').some(line => line.trim() === serverLine)) {
item.classList.add('bg-yellow-100', 'dark:bg-yellow-800/30');
setTimeout(() => item.classList.remove('bg-yellow-100', 'dark:bg-yellow-800/30'), 500);
return;
}
textarea.value = currentValue ? currentValue + '\n' + serverLine : serverLine;
item.classList.add('bg-green-100', 'dark:bg-green-800/30');
setTimeout(() => item.classList.remove('bg-green-100', 'dark:bg-green-800/30'), 500);
}
});
});
}
panel.classList.remove('hidden');
})
.catch(err => {
listContainer.innerHTML = `<div class="text-xs text-red-500">Failed to discover servers: ${err.message}</div>`;
panel.classList.remove('hidden');
})
.finally(() => {
button.innerHTML = originalHtml;
button.disabled = false;
});
};
SettingsPage.cleanup = function() { SettingsPage.cleanup = function() {
}; };
+281 -4
View File
@@ -13,6 +13,7 @@
this.setupWithdrawalConfirmation(); this.setupWithdrawalConfirmation();
this.setupTransactionDisplay(); this.setupTransactionDisplay();
this.setupWebSocketUpdates(); this.setupWebSocketUpdates();
this.setupTransactionPagination();
}, },
setupAddressCopy: function() { setupAddressCopy: function() {
@@ -340,13 +341,289 @@
}, },
handleBalanceUpdate: function(balanceData) { handleBalanceUpdate: function(balanceData) {
if (!balanceData || !Array.isArray(balanceData)) return;
console.log('Balance updated:', balanceData);
const coinId = this.currentCoinId;
if (!coinId) return;
const matchingCoins = balanceData.filter(coin =>
coin.ticker && coin.ticker.toLowerCase() === coinId.toLowerCase()
);
matchingCoins.forEach(coinData => {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) {
const currentText = element.textContent;
const ticker = coinData.ticker || coinId.toUpperCase();
const newBalance = `${coinData.balance} ${ticker}`;
if (currentText !== newBalance) {
element.textContent = newBalance;
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`);
}
}
});
this.updatePendingForCoin(coinData);
});
this.refreshTransactions();
},
updatePendingForCoin: function(coinData) {
const pendingAmount = parseFloat(coinData.pending || '0');
const pendingElements = document.querySelectorAll('.inline-block.py-1.px-2.rounded-full.bg-green-100');
pendingElements.forEach(el => {
const text = el.textContent || '';
if (text.includes('Pending:') && text.includes(coinData.ticker)) {
if (pendingAmount > 0) {
el.textContent = `Pending: +${coinData.pending} ${coinData.ticker}`;
el.style.display = '';
} else {
el.style.display = 'none';
}
}
});
},
refreshTransactions: function() {
const txTable = document.querySelector('#transaction-history-section tbody');
if (txTable) {
const pathParts = window.location.pathname.split('/');
const ticker = pathParts[pathParts.length - 1];
fetch(`/json/wallettransactions/${ticker}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_no: 1 })
})
.then(response => response.json())
.then(data => {
if (data.transactions && data.transactions.length > 0) {
const currentPageSpan = document.getElementById('currentPageTx');
const totalPagesSpan = document.getElementById('totalPagesTx');
if (currentPageSpan) currentPageSpan.textContent = data.page_no;
if (totalPagesSpan) totalPagesSpan.textContent = data.total_pages;
}
})
.catch(error => console.error('Error refreshing transactions:', error));
}
}, },
handleSwapEvent: function(eventData) { handleSwapEvent: function(eventData) {
if (window.BalanceUpdatesManager) {
console.log('Swap event:', eventData); window.BalanceUpdatesManager.fetchBalanceData()
.then(data => this.handleBalanceUpdate(data))
.catch(error => console.error('Error updating balance after swap:', error));
}
},
setupTransactionPagination: function() {
const txContainer = document.getElementById('tx-container');
if (!txContainer) return;
const pathParts = window.location.pathname.split('/');
const ticker = pathParts[pathParts.length - 1];
let currentPage = 1;
let totalPages = 1;
let isLoading = false;
const prevBtn = document.getElementById('prevPageTx');
const nextBtn = document.getElementById('nextPageTx');
const currentPageSpan = document.getElementById('currentPageTx');
const totalPagesSpan = document.getElementById('totalPagesTx');
const paginationControls = document.getElementById('tx-pagination-section');
const copyToClipboard = (text, button) => {
const showSuccess = () => {
const originalHTML = button.innerHTML;
button.innerHTML = `<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>`;
setTimeout(() => {
button.innerHTML = originalHTML;
}, 1500);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(showSuccess).catch(err => {
console.error('Clipboard API failed:', err);
fallbackCopy(text, showSuccess);
});
} else {
fallbackCopy(text, showSuccess);
}
};
const fallbackCopy = (text, onSuccess) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
onSuccess();
} catch (err) {
console.error('Fallback copy failed:', err);
}
document.body.removeChild(textArea);
};
const loadTransactions = async (page) => {
if (isLoading) return;
isLoading = true;
try {
const response = await fetch(`/json/wallettransactions/${ticker}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ page_no: page })
});
const data = await response.json();
if (data.error) {
console.error('Error loading transactions:', data.error);
return;
}
currentPage = data.page_no;
totalPages = data.total_pages;
currentPageSpan.textContent = currentPage;
totalPagesSpan.textContent = totalPages;
txContainer.innerHTML = '';
if (data.transactions && data.transactions.length > 0) {
data.transactions.forEach(tx => {
const card = document.createElement('div');
card.className = 'bg-white dark:bg-gray-600 rounded-lg border border-gray-200 dark:border-gray-500 p-4 hover:shadow-md transition-shadow';
let typeClass = 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300';
let amountClass = 'text-gray-700 dark:text-gray-200';
let typeIcon = '';
let amountPrefix = '';
if (tx.type === 'Incoming') {
typeClass = 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-300';
amountClass = 'text-green-600 dark:text-green-400';
typeIcon = '↓';
amountPrefix = '+';
} else if (tx.type === 'Outgoing') {
typeClass = 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300';
amountClass = 'text-red-600 dark:text-red-400';
typeIcon = '↑';
amountPrefix = '-';
}
let confirmClass = 'text-gray-600 dark:text-gray-300';
if (tx.confirmations === 0) {
confirmClass = 'text-yellow-600 dark:text-yellow-400 font-medium';
} else if (tx.confirmations >= 1 && tx.confirmations <= 5) {
confirmClass = 'text-blue-600 dark:text-blue-400';
} else if (tx.confirmations >= 6) {
confirmClass = 'text-green-600 dark:text-green-400';
}
card.innerHTML = `
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-3">
<span class="inline-flex items-center gap-1 py-1 px-2 rounded-full text-xs font-semibold ${typeClass}">
${typeIcon} ${tx.type}
</span>
<span class="font-semibold ${amountClass}">
${amountPrefix}${tx.amount} ${ticker.toUpperCase()}
</span>
</div>
<div class="flex items-center gap-4 text-sm">
<span class="${confirmClass}">${tx.confirmations} Confirmations</span>
<span class="text-gray-500 dark:text-gray-400">${tx.timestamp}</span>
</div>
</div>
${tx.address ? `
<div class="flex items-center gap-2 mb-2">
<span class="text-xs text-gray-500 dark:text-gray-400 w-16 flex-shrink-0">Address:</span>
<span class="font-mono text-xs text-gray-700 dark:text-gray-200 break-all flex-1">${tx.address}</span>
<button class="copy-address-btn p-1.5 hover:bg-gray-100 dark:hover:bg-gray-500 rounded flex-shrink-0 focus:outline-none focus:ring-0" title="Copy Address">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
` : ''}
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500 dark:text-gray-400 w-16 flex-shrink-0">Txid:</span>
<span class="font-mono text-xs text-gray-700 dark:text-gray-200 break-all flex-1">${tx.txid}</span>
<button class="copy-txid-btn p-1.5 hover:bg-gray-100 dark:hover:bg-gray-500 rounded flex-shrink-0 focus:outline-none focus:ring-0" title="Copy Transaction ID">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
`;
const copyAddressBtn = card.querySelector('.copy-address-btn');
if (copyAddressBtn) {
copyAddressBtn.addEventListener('click', () => copyToClipboard(tx.address, copyAddressBtn));
}
const copyTxidBtn = card.querySelector('.copy-txid-btn');
if (copyTxidBtn) {
copyTxidBtn.addEventListener('click', () => copyToClipboard(tx.txid, copyTxidBtn));
}
txContainer.appendChild(card);
});
if (totalPages > 1 && paginationControls) {
paginationControls.style.display = 'block';
} else if (paginationControls) {
paginationControls.style.display = 'none';
}
} else {
txContainer.innerHTML = '<div class="text-center py-8 text-gray-500 dark:text-gray-400">No transactions found</div>';
if (paginationControls) paginationControls.style.display = 'none';
}
prevBtn.style.display = currentPage > 1 ? 'inline-flex' : 'none';
nextBtn.style.display = currentPage < totalPages ? 'inline-flex' : 'none';
} catch (error) {
console.error('Error fetching transactions:', error);
} finally {
isLoading = false;
}
};
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
loadTransactions(currentPage - 1);
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentPage < totalPages) {
loadTransactions(currentPage + 1);
}
});
}
loadTransactions(1);
} }
}; };
+71 -2
View File
@@ -86,6 +86,72 @@
} }
} }
} }
if (coinData.scan_status || coinData.electrum_synced !== undefined) {
this.updateScanStatus(coinData);
}
if (coinData.version) {
const versionEl = document.querySelector(`.electrum-version[data-coin="${coinData.name}"]`);
if (versionEl && versionEl.textContent !== coinData.version) {
versionEl.textContent = coinData.version;
}
}
if (coinData.electrum_server) {
const serverEl = document.querySelector(`.electrum-server[data-coin="${coinData.name}"]`);
if (serverEl && serverEl.textContent !== coinData.electrum_server) {
serverEl.textContent = coinData.electrum_server;
}
}
},
updateScanStatus: function(coinData) {
const scanStatusEl = document.querySelector(`.scan-status[data-coin="${coinData.name}"]`);
if (!scanStatusEl) return;
const status = coinData.scan_status;
if (status && status.in_progress) {
scanStatusEl.innerHTML = `
<div class="flex items-center justify-between text-xs">
<span class="text-blue-600 dark:text-blue-300">
<svg class="inline-block w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Scanning ${status.status}
</span>
<span class="text-blue-500 dark:text-blue-200 font-medium">${status.progress}%</span>
</div>
<div class="w-full bg-blue-200 dark:bg-gray-700 rounded-full h-1 mt-1">
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full transition-all" style="width: ${status.progress}%"></div>
</div>
`;
} else if (coinData.electrum_synced) {
const height = coinData.electrum_height || '';
scanStatusEl.innerHTML = `
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
Electrum Wallet Synced (${height})
</div>
</div>
`;
} else if (coinData.electrum_synced === false) {
scanStatusEl.innerHTML = `
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
Waiting for Electrum Server...
</div>
</div>
`;
} else {
scanStatusEl.innerHTML = `
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
Electrum Wallet Synced
</div>
</div>
`;
}
}, },
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) { updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
@@ -102,12 +168,13 @@
const currentLabel = labelElement.textContent.trim(); const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) { if (currentLabel === labelText) {
const cleanBalance = balance.toString().replace(/^\+/, '');
if (isPending) { if (isPending) {
const cleanBalance = balance.toString().replace(/^\+/, '');
element.textContent = `+${cleanBalance} ${ticker}`; element.textContent = `+${cleanBalance} ${ticker}`;
} else { } else {
element.textContent = `${balance} ${ticker}`; element.textContent = `${balance} ${ticker}`;
} }
element.setAttribute('data-original-value', `${cleanBalance} ${ticker}`);
} }
} }
} }
@@ -139,6 +206,7 @@
if (pendingSpan) { if (pendingSpan) {
const cleanPending = coinData.pending.toString().replace(/^\+/, ''); const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`; pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${coinData.ticker || coinData.name}`);
} }
let initialUSD = '$0.00'; let initialUSD = '$0.00';
@@ -218,7 +286,7 @@
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) { for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) { if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-white, .dark\\:bg-gray-500'); return element.closest('.bg-gray-50, .dark\\:bg-gray-500');
} }
} }
return null; return null;
@@ -330,6 +398,7 @@
if (pendingSpan) { if (pendingSpan) {
const cleanPending = pendingAmount.toString().replace(/^\+/, ''); const cleanPending = pendingAmount.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${ticker}`; pendingSpan.textContent = `+${cleanPending} ${ticker}`;
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${ticker}`);
} }
} }
} }
+12 -2
View File
@@ -525,14 +525,14 @@
</div> </div>
{% if data.can_abandon == true and not edit_bid %} {% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button> <button name="abandon_bid" type="submit" value="Abandon Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button> <button name="accept_bid" value="Accept Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -689,6 +689,16 @@ document.addEventListener('DOMContentLoaded', function() {
overrideButtonConfirm(acceptBidBtn, 'Accept'); overrideButtonConfirm(acceptBidBtn, 'Accept');
}); });
</script> </script>
<script src="/static/js/pages/bid-page.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
BidPage.init('{{ bid_id }}', {{ data.bid_state_ind }}, {{ data.created_at_timestamp }}, {{ data.state_time_timestamp or 'null' }}, {
swapType: 'secret-hash',
coinFrom: '{{ data.ticker_from }}',
coinTo: '{{ data.ticker_to }}'
});
});
</script>
</div> </div>
{% include 'footer.html' %} {% include 'footer.html' %}
</body> </body>
+12 -2
View File
@@ -801,14 +801,14 @@
</div> </div>
{% if data.can_abandon == true %} {% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button> <button name="abandon_bid" type="submit" value="Abandon Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button> <button name="accept_bid" value="Accept Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -965,6 +965,16 @@ document.addEventListener('DOMContentLoaded', function() {
overrideButtonConfirm(acceptBidBtn, 'Accept'); overrideButtonConfirm(acceptBidBtn, 'Accept');
}); });
</script> </script>
<script src="/static/js/pages/bid-page.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
BidPage.init('{{ bid_id }}', {{ data.bid_state_ind }}, {{ data.created_at_timestamp }}, {{ data.state_time_timestamp or 'null' }}, {
swapType: 'adaptor-sig',
coinFrom: '{{ data.ticker_from }}',
coinTo: '{{ data.ticker_to }}'
});
});
</script>
</div> </div>
{% include 'footer.html' %} {% include 'footer.html' %}
</body> </body>
+2 -2
View File
@@ -24,9 +24,9 @@
<div class="w-full md:w-1/2 mb-6 md:mb-0"> <div class="w-full md:w-1/2 mb-6 md:mb-0">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-gray-90 dark:text-white font-medium">© 2026~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.3.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">GUI: v3.4.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ love_svg | safe }}
</div> </div>
+223 -5
View File
@@ -105,17 +105,31 @@
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4"> <h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
Connection Connection
</h4> </h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4"> <div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Connection Type</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Connection Type</label>
{% if c.supports_electrum %}
<div class="relative">
<select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none" name="connection_type_{{ c.name }}" data-original-value="{{ c.connection_type }}">
<option value="rpc" {% if c.connection_type == 'rpc' %} selected{% endif %}>Full Node (RPC)</option>
<option value="electrum" {% if c.connection_type == 'electrum' %} selected{% endif %}>Light Wallet (Electrum)</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
{% else %}
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg text-sm text-gray-900 dark:text-gray-100"> <div class="px-3 py-2 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg text-sm text-gray-900 dark:text-gray-100">
{{ c.connection_type }} {{ c.connection_type }}
</div> </div>
{% endif %}
</div> </div>
{% if c.manage_daemon is defined %} {% if c.manage_daemon is defined %}
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manage Daemon</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manage Daemon</label>
@@ -138,12 +152,169 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if c.supports_electrum %}
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
Mode Information
</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 space-y-4">
<div>
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">Light Wallet Mode (Electrum):</p>
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
<li>• No blockchain download needed - connect via external Electrum servers</li>
<li>• Uses BIP84 derivation (native SegWit) - lower fees, modern addresses (bc1q.../ltc1q...)</li>
<li>• You receive an extended private key (zprv/...) that can be imported into external wallets</li>
<li>• Best for: fresh installs, low storage, quick setup, mobile-friendly</li>
</ul>
</div>
<div>
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">Full Node Mode (RPC):</p>
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
<li>• Maximum privacy - no external servers, your node validates everything</li>
<li>• More wallet features: coin control, RBF, CPFP, raw transactions</li>
<li>• Supports legacy address types and coin-specific features (e.g. MWEB for LTC)</li>
<li>• Best for: existing node users, power users, maximum control</li>
</ul>
</div>
<div>
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">When switching modes:</p>
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
<li>• To Light: Save your BIP84 key shown during switch (for external wallet import)</li>
<li>• To Full Node: Funds on light wallet addresses must be transferred (network fee applies)</li>
<li>• Both modes share the same seed - switching is safe, just save keys when shown</li>
</ul>
</div>
<div class="pt-2 border-t border-gray-200 dark:border-gray-600">
<p class="text-xs text-red-600 dark:text-red-400"><strong>Active Swaps:</strong> Complete all swaps before switching modes.</p>
{% if c.name == 'litecoin' %}
<p class="text-xs text-gray-700 dark:text-gray-200 mt-1"><strong>MWEB:</strong> Not supported in light wallet mode.</p>
{% endif %}
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1"><strong>If TOR enabled:</strong> Electrum connections routed through TOR.</p>
</div>
</div>
</div>
{% if c.supports_electrum %}
<div id="electrum-section-{{ c.name }}" class="mb-6 {% if c.connection_type != 'electrum' %}hidden{% endif %}"
data-default-clearnet="{{ c.clearnet_servers_text }}"
data-default-onion="{{ c.onion_servers_text }}">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
Electrum Servers
</h4>
<div class="mb-6">
<h5 class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-3">Clearnet</h5>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<textarea class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none font-mono" name="electrum_clearnet_{{ c.name }}" id="electrum_clearnet_{{ c.name }}" rows="3" placeholder="electrum.blockstream.info:50002&#10;electrum.emzy.de:50002">{% if c.connection_type == 'electrum' %}{{ c.clearnet_servers_text }}{% endif %}</textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">One per line. Format: host:port (50002=SSL, 50001=non-SSL)</p>
</div>
</div>
<div class="mb-4 pt-2">
<h5 class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-3">TOR (.onion)</h5>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<textarea class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none font-mono text-xs" name="electrum_onion_{{ c.name }}" id="electrum_onion_{{ c.name }}" rows="3" placeholder="explorerzyd...onion:110&#10;lksvbmwwi2b...onion:50001">{% if c.connection_type == 'electrum' %}{{ c.onion_servers_text }}{% endif %}</textarea>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">One per line. Used when TOR is enabled.</p>
</div>
</div>
<!-- Discover Servers Button -->
<div class="mb-4 flex justify-end">
<button type="button" class="discover-servers-btn inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-md transition-colors focus:outline-none focus:ring-0" data-coin="{{ c.name }}">
Discover {{ c.name }} electrum servers
</button>
</div>
<!-- Discovered Servers Panel -->
<div id="discovered-servers-{{ c.name }}" class="hidden mb-4">
<div class="bg-gray-50 dark:bg-gray-700 rounded-xl overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<h5 class="text-sm font-semibold text-gray-900 dark:text-white">
Discovered Servers
</h5>
<button type="button" class="close-discovered-btn text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-white transition-colors" data-coin="{{ c.name }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="px-6 py-4">
<div id="discovered-list-{{ c.name }}" class="space-y-1 max-h-64 overflow-y-auto">
<div class="text-sm text-gray-500 dark:text-gray-400">Click "Discover Servers" to find available servers...</div>
</div>
</div>
</div>
</div>
</div>
<div id="fund-transfer-section-{{ c.name }}" class="mb-6 hidden">
{% if c.lite_wallet_balance %}
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
Pending Balance
</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div class="p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-700 rounded-lg">
<p class="text-sm font-medium text-orange-800 dark:text-orange-200 mb-2">
<svg class="inline w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
Light Wallet Balance Detected
</p>
<div class="text-xs text-orange-700 dark:text-orange-300 space-y-1">
<p><strong>Confirmed:</strong> {{ "%.8f"|format(c.lite_wallet_balance.confirmed) }} {{ c.display_name }}</p>
{% if c.lite_wallet_balance.unconfirmed > 0 %}
<p><strong>Unconfirmed:</strong> {{ "%.8f"|format(c.lite_wallet_balance.unconfirmed) }} {{ c.display_name }}</p>
{% endif %}
<p class="text-xs text-orange-600 dark:text-orange-400 mt-2">
{% if c.lite_wallet_balance.is_pending_sweep %}
<span class="inline-flex items-center"><svg class="animate-spin h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Sweep pending - waiting for confirmations...</span>
{% else %}
These funds will be swept to your RPC wallet automatically.
{% endif %}
</p>
{% if c.lite_wallet_balance.confirmed > 0 %}
<div class="mt-3">
<button type="submit" name="force_sweep_{{ c.name }}" value="1" class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 dark:focus:ring-offset-gray-800" onclick="return confirm('Sweep {{ '%.8f'|format(c.lite_wallet_balance.confirmed) }} {{ c.display_name }} to your RPC wallet now? Network fee will apply.');">
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Force Sweep Now
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if general_settings.debug %}
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4 {% if c.lite_wallet_balance %}mt-6{% endif %}">
Advanced
</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address Gap Limit</label>
<div class="flex items-center">
<input type="number" name="gap_limit_{{ c.name }}" value="{{ c.address_gap_limit }}" min="5" max="100" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-24 p-2.5 focus:ring-1 focus:outline-none" placeholder="50">
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(5-100)</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Number of consecutive unfunded addresses to scan. Increase if you generated many unused addresses.</p>
</div>
</div>
{% endif %}
</div>
{% endif %}
{% endif %}
{% if c.name in ('wownero', 'monero') %} {% if c.name in ('wownero', 'monero') %}
<div class="mb-6"> <div class="mb-6">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4"> <h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
@@ -656,6 +827,53 @@
</div> </div>
</div> </div>
<div id="migrationModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg">
<div class="text-center">
<div class="flex justify-center mb-4">
<svg class="animate-spin h-12 w-12 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2" id="migrationTitle">Migrating Wallet</h2>
<p class="text-gray-600 dark:text-gray-200" id="migrationMessage">Extracting addresses from wallet. Please wait...</p>
</div>
</div>
</div>
</div>
<div id="walletModeModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-lg w-full p-6 shadow-lg">
<div class="text-center">
<div class="flex justify-center mb-4">
<svg class="h-12 w-12 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="walletModeTitle">Switch Wallet Mode</h2>
<p class="text-gray-600 dark:text-gray-200 mb-4" id="walletModeMessage">Are you sure you want to switch wallet modes?</p>
<div id="walletModeDetails" class="text-left bg-gray-100 dark:bg-gray-600 rounded-lg p-4 mb-4 text-sm text-gray-700 dark:text-gray-200">
</div>
<div class="flex justify-center gap-4">
<button type="button" id="walletModeConfirm"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Switch Mode
</button>
<button type="button" id="walletModeCancel"
class="px-4 py-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 font-medium text-sm text-gray-800 dark:text-white rounded-md border border-gray-300 dark:border-gray-500 focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/pages/settings-page.js"></script> <script src="/static/js/pages/settings-page.js"></script>
{% include 'footer.html' %} {% include 'footer.html' %}
+1 -1
View File
@@ -101,7 +101,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div id="caps-warning" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center"> <div id="caps-warning" class="hidden mt-2 text-sm text-red-600 dark:text-white flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path> <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg> </svg>
+297 -11
View File
@@ -35,6 +35,8 @@
</section> </section>
{% endif %} {% endif %}
{% if w.havedata %} {% if w.havedata %}
{% if w.error %} {% if w.error %}
<section class="py-4 px-6" id="messages_error" role="alert"> <section class="py-4 px-6" id="messages_error" role="alert">
@@ -82,6 +84,36 @@
</section> </section>
{% endif %} {% endif %}
{% if legacy_funds_info and legacy_funds_info.has_legacy_funds %}
<section class="py-4 px-6" id="legacy_funds_warning">
<div class="lg:container mx-auto">
<div class="p-6 rounded-lg bg-yellow-50 border border-yellow-400 dark:bg-yellow-900/30 dark:border-yellow-700">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3 flex-1">
<p class="font-semibold text-lg lg:text-sm text-yellow-700 dark:text-yellow-300">Legacy Address Funds</p>
<p class="mt-1 text-sm text-yellow-600 dark:text-yellow-400">
{{ legacy_funds_info.legacy_balance }} {{ legacy_funds_info.coin }} on legacy addresses won't be visible in external Electrum wallet.
To use funds with external wallets, transfer to a new address.
</p>
<p class="mt-2 text-xs text-yellow-500 dark:text-yellow-500">
Use the withdraw function below to send funds to a new <code class="bg-yellow-100 dark:bg-yellow-800/50 px-1 rounded">{{ w.ticker | lower }}1...</code> address.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
<section> <section>
<form method="post" autocomplete="off"> <form method="post" autocomplete="off">
<div class="px-6 py-0 h-full overflow-hidden"> <div class="px-6 py-0 h-full overflow-hidden">
@@ -111,6 +143,9 @@
{% if w.pending %} {% if w.pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
{% endif %} {% endif %}
{% if w.pending_out %}
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-yellow-600 dark:bg-gray-500 dark:text-yellow-400">Unconfirmed: -{{ w.pending_out }} {{ w.ticker }} </span>
{% endif %}
</td> </td>
</tr> </tr>
{% if w.cid == '1' %} {# PART #} {% if w.cid == '1' %} {# PART #}
@@ -139,15 +174,42 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.mweb_pending %} {% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
{% endif %} {% endif %}
{% endif %}
</td>
</tr>
{% elif w.cid == '13' %} {# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
{% endif %}
</td>
</tr>
{% elif w.cid == '13' %} {# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
{% endif %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{# / LTC #} {# / LTC #}
{# / FIRO #}
{% if w.locked_utxos %} {% if w.locked_utxos %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Locked Outputs:</td> <td class="py-3 px-6 bold">Locked Outputs:</td>
@@ -163,6 +225,19 @@
<td class="py-3 px-6 bold">{{ w.name }} Version:</td> <td class="py-3 px-6 bold">{{ w.name }} Version:</td>
<td class="py-3 px-6">{{ w.version }}</td> <td class="py-3 px-6">{{ w.version }}</td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Wallet Mode:</td>
<td class="py-3 px-6">
{% if w.connection_type == 'electrum' %}
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Light Wallet (Electrum)</span>
{% else %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">Full Node</span>
{% endif %}
{% if use_tor %}
<span class="inline-block py-1 px-2 ml-2 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" title="Electrum connections routed through TOR">TOR</span>
{% endif %}
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Blockheight:</td> <td class="py-3 px-6 bold">Blockheight:</td>
<td class="py-3 px-6">{{ w.blocks }} <td class="py-3 px-6">{{ w.blocks }}
@@ -178,8 +253,70 @@
{% endif %} {% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Synced:</td> <td class="py-3 px-6 bold">Synced:</td>
<td class="py-3 px-6">{{ w.synced }}</td> <td class="py-3 px-6">
{% if is_electrum_mode %}
{% if w.electrum_synced %}
<span class="text-green-600 dark:text-green-400">Electrum Wallet Synced ({{ w.electrum_height }})</span>
{% else %}
<span class="text-yellow-600 dark:text-yellow-400">Waiting for Electrum Server...</span>
{% endif %}
{% else %}
{{ w.synced }}
{% endif %}
</td>
</tr> </tr>
{% if is_electrum_mode and w.electrum_server %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Server:</td>
<td class="py-3 px-6 font-mono text-sm">
{{ w.electrum_server }}
{% if w.electrum_status == 'connected' %}
<span class="ml-2 inline-flex items-center">
<span class="text-xs text-green-600 dark:text-green-400">(Connected)</span>
</span>
{% elif w.electrum_status == 'all_failed' %}
<span class="ml-2 inline-flex items-center">
<span class="text-xs text-red-600 dark:text-red-400">(All Servers Failed)</span>
</span>
{% elif w.electrum_status == 'disconnected' %}
<span class="ml-2 inline-flex items-center">
<span class="text-xs text-red-600 dark:text-red-400">(Disconnected - Reconnecting...)</span>
</span>
{% elif w.electrum_status == 'error' %}
<span class="ml-2 inline-flex items-center">
<span class="w-2 h-2 rounded-full bg-yellow-500 mr-1"></span>
<span class="text-xs text-yellow-600 dark:text-yellow-400">(Connection Error)</span>
</span>
{% endif %}
</td>
</tr>
{% endif %}
{% if is_electrum_mode and w.electrum_all_failed and w.electrum_using_defaults %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td colspan="2" class="py-3 px-6">
<div class="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded-lg">
<p class="text-sm font-medium text-red-800 dark:text-red-200 mb-1">
<svg class="inline w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
Default Electrum Servers Unavailable
</p>
<p class="text-xs text-red-700 dark:text-red-300">
All default servers failed to connect. Please configure custom Electrum servers in
<a href="/settings" class="underline font-medium hover:text-red-900 dark:hover:text-red-100">Settings</a>
under the {{ w.name }} section.
</p>
{% if w.electrum_last_error %}
<p class="text-xs text-red-600 dark:text-red-400 mt-1 font-mono">Last error: {{ w.electrum_last_error }}</p>
{% endif %}
</div>
</td>
</tr>
{% endif %}
{% if is_electrum_mode and w.electrum_version %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Server Version:</td>
<td class="py-3 px-6">{{ w.electrum_version }}</td>
</tr>
{% endif %}
{% if w.bootstrapping %} {% if w.bootstrapping %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Bootstrapping:</td> <td class="py-3 px-6 bold">Bootstrapping:</td>
@@ -286,8 +423,8 @@
</div> </div>
</div> </div>
</div> </div>
{% if w.cid in '1, 3, 6, 9' %} {% if w.cid in '1, 3, 6, 9, 13' %}
{# PART | LTC | XMR | WOW | #} {# PART | LTC | XMR | WOW | FIRO #}
<div class="w-full md:w-1/2 p-3 flex justify-center items-center"> <div class="w-full md:w-1/2 p-3 flex justify-center items-center">
<div class="h-full"> <div class="h-full">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
@@ -319,13 +456,14 @@
</div> </div>
{# / PART #} {# / PART #}
{% elif w.cid == '3' %} {% elif w.cid == '3' %}
{# LTC #} {# LTC - MWEB not available in light mode #}
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div> {% if not is_electrum_mode %}
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div>
</div> </div>
</div> </div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div> <div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
<div class="text-center relative"> <div class="text-center relative">
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.mweb_address }}</div> <div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="mweb_address">{{ w.mweb_address }}</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span> <span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
</div> </div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center"> <div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
@@ -333,7 +471,24 @@
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newmwebaddr_{{ w.cid }}" value="New MWEB Address"> {{ circular_arrows_svg }} New MWEB Address </button> <button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newmwebaddr_{{ w.cid }}" value="New MWEB Address"> {{ circular_arrows_svg }} New MWEB Address </button>
</div> </div>
</div> </div>
{% endif %}
{# / LTC #} {# / LTC #}
{% elif w.cid == '13' %}
{# FIRO #}
<div id="qrcode-spark" class="qrcode" data-qrcode data-address="{{ w.spark_address }}"> </div>
</div>
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Spark Address: </div>
<div class="text-center relative">
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.spark_address }}</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
</div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
<div class="py-3 px-6 bold mt-5">
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newsparkaddr_{{ w.cid }}" value="New Spark Address"> {{ circular_arrows_svg }} New Spark Address </button>
</div>
</div>
{# / FIRO #}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -349,10 +504,6 @@
</div> </div>
</section> </section>
<!-- Address copy functionality handled by external JS -->
<section class="p-6"> <section class="p-6">
<div class="lg:container mx-auto"> <div class="lg:container mx-auto">
<div class="flex items-center"> <div class="flex items-center">
@@ -393,8 +544,30 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td> <td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% endif %}
</td>
</tr>
{% elif w.cid == '13' %}
{# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
{% elif w.cid == '13' %}
{# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
{% elif w.cid == '1' %} {% elif w.cid == '1' %}
@@ -487,6 +660,14 @@
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
{# / LTC #} {# / LTC #}
{% elif w.cid == '13' %}
{# FIRO #}
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">25%</button>
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">100%</button>
{# / FIRO #}
{% else %} {% else %}
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button> <button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button> <button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
@@ -541,7 +722,7 @@
</td> </td>
</tr> </tr>
{# / PART #} {# / PART #}
{% elif w.cid == '3' %} {# LTC #} {% elif w.cid == '3' and not is_electrum_mode %} {# LTC - only show in full node mode #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Type From:</td> <td class="py-3 px-6 bold">Type From:</td>
<td class="py-3 px-6"> <td class="py-3 px-6">
@@ -553,8 +734,21 @@
</div> </div>
</td> </td>
</tr> </tr>
{% elif w.cid == '13' %} {# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Type From:</td>
<td class="py-3 px-6">
<div class="w-full md:flex-1">
<div class="relative"> {{ select_box_arrow_svg }} <select id="withdraw_type" class="{{ select_box_class }}" name="withdraw_type_from_{{ w.cid }}">
<option value="spark" {% if w.wd_type_from=='spark' %} selected{% endif %}>Spark</option>
<option value="plain" {% if w.wd_type_from=='plain' %} selected{% endif %}>Plain</option>
</select> </div>
</div>
</td>
</tr>
{% endif %} {% endif %}
{# / LTC #} {# / LTC #}
{# / FIRO #}
{% if w.cid not in '6,9' %} {# Not XMR WOW #} {% if w.cid not in '6,9' %} {# Not XMR WOW #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Rate:</td> <td class="py-3 px-6 bold">Fee Rate:</td>
@@ -593,6 +787,8 @@
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div> <div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div>
{# / XMR | WOW #} {# / XMR | WOW #}
{% elif w.show_utxo_groups %} {% elif w.show_utxo_groups %}
{% elif is_electrum_mode %}
{# Hide UTXO Groups button in electrum/lite wallet mode #}
{% else %} {% else %}
<div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div> <div class="w-full md:w-auto p-1.5 mx-1"> <button type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div>
{% endif %} {% endif %}
@@ -680,6 +876,96 @@
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</form> </form>
{% if w.havedata and not w.error %}
<section class="p-6">
<div class="lg:container mx-auto">
<div class="flex items-center">
<h4 class="font-semibold text-2xl text-black dark:text-white">Transaction History</h4>
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="lg:container mt-5 mx-auto">
<div id="transaction-history-section" class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
{% if is_electrum_mode %}
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
Transaction history is not available in Light Wallet mode.
</div>
{% else %}
<div id="tx-container" class="space-y-3 pb-6">
<div class="text-center py-8 text-gray-500 dark:text-gray-400">Loading transactions...</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% if not is_electrum_mode %}
<section id="tx-pagination-section" style="display: none;">
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="lg:container mx-auto">
<div class="py-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
<div class="px-6">
<div class="flex flex-wrap justify-end items-center space-x-4">
<button id="prevPageTx" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-md transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">
Page <span id="currentPageTx">1</span> of <span id="totalPagesTx">1</span>
</p>
<button id="nextPageTx" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-md transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% if is_electrum_mode %}
<section id="tx-pagination-section">
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="lg:container mx-auto">
<div class="py-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% endif %}
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto"> <div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div> <div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center"> <div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
+74 -2
View File
@@ -48,8 +48,18 @@
<div class="px-6 mb-6"> <div class="px-6 mb-6">
<h4 class="text-xl font-bold dark:text-white">{{ w.name }} <h4 class="text-xl font-bold dark:text-white">{{ w.name }}
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span> <span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
{% if w.connection_type == 'electrum' %}
<span class="inline-block py-1 px-2 rounded-full bg-yellow-100 text-xs text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Light Wallet (Electrum)</span>
{% else %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-700 dark:bg-green-900 dark:text-green-300">Full Node</span>
{% endif %}
{% if use_tor %}
<span class="inline-block py-1 px-2 rounded-full bg-purple-100 text-xs text-purple-700 dark:bg-purple-900 dark:text-purple-300">TOR</span>
{% endif %}
</h4> </h4>
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p> <p class="pt-2 text-xs text-gray-500 dark:text-gray-200">Version: <span class="electrum-version" data-coin="{{ w.name }}">{{ w.version }}</span> {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span>{% endif %}</p>
{% if w.electrum_server %}
<p class="text-xs text-gray-500 dark:text-gray-200">Server: <span class="electrum-server" data-coin="{{ w.name }}">{{ w.electrum_server }}</span></p>
{% endif %} {% endif %}
</div> </div>
<div class="p-6 bg-coolGray-100 dark:bg-gray-600"> <div class="p-6 bg-coolGray-100 dark:bg-gray-600">
@@ -71,6 +81,12 @@
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div> <div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div> </div>
{% endif %} {% endif %}
{% if w.pending_out %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-yellow-600 dark:text-yellow-400">Unconfirmed:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-yellow-100 text-xs text-yellow-600 dark:bg-gray-500 dark:text-yellow-400 coinname-value" data-coinname="{{ w.name }}">-{{ w.pending_out }} {{ w.ticker }}</span>
</div>
{% endif %}
{% if w.cid == '1' %} {# PART #} {% if w.cid == '1' %} {# PART #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4> <h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4>
@@ -110,7 +126,7 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {# / PART #} {% endif %} {# / PART #}
{% if w.cid == '3' %} {# LTC #} {% if w.cid == '3' and w.connection_type != 'electrum' %} {# LTC - MWEB not available in electrum mode #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4> <h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
@@ -132,6 +148,28 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{# / LTC #} {# / LTC #}
{% if w.cid == '13' %} {# FIRO #}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 usd-value"></div>
</div>
{% if w.spark_pending %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Spark Pending:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="{{ w.name }}">
+{{ w.spark_pending }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Spark Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div>
{% endif %}
{% endif %}
{# / FIRO #}
<hr class="border-t border-gray-100 dark:border-gray-500 my-5"> <hr class="border-t border-gray-100 dark:border-gray-500 my-5">
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blocks:</h4> <h4 class="text-xs font-medium dark:text-white">Blocks:</h4>
@@ -159,6 +197,39 @@
<h4 class="text-xs font-medium dark:text-white">Expected Seed:</h4> <h4 class="text-xs font-medium dark:text-white">Expected Seed:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.expected_seed }}</span> <span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.expected_seed }}</span>
</div> </div>
{% if w.connection_type == 'electrum' %}
<div class="scan-status mt-10 p-2 rounded" data-coin="{{ w.name }}">
{% if w.scan_status and w.scan_status.in_progress %}
<div class="bg-blue-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center justify-between text-xs">
<span class="text-blue-600 dark:text-blue-300">
<svg class="inline-block w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Scanning {{ w.scan_status.status }}
</span>
<span class="text-blue-500 dark:text-blue-200 font-medium">{{ w.scan_status.progress }}%</span>
</div>
<div class="w-full bg-blue-200 dark:bg-gray-700 rounded-full h-1 mt-1">
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full" style="width: {{ w.scan_status.progress }}%"></div>
</div>
</div>
{% elif w.electrum_synced %}
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
Electrum Wallet Synced ({{ w.electrum_height }})
</div>
</div>
{% else %}
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
Waiting for Electrum Server...
</div>
</div>
{% endif %}
</div>
{% else %}
<div class="flex justify-between mb-1 mt-10"> <div class="flex justify-between mb-1 mt-10">
<span class="text-xs font-medium dark:text-gray-200">Blockchain</span> <span class="text-xs font-medium dark:text-gray-200">Blockchain</span>
<span class="text-xs font-medium dark:text-gray-200">{{ w.synced }}%</span> <span class="text-xs font-medium dark:text-gray-200">{{ w.synced }}%</span>
@@ -179,6 +250,7 @@
</div> </div>
</span> </span>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
+2 -1
View File
@@ -1253,7 +1253,8 @@ def amm_autostart_api(swap_client, post_string, params=None):
settings_path = os.path.join(swap_client.data_dir, cfg.CONFIG_FILENAME) settings_path = os.path.join(swap_client.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new" settings_path_new = settings_path + ".new"
shutil.copyfile(settings_path, settings_path + ".last") if os.path.exists(settings_path):
shutil.copyfile(settings_path, settings_path + ".last")
with open(settings_path_new, "w") as fp: with open(settings_path_new, "w") as fp:
json.dump(swap_client.settings, fp, indent=4) json.dump(swap_client.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path) shutil.move(settings_path_new, settings_path)
+21 -1
View File
@@ -218,6 +218,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_data_entry(form_data, "fee_from_extra"): if have_data_entry(form_data, "fee_from_extra"):
page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra")) page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra"))
parsed_data["fee_from_extra"] = page_data["fee_from_extra"] parsed_data["fee_from_extra"] = page_data["fee_from_extra"]
else:
page_data["fee_from_extra"] = 0
if have_data_entry(form_data, "fee_to_conf"): if have_data_entry(form_data, "fee_to_conf"):
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf")) page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
@@ -226,6 +228,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_data_entry(form_data, "fee_to_extra"): if have_data_entry(form_data, "fee_to_extra"):
page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra")) page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra"))
parsed_data["fee_to_extra"] = page_data["fee_to_extra"] parsed_data["fee_to_extra"] = page_data["fee_to_extra"]
else:
page_data["fee_to_extra"] = 0
if have_data_entry(form_data, "check_offer"): if have_data_entry(form_data, "check_offer"):
page_data["check_offer"] = True page_data["check_offer"] = True
@@ -249,6 +253,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
get_data_entry(form_data, "valid_for_seconds") get_data_entry(form_data, "valid_for_seconds")
) )
if swap_client.debug:
if have_data_entry(form_data, "lock_type"):
parsed_data["lock_type"] = TxLockTypes(
int(get_data_entry(form_data, "lock_type"))
)
if have_data_entry(form_data, "lock_blocks"):
parsed_data["lock_blocks"] = int(get_data_entry(form_data, "lock_blocks"))
try: try:
if len(errors) == 0 and page_data["swap_style"] == "xmr": if len(errors) == 0 and page_data["swap_style"] == "xmr":
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to) reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
@@ -342,7 +354,15 @@ def postNewOfferFromParsed(swap_client, parsed_data):
lock_type = TxLockTypes.ABS_LOCK_TIME lock_type = TxLockTypes.ABS_LOCK_TIME
extra_options = {} extra_options = {}
lock_value: int = parsed_data.get("lock_seconds", -1)
if swap_client.debug:
if "lock_type" in parsed_data:
lock_type = parsed_data["lock_type"]
if "lock_blocks" in parsed_data:
lock_value = parsed_data["lock_blocks"]
if "fee_from_conf" in parsed_data:
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
if "fee_from_conf" in parsed_data: if "fee_from_conf" in parsed_data:
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"] extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
if "from_fee_multiplier_percent" in parsed_data: if "from_fee_multiplier_percent" in parsed_data:
@@ -393,7 +413,7 @@ def postNewOfferFromParsed(swap_client, parsed_data):
parsed_data["amt_bid_min"], parsed_data["amt_bid_min"],
swap_type, swap_type,
lock_type=lock_type, lock_type=lock_type,
lock_value=parsed_data["lock_seconds"], lock_value=lock_value,
addr_send_from=parsed_data["addr_from"], addr_send_from=parsed_data["addr_from"],
extra_options=extra_options, extra_options=extra_options,
) )
+130 -2
View File
@@ -104,6 +104,10 @@ def page_settings(self, url_split, post_string):
"TODO: If running in docker see doc/tor.md to enable/disable tor." "TODO: If running in docker see doc/tor.md to enable/disable tor."
) )
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
for name, c in swap_client.settings["chainclients"].items(): for name, c in swap_client.settings["chainclients"].items():
if have_data_entry(form_data, "apply_" + name): if have_data_entry(form_data, "apply_" + name):
data = {"lookups": get_data_entry(form_data, "lookups_" + name)} data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
@@ -138,10 +142,70 @@ def page_settings(self, url_split, post_string):
data["anon_tx_ring_size"] = int( data["anon_tx_ring_size"] = int(
get_data_entry(form_data, "rct_ring_size_" + name) get_data_entry(form_data, "rct_ring_size_" + name)
) )
if name in electrum_supported_coins:
new_connection_type = get_data_entry_or(
form_data, "connection_type_" + name, None
)
if new_connection_type and new_connection_type != c.get(
"connection_type"
):
coin_id = swap_client.getCoinIdFromName(name)
has_active_swaps = False
for bid_id, (bid, offer) in list(
swap_client.swaps_in_progress.items()
):
if (
offer.coin_from == coin_id
or offer.coin_to == coin_id
):
has_active_swaps = True
break
if has_active_swaps:
display_name = getCoinName(coin_id)
err_messages.append(
f"Cannot change {display_name} connection mode while swaps are in progress. "
f"Please wait for all {display_name} swaps to complete."
)
else:
data["connection_type"] = new_connection_type
if new_connection_type == "electrum":
data["manage_daemon"] = False
elif new_connection_type == "rpc":
data["manage_daemon"] = True
clearnet_servers = get_data_entry_or(
form_data, "electrum_clearnet_" + name, ""
).strip()
data["electrum_clearnet_servers"] = clearnet_servers
onion_servers = get_data_entry_or(
form_data, "electrum_onion_" + name, ""
).strip()
data["electrum_onion_servers"] = onion_servers
auto_transfer_now = have_data_entry(
form_data, "auto_transfer_now_" + name
)
if auto_transfer_now:
transfer_value = get_data_entry_or(
form_data, "auto_transfer_now_" + name, "false"
)
data["auto_transfer_now"] = transfer_value == "true"
gap_limit_str = get_data_entry_or(
form_data, "gap_limit_" + name, "50"
).strip()
try:
gap_limit = int(gap_limit_str)
if gap_limit < 5:
gap_limit = 5
elif gap_limit > 100:
gap_limit = 100
data["address_gap_limit"] = gap_limit
except ValueError:
pass
settings_changed, suggest_reboot = swap_client.editSettings( settings_changed, suggest_reboot, migration_message = (
name, data swap_client.editSettings(name, data)
) )
if migration_message:
messages.append(migration_message)
if settings_changed is True: if settings_changed is True:
messages.append("Settings applied.") messages.append("Settings applied.")
if suggest_reboot is True: if suggest_reboot is True:
@@ -156,19 +220,71 @@ def page_settings(self, url_split, post_string):
display_name = getCoinName(swap_client.getCoinIdFromName(name)) display_name = getCoinName(swap_client.getCoinIdFromName(name))
messages.append(display_name + " disabled, shutting down.") messages.append(display_name + " disabled, shutting down.")
swap_client.stopRunning() swap_client.stopRunning()
elif have_data_entry(form_data, "force_sweep_" + name):
coin_id = swap_client.getCoinIdFromName(name)
display_name = getCoinName(coin_id)
try:
result = swap_client.sweepLiteWalletFunds(coin_id)
if result.get("success"):
amount = result.get("amount", 0)
fee = result.get("fee", 0)
txid = result.get("txid", "")
messages.append(
f"Successfully swept {amount:.8f} {display_name} to RPC wallet. "
f"Fee: {fee:.8f}. TXID: {txid} (1 confirmation required)"
)
elif result.get("skipped"):
messages.append(
f"{display_name}: {result.get('reason', 'Sweep skipped')}"
)
else:
err_messages.append(
f"{display_name}: Sweep failed - {result.get('error', 'Unknown error')}"
)
except Exception as e:
err_messages.append(f"{display_name}: Sweep failed - {str(e)}")
except InactiveCoin as ex: except InactiveCoin as ex:
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name)) err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
except Exception as e: except Exception as e:
err_messages.append(str(e)) err_messages.append(str(e))
chains_formatted = [] chains_formatted = []
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
sorted_names = sorted(swap_client.settings["chainclients"].keys()) sorted_names = sorted(swap_client.settings["chainclients"].keys())
from basicswap.interface.electrumx import (
DEFAULT_ELECTRUM_SERVERS,
DEFAULT_ONION_SERVERS,
)
for name in sorted_names: for name in sorted_names:
c = swap_client.settings["chainclients"][name] c = swap_client.settings["chainclients"][name]
try: try:
display_name = getCoinName(swap_client.getCoinIdFromName(name)) display_name = getCoinName(swap_client.getCoinIdFromName(name))
except Exception: except Exception:
display_name = name display_name = name
clearnet_servers = c.get("electrum_clearnet_servers", None)
onion_servers = c.get("electrum_onion_servers", None)
if not clearnet_servers:
default_clearnet = DEFAULT_ELECTRUM_SERVERS.get(name, [])
clearnet_servers = [
f"{s['host']}:{s['port']}:{str(s.get('ssl', True)).lower()}"
for s in default_clearnet
]
if not onion_servers:
default_onion = DEFAULT_ONION_SERVERS.get(name, [])
onion_servers = [
f"{s['host']}:{s['port']}:{str(s.get('ssl', False)).lower()}"
for s in default_onion
]
clearnet_text = "\n".join(clearnet_servers) if clearnet_servers else ""
onion_text = "\n".join(onion_servers) if onion_servers else ""
chains_formatted.append( chains_formatted.append(
{ {
"name": name, "name": name,
@@ -176,6 +292,10 @@ def page_settings(self, url_split, post_string):
"lookups": c.get("chain_lookups", "local"), "lookups": c.get("chain_lookups", "local"),
"manage_daemon": c.get("manage_daemon", "Unknown"), "manage_daemon": c.get("manage_daemon", "Unknown"),
"connection_type": c.get("connection_type", "Unknown"), "connection_type": c.get("connection_type", "Unknown"),
"supports_electrum": name in electrum_supported_coins,
"clearnet_servers_text": clearnet_text,
"onion_servers_text": onion_text,
"address_gap_limit": c.get("address_gap_limit", 50),
} }
) )
if name in ("monero", "wownero"): if name in ("monero", "wownero"):
@@ -203,6 +323,14 @@ def page_settings(self, url_split, post_string):
else: else:
chains_formatted[-1]["can_disable"] = True chains_formatted[-1]["can_disable"] = True
try:
coin_id = swap_client.getCoinIdFromName(name)
lite_balance_info = swap_client.getLiteWalletBalanceInfo(coin_id)
if lite_balance_info:
chains_formatted[-1]["lite_wallet_balance"] = lite_balance_info
except Exception:
pass
general_settings = { general_settings = {
"debug": swap_client.debug, "debug": swap_client.debug,
"debug_ui": swap_client.debug_ui, "debug_ui": swap_client.debug_ui,
+184 -7
View File
@@ -32,11 +32,15 @@ DONATION_ADDRESSES = {
def format_wallet_data(swap_client, ci, w): def format_wallet_data(swap_client, ci, w):
coin_id = ci.coin_type()
connection_type = swap_client.coin_clients.get(coin_id, {}).get(
"connection_type", w.get("connection_type", "rpc")
)
wf = { wf = {
"name": ci.coin_name(), "name": ci.coin_name(),
"version": w.get("version", "?"), "version": w.get("version", "?"),
"ticker": ci.ticker_mainnet(), "ticker": ci.ticker_mainnet(),
"cid": str(int(ci.coin_type())), "cid": str(int(coin_id)),
"balance": w.get("balance", "?"), "balance": w.get("balance", "?"),
"blocks": w.get("blocks", "?"), "blocks": w.get("blocks", "?"),
"synced": w.get("synced", "?"), "synced": w.get("synced", "?"),
@@ -45,6 +49,7 @@ def format_wallet_data(swap_client, ci, w):
"locked": w.get("locked", "?"), "locked": w.get("locked", "?"),
"updating": w.get("updating", "?"), "updating": w.get("updating", "?"),
"havedata": True, "havedata": True,
"connection_type": connection_type,
} }
if "wallet_blocks" in w: if "wallet_blocks" in w:
@@ -70,6 +75,9 @@ def format_wallet_data(swap_client, ci, w):
if pending > 0.0: if pending > 0.0:
wf["pending"] = ci.format_amount(pending) wf["pending"] = ci.format_amount(pending)
if "unconfirmed" in w and float(w["unconfirmed"]) < 0.0:
wf["pending_out"] = ci.format_amount(abs(ci.make_int(w["unconfirmed"])))
if ci.coin_type() == Coins.PART: if ci.coin_type() == Coins.PART:
wf["stealth_address"] = w.get("stealth_address", "?") wf["stealth_address"] = w.get("stealth_address", "?")
wf["blind_balance"] = w.get("blind_balance", "?") wf["blind_balance"] = w.get("blind_balance", "?")
@@ -82,11 +90,97 @@ def format_wallet_data(swap_client, ci, w):
wf["mweb_address"] = w.get("mweb_address", "?") wf["mweb_address"] = w.get("mweb_address", "?")
wf["mweb_balance"] = w.get("mweb_balance", "?") wf["mweb_balance"] = w.get("mweb_balance", "?")
wf["mweb_pending"] = w.get("mweb_pending", "?") wf["mweb_pending"] = w.get("mweb_pending", "?")
elif ci.coin_type() == Coins.FIRO:
wf["spark_address"] = w.get("spark_address", "?")
wf["spark_balance"] = w.get("spark_balance", "?")
wf["spark_pending"] = w.get("spark_pending", "?")
if hasattr(ci, "getScanStatus"):
wf["scan_status"] = ci.getScanStatus()
if connection_type == "electrum" and hasattr(ci, "_backend") and ci._backend:
backend = ci._backend
wf["electrum_server"] = backend.getServerHost()
wf["electrum_version"] = backend.getServerVersion()
try:
conn_status = backend.getConnectionStatus()
wf["electrum_connected"] = conn_status.get("connected", False)
wf["electrum_failures"] = conn_status.get("failures", 0)
wf["electrum_using_defaults"] = conn_status.get("using_defaults", True)
wf["electrum_all_failed"] = conn_status.get("all_failed", False)
wf["electrum_last_error"] = conn_status.get("last_error")
if conn_status.get("connected"):
wf["electrum_status"] = "connected"
elif conn_status.get("all_failed"):
wf["electrum_status"] = "all_failed"
else:
wf["electrum_status"] = "disconnected"
except Exception:
wf["electrum_connected"] = False
wf["electrum_status"] = "error"
try:
sync_status = backend.getSyncStatus()
wf["electrum_synced"] = sync_status.get("synced", False)
wf["electrum_height"] = sync_status.get("height", 0)
except Exception:
wf["electrum_synced"] = False
wf["electrum_height"] = 0
checkAddressesOwned(swap_client, ci, wf) checkAddressesOwned(swap_client, ci, wf)
return wf return wf
def format_transactions(ci, transactions, coin_id):
formatted_txs = []
if coin_id in (Coins.XMR, Coins.WOW):
for tx in transactions:
tx_type = tx.get("type", "")
direction = (
"Incoming"
if tx_type == "in"
else "Outgoing" if tx_type == "out" else tx_type.capitalize()
)
formatted_txs.append(
{
"txid": tx.get("txid", ""),
"type": direction,
"amount": ci.format_amount(tx.get("amount", 0)),
"confirmations": tx.get("confirmations", 0),
"timestamp": format_timestamp(tx.get("timestamp", 0)),
"height": tx.get("height", 0),
}
)
else:
for tx in transactions:
category = tx.get("category", "")
if category == "send":
direction = "Outgoing"
amount = abs(tx.get("amount", 0))
elif category == "receive":
direction = "Incoming"
amount = tx.get("amount", 0)
else:
direction = category.capitalize()
amount = abs(tx.get("amount", 0))
formatted_txs.append(
{
"txid": tx.get("txid", ""),
"type": direction,
"amount": ci.format_amount(ci.make_int(amount)),
"confirmations": tx.get("confirmations", 0),
"timestamp": format_timestamp(
tx.get("time", tx.get("timereceived", 0))
),
"address": tx.get("address", ""),
}
)
return formatted_txs
def page_wallets(self, url_split, post_string): def page_wallets(self, url_split, post_string):
server = self.server server = self.server
swap_client = server.swap_client swap_client = server.swap_client
@@ -131,6 +225,7 @@ def page_wallets(self, url_split, post_string):
"err_messages": err_messages, "err_messages": err_messages,
"wallets": wallets_formatted, "wallets": wallets_formatted,
"summary": summary, "summary": summary,
"use_tor": getattr(swap_client, "use_tor_proxy", False),
}, },
) )
@@ -151,8 +246,25 @@ def page_wallet(self, url_split, post_string):
show_utxo_groups: bool = False show_utxo_groups: bool = False
withdrawal_successful: bool = False withdrawal_successful: bool = False
force_refresh: bool = False force_refresh: bool = False
tx_filters = {
"page_no": 1,
"limit": 30,
"offset": 0,
}
form_data = self.checkForm(post_string, "wallet", err_messages) form_data = self.checkForm(post_string, "wallet", err_messages)
if form_data: if form_data:
if have_data_entry(form_data, "pageback"):
tx_filters["page_no"] = int(form_data[b"pageno"][0]) - 1
if tx_filters["page_no"] < 1:
tx_filters["page_no"] = 1
elif have_data_entry(form_data, "pageforwards"):
tx_filters["page_no"] = int(form_data[b"pageno"][0]) + 1
if tx_filters["page_no"] > 1:
tx_filters["offset"] = (tx_filters["page_no"] - 1) * 30
cid = str(int(coin_id)) cid = str(int(coin_id))
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid) estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
@@ -163,6 +275,8 @@ def page_wallet(self, url_split, post_string):
force_refresh = True force_refresh = True
elif have_data_entry(form_data, "newmwebaddr_" + cid): elif have_data_entry(form_data, "newmwebaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id) swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, "newsparkaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, "reseed_" + cid): elif have_data_entry(form_data, "reseed_" + cid):
try: try:
swap_client.reseedWallet(coin_id) swap_client.reseedWallet(coin_id)
@@ -170,6 +284,22 @@ def page_wallet(self, url_split, post_string):
except Exception as ex: except Exception as ex:
err_messages.append("Reseed failed " + str(ex)) err_messages.append("Reseed failed " + str(ex))
swap_client.updateWalletsInfo(True, coin_id) swap_client.updateWalletsInfo(True, coin_id)
elif have_data_entry(form_data, "importkey_" + cid):
try:
wif_key = form_data[bytes("wifkey_" + cid, "utf-8")][0].decode("utf-8")
if wif_key:
result = swap_client.importWIFKey(coin_id, wif_key)
if result.get("success"):
messages.append(
f"Imported key for address: {result['address']}"
)
else:
err_messages.append(f"Import failed: {result.get('error')}")
else:
err_messages.append("Missing WIF key")
except Exception as ex:
err_messages.append(f"Import failed: {ex}")
swap_client.updateWalletsInfo(True, coin_id)
elif withdraw or estimate_fee: elif withdraw or estimate_fee:
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
page_data["wd_subfee_" + cid] = subfee page_data["wd_subfee_" + cid] = subfee
@@ -208,14 +338,21 @@ def page_wallet(self, url_split, post_string):
page_data["wd_type_to_" + cid] = type_to page_data["wd_type_to_" + cid] = type_to
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
err_messages.append("Missing type") err_messages.append("Missing type")
elif coin_id == Coins.LTC: elif coin_id in (Coins.LTC, Coins.FIRO):
try: try:
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][ type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
0 0
].decode("utf-8") ].decode("utf-8")
page_data["wd_type_from_" + cid] = type_from page_data["wd_type_from_" + cid] = type_from
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
err_messages.append("Missing type") if (
swap_client.coin_clients[coin_id].get("connection_type")
== "electrum"
):
type_from = "plain"
page_data["wd_type_from_" + cid] = type_from
else:
err_messages.append("Missing type")
if len(err_messages) == 0: if len(err_messages) == 0:
ci = swap_client.ci(coin_id) ci = swap_client.ci(coin_id)
@@ -230,9 +367,9 @@ def page_wallet(self, url_split, post_string):
value, ticker, type_from, type_to, address, txid value, ticker, type_from, type_to, address, txid
) )
) )
elif coin_id == Coins.LTC: elif coin_id in (Coins.LTC, Coins.FIRO):
txid = swap_client.withdrawLTC( txid = swap_client.withdrawCoinExtended(
type_from, value, address, subfee coin_id, type_from, value, address, subfee
) )
messages.append( messages.append(
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format( "Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
@@ -305,8 +442,12 @@ def page_wallet(self, url_split, post_string):
if swap_client.debug is True: if swap_client.debug is True:
swap_client.log.error(traceback.format_exc()) swap_client.log.error(traceback.format_exc())
is_electrum_mode = (
swap_client.coin_clients.get(coin_id, {}).get("connection_type") == "electrum"
)
swap_client.updateWalletsInfo( swap_client.updateWalletsInfo(
force_refresh, only_coin=coin_id, wait_for_complete=True force_refresh, only_coin=coin_id, wait_for_complete=not is_electrum_mode
) )
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id}) wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
wallet_data = {} wallet_data = {}
@@ -328,6 +469,9 @@ def page_wallet(self, url_split, post_string):
cid = str(int(coin_id)) cid = str(int(coin_id))
wallet_data = format_wallet_data(swap_client, ci, w) wallet_data = format_wallet_data(swap_client, ci, w)
wallet_data["is_electrum_mode"] = (
getattr(ci, "_connection_type", "rpc") == "electrum"
)
fee_rate, fee_src = swap_client.getFeeRateForCoin(k) fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate) est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
@@ -342,6 +486,8 @@ def page_wallet(self, url_split, post_string):
wallet_data["main_address"] = w.get("main_address", "Refresh necessary") wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
elif k == Coins.LTC: elif k == Coins.LTC:
wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary") wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary")
elif k == Coins.FIRO:
wallet_data["spark_address"] = w.get("spark_address", "Refresh necessary")
if "wd_type_from_" + cid in page_data: if "wd_type_from_" + cid in page_data:
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid] wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
@@ -400,6 +546,30 @@ def page_wallet(self, url_split, post_string):
"coin_name": wallet_data.get("name", ticker), "coin_name": wallet_data.get("name", ticker),
} }
transactions = []
total_transactions = 0
is_electrum_mode = False
legacy_funds_info = None
if wallet_data.get("havedata", False) and not wallet_data.get("error"):
try:
ci = swap_client.ci(coin_id)
is_electrum_mode = getattr(ci, "_connection_type", "rpc") == "electrum"
if not is_electrum_mode:
count = tx_filters.get("limit", 30)
skip = tx_filters.get("offset", 0)
all_txs = ci.listWalletTransactions(count=10000, skip=0)
all_txs = list(reversed(all_txs)) if all_txs else []
total_transactions = len(all_txs)
raw_txs = all_txs[skip : skip + count] if all_txs else []
transactions = format_transactions(ci, raw_txs, coin_id)
else:
if coin_id in (Coins.BTC, Coins.LTC):
legacy_funds_info = swap_client.getElectrumLegacyFundsInfo(coin_id)
except Exception as e:
swap_client.log.warning(f"Failed to fetch transactions for {ticker}: {e}")
template = server.env.get_template("wallet.html") template = server.env.get_template("wallet.html")
return self.render_template( return self.render_template(
template, template,
@@ -411,5 +581,12 @@ def page_wallet(self, url_split, post_string):
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets, "block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
"donation_info": donation_info, "donation_info": donation_info,
"debug_ui": swap_client.debug_ui, "debug_ui": swap_client.debug_ui,
"transactions": transactions,
"tx_page_no": tx_filters.get("page_no", 1),
"tx_total": total_transactions,
"tx_limit": tx_filters.get("limit", 30),
"is_electrum_mode": is_electrum_mode,
"legacy_funds_info": legacy_funds_info,
"use_tor": getattr(swap_client, "use_tor_proxy", False),
}, },
) )
+48 -10
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -251,7 +251,7 @@ def describeBid(
elif bid.state == BidStates.BID_ABANDONED: elif bid.state == BidStates.BID_ABANDONED:
state_description = "Bid abandoned" state_description = "Bid abandoned"
elif bid.state == BidStates.BID_ERROR: elif bid.state == BidStates.BID_ERROR:
state_description = bid.state_note state_description = "Bid error"
elif offer.swap_type == SwapTypes.XMR_SWAP: elif offer.swap_type == SwapTypes.XMR_SWAP:
if bid.state == BidStates.BID_SENT: if bid.state == BidStates.BID_SENT:
state_description = "Waiting for offerer to accept" state_description = "Waiting for offerer to accept"
@@ -331,6 +331,7 @@ def describeBid(
"ticker_from": ci_from.ticker(), "ticker_from": ci_from.ticker(),
"ticker_to": ci_to.ticker(), "ticker_to": ci_to.ticker(),
"bid_state": strBidState(bid.state), "bid_state": strBidState(bid.state),
"bid_state_ind": int(bid.state),
"state_description": state_description, "state_description": state_description,
"itx_state": strTxState(bid.getITxState()), "itx_state": strTxState(bid.getITxState()),
"ptx_state": strTxState(bid.getPTxState()), "ptx_state": strTxState(bid.getPTxState()),
@@ -343,6 +344,8 @@ def describeBid(
if for_api if for_api
else format_timestamp(bid.created_at, with_seconds=True) else format_timestamp(bid.created_at, with_seconds=True)
), ),
"created_at_timestamp": bid.created_at,
"state_time_timestamp": getLastStateTimestamp(bid),
"expired_at": ( "expired_at": (
bid.expire_at bid.expire_at
if for_api if for_api
@@ -623,6 +626,14 @@ def listOldBidStates(bid):
return old_states return old_states
def getLastStateTimestamp(bid):
if not bid.states or len(bid.states) < 12:
return None
num_states = len(bid.states) // 12
last_entry = struct.unpack_from("<iq", bid.states[(num_states - 1) * 12 :])
return last_entry[1]
def getCoinName(c): def getCoinName(c):
if c == Coins.PART_ANON: if c == Coins.PART_ANON:
return chainparams[Coins.PART]["name"].capitalize() + " Anon" return chainparams[Coins.PART]["name"].capitalize() + " Anon"
@@ -643,7 +654,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
for k, v in swap_client.coin_clients.items(): for k, v in swap_client.coin_clients.items():
if k not in chainparams: if k not in chainparams:
continue continue
if v["connection_type"] == "rpc": if v["connection_type"] in ("rpc", "electrum"):
coins.append((int(k), getCoinName(k))) coins.append((int(k), getCoinName(k)))
if split_from: if split_from:
coins_from.append(coins[-1]) coins_from.append(coins[-1])
@@ -670,7 +681,7 @@ def listAvailableCoinsWithBalances(swap_client, with_variants=True, split_from=F
for k, v in swap_client.coin_clients.items(): for k, v in swap_client.coin_clients.items():
if k not in chainparams: if k not in chainparams:
continue continue
if v["connection_type"] == "rpc": if v["connection_type"] in ("rpc", "electrum"):
balance = "0.0" balance = "0.0"
if k in wallets: if k in wallets:
@@ -735,10 +746,23 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
if wallet_info["stealth_address"] != "?": if wallet_info["stealth_address"] != "?":
if not ci.isAddressMine(wallet_info["stealth_address"]): if not ci.isAddressMine(wallet_info["stealth_address"]):
ci._log.error( ci._log.warning(
"Unowned stealth address: {}".format(wallet_info["stealth_address"]) "Unowned stealth address: {} - clearing cache and regenerating".format(
wallet_info["stealth_address"]
)
) )
wallet_info["stealth_address"] = "Error: unowned address" key_str = "stealth_addr_" + ci.coin_name().lower()
swap_client.clearStringKV(key_str)
try:
new_addr = ci.getNewStealthAddress()
swap_client.setStringKV(key_str, new_addr)
wallet_info["stealth_address"] = new_addr
ci._log.info(
"Regenerated stealth address for {}".format(ci.coin_name())
)
except Exception as e:
ci._log.error("Failed to regenerate stealth address: {}".format(e))
wallet_info["stealth_address"] = "Error: unowned address"
elif ( elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed() swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
): ):
@@ -747,10 +771,24 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
if "deposit_address" in wallet_info: if "deposit_address" in wallet_info:
if wallet_info["deposit_address"] != "Refresh necessary": if wallet_info["deposit_address"] != "Refresh necessary":
if not ci.isAddressMine(wallet_info["deposit_address"]): if not ci.isAddressMine(wallet_info["deposit_address"]):
ci._log.error( ci._log.warning(
"Unowned deposit address: {}".format(wallet_info["deposit_address"]) "Unowned deposit address: {} - clearing cache and regenerating".format(
wallet_info["deposit_address"]
)
) )
wallet_info["deposit_address"] = "Error: unowned address" key_str = "receive_addr_" + ci.coin_name().lower()
swap_client.clearStringKV(key_str)
try:
coin_type = ci.coin_type()
new_addr = swap_client.getReceiveAddressForCoin(coin_type)
swap_client.setStringKV(key_str, new_addr)
wallet_info["deposit_address"] = new_addr
ci._log.info(
"Regenerated deposit address for {}".format(ci.coin_name())
)
except Exception as e:
ci._log.error("Failed to regenerate deposit address: {}".format(e))
wallet_info["deposit_address"] = "Error: unowned address"
elif ( elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed() swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
): ):
+5 -2
View File
@@ -190,11 +190,14 @@ def format_amount(i: int, display_scale: int, scale: int = None) -> str:
return rv return rv
def format_timestamp(value: int, with_seconds: bool = False) -> str: def format_timestamp(
value: int, with_seconds: bool = False, with_timezone: bool = False
) -> str:
str_format = "%Y-%m-%d %H:%M" str_format = "%Y-%m-%d %H:%M"
if with_seconds: if with_seconds:
str_format += ":%S" str_format += ":%S"
str_format += " %z" if with_timezone:
str_format += " %z"
return time.strftime(str_format, time.localtime(value)) return time.strftime(str_format, time.localtime(value))
+821
View File
@@ -0,0 +1,821 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class WalletBackend(ABC):
@abstractmethod
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
pass
def findAddressWithBalance(
self, addresses: List[str], min_balance: int
) -> Optional[tuple]:
balances = self.getBalance(addresses)
for addr, balance in balances.items():
if balance >= min_balance:
return (addr, balance)
return None
@abstractmethod
def getUnspentOutputs(
self, addresses: List[str], min_confirmations: int = 0
) -> List[dict]:
pass
@abstractmethod
def broadcastTransaction(self, tx_hex: str) -> str:
pass
@abstractmethod
def getTransaction(self, txid: str) -> Optional[dict]:
pass
@abstractmethod
def getTransactionRaw(self, txid: str) -> Optional[str]:
pass
@abstractmethod
def getBlockHeight(self) -> int:
pass
@abstractmethod
def estimateFee(self, blocks: int = 6) -> int:
pass
@abstractmethod
def isConnected(self) -> bool:
pass
@abstractmethod
def getAddressHistory(self, address: str) -> List[dict]:
pass
class FullNodeBackend(WalletBackend):
def __init__(self, rpc_client, coin_type, log):
self._rpc = rpc_client
self._coin_type = coin_type
self._log = log
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
result = {}
for addr in addresses:
result[addr] = 0
try:
utxos = self._rpc("listunspent", [0, 9999999, addresses])
for utxo in utxos:
addr = utxo.get("address")
if addr in result:
result[addr] += int(utxo.get("amount", 0) * 1e8)
except Exception as e:
self._log.warning(f"FullNodeBackend.getBalance error: {e}")
return result
def getUnspentOutputs(
self, addresses: List[str], min_confirmations: int = 0
) -> List[dict]:
try:
utxos = self._rpc("listunspent", [min_confirmations, 9999999, addresses])
result = []
for utxo in utxos:
result.append(
{
"txid": utxo.get("txid"),
"vout": utxo.get("vout"),
"value": int(utxo.get("amount", 0) * 1e8),
"address": utxo.get("address"),
"confirmations": utxo.get("confirmations", 0),
"scriptPubKey": utxo.get("scriptPubKey"),
}
)
return result
except Exception as e:
self._log.warning(f"FullNodeBackend.getUnspentOutputs error: {e}")
return []
def broadcastTransaction(self, tx_hex: str) -> str:
return self._rpc("sendrawtransaction", [tx_hex])
def getTransaction(self, txid: str) -> Optional[dict]:
try:
return self._rpc("getrawtransaction", [txid, True])
except Exception:
return None
def getTransactionRaw(self, txid: str) -> Optional[str]:
try:
return self._rpc("getrawtransaction", [txid, False])
except Exception:
return None
def getBlockHeight(self) -> int:
return self._rpc("getblockcount")
def estimateFee(self, blocks: int = 6) -> int:
try:
result = self._rpc("estimatesmartfee", [blocks])
if "feerate" in result:
return int(result["feerate"] * 1e8 / 1000)
return 1
except Exception:
return 1
def isConnected(self) -> bool:
try:
self._rpc("getblockchaininfo")
return True
except Exception:
return False
def getAddressHistory(self, address: str) -> List[dict]:
return []
def importAddress(self, address: str, label: str = "", rescan: bool = False):
try:
self._rpc("importaddress", [address, label, rescan])
except Exception as e:
if "already in wallet" not in str(e).lower():
raise
class ElectrumBackend(WalletBackend):
def __init__(
self,
coin_type,
log,
clearnet_servers=None,
onion_servers=None,
chain="mainnet",
proxy_host=None,
proxy_port=None,
):
from basicswap.interface.electrumx import ElectrumServer
from basicswap.chainparams import Coins, chainparams
self._coin_type = coin_type
self._log = log
self._subscribed_scripthashes = set()
coin_params = chainparams.get(coin_type, chainparams.get(Coins.BTC))
self._network_params = coin_params.get(chain, coin_params.get("mainnet", {}))
coin_name_map = {
Coins.BTC: "bitcoin",
Coins.LTC: "litecoin",
}
coin_name = coin_name_map.get(coin_type, "bitcoin")
self._host = "localhost"
self._port = 50002
self._use_ssl = True
self._server = ElectrumServer(
coin_name,
clearnet_servers=clearnet_servers,
onion_servers=onion_servers,
log=log,
proxy_host=proxy_host,
proxy_port=proxy_port,
)
self._realtime_callback = None
self._address_to_scripthash = {}
self._cached_height = 0
self._cached_height_time = 0
self._height_cache_ttl = 5
self._cached_fee = {}
self._cached_fee_time = {}
self._fee_cache_ttl = 300
self._max_batch_size = 5
self._background_mode = False
def setBackgroundMode(self, enabled: bool):
self._background_mode = enabled
def _call(self, method: str, params: list = None, timeout: int = 10):
if self._background_mode and hasattr(self._server, "call_background"):
return self._server.call_background(method, params, timeout)
if hasattr(self._server, "call_user"):
return self._server.call_user(method, params, timeout)
return self._server.call(method, params, timeout)
def _call_batch(self, calls: list, timeout: int = 15):
if self._background_mode and hasattr(self._server, "call_batch_background"):
return self._server.call_batch_background(calls, timeout)
if hasattr(self._server, "call_batch_user"):
return self._server.call_batch_user(calls, timeout)
return self._server.call_batch(calls, timeout)
def _is_server_stopping(self) -> bool:
return getattr(self._server, "_stopping", False)
def _split_batch_call(
self, scripthashes: list, method: str, batch_size: int = None
) -> list:
if batch_size is None:
batch_size = self._max_batch_size
all_results = []
for i in range(0, len(scripthashes), batch_size):
if self._is_server_stopping():
self._log.debug("_split_batch_call: server stopping, aborting")
break
chunk = scripthashes[i : i + batch_size]
try:
calls = [(method, [sh]) for sh in chunk]
results = self._call_batch(calls)
all_results.extend(results)
except Exception:
if self._is_server_stopping():
self._log.debug(
"_split_batch_call: server stopping after batch failure, aborting"
)
break
for sh in chunk:
if self._is_server_stopping():
self._log.debug(
"_split_batch_call: server stopping during fallback, aborting"
)
break
try:
result = self._call(method, [sh])
all_results.append(result)
except Exception:
all_results.append(None)
return all_results
def _isUnsupportedAddress(self, address: str) -> bool:
if address.startswith("ltcmweb1"):
return True
return False
def _addressToScripthash(self, address: str) -> str:
from basicswap.interface.electrumx import scripthash_from_address
return scripthash_from_address(address, self._network_params)
def getBalance(self, addresses: List[str]) -> Dict[str, int]:
result = {}
for addr in addresses:
result[addr] = 0
if not addresses:
return result
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
if not addr_list:
return result
addr_to_scripthash = {}
for addr in addr_list:
try:
addr_to_scripthash[addr] = self._addressToScripthash(addr)
except Exception as e:
self._log.debug(f"getBalance: scripthash error for {addr[:10]}...: {e}")
if not addr_to_scripthash:
return result
scripthashes = list(addr_to_scripthash.values())
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
batch_results = self._split_batch_call(
scripthashes, "blockchain.scripthash.get_balance"
)
for i, balance in enumerate(batch_results):
if balance and isinstance(balance, dict):
addr = scripthash_to_addr.get(scripthashes[i])
if addr:
confirmed = balance.get("confirmed", 0)
unconfirmed = balance.get("unconfirmed", 0)
result[addr] = confirmed + unconfirmed
return result
def getDetailedBalance(self, addresses: List[str]) -> Dict[str, dict]:
result = {}
for addr in addresses:
result[addr] = {"confirmed": 0, "unconfirmed": 0}
if not addresses:
return result
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
if not addr_list:
return result
batch_size = self._max_batch_size
for batch_start in range(0, len(addr_list), batch_size):
if self._is_server_stopping():
break
batch = addr_list[batch_start : batch_start + batch_size]
addr_to_scripthash = {}
for addr in batch:
try:
addr_to_scripthash[addr] = self._addressToScripthash(addr)
except Exception as e:
self._log.debug(
f"getDetailedBalance: scripthash error for {addr[:10]}...: {e}"
)
if not addr_to_scripthash:
continue
scripthashes = list(addr_to_scripthash.values())
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
batch_success = False
for attempt in range(2):
try:
batch_results = self._server.get_balance_batch(scripthashes)
for i, balance in enumerate(batch_results):
if balance and isinstance(balance, dict):
addr = scripthash_to_addr.get(scripthashes[i])
if addr:
result[addr] = {
"confirmed": balance.get("confirmed", 0),
"unconfirmed": balance.get("unconfirmed", 0),
}
batch_success = True
break
except Exception as e:
if self._is_server_stopping():
break
if attempt == 0:
self._log.debug(
f"Batch detailed balance query failed, reconnecting: {e}"
)
try:
self._server.disconnect()
except Exception:
pass
time.sleep(0.5)
else:
self._log.debug(
f"Batch detailed balance query failed after retry, falling back: {e}"
)
if not batch_success:
for addr, scripthash in addr_to_scripthash.items():
if self._is_server_stopping():
break
try:
balance = self._call(
"blockchain.scripthash.get_balance", [scripthash]
)
if balance and isinstance(balance, dict):
result[addr] = {
"confirmed": balance.get("confirmed", 0),
"unconfirmed": balance.get("unconfirmed", 0),
}
except Exception as e:
self._log.debug(
f"ElectrumBackend.getDetailedBalance error for {addr[:10]}...: {e}"
)
return result
def findAddressWithBalance(
self, addresses: List[str], min_balance: int
) -> Optional[tuple]:
if not addresses:
return None
addr_list = [addr for addr in addresses if not self._isUnsupportedAddress(addr)]
if not addr_list:
return None
batch_size = 50
for batch_start in range(0, len(addr_list), batch_size):
batch = addr_list[batch_start : batch_start + batch_size]
addr_to_scripthash = {}
for addr in batch:
try:
addr_to_scripthash[addr] = self._addressToScripthash(addr)
except Exception:
continue
if not addr_to_scripthash:
continue
try:
scripthashes = list(addr_to_scripthash.values())
batch_results = self._server.get_balance_batch(scripthashes)
scripthash_to_addr = {v: k for k, v in addr_to_scripthash.items()}
for i, balance in enumerate(batch_results):
if balance and isinstance(balance, dict):
confirmed = balance.get("confirmed", 0)
unconfirmed = balance.get("unconfirmed", 0)
total = confirmed + unconfirmed
if total >= min_balance:
addr = scripthash_to_addr.get(scripthashes[i])
if addr:
return (addr, total)
except Exception as e:
self._log.debug(f"findAddressWithBalance batch error: {e}")
return None
def getUnspentOutputs(
self, addresses: List[str], min_confirmations: int = 0
) -> List[dict]:
result = []
if not addresses:
return result
try:
current_height = self.getBlockHeight()
for addr in addresses:
if self._isUnsupportedAddress(addr):
continue
try:
scripthash = self._addressToScripthash(addr)
utxos = self._call(
"blockchain.scripthash.listunspent", [scripthash]
)
if utxos:
for utxo in utxos:
height = utxo.get("height", 0)
if height <= 0:
confirmations = 0
else:
confirmations = current_height - height + 1
if confirmations >= min_confirmations:
result.append(
{
"txid": utxo.get("tx_hash"),
"vout": utxo.get("tx_pos"),
"value": utxo.get("value", 0),
"address": addr,
"confirmations": confirmations,
}
)
except Exception as e:
self._log.debug(
f"ElectrumBackend.getUnspentOutputs error for {addr[:10]}...: {e}"
)
except Exception as e:
self._log.warning(f"ElectrumBackend.getUnspentOutputs error: {e}")
return result
def broadcastTransaction(self, tx_hex: str) -> str:
import time
max_retries = 3
retry_delay = 0.5
for attempt in range(max_retries):
try:
result = self._server.call("blockchain.transaction.broadcast", [tx_hex])
if result:
return result
except Exception as e:
error_msg = str(e).lower()
if any(
pattern in error_msg
for pattern in [
"missing inputs",
"bad-txns",
"txn-mempool-conflict",
"already in block chain",
"transaction already exists",
"insufficient fee",
"dust",
"non-bip68-final",
"non-final",
"locktime",
]
):
raise
if attempt < max_retries - 1:
self._log.debug(
f"broadcastTransaction retry {attempt + 1}/{max_retries}: {e}"
)
time.sleep(retry_delay * (2**attempt)) # Exponential backoff
continue
raise
return None
def getTransaction(self, txid: str) -> Optional[dict]:
try:
return self._call("blockchain.transaction.get", [txid, True])
except Exception:
return None
def getTransactionRaw(self, txid: str) -> Optional[str]:
try:
tx_hex = self._call("blockchain.transaction.get", [txid, False])
return tx_hex
except Exception as e:
self._log.warning(f"getTransactionRaw failed for {txid[:16]}...: {e}")
return None
def getTransactionBatch(self, txids: List[str]) -> Dict[str, Optional[dict]]:
result = {}
if not txids:
return result
try:
calls = [("blockchain.transaction.get", [txid, True]) for txid in txids]
responses = self._call_batch(calls)
for txid, tx_info in zip(txids, responses):
result[txid] = tx_info if tx_info else None
except Exception as e:
self._log.debug(f"getTransactionBatch error: {e}")
for txid in txids:
result[txid] = self.getTransaction(txid)
return result
def getTransactionBatchRaw(self, txids: List[str]) -> Dict[str, Optional[str]]:
result = {}
if not txids:
return result
try:
calls = [("blockchain.transaction.get", [txid, False]) for txid in txids]
responses = self._call_batch(calls)
for txid, tx_hex in zip(txids, responses):
result[txid] = tx_hex if tx_hex else None
except Exception as e:
self._log.debug(f"getTransactionBatchRaw error: {e}")
for txid in txids:
result[txid] = self.getTransactionRaw(txid)
return result
def getBlockHeight(self) -> int:
import time
if hasattr(self._server, "get_subscribed_height"):
subscribed_height = self._server.get_subscribed_height()
if subscribed_height > 0:
if subscribed_height > self._cached_height:
self._cached_height = subscribed_height
self._cached_height_time = time.time()
return subscribed_height
now = time.time()
if (
self._cached_height > 0
and (now - self._cached_height_time) < self._height_cache_ttl
):
return self._cached_height
try:
header = self._call("blockchain.headers.subscribe", [])
if header:
height = header.get("height", 0)
if height > 0:
self._cached_height = height
self._cached_height_time = now
return height
return self._cached_height if self._cached_height > 0 else 0
except Exception:
return self._cached_height if self._cached_height > 0 else 0
def estimateFee(self, blocks: int = 6) -> int:
now = time.time()
cache_key = blocks
if cache_key in self._cached_fee:
if (now - self._cached_fee_time.get(cache_key, 0)) < self._fee_cache_ttl:
return self._cached_fee[cache_key]
try:
fee = self._call("blockchain.estimatefee", [blocks])
if fee and fee > 0:
result = int(fee * 1e8 / 1000)
self._cached_fee[cache_key] = result
self._cached_fee_time[cache_key] = now
return result
return self._cached_fee.get(cache_key, 1)
except Exception:
return self._cached_fee.get(cache_key, 1)
def isConnected(self) -> bool:
try:
self._call("server.ping", [])
return True
except Exception:
return False
def getServerVersion(self) -> str:
version = self._server.get_server_version()
if not version:
try:
self._call("server.ping", [])
version = self._server.get_server_version()
except Exception:
pass
return version or "electrum"
def getServerHost(self) -> str:
host, port = self._server.get_current_server()
if host and port:
return f"{host}:{port}"
return f"{self._host}:{self._port}"
def getConnectionStatus(self) -> dict:
if hasattr(self._server, "getConnectionStatus"):
status = self._server.getConnectionStatus()
else:
status = {
"connected": self.isConnected(),
"failures": 0,
"last_error": None,
"all_failed": False,
"using_defaults": True,
"server_count": 1,
}
status["server"] = self.getServerHost()
status["version"] = self.getServerVersion()
return status
def recentlyReconnected(self, grace_seconds: int = 30) -> bool:
if hasattr(self._server, "recently_reconnected"):
return self._server.recently_reconnected(grace_seconds)
return False
def getAddressHistory(self, address: str) -> List[dict]:
if self._isUnsupportedAddress(address):
return []
try:
scripthash = self._addressToScripthash(address)
history = self._call("blockchain.scripthash.get_history", [scripthash])
if history:
return [
{"txid": h.get("tx_hash"), "height": h.get("height", 0)}
for h in history
]
return []
except Exception:
return []
def getAddressHistoryBackground(self, address: str) -> List[dict]:
if self._isUnsupportedAddress(address):
return []
try:
scripthash = self._addressToScripthash(address)
history = self._server.call_background(
"blockchain.scripthash.get_history", [scripthash]
)
if history:
return [
{"txid": h.get("tx_hash"), "height": h.get("height", 0)}
for h in history
]
return []
except Exception:
return []
def getBatchBalance(self, scripthashes: List[str]) -> Dict[str, int]:
result = {}
for sh in scripthashes:
result[sh] = 0
try:
calls = [("blockchain.scripthash.get_balance", [sh]) for sh in scripthashes]
responses = self._call_batch(calls)
for sh, balance in zip(scripthashes, responses):
if balance:
confirmed = balance.get("confirmed", 0)
unconfirmed = balance.get("unconfirmed", 0)
result[sh] = confirmed + unconfirmed
except Exception as e:
self._log.warning(f"ElectrumBackend.getBatchBalance error: {e}")
return result
def getBatchUnspent(
self, scripthashes: List[str], min_confirmations: int = 0
) -> Dict[str, List[dict]]:
result = {}
for sh in scripthashes:
result[sh] = []
try:
current_height = self.getBlockHeight()
calls = [("blockchain.scripthash.listunspent", [sh]) for sh in scripthashes]
responses = self._call_batch(calls)
for sh, utxos in zip(scripthashes, responses):
if utxos:
for utxo in utxos:
height = utxo.get("height", 0)
if height <= 0:
confirmations = 0
else:
confirmations = current_height - height + 1
if confirmations >= min_confirmations:
result[sh].append(
{
"txid": utxo.get("tx_hash"),
"vout": utxo.get("tx_pos"),
"value": utxo.get("value", 0),
"confirmations": confirmations,
}
)
except Exception as e:
self._log.warning(f"ElectrumBackend.getBatchUnspent error: {e}")
return result
def enableRealtimeNotifications(self, callback) -> None:
self._realtime_callback = callback
self._server.enable_realtime_notifications()
self._log.info(f"Real-time notifications enabled for {self._coin_type}")
def _create_scripthash_callback(self, scripthash):
def callback(sh, new_status):
self._handle_scripthash_notification(sh, new_status)
return callback
def _handle_scripthash_notification(self, scripthash, new_status):
if not self._realtime_callback:
return
address = None
for addr, sh in self._address_to_scripthash.items():
if sh == scripthash:
address = addr
break
try:
self._realtime_callback(
self._coin_type, address, scripthash, "balance_change"
)
except Exception as e:
self._log.debug(f"Error in realtime callback: {e}")
def subscribeAddressWithCallback(self, address: str) -> str:
if self._isUnsupportedAddress(address):
return None
try:
scripthash = self._addressToScripthash(address)
self._address_to_scripthash[address] = scripthash
if self._realtime_callback:
status = self._server.subscribe_with_callback(
scripthash, self._create_scripthash_callback(scripthash)
)
else:
status = self._call("blockchain.scripthash.subscribe", [scripthash])
self._subscribed_scripthashes.add(scripthash)
return status
except Exception as e:
self._log.debug(f"Failed to subscribe to {address}: {e}")
return None
def getSyncStatus(self) -> dict:
import time
height = 0
height_time = 0
if hasattr(self._server, "get_subscribed_height"):
height = self._server.get_subscribed_height()
height_time = getattr(self._server, "_subscribed_height_time", 0)
if self._cached_height > 0:
if self._cached_height > height:
height = self._cached_height
if self._cached_height_time > height_time:
height_time = self._cached_height_time
now = time.time()
stale_threshold = 300
is_synced = height > 0 and (now - height_time) < stale_threshold
return {
"height": height,
"synced": is_synced,
"last_update": height_time,
}
def getServer(self):
return self._server
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -79,13 +79,13 @@ def main():
continue continue
if coin_name in ("monero", "wownero"): if coin_name in ("monero", "wownero"):
with open( with open(
os.path.join(fragments_dir, "1_{coin_name}-wallet.yml"), "rb" os.path.join(fragments_dir, f"1_{coin_name}-wallet.yml"), "rb"
) as fp_in: ) as fp_in:
for line in fp_in: for line in fp_in:
fp.write(line) fp.write(line)
fpp.write(line) fpp.write(line)
with open( with open(
os.path.join(fragments_dir, "8_{coin_name}-daemon.yml"), "rb" os.path.join(fragments_dir, f"8_{coin_name}-daemon.yml"), "rb"
) as fp_in: ) as fp_in:
for line in fp_in: for line in fp_in:
fp.write(line) fp.write(line)
+2 -2
View File
@@ -140,10 +140,10 @@
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
(url "https://github.com/basicswap/basicswap") (url "https://github.com/basicswap/basicswap")
(commit "2259e964b11925587301070f9fc15e2226b2eb69"))) (commit "83807d213fab52c99f69dbc06fa7baedb449d66f")))
(sha256 (sha256
(base32 (base32
"0d544bj4kvra9y6rpqai1yvnk2igxj4z8s14g5jpksx5z9l0nyy3")) "08ykwn2wbcny5k6kwj3xkfkim40kmzcb988lpcd70r7kcmn8ggp0"))
(file-name (git-file-name name version)))) (file-name (git-file-name name version))))
(build-system pyproject-build-system) (build-system pyproject-build-system)
+1 -1
View File
@@ -1,5 +1,5 @@
pyzmq==27.1.0 pyzmq==27.1.0
python-gnupg==0.5.5 python-gnupg==0.5.6
Jinja2==3.1.6 Jinja2==3.1.6
pycryptodome==3.23.0 pycryptodome==3.23.0
PySocks==1.7.1 PySocks==1.7.1
+3 -3
View File
@@ -122,9 +122,9 @@ pysocks==1.7.1 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \ --hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 --hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via -r requirements.in # via -r requirements.in
python-gnupg==0.5.5 \ python-gnupg==0.5.6 \
--hash=sha256:3fdcaf76f60a1b948ff8e37dc398d03cf9ce7427065d583082b92da7a4ff5a63 \ --hash=sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac \
--hash=sha256:51fa7b8831ff0914bc73d74c59b99c613de7247b91294323c39733bb85ac3fc1 --hash=sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a
# via -r requirements.in # via -r requirements.in
pyzmq==27.1.0 \ pyzmq==27.1.0 \
--hash=sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d \ --hash=sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d \
+19 -6
View File
@@ -729,7 +729,25 @@ def process_offers(args, config, script_state) -> None:
matching_sent_offers.append(offer) matching_sent_offers.append(offer)
offers_found += 1 offers_found += 1
if wallet_balance <= float(offer_template["min_coin_from_amt"]): offer_amount_from = float(offer.get("amount_from", 0))
min_coin_from_amt = float(offer_template.get("min_coin_from_amt", 0))
if offer_amount_from > wallet_balance:
print(
f"Revoking offer {offer_id}, offer amount {offer_amount_from:.8f} > wallet balance {wallet_balance:.8f}"
)
result = read_json_api(f"revokeoffer/{offer_id}")
if args.debug:
print("revokeoffer", result)
else:
print("Offer revoked, will repost with accurate amount")
for i, prev_offer in enumerate(prev_template_offers):
if prev_offer.get("offer_id") == offer_id:
del prev_template_offers[i]
break
write_state(args.statefile, script_state)
offers_found -= 1
elif wallet_balance <= min_coin_from_amt:
print( print(
"Revoking offer {}, wallet from balance below minimum".format( "Revoking offer {}, wallet from balance below minimum".format(
offer_id offer_id
@@ -1169,11 +1187,6 @@ def process_offers(args, config, script_state) -> None:
) )
use_rate = offer_template["minrate"] use_rate = offer_template["minrate"]
# Final minimum rate check after all adjustments
if use_rate < offer_template["minrate"]:
print("Warning: Final rate clamping to minimum after all adjustments.")
use_rate = offer_template["minrate"]
if args.debug: if args.debug:
print( print(
"Creating offer for: {} at rate: {}".format( "Creating offer for: {} at rate: {}".format(
+61 -1
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
@@ -15,6 +15,7 @@ import subprocess
from urllib.request import urlopen from urllib.request import urlopen
from .util import read_json_api from .util import read_json_api
from basicswap.basicswap import Coins
from basicswap.rpc import callrpc from basicswap.rpc import callrpc
from basicswap.util import toBool from basicswap.util import toBool
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
@@ -125,6 +126,65 @@ def prepareDataDir(
return node_dir return node_dir
def prepare_balance(
use_delay_event,
coin,
amount: float,
port_target_node: int,
port_take_from_node: int,
test_balance: bool = True,
) -> None:
if coin == Coins.PART_BLIND:
coin_ticker: str = "PART"
balance_type: str = "blind_balance"
address_type: str = "stealth_address"
type_to: str = "blind"
elif coin == Coins.PART_ANON:
coin_ticker: str = "PART"
balance_type: str = "anon_balance"
address_type: str = "stealth_address"
type_to: str = "anon"
else:
coin_ticker: str = coin.name
balance_type: str = "balance"
address_type: str = "deposit_address"
js_w = read_json_api(port_target_node, "wallets")
current_balance: float = float(js_w[coin_ticker][balance_type])
if test_balance and current_balance >= amount:
return
post_json = {
"value": amount,
"address": js_w[coin_ticker][address_type],
"subfee": False,
}
if coin in (Coins.XMR, Coins.WOW):
post_json["sweepall"] = False
if coin in (Coins.PART_BLIND, Coins.PART_ANON):
post_json["type_to"] = type_to
json_rv = read_json_api(
port_take_from_node,
"wallets/{}/withdraw".format(coin_ticker.lower()),
post_json,
)
assert len(json_rv["txid"]) == 64
wait_for_amount: float = amount
if not test_balance:
wait_for_amount += current_balance
delay_iterations = 100 if coin == Coins.NAV else 30
delay_time = 5 if coin == Coins.NAV else 3
wait_for_balance(
use_delay_event,
"http://127.0.0.1:{}/json/wallets/{}".format(
port_target_node, coin_ticker.lower()
),
balance_type,
wait_for_amount,
iterations=delay_iterations,
delay_time=delay_time,
)
def checkForks(ro): def checkForks(ro):
try: try:
if "bip9_softforks" in ro: if "bip9_softforks" in ro:
+17 -14
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -139,6 +139,7 @@ def run_prepare(
use_rpcauth=False, use_rpcauth=False,
extra_settings={}, extra_settings={},
port_ofs=0, port_ofs=0,
extra_args=[],
): ):
config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME) config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME)
@@ -180,7 +181,7 @@ def run_prepare(
"-noextractover", "-noextractover",
"-noreleasesizecheck", "-noreleasesizecheck",
"-xmrrestoreheight=0", "-xmrrestoreheight=0",
] ] + extra_args
if mnemonic_in: if mnemonic_in:
testargs.append(f'-particl_mnemonic="{mnemonic_in}"') testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
@@ -622,6 +623,18 @@ class TestBase(unittest.TestCase):
raise ValueError(f"wait_for_particl_height failed http_port: {http_port}") raise ValueError(f"wait_for_particl_height failed http_port: {http_port}")
def run_process(client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
class XmrTestBase(TestBase): class XmrTestBase(TestBase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -632,23 +645,13 @@ class XmrTestBase(TestBase):
prepare_nodes(3, "monero") prepare_nodes(3, "monero")
def run_thread(self, client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
def start_processes(self): def start_processes(self):
multiprocessing.set_start_method("fork")
self.delay_event.clear() self.delay_event.clear()
for i in range(3): for i in range(3):
self.processes.append( self.processes.append(
multiprocessing.Process(target=self.run_thread, args=(i,)) multiprocessing.Process(target=run_process, args=(i,))
) )
self.processes[-1].start() self.processes[-1].start()
+5 -1
View File
@@ -1489,7 +1489,11 @@ class Test(BaseTest):
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) result = ci.publishBLockTx(v, S, amount, fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
test_delay_event.wait(1) test_delay_event.wait(1)
addr_out = ci.getNewAddress(True) addr_out = ci.getNewAddress(True)
+5 -1
View File
@@ -412,7 +412,11 @@ class Test(TestFunctions):
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) result = ci.publishBLockTx(v, S, amount, fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
test_delay_event.wait(1) test_delay_event.wait(1)
addr_out = ci.getNewAddress(False) addr_out = ci.getNewAddress(False)
@@ -32,6 +32,7 @@ from tests.basicswap.common import (
waitForNumSwapping, waitForNumSwapping,
) )
from tests.basicswap.common_xmr import ( from tests.basicswap.common_xmr import (
run_process,
XmrTestBase, XmrTestBase,
) )
@@ -122,7 +123,7 @@ class Test(XmrTestBase):
c1 = self.processes[1] c1 = self.processes[1]
c1.terminate() c1.terminate()
c1.join() c1.join()
self.processes[1] = multiprocessing.Process(target=self.run_thread, args=(1,)) self.processes[1] = multiprocessing.Process(target=run_process, args=(1,))
self.processes[1].start() self.processes[1].start()
waitForServer(self.delay_event, 12701) waitForServer(self.delay_event, 12701)
+27 -13
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -199,7 +199,7 @@ class Test(BaseTest):
0, "getnewaddress", ["mining_addr"], base_rpc_port=FIRO_BASE_RPC_PORT 0, "getnewaddress", ["mining_addr"], base_rpc_port=FIRO_BASE_RPC_PORT
) )
# cls.firo_addr = callnoderpc(0, 'addwitnessaddress', [cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT) # cls.firo_addr = callnoderpc(0, 'addwitnessaddress', [cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT)
logging.info("Mining %d Firo blocks to %s", num_blocks, cls.firo_addr) logging.info(f"Mining {num_blocks} Firo blocks to {cls.firo_addr}")
callnoderpc( callnoderpc(
0, 0,
"generatetoaddress", "generatetoaddress",
@@ -230,7 +230,7 @@ class Test(BaseTest):
0, "getblockcount", base_rpc_port=FIRO_BASE_RPC_PORT 0, "getblockcount", base_rpc_port=FIRO_BASE_RPC_PORT
) )
num_blocks = 1352 - chain_height # Activate CTLV (bip65) num_blocks = 1352 - chain_height # Activate CTLV (bip65)
logging.info("Mining %d Firo blocks to %s", num_blocks, cls.firo_addr) logging.info(f"Mining {num_blocks} Firo blocks to {cls.firo_addr}")
callnoderpc( callnoderpc(
0, 0,
"generatetoaddress", "generatetoaddress",
@@ -286,7 +286,7 @@ class Test(BaseTest):
self.callnoderpc("generatetoaddress", [num_blocks, self.firo_addr]) self.callnoderpc("generatetoaddress", [num_blocks, self.firo_addr])
def test_001_firo(self): def test_001_firo(self):
logging.info("---------- Test {} segwit".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} segwit")
""" """
Segwit is not currently enabled: Segwit is not currently enabled:
@@ -339,7 +339,7 @@ class Test(BaseTest):
assert txid_with_scriptsig == tx_signed_decoded["txid"] assert txid_with_scriptsig == tx_signed_decoded["txid"]
def test_007_hdwallet(self): def test_007_hdwallet(self):
logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} hdwallet")
swap_client = self.swap_clients[0] swap_client = self.swap_clients[0]
# Run initialiseWallet to set 'main_wallet_seedid_' # Run initialiseWallet to set 'main_wallet_seedid_'
@@ -349,7 +349,7 @@ class Test(BaseTest):
assert swap_client.checkWalletSeed(self.test_coin_from) is True assert swap_client.checkWalletSeed(self.test_coin_from) is True
def test_008_gettxout(self): def test_008_gettxout(self):
logging.info("---------- Test {} gettxout".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} gettxout")
swap_client = self.swap_clients[0] swap_client = self.swap_clients[0]
@@ -428,7 +428,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount assert amount_proved >= require_amount
def test_08_wallet(self): def test_08_wallet(self):
logging.info("---------- Test {} wallet".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} wallet")
logging.info("Test withdrawal") logging.info("Test withdrawal")
addr = self.callnoderpc( addr = self.callnoderpc(
@@ -447,7 +447,7 @@ class Test(BaseTest):
} }
json_rv = read_json_api( json_rv = read_json_api(
TEST_HTTP_PORT + 0, TEST_HTTP_PORT + 0,
"wallets/{}/withdraw".format(self.test_coin_from.name.lower()), f"wallets/{self.test_coin_from.name.lower()}/withdraw",
post_json, post_json,
) )
assert len(json_rv["txid"]) == 64 assert len(json_rv["txid"]) == 64
@@ -458,7 +458,7 @@ class Test(BaseTest):
} }
json_rv = read_json_api( json_rv = read_json_api(
TEST_HTTP_PORT + 0, TEST_HTTP_PORT + 0,
"wallets/{}/createutxo".format(self.test_coin_from.name.lower()), f"wallets/{self.test_coin_from.name.lower()}/createutxo",
post_json, post_json,
) )
assert len(json_rv["txid"]) == 64 assert len(json_rv["txid"]) == 64
@@ -473,6 +473,14 @@ class Test(BaseTest):
ci_from = swap_clients[0].ci(coin_from) ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[1].ci(coin_to) ci_to = swap_clients[1].ci(coin_to)
id_bidder: int = 1
self.prepare_balance(
coin_to,
100.0,
1800 + id_bidder,
1801 if coin_to in (Coins.XMR,) else 1800,
)
swap_value = ci_from.make_int(random.uniform(0.2, 20.0), r=1) swap_value = ci_from.make_int(random.uniform(0.2, 20.0), r=1)
rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
@@ -506,9 +514,7 @@ class Test(BaseTest):
coin_from = Coins.BTC coin_from = Coins.BTC
coin_to = Coins.FIRO coin_to = Coins.FIRO
logging.info( logging.info(
"---------- Test {} to {} follower recovers coin b lock tx".format( f"---------- Test {coin_from.name} to {coin_to.name} follower recovers coin b lock tx"
coin_from.name, coin_to.name
)
) )
swap_clients = self.swap_clients swap_clients = self.swap_clients
@@ -568,6 +574,14 @@ class Test(BaseTest):
coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type
) )
id_bidder: int = 1
self.prepare_balance(
coin_to,
100.0,
1800 + id_bidder,
1801 if coin_to in (Coins.XMR,) else 1800,
)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
@@ -592,7 +606,7 @@ class Test(BaseTest):
) )
def test_101_full_swap(self): def test_101_full_swap(self):
logging.info("---------- Test {} to XMR".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} to XMR")
if not self.test_xmr: if not self.test_xmr:
logging.warning("Skipping test") logging.warning("Skipping test")
return return
+13 -12
View File
@@ -45,6 +45,18 @@ if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
def run_process(client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
class Test(unittest.TestCase): class Test(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -64,24 +76,13 @@ class Test(unittest.TestCase):
run_prepare(i, client_path, bins_path, "monero,bitcoin", mnemonics[0]) run_prepare(i, client_path, bins_path, "monero,bitcoin", mnemonics[0])
def run_thread(self, client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
def test_wallet(self): def test_wallet(self):
update_thread = None update_thread = None
processes = [] processes = []
time.sleep(5) time.sleep(5)
for i in range(2): for i in range(2):
processes.append(multiprocessing.Process(target=self.run_thread, args=(i,))) processes.append(multiprocessing.Process(target=run_process, args=(i,)))
processes[-1].start() processes[-1].start()
try: try:
+14 -13
View File
@@ -102,6 +102,18 @@ def prepare_node(node_id, mnemonic):
) )
def run_process(client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
class Test(TestBase): class Test(TestBase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -112,17 +124,6 @@ class Test(TestBase):
for i in range(3): for i in range(3):
cls.used_mnemonics.append(prepare_node(i, mnemonics[0] if i == 0 else None)) cls.used_mnemonics.append(prepare_node(i, mnemonics[0] if i == 0 else None))
def run_thread(self, client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id))
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
def finalise(self, processes): def finalise(self, processes):
self.delay_event.set() self.delay_event.set()
if self.update_thread: if self.update_thread:
@@ -136,7 +137,7 @@ class Test(TestBase):
processes = [] processes = []
for i in range(3): for i in range(3):
processes.append(multiprocessing.Process(target=self.run_thread, args=(i,))) processes.append(multiprocessing.Process(target=run_process, args=(i,)))
processes[-1].start() processes[-1].start()
try: try:
@@ -201,7 +202,7 @@ class Test(TestBase):
logging.info("Starting a new node on the same mnemonic as the first") logging.info("Starting a new node on the same mnemonic as the first")
prepare_node(3, self.used_mnemonics[0]) prepare_node(3, self.used_mnemonics[0])
processes.append(multiprocessing.Process(target=self.run_thread, args=(3,))) processes.append(multiprocessing.Process(target=run_process, args=(3,)))
processes[-1].start() processes[-1].start()
waitForServer(self.delay_event, 12703) waitForServer(self.delay_event, 12703)
+37 -36
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -209,7 +209,7 @@ def updateThread(cls):
calldogerpc(0, "generatetoaddress", [1, cls.doge_addr]) calldogerpc(0, "generatetoaddress", [1, cls.doge_addr])
except Exception as e: except Exception as e:
print("updateThread error", str(e)) print("updateThread error", str(e))
cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max)) cls.delay_event.wait(random.uniform(cls.update_min, cls.update_max))
def updateThreadXMR(cls): def updateThreadXMR(cls):
@@ -228,7 +228,7 @@ def updateThreadXMR(cls):
) )
except Exception as e: except Exception as e:
print("updateThreadXMR error", str(e)) print("updateThreadXMR error", str(e))
cls.delay_event.wait(random.randrange(cls.xmr_update_min, cls.xmr_update_max)) cls.delay_event.wait(random.uniform(cls.xmr_update_min, cls.xmr_update_max))
def updateThreadDCR(cls): def updateThreadDCR(cls):
@@ -262,7 +262,7 @@ def updateThreadDCR(cls):
logging.warning("updateThreadDCR generate {}".format(e)) logging.warning("updateThreadDCR generate {}".format(e))
except Exception as e: except Exception as e:
print("updateThreadDCR error", str(e)) print("updateThreadDCR error", str(e))
cls.delay_event.wait(random.randrange(cls.dcr_update_min, cls.dcr_update_max)) cls.delay_event.wait(random.uniform(cls.dcr_update_min, cls.dcr_update_max))
def signal_handler(self, sig, frame): def signal_handler(self, sig, frame):
@@ -270,7 +270,7 @@ def signal_handler(self, sig, frame):
self.delay_event.set() self.delay_event.set()
def run_thread(self, client_id): def run_process(client_id):
client_path = os.path.join(test_path, "client{}".format(client_id)) client_path = os.path.join(test_path, "client{}".format(client_id))
testargs = [ testargs = [
"basicswap-run", "basicswap-run",
@@ -283,16 +283,14 @@ def run_thread(self, client_id):
def start_processes(self): def start_processes(self):
multiprocessing.set_start_method("spawn")
self.delay_event.clear() self.delay_event.clear()
for i in range(NUM_NODES): for i in range(NUM_NODES):
self.processes.append( self.processes.append(
multiprocessing.Process( multiprocessing.Process(
target=run_thread, target=run_process,
args=( args=(i,),
self,
i,
),
) )
) )
self.processes[-1].start() self.processes[-1].start()
@@ -302,7 +300,7 @@ def start_processes(self):
wallets = read_json_api(UI_PORT + 1, "wallets") wallets = read_json_api(UI_PORT + 1, "wallets")
if "monero" in TEST_COINS_LIST: if "monero" in self.test_coins_list:
xmr_auth = None xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "": if os.getenv("XMR_RPC_USER", "") != "":
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", "")) xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
@@ -336,7 +334,7 @@ def start_processes(self):
callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr]) callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
logging.info("BTC blocks: {}".format(callbtcrpc(0, "getblockcount"))) logging.info("BTC blocks: {}".format(callbtcrpc(0, "getblockcount")))
if "litecoin" in TEST_COINS_LIST: if "litecoin" in self.test_coins_list:
self.ltc_addr = callltcrpc( self.ltc_addr = callltcrpc(
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat" 0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
) )
@@ -367,7 +365,7 @@ def start_processes(self):
wallet="wallet.dat", wallet="wallet.dat",
) )
if "decred" in TEST_COINS_LIST: if "decred" in self.test_coins_list:
if RESET_TEST: if RESET_TEST:
_ = calldcrrpc(0, "getnewaddress") _ = calldcrrpc(0, "getnewaddress")
# assert (addr == self.dcr_addr) # assert (addr == self.dcr_addr)
@@ -397,7 +395,7 @@ def start_processes(self):
self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,)) self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,))
self.update_thread_dcr.start() self.update_thread_dcr.start()
if "firo" in TEST_COINS_LIST: if "firo" in self.test_coins_list:
self.firo_addr = callfirorpc(0, "getnewaddress", ["mining_addr"]) self.firo_addr = callfirorpc(0, "getnewaddress", ["mining_addr"])
num_blocks: int = 200 num_blocks: int = 200
have_blocks: int = callfirorpc(0, "getblockcount") have_blocks: int = callfirorpc(0, "getblockcount")
@@ -413,7 +411,7 @@ def start_processes(self):
[num_blocks - have_blocks, self.firo_addr], [num_blocks - have_blocks, self.firo_addr],
) )
if "bitcoincash" in TEST_COINS_LIST: if "bitcoincash" in self.test_coins_list:
self.bch_addr = callbchrpc( self.bch_addr = callbchrpc(
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat" 0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
) )
@@ -432,7 +430,7 @@ def start_processes(self):
wallet="wallet.dat", wallet="wallet.dat",
) )
if "dogecoin" in TEST_COINS_LIST: if "dogecoin" in self.test_coins_list:
self.doge_addr = calldogerpc(0, "getnewaddress", ["mining_addr"]) self.doge_addr = calldogerpc(0, "getnewaddress", ["mining_addr"])
num_blocks: int = 200 num_blocks: int = 200
have_blocks: int = calldogerpc(0, "getblockcount") have_blocks: int = calldogerpc(0, "getblockcount")
@@ -446,7 +444,7 @@ def start_processes(self):
0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr] 0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr]
) )
if "namecoin" in TEST_COINS_LIST: if "namecoin" in self.test_coins_list:
self.nmc_addr = callnmcrpc(0, "getnewaddress", ["mining_addr", "bech32"]) self.nmc_addr = callnmcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
num_blocks: int = 500 num_blocks: int = 500
have_blocks: int = callnmcrpc(0, "getblockcount") have_blocks: int = callnmcrpc(0, "getblockcount")
@@ -539,7 +537,22 @@ class BaseTestWithPrepare(unittest.TestCase):
firo_addr = None firo_addr = None
bch_addr = None bch_addr = None
doge_addr = None doge_addr = None
initialised = False test_coins_list = TEST_COINS_LIST
@classmethod
def modifyConfig(cls, test_path, i):
modifyConfig(test_path, i)
@classmethod
def setupNodes(cls):
logging.info(f"Preparing {NUM_NODES} nodes.")
prepare_nodes(
NUM_NODES,
cls.test_coins_list,
True,
{"min_sequence_lock_seconds": 60},
PORT_OFS,
)
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -550,22 +563,18 @@ class BaseTestWithPrepare(unittest.TestCase):
if os.path.exists(test_path) and not RESET_TEST: if os.path.exists(test_path) and not RESET_TEST:
logging.info(f"Continuing with existing directory: {test_path}") logging.info(f"Continuing with existing directory: {test_path}")
else: else:
logging.info(f"Preparing {NUM_NODES} nodes.") cls.setupNodes()
prepare_nodes(
NUM_NODES,
TEST_COINS_LIST,
True,
{"min_sequence_lock_seconds": 60},
PORT_OFS,
)
for i in range(NUM_NODES): for i in range(NUM_NODES):
modifyConfig(test_path, i) cls.modifyConfig(test_path, i)
signal.signal( signal.signal(
signal.SIGINT, lambda signal, frame: signal_handler(cls, signal, frame) signal.SIGINT, lambda signal, frame: signal_handler(cls, signal, frame)
) )
start_processes(cls)
waitForServer(cls.delay_event, UI_PORT + 0)
waitForServer(cls.delay_event, UI_PORT + 1)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Stopping test") logging.info("Stopping test")
@@ -585,14 +594,6 @@ class BaseTestWithPrepare(unittest.TestCase):
cls.update_thread_dcr = None cls.update_thread_dcr = None
cls.processes = [] cls.processes = []
def setUp(self):
if self.initialised:
return
start_processes(self)
waitForServer(self.delay_event, UI_PORT + 0)
waitForServer(self.delay_event, UI_PORT + 1)
self.initialised = True
class Test(BaseTestWithPrepare): class Test(BaseTestWithPrepare):
def test_persistent(self): def test_persistent(self):
+29 -8
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -921,10 +921,10 @@ class BasicSwapTest(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(BasicSwapTest, cls).setUpClass() super(BasicSwapTest, cls).setUpClass()
if False:
for client in cls.swap_clients: @classmethod
client.log.safe_logs = True def addCoinSettings(cls, settings, datadir, node_id):
client.log.safe_logs_prefix = b"tests" settings["fetchpricesthread"] = False
def test_001_nested_segwit(self): def test_001_nested_segwit(self):
# p2sh-p2wpkh # p2sh-p2wpkh
@@ -1509,6 +1509,23 @@ class BasicSwapTest(TestFunctions):
vsize = tx_decoded["vsize"] vsize = tx_decoded["vsize"]
expect_fee_int = round(self.test_fee_rate * vsize / 1000) expect_fee_int = round(self.test_fee_rate * vsize / 1000)
tx_obj = ci.loadTx(lock_tx)
vsize_from_ci = ci.getTxVSize(tx_obj)
assert vsize == vsize_from_ci
tx_no_witness = tx_obj.serialize_without_witness()
dummy_witness_stack = []
for txi in tx_obj.vin:
dummy_witness_stack.append(ci.getP2WPKHDummyWitness())
witness_bytes_len_est: int = ci.getWitnessStackSerialisedLength(
dummy_witness_stack
)
tx_obj_no_witness = ci.loadTx(tx_no_witness)
vsize_estimated = ci.getTxVSize(
tx_obj_no_witness, add_witness_bytes=witness_bytes_len_est
)
assert vsize <= vsize_estimated and vsize_estimated - vsize < 4
out_value: int = 0 out_value: int = 0
for txo in tx_decoded["vout"]: for txo in tx_decoded["vout"]:
if "value" in txo: if "value" in txo:
@@ -1556,13 +1573,17 @@ class BasicSwapTest(TestFunctions):
expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize() expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize()
assert expect_vsize >= vsize_actual assert expect_vsize >= vsize_actual
assert expect_vsize - vsize_actual < 10 assert expect_vsize - vsize_actual <= 10
# Test chain b (no-script) lock tx size # Test chain b (no-script) lock tx size
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, self.test_fee_rate) result = ci.publishBLockTx(v, S, amount, self.test_fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
addr_out = ci.getNewAddress(True) addr_out = ci.getNewAddress(True)
lock_tx_b_spend_txid = ci.spendBLockTx( lock_tx_b_spend_txid = ci.spendBLockTx(
@@ -1577,7 +1598,7 @@ class BasicSwapTest(TestFunctions):
expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize() expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert expect_vsize >= lock_tx_b_spend_decoded["vsize"] assert expect_vsize >= lock_tx_b_spend_decoded["vsize"]
assert expect_vsize - lock_tx_b_spend_decoded["vsize"] < 10 assert expect_vsize - lock_tx_b_spend_decoded["vsize"] <= 10
def test_011_p2sh(self): def test_011_p2sh(self):
# Not used in bsx for native-segwit coins # Not used in bsx for native-segwit coins
+859
View File
@@ -0,0 +1,859 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
# Ensure Electrumx is installed to a venv in ELECTRUMX_SRC_DIR/venv
# Example setup with default paths:
The leveldb system package may be required to install plyvel:
sudo pacman -S leveldb
cd ~/tmp/
git clone git@github.com:spesmilo/electrumx.git
cd electrumx
python3 -m venv venv
. venv/bin/activate
pip install ".[ujson]"
# Run test
export TEST_PATH=/tmp/test_electrum
mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export ELECTRUMX_SRC_DIR="~/tmp/electrumx"
export EXTRA_CONFIG_JSON=$(cat <<EOF | jq -r @json
{
"btc0":["txindex=1","rpcworkqueue=1100"]
}
EOF
)
export TEST_COINS_LIST="bitcoin,monero"
export PYTHONPATH=$(pwd)
pytest -v -s --log-cli-level=DEBUG tests/basicswap/test_electrum.py
# Run select test
pytest -v -s --log-cli-level=DEBUG tests/basicswap/test_electrum.py::Test::test_01_b_full_swap_xmr
# Optionally copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
"""
import json
import logging
import os
import random
import shutil
import subprocess
import sys
import unittest
import basicswap.config as cfg
from basicswap.basicswap_util import (
BidStates,
DebugTypes,
TxLockTypes,
strBidState,
)
from basicswap.chainparams import (
Coins,
chainparams,
getCoinIdFromName,
)
from basicswap.util.daemon import Daemon
from tests.basicswap.common import (
prepare_balance,
stopDaemons,
)
from tests.basicswap.common_xmr import run_prepare, TEST_PATH
from tests.basicswap.extended.test_xmr_persistent import (
BaseTestWithPrepare,
NUM_NODES,
PORT_OFS,
RESET_TEST,
)
from tests.basicswap.mnemonics import mnemonics
from tests.basicswap.util import (
read_json_api,
post_json_api,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
def modify_config(test_path, i):
config_path = os.path.join(test_path, f"client{i}", cfg.CONFIG_FILENAME)
with open(config_path) as fp:
settings = json.load(fp)
if i == 1:
settings["debug_ui"] = True
settings.update(
{
"fetchpricesthread": False,
"check_progress_seconds": 2,
"check_watched_seconds": 3,
"check_expired_seconds": 60,
"check_events_seconds": 1,
"check_xmr_swaps_seconds": 1,
"min_delay_event": 1,
"max_delay_event": 4,
"min_delay_event_short": 1,
"max_delay_event_short": 3,
"min_delay_retry": 2,
"max_delay_retry": 10,
}
)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
def wait_for_bid_state(
delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30
) -> None:
logger.info(f"TEST: wait_for_bid {bid_id}, node {node_port}, state {state}")
pass_state_strs = []
if isinstance(state, (list, tuple)):
for s in state:
pass_state_strs.append(strBidState(s))
elif state is not None:
pass_state_strs.append(strBidState(state))
for i in range(wait_for):
if delay_event.is_set():
raise ValueError("Test stopped.")
delay_event.wait(1)
try:
rv = read_json_api(node_port, f"bids/{bid_id}")
if rv["bid_state"] in pass_state_strs or state is None:
return
except Exception as e: # noqa: F841
pass
# logger.debug(f"TEST: wait_for_bid {bid_id}, error {e}")
raise ValueError(f"wait_for_bid timed out {bid_id}.")
def wait_for_offer(
delay_event, node_port: int, offer_id: str, state=None, wait_for: int = 30
) -> None:
logger.info(f"TEST: wait_for_offer {offer_id}, node {node_port}, state {state}")
for i in range(wait_for):
if delay_event.is_set():
raise ValueError("Test stopped.")
delay_event.wait(1)
try:
rv = read_json_api(node_port, f"offers/{offer_id}")
if any(offer["offer_id"] == offer_id for offer in rv):
return
except Exception as e: # noqa: F841
pass
# logger.debug(f"TEST: wait_for_offer {offer_id}, error {e}")
raise ValueError(f"wait_for_offer timed out {offer_id}.")
class TestFunctions(BaseTestWithPrepare):
__test__ = False
port_node_0 = 12701
port_node_1 = 12702
def do_test_01_full_swap(
self,
coin_from: Coins,
coin_to: Coins,
port_node_from: int = port_node_0,
port_node_to: int = port_node_1,
) -> None:
logger.info(
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to})"
)
ticker_from: str = chainparams[coin_from]["ticker"]
ticker_to: str = chainparams[coin_to]["ticker"]
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
data = {
"addr_from": "-1",
"coin_from": ticker_from,
"coin_to": ticker_to,
"amt_from": amt_from_str,
"amt_to": amt_to_str,
"lockhrs": "24",
"swap_type": "adaptor_sig",
}
logger.info(
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
)
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
wait_for_offer(self.delay_event, port_node_to, offer_id)
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
assert offer["offer_id"] == offer_id
data = {
"offer_id": offer_id,
"amount_from": offer["amount_from"],
"validmins": 60,
}
rv = post_json_api(port_node_to, "bids/new", data)
bid_id: str = rv["bid_id"]
wait_for_bid_state(
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
)
rv = post_json_api(port_node_from, f"bids/{bid_id}", {"accept": True})
assert rv["bid_state"] in ("Accepted", "Request accepted")
logger.info("Completing swap")
wait_for_bid_state(
self.delay_event, port_node_from, bid_id, BidStates.SWAP_COMPLETED, 240
)
wait_for_bid_state(
self.delay_event, port_node_to, bid_id, BidStates.SWAP_COMPLETED, 240
)
def do_test_02_leader_recover_a_lock_tx(
self,
coin_from: Coins,
coin_to: Coins,
port_node_from: int = port_node_0,
port_node_to: int = port_node_1,
lock_value: int = 12,
) -> None:
logger.info(
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to}) leader recovers coin a lock tx"
)
ticker_from: str = chainparams[coin_from]["ticker"]
ticker_to: str = chainparams[coin_to]["ticker"]
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
port_offerer: int = port_node_from
port_bidder: int = port_node_to
port_leader: int = port_bidder if reverse_bid else port_offerer
port_follower: int = port_offerer if reverse_bid else port_bidder
logger.info(
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
)
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
data = {
"addr_from": "-1",
"coin_from": ticker_from,
"coin_to": ticker_to,
"amt_from": amt_from_str,
"amt_to": amt_to_str,
"swap_type": "adaptor_sig",
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
"lock_blocks": str(lock_value),
}
logger.info(
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
)
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
wait_for_offer(self.delay_event, port_node_to, offer_id)
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
assert offer["offer_id"] == offer_id
data = {
"offer_id": offer_id,
"amount_from": offer["amount_from"],
"validmins": 60,
}
rv = post_json_api(port_node_to, "bids/new", data)
bid_id: str = rv["bid_id"]
wait_for_bid_state(self.delay_event, port_follower, bid_id)
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{"debugind": DebugTypes.BID_STOP_AFTER_COIN_A_LOCK},
)
assert "bid_state" in rv # Test that the return didn't fail
wait_for_bid_state(
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
)
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
assert rv["bid_state"] in ("Accepted", "Request accepted")
wait_for_bid_state(
self.delay_event,
port_leader,
bid_id,
BidStates.XMR_SWAP_FAILED_REFUNDED,
240,
)
wait_for_bid_state(
self.delay_event,
port_follower,
bid_id,
[BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED],
240,
)
def do_test_03_follower_recover_a_lock_tx(
self,
coin_from: Coins,
coin_to: Coins,
port_node_from: int = port_node_0,
port_node_to: int = port_node_1,
lock_value: int = 12,
with_mercy: bool = True,
) -> None:
logger.info(
"---------- Test {} ({}) to {} ({}) follower recovers coin a lock tx{}".format(
coin_from.name,
port_node_from,
coin_to.name,
port_node_to,
" (with mercy tx)" if with_mercy else "",
)
)
# Leader is too slow to recover the coin a lock tx and follower swipes it
# Coin B lock tx remains unspent unless a mercy output revealing the follower's keyshare is sent
ticker_from: str = chainparams[coin_from]["ticker"]
ticker_to: str = chainparams[coin_to]["ticker"]
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
port_offerer: int = port_node_from
port_bidder: int = port_node_to
port_leader: int = port_bidder if reverse_bid else port_offerer
port_follower: int = port_offerer if reverse_bid else port_bidder
logger.info(
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
)
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
data = {
"addr_from": "-1",
"coin_from": ticker_from,
"coin_to": ticker_to,
"amt_from": amt_from_str,
"amt_to": amt_to_str,
"swap_type": "adaptor_sig",
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
"lock_blocks": str(lock_value),
}
logger.info(
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
)
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
wait_for_offer(self.delay_event, port_node_to, offer_id)
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
assert offer["offer_id"] == offer_id
data = {
"offer_id": offer_id,
"amount_from": offer["amount_from"],
"validmins": 60,
}
rv = post_json_api(port_node_to, "bids/new", data)
bid_id: str = rv["bid_id"]
wait_for_bid_state(self.delay_event, port_leader, bid_id)
wait_for_bid_state(self.delay_event, port_follower, bid_id)
rv = post_json_api(
port_leader,
f"bids/{bid_id}",
{"debugind": DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2},
)
assert "bid_state" in rv # Test that the return didn't fail
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{"debugind": DebugTypes.BID_DONT_SPEND_COIN_B_LOCK},
)
assert "bid_state" in rv
for node_port in (port_leader, port_follower):
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{
"debugind": DebugTypes.BID_DONT_SPEND_COIN_B_LOCK,
"maindebugind": False,
},
)
assert "bid_state" in rv
wait_for_bid_state(
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
)
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
assert rv["bid_state"] in ("Accepted", "Request accepted")
expect_state = (
(BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED, BidStates.SWAP_COMPLETED)
if with_mercy
else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED)
)
wait_for_bid_state(
self.delay_event,
port_leader,
bid_id,
expect_state,
240,
)
wait_for_bid_state(
self.delay_event,
port_follower,
bid_id,
[BidStates.XMR_SWAP_FAILED_SWIPED],
240,
)
rv = post_json_api(
port_leader,
f"bids/{bid_id}",
{"show_extra": True, "with_events": True},
)
events = rv["events"]
logger.info(f"Initiator events: {events}")
if with_mercy:
assert any(
event["desc"] == "Lock tx B spend tx published" for event in events
)
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{"show_extra": True, "with_events": True},
)
events = rv["events"]
logger.info(f"Participant events: {events}")
assert any(
event["desc"] == "Lock tx A refund swipe tx published" for event in events
)
def do_test_04_follower_recover_b_lock_tx(
self,
coin_from: Coins,
coin_to: Coins,
port_node_from: int = port_node_0,
port_node_to: int = port_node_1,
lock_value: int = 16,
) -> None:
logger.info(
f"---------- Test {coin_from.name} ({port_node_from}) to {coin_to.name} ({port_node_to}) follower recovers coin b lock tx"
)
ticker_from: str = chainparams[coin_from]["ticker"]
ticker_to: str = chainparams[coin_to]["ticker"]
reverse_bid: bool = True if coin_from in (Coins.XMR,) else False
port_offerer: int = port_node_from
port_bidder: int = port_node_to
port_leader: int = port_bidder if reverse_bid else port_offerer
port_follower: int = port_offerer if reverse_bid else port_bidder
logger.info(
f"Offerer, bidder, leader, follower: {port_offerer}, {port_bidder}, {port_leader}, {port_follower}"
)
amt_from_str = f"{random.uniform(0.5, 10.0):.{8}f}"
amt_to_str = f"{random.uniform(0.5, 10.0):.{8}f}"
data = {
"addr_from": "-1",
"coin_from": ticker_from,
"coin_to": ticker_to,
"amt_from": amt_from_str,
"amt_to": amt_to_str,
"swap_type": "adaptor_sig",
"lock_type": str(int(TxLockTypes.SEQUENCE_LOCK_BLOCKS)),
"lock_blocks": str(lock_value),
}
logger.info(
f"Creating offer {amt_from_str} {ticker_from} -> {amt_to_str} {ticker_to}"
)
offer_id: str = post_json_api(port_node_from, "offers/new", data)["offer_id"]
wait_for_offer(self.delay_event, port_node_to, offer_id)
offer = read_json_api(port_node_to, f"offers/{offer_id}")[0]
assert offer["offer_id"] == offer_id
data = {
"offer_id": offer_id,
"amount_from": offer["amount_from"],
"validmins": 60,
}
rv = post_json_api(port_node_to, "bids/new", data)
bid_id: str = rv["bid_id"]
wait_for_bid_state(self.delay_event, port_follower, bid_id)
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{"debugind": DebugTypes.CREATE_INVALID_COIN_B_LOCK},
)
assert "bid_state" in rv
wait_for_bid_state(
self.delay_event, port_node_from, bid_id, BidStates.BID_RECEIVED
)
rv = post_json_api(port_offerer, f"bids/{bid_id}", {"accept": True})
assert rv["bid_state"] in ("Accepted", "Request accepted")
wait_for_bid_state(
self.delay_event,
port_leader,
bid_id,
BidStates.XMR_SWAP_FAILED_REFUNDED,
240,
)
wait_for_bid_state(
self.delay_event,
port_follower,
bid_id,
BidStates.XMR_SWAP_FAILED_REFUNDED,
240,
)
rv = post_json_api(
port_leader,
f"bids/{bid_id}",
{"show_extra": True, "with_events": True},
)
events = rv["events"]
logger.info(f"Initiator events: {events}")
assert any(event["desc"] == "Detected invalid lock Tx B" for event in events)
assert any(
event["desc"] == "Lock tx A refund spend tx published" for event in events
)
rv = post_json_api(
port_follower,
f"bids/{bid_id}",
{"show_extra": True, "with_events": True},
)
events = rv["events"]
logger.info(f"Participant events: {events}")
assert any(event["desc"] == "Lock tx B refund tx published" for event in events)
class Test(TestFunctions):
__test__ = True
update_min = 1.7
daemons = []
test_coin_a = Coins.PART
test_coin_b = Coins.BTC
test_coin_xmr = Coins.XMR
@classmethod
def addElectrumxDaemon(cls, coin_name: str, node_rpc_port: int, services_port: int):
coin_type: Coins = getCoinIdFromName(coin_name)
ticker: str = chainparams[coin_type]["ticker"]
ticker_lc: str = ticker.lower()
logger.info(f"Starting Electrumx for {ticker}")
ELECTRUMX_SRC_DIR = os.path.expanduser(os.getenv("ELECTRUMX_SRC_DIR"))
if ELECTRUMX_SRC_DIR is None:
raise ValueError("Please set ELECTRUMX_SRC_DIR")
ELECTRUMX_VENV = os.getenv(
"ELECTRUMX_VENV", os.path.join(ELECTRUMX_SRC_DIR, "venv")
)
ELECTRUMX_DATADIR = os.getenv(
f"ELECTRUMX_DATADIR_{ticker}", f"/tmp/electrumx_{ticker_lc}"
)
SSL_CERTFILE = f"{ELECTRUMX_DATADIR}/certfile.crt"
SSL_KEYFILE = f"{ELECTRUMX_DATADIR}/keyfile.key"
if os.path.isdir(ELECTRUMX_DATADIR):
if RESET_TEST:
logger.info("Removing " + ELECTRUMX_DATADIR)
shutil.rmtree(ELECTRUMX_DATADIR)
if not os.path.exists(ELECTRUMX_DATADIR):
os.makedirs(os.path.join(ELECTRUMX_DATADIR, "db"))
with open(os.path.join(ELECTRUMX_DATADIR, "banner"), "w") as fp:
fp.write("TEST BANNER")
try:
stdout = subprocess.check_output(
[
"openssl",
"req",
"-nodes",
"-new",
"-x509",
"-keyout",
SSL_KEYFILE,
"-out",
SSL_CERTFILE,
"-subj",
'/C=CA/ST=Quebec/L=Montreal/O="Poutine LLC"/OU=devops/CN=*.poutine.net\n',
],
text=True,
)
logger.info(f"openssl {stdout}")
except subprocess.CalledProcessError as e:
logger.info(f"Error openssl {e.output}")
electrumx_env = {
"COIN": coin_name.capitalize(),
"NET": "regtest",
"LOG_LEVEL": "debug",
"SERVICES": f"tcp://:{services_port},ssl://:{services_port + 1},rpc://",
"CACHE_MB": "400",
"DAEMON_URL": f"http://test_{ticker_lc}_0:test_{ticker_lc}_pwd_0@127.0.0.1:{node_rpc_port}",
"DB_DIRECTORY": f"{ELECTRUMX_DATADIR}/db",
"SSL_CERTFILE": f"{ELECTRUMX_DATADIR}/certfile.crt",
"SSL_KEYFILE": f"{ELECTRUMX_DATADIR}/keyfile.key",
"BANNER_FILE": f"{ELECTRUMX_DATADIR}/banner",
"DAEMON_POLL_INTERVAL_BLOCKS": "1000",
"DAEMON_POLL_INTERVAL_MEMPOOL": "1000",
}
opened_files = []
stdout_dest = open(f"{ELECTRUMX_DATADIR}/electrumx.log", "w")
stderr_dest = stdout_dest
cls.daemons.append(
Daemon(
subprocess.Popen(
[
os.path.join(ELECTRUMX_VENV, "bin", "python"),
os.path.join(ELECTRUMX_SRC_DIR, "electrumx_server"),
],
shell=False,
stdin=subprocess.PIPE,
stdout=stdout_dest,
stderr=stderr_dest,
cwd=ELECTRUMX_SRC_DIR,
env=electrumx_env,
),
[
opened_files,
],
f"electrumx_{ticker_lc}",
)
)
@classmethod
def setUpClass(cls):
cls.addElectrumxDaemon("bitcoin", 32793, 50001)
super(Test, cls).setUpClass()
@classmethod
def modifyConfig(cls, test_path, i):
modify_config(test_path, i)
@classmethod
def setupNodes(cls):
logger.info(f"Preparing {NUM_NODES} nodes.")
bins_path = os.path.join(TEST_PATH, "bin")
for i in range(NUM_NODES):
logger.info(f"Preparing node: {i}.")
client_path = os.path.join(TEST_PATH, f"client{i}")
try:
shutil.rmtree(client_path)
except Exception as ex:
logger.warning(f"setupNodes {ex}")
extra_args = []
if i == 1:
extra_args = [
"--btc-mode=electrum",
"--btc-electrum-server=127.0.0.1:50001",
]
run_prepare(
i,
client_path,
bins_path,
cls.test_coins_list,
mnemonics[i] if i < len(mnemonics) else None,
num_nodes=NUM_NODES,
use_rpcauth=True,
extra_settings={"min_sequence_lock_seconds": 10},
port_ofs=PORT_OFS,
extra_args=extra_args,
)
@classmethod
def tearDownClass(cls):
logger.info("Finalising Test")
super().tearDownClass()
stopDaemons(cls.daemons)
def test_01_a_full_swap_xmr(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
1000,
self.port_node_1,
self.port_node_0,
True,
)
self.do_test_01_full_swap(self.test_coin_a, self.test_coin_b)
def test_01_b_full_swap_xmr(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
self.do_test_01_full_swap(self.test_coin_b, self.test_coin_xmr)
def test_01_c_full_swap_xmr_reverse(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
1000,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_01_full_swap(
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
)
def test_02_a_leader_recover_a_lock_tx(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_02_leader_recover_a_lock_tx(
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
)
def test_02_b_leader_recover_a_lock_tx_reverse(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_02_leader_recover_a_lock_tx(
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
)
def test_03_a_follower_recover_a_lock_tx(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_03_follower_recover_a_lock_tx(
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
)
def test_03_b_follower_recover_a_lock_tx_reverse(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_03_follower_recover_a_lock_tx(
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
)
def test_04_a_follower_recover_b_lock_tx(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_04_follower_recover_b_lock_tx(
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0
)
def test_04_b_follower_recover_b_lock_tx_reverse(self):
prepare_balance(
self.delay_event,
self.test_coin_b,
100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0,
self.port_node_1,
True,
)
self.do_test_04_follower_recover_b_lock_tx(
self.test_coin_xmr, self.test_coin_b, self.port_node_0, self.port_node_1
)
if __name__ == "__main__":
unittest.main()
+122 -37
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -67,6 +67,18 @@ logger = logging.getLogger()
class Test(unittest.TestCase): class Test(unittest.TestCase):
@staticmethod
def ci_btc():
btc_coin_settings = {"rpcport": 0, "rpcauth": "none"}
btc_coin_settings.update(REQUIRED_SETTINGS)
return BTCInterface(btc_coin_settings, "regtest")
@staticmethod
def ci_xmr():
xmr_coin_settings = {"rpcport": 0, "walletrpcport": 0, "walletrpcauth": "none"}
xmr_coin_settings.update(REQUIRED_SETTINGS)
return XMRInterface(xmr_coin_settings, "regtest")
def test_serialise_num(self): def test_serialise_num(self):
def test_case(v, nb=None): def test_case(v, nb=None):
b = SerialiseNum(v) b = SerialiseNum(v)
@@ -86,10 +98,7 @@ class Test(unittest.TestCase):
test_case(4194642) test_case(4194642)
def test_sequence(self): def test_sequence(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
time_val = 48 * 60 * 60 time_val = 48 * 60 * 60
encoded = ci.getExpectedSequence(TxLockTypes.SEQUENCE_LOCK_TIME, time_val) encoded = ci.getExpectedSequence(TxLockTypes.SEQUENCE_LOCK_TIME, time_val)
@@ -197,10 +206,47 @@ class Test(unittest.TestCase):
"5c26c518fb698e91a5858c33e9075488c55c235f391162fe9e6cbd4f694f80aa" "5c26c518fb698e91a5858c33e9075488c55c235f391162fe9e6cbd4f694f80aa"
) )
def test_key_summing(self):
ci_btc = self.ci_btc()
ci_xmr = self.ci_xmr()
keys = [
bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
),
bytes.fromhex(
"b725b6359bd2b510d9d5a7bba7bdee17abbf113253f6338ea50a8f0cf45fd0d0"
),
]
sum_secp256k1: bytes = ci_btc.sumKeys(keys[0], keys[1])
assert (
sum_secp256k1.hex()
== "9dde9df8660d3e0f28fe00d648b70e052511ad800a07783f284455b1d2f5a939"
)
sum_ed25519: bytes = ci_xmr.sumKeys(keys[0], keys[1])
assert (
sum_ed25519.hex()
== "0dde9df8660d3e0f28fe00d648b70e0323e9c192fe9b94f1cf7138515e877725"
)
sum_secp256k1 = ci_btc.sumPubkeys(
ci_btc.getPubkey(keys[0]), ci_btc.getPubkey(keys[1])
)
assert (
sum_secp256k1.hex()
== "028c30392e35620af0787b363a03cf9a695336759664436e1f609481c869541a5c"
)
sum_ed25519 = ci_xmr.sumPubkeys(
ci_xmr.getPubkey(keys[0]), ci_xmr.getPubkey(keys[1])
)
assert (
sum_ed25519.hex()
== "4b2dd2dc9acc9be7efed4fdbfb96f0002aeb9e4c8638c5b24562a7158b283626"
)
def test_ecdsa_otves(self): def test_ecdsa_otves(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
vk_sign = ci.getNewRandomKey() vk_sign = ci.getNewRandomKey()
vk_encrypt = ci.getNewRandomKey() vk_encrypt = ci.getNewRandomKey()
@@ -209,21 +255,16 @@ class Test(unittest.TestCase):
sign_hash = secrets.token_bytes(32) sign_hash = secrets.token_bytes(32)
cipher_text = ecdsaotves_enc_sign(vk_sign, pk_encrypt, sign_hash) cipher_text = ecdsaotves_enc_sign(vk_sign, pk_encrypt, sign_hash)
assert ecdsaotves_enc_verify(pk_sign, pk_encrypt, sign_hash, cipher_text) assert ecdsaotves_enc_verify(pk_sign, pk_encrypt, sign_hash, cipher_text)
sig = ecdsaotves_dec_sig(vk_encrypt, cipher_text) sig = ecdsaotves_dec_sig(vk_encrypt, cipher_text)
assert ci.verifySig(pk_sign, sign_hash, sig) assert ci.verifySig(pk_sign, sign_hash, sig)
recovered_key = ecdsaotves_rec_enc_key(pk_encrypt, cipher_text, sig) recovered_key = ecdsaotves_rec_enc_key(pk_encrypt, cipher_text, sig)
assert vk_encrypt == recovered_key assert vk_encrypt == recovered_key
def test_sign(self): def test_sign(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
vk = ci.getNewRandomKey() vk = ci.getNewRandomKey()
pk = ci.getPubkey(vk) pk = ci.getPubkey(vk)
@@ -236,9 +277,7 @@ class Test(unittest.TestCase):
ci.verifySig(pk, message_hash, sig) ci.verifySig(pk, message_hash, sig)
def test_sign_compact(self): def test_sign_compact(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
vk = ci.getNewRandomKey() vk = ci.getNewRandomKey()
pk = ci.getPubkey(vk) pk = ci.getPubkey(vk)
@@ -251,9 +290,7 @@ class Test(unittest.TestCase):
assert sig == sig2 assert sig == sig2
def test_sign_recoverable(self): def test_sign_recoverable(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
vk = ci.getNewRandomKey() vk = ci.getNewRandomKey()
pk = ci.getPubkey(vk) pk = ci.getPubkey(vk)
@@ -267,18 +304,13 @@ class Test(unittest.TestCase):
assert sig == sig2 assert sig == sig2
def test_pubkey_to_address(self): def test_pubkey_to_address(self):
coin_settings = {"rpcport": 0, "rpcauth": "none"} ci = self.ci_btc()
coin_settings.update(REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, "regtest")
pk = h2b("02c26a344e7d21bcc6f291532679559f2fd234c881271ff98714855edc753763a6") pk = h2b("02c26a344e7d21bcc6f291532679559f2fd234c881271ff98714855edc753763a6")
addr = ci.pubkey_to_address(pk) addr = ci.pubkey_to_address(pk)
assert addr == "mj6SdSxmWRmdDqR5R3FfZmRiLmQfQAsLE8" assert addr == "mj6SdSxmWRmdDqR5R3FfZmRiLmQfQAsLE8"
def test_dleag(self): def test_dleag(self):
coin_settings = {"rpcport": 0, "walletrpcport": 0, "walletrpcauth": "none"} ci = self.ci_xmr()
coin_settings.update(REQUIRED_SETTINGS)
ci = XMRInterface(coin_settings, "regtest")
key = ci.getNewRandomKey() key = ci.getNewRandomKey()
proof = ci.proveDLEAG(key) proof = ci.proveDLEAG(key)
@@ -409,15 +441,8 @@ class Test(unittest.TestCase):
amount_to_recreate = int((amount_from * rate) // (10**scale_from)) amount_to_recreate = int((amount_from * rate) // (10**scale_from))
assert "10.00000000" == format_amount(amount_to_recreate, scale_to) assert "10.00000000" == format_amount(amount_to_recreate, scale_to)
coin_settings = { ci_xmr = self.ci_xmr()
"rpcport": 0, ci_btc = self.ci_btc()
"rpcauth": "none",
"walletrpcport": 0,
"walletrpcauth": "none",
}
coin_settings.update(REQUIRED_SETTINGS)
ci_xmr = XMRInterface(coin_settings, "regtest")
ci_btc = BTCInterface(coin_settings, "regtest")
for i in range(10000): for i in range(10000):
test_pairs = random.randint(0, 3) test_pairs = random.randint(0, 3)
@@ -651,7 +676,7 @@ class Test(unittest.TestCase):
def test_db(self): def test_db(self):
db_test = DBMethods() db_test = DBMethods()
db_test.sqlite_file = ":memory:" db_test.sqlite_file = ":memory:"
db_test.mxDB = threading.Lock() db_test.mxDB = threading.RLock()
cursor = db_test.openDB() cursor = db_test.openDB()
try: try:
create_db_(db_test._db_con, logger) create_db_(db_test._db_con, logger)
@@ -663,6 +688,7 @@ class Test(unittest.TestCase):
ki.record_id = 1 ki.record_id = 1
ki.address = "test1" ki.address = "test1"
ki.label = "test1" ki.label = "test1"
ki.note = "note1"
try: try:
db_test.add(ki, cursor, upsert=False) db_test.add(ki, cursor, upsert=False)
except Exception as e: except Exception as e:
@@ -670,6 +696,65 @@ class Test(unittest.TestCase):
else: else:
raise ValueError("Should have errored.") raise ValueError("Should have errored.")
db_test.add(ki, cursor, upsert=True) db_test.add(ki, cursor, upsert=True)
# Test columns list
ki_test = db_test.queryOne(
KnownIdentity,
cursor,
{"address": "test1"},
columns_list=[
"label",
],
)
assert ki_test.label == "test1"
assert ki_test.address is None
# Test updating partial row
ki_test.label = "test2"
ki_test.record_id = 1
db_test.add(
ki_test,
cursor,
upsert=True,
columns_list=[
"record_id",
"label",
],
)
ki_test = db_test.queryOne(KnownIdentity, cursor, {"address": "test1"})
assert ki_test.record_id == 1
assert ki_test.address == "test1"
assert ki_test.label == "test2"
assert ki_test.note == "note1"
ki_test.note = "test2"
ki_test.label = "test3"
db_test.updateDB(
ki_test,
cursor,
["record_id"],
columns_list=[
"label",
],
)
ki_test = db_test.queryOne(KnownIdentity, cursor, {"address": "test1"})
assert ki_test.record_id == 1
assert ki_test.address == "test1"
assert ki_test.label == "test3"
assert ki_test.note == "note1"
# Test partially initialised object
ki_test_p = KnownIdentity(
_init_all_columns=False, record_id=1, label="test4"
)
db_test.add(ki_test_p, cursor, upsert=True)
ki_test = db_test.queryOne(KnownIdentity, cursor, {"address": "test1"})
assert ki_test.record_id == 1
assert ki_test.address == "test1"
assert ki_test.label == "test4"
assert ki_test.note == "note1"
finally: finally:
db_test.closeDB(cursor) db_test.closeDB(cursor)
+14 -13
View File
@@ -69,6 +69,18 @@ def updateThread():
delay_event.wait(5) delay_event.wait(5)
def run_process(client_id):
client_path = os.path.join(TEST_PATH, f"client{client_id}")
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
class Test(unittest.TestCase): class Test(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -76,17 +88,6 @@ class Test(unittest.TestCase):
prepare_nodes(3, "bitcoin") prepare_nodes(3, "bitcoin")
def run_thread(self, client_id):
client_path = os.path.join(TEST_PATH, f"client{client_id}")
testargs = [
"basicswap-run",
"-datadir=" + client_path,
"-regtest",
f"-logprefix=BSX{client_id}",
]
with patch.object(sys, "argv", testargs):
runSystem.main()
def wait_for_node_height(self, port=12701, wallet_ticker="part", wait_for_blocks=3): def wait_for_node_height(self, port=12701, wallet_ticker="part", wait_for_blocks=3):
# Wait for height, or sequencelock is thrown off by genesis blocktime # Wait for height, or sequencelock is thrown off by genesis blocktime
logging.info( logging.info(
@@ -112,7 +113,7 @@ class Test(unittest.TestCase):
processes = [] processes = []
for i in range(3): for i in range(3):
processes.append(multiprocessing.Process(target=self.run_thread, args=(i,))) processes.append(multiprocessing.Process(target=run_process, args=(i,)))
processes[-1].start() processes[-1].start()
try: try:
@@ -169,7 +170,7 @@ class Test(unittest.TestCase):
c1 = processes[1] c1 = processes[1]
c1.terminate() c1.terminate()
c1.join() c1.join()
processes[1] = multiprocessing.Process(target=self.run_thread, args=(1,)) processes[1] = multiprocessing.Process(target=run_process, args=(1,))
processes[1].start() processes[1].start()
waitForServer(delay_event, 12701) waitForServer(delay_event, 12701)
+22 -50
View File
@@ -60,6 +60,7 @@ from tests.basicswap.util import (
from tests.basicswap.common import ( from tests.basicswap.common import (
callrpc_cli, callrpc_cli,
prepareDataDir, prepareDataDir,
prepare_balance,
make_rpc_func, make_rpc_func,
checkForks, checkForks,
stopDaemons, stopDaemons,
@@ -1055,54 +1056,13 @@ class BaseTest(unittest.TestCase):
port_take_from_node: int, port_take_from_node: int,
test_balance: bool = True, test_balance: bool = True,
) -> None: ) -> None:
delay_iterations = 100 if coin == Coins.NAV else 20 prepare_balance(
delay_time = 5 if coin == Coins.NAV else 3
if coin == Coins.PART_BLIND:
coin_ticker: str = "PART"
balance_type: str = "blind_balance"
address_type: str = "stealth_address"
type_to: str = "blind"
elif coin == Coins.PART_ANON:
coin_ticker: str = "PART"
balance_type: str = "anon_balance"
address_type: str = "stealth_address"
type_to: str = "anon"
else:
coin_ticker: str = coin.name
balance_type: str = "balance"
address_type: str = "deposit_address"
js_w = read_json_api(port_target_node, "wallets")
current_balance: float = float(js_w[coin_ticker][balance_type])
if test_balance and current_balance >= amount:
return
post_json = {
"value": amount,
"address": js_w[coin_ticker][address_type],
"subfee": False,
}
if coin in (Coins.XMR, Coins.WOW):
post_json["sweepall"] = False
if coin in (Coins.PART_BLIND, Coins.PART_ANON):
post_json["type_to"] = type_to
json_rv = read_json_api(
port_take_from_node,
"wallets/{}/withdraw".format(coin_ticker.lower()),
post_json,
)
assert len(json_rv["txid"]) == 64
wait_for_amount: float = amount
if not test_balance:
wait_for_amount += current_balance
wait_for_balance(
test_delay_event, test_delay_event,
"http://127.0.0.1:{}/json/wallets/{}".format( coin,
port_target_node, coin_ticker.lower() amount,
), port_target_node,
balance_type, port_take_from_node,
wait_for_amount, test_balance,
iterations=delay_iterations,
delay_time=delay_time,
) )
@@ -1258,7 +1218,11 @@ class Test(BaseTest):
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) result = ci.publishBLockTx(v, S, amount, fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
addr_out = ci.getNewAddress(True) addr_out = ci.getNewAddress(True)
lock_tx_b_spend_txid = ci.spendBLockTx( lock_tx_b_spend_txid = ci.spendBLockTx(
@@ -1287,7 +1251,11 @@ class Test(BaseTest):
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) result = ci.publishBLockTx(v, S, amount, fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
addr_out = ci.getNewAddress(True) addr_out = ci.getNewAddress(True)
for i in range(20): for i in range(20):
@@ -2346,7 +2314,11 @@ class Test(BaseTest):
v = ci.getNewRandomKey() v = ci.getNewRandomKey()
s = ci.getNewRandomKey() s = ci.getNewRandomKey()
S = ci.getPubkey(s) S = ci.getPubkey(s)
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate) result = ci.publishBLockTx(v, S, amount, fee_rate)
if isinstance(result, tuple):
lock_tx_b_txid, lock_tx_b_vout = result
else:
lock_tx_b_txid = result
addr_out = ci.getNewStealthAddress() addr_out = ci.getNewStealthAddress()
lock_tx_b_spend_txid = None lock_tx_b_spend_txid = None
+2 -1
View File
@@ -30,6 +30,7 @@ from tests.basicswap.common import (
waitForNumBids, waitForNumBids,
) )
from tests.basicswap.common_xmr import ( from tests.basicswap.common_xmr import (
run_process,
XmrTestBase, XmrTestBase,
waitForBidState, waitForBidState,
) )
@@ -104,7 +105,7 @@ class Test(XmrTestBase):
self.delay_event.wait(5) self.delay_event.wait(5)
logger.info("Starting node 0") logger.info("Starting node 0")
self.processes[0] = multiprocessing.Process(target=self.run_thread, args=(0,)) self.processes[0] = multiprocessing.Process(target=run_process, args=(0,))
self.processes[0].start() self.processes[0].start()
waitForServer(self.delay_event, 12700) waitForServer(self.delay_event, 12700)
+5 -3
View File
@@ -27,11 +27,12 @@ from tests.basicswap.util import (
waitForServer, waitForServer,
) )
from tests.basicswap.common import ( from tests.basicswap.common import (
waitForNumOffers,
waitForNumBids, waitForNumBids,
waitForNumOffers,
waitForNumSwapping, waitForNumSwapping,
) )
from tests.basicswap.common_xmr import ( from tests.basicswap.common_xmr import (
run_process,
XmrTestBase, XmrTestBase,
) )
@@ -94,12 +95,13 @@ class Test(XmrTestBase):
waitForNumBids(self.delay_event, 12700, 1) waitForNumBids(self.delay_event, 12700, 1)
for i in range(10): for i in range(20):
bids = read_json_api(12700, "bids") bids = read_json_api(12700, "bids")
bid = bids[0] bid = bids[0]
if bid["bid_state"] == "Received": if bid["bid_state"] == "Received":
break break
self.delay_event.wait(1) self.delay_event.wait(1)
assert bid["bid_state"] == "Received"
assert bid["expire_at"] == bid["created_at"] + data["validmins"] * 60 assert bid["expire_at"] == bid["created_at"] + data["validmins"] * 60
data = {"accept": True} data = {"accept": True}
@@ -112,7 +114,7 @@ class Test(XmrTestBase):
c1 = self.processes[1] c1 = self.processes[1]
c1.terminate() c1.terminate()
c1.join() c1.join()
self.processes[1] = multiprocessing.Process(target=self.run_thread, args=(1,)) self.processes[1] = multiprocessing.Process(target=run_process, args=(1,))
self.processes[1].start() self.processes[1].start()
waitForServer(self.delay_event, 12701) waitForServer(self.delay_event, 12701)