Compare commits

..

219 Commits

Author SHA1 Message Date
tecnovert 4d10c4b385 build, guix: update packed version 2026-06-09 00:02:17 +02:00
tecnovert ced017ab3a build: raise version to 0.16.3 2026-06-08 23:47:04 +02:00
tecnovert a64a65fe53 Merge pull request #491 from gerlofvanek/electrum_fixes_3
Verify initiate lock-tx amount.
2026-06-08 21:43:07 +00:00
gerlofvanek 4ac0321acb Verify initiate lock-tx amount. 2026-06-08 22:47:23 +02:00
tecnovert d16e658a66 fix: match Part address type used for funded itx and createSCLockTx 2026-06-07 19:17:24 +02:00
tecnovert 6861975f9a Merge pull request #489 from tecnovert/csv_check_fix
fix: get refund tx block info for CSV check
2026-06-06 21:53:08 +00:00
tecnovert f47320e0e4 fix: get actual blinded Part refund vout 2026-06-06 23:52:09 +02:00
tecnovert 2b2d14b86a test: simplify PIVX test 2026-06-06 23:52:09 +02:00
tecnovert 554d362a45 test: fix Dash tests 2026-06-06 23:52:08 +02:00
tecnovert 7655f1ad81 fix: get refund tx block info for CSV check 2026-06-06 23:52:08 +02:00
tecnovert 3264a5845e Merge pull request #487 from tecnovert/subfee_ui
Subfee Bids
2026-06-06 21:50:07 +00:00
tecnovert 46e3d0266b refactor: make adaptor-sig the default swap type 2026-06-05 23:07:35 +02:00
tecnovert 34b6f816ee refactor: remove unused function 2026-06-05 23:07:34 +02:00
tecnovert 04e2020ff3 feat: add subfee bids 2026-06-05 23:07:34 +02:00
tecnovert 1f8d2f2eb8 Merge pull request #485 from nahuhh/xmr_max_log
xmr: reduce max log files to from 50 -> 5 (x100MB)
2026-06-03 21:50:57 +00:00
tecnovert ea3dc7bdb0 Merge pull request #486 from nahuhh/daemon_updates
firo: bump to v0.14.16.1 [mandatory]
2026-06-03 19:11:14 +00:00
nahuhh 67d0ffa3cf firo: bump to v0.14.16.1 [mandatory] 2026-06-03 18:06:00 +00:00
nahuhh dc80795d08 xmr: reduce max log files to from 50 -> 5 (x100MB) 2026-06-01 03:50:26 +00:00
tecnovert cfca799df2 Merge pull request #483 from tecnovert/feerate
fix: raise default max fee rate from 2 to 4x estimated fee
2026-05-31 10:59:58 +00:00
tecnovert 79f50f27f7 fix: ui, display offer chain a feerate for reversed swaps 2026-05-30 22:54:14 +02:00
tecnovert 5c8f1bc6d1 fix: raise default max fee rate from 2 to 4x estimated fee 2026-05-30 22:11:25 +02:00
tecnovert 590601a969 Merge pull request #459 from nahuhh/daemon_updates
prepare: daemon updates 2026-05-12
2026-05-30 20:08:32 +00:00
tecnovert 1c0cdc0bd6 Merge pull request #480 from tecnovert/feerate
Feerate validation
2026-05-30 18:10:09 +00:00
tecnovert 119a116918 test: add codespell extra dictionary and add config to .toml 2026-05-30 19:44:29 +02:00
tecnovert f5249448bb doc: update release notes 2026-05-30 19:44:29 +02:00
tecnovert fa063e5f01 test: add tests for automatic feerate validation 2026-05-30 19:44:29 +02:00
tecnovert 48ea745cc2 feat: automatically verify feerate 2026-05-30 19:44:23 +02:00
tecnovert 7840f12814 Merge pull request #482 from gerlofvanek/electrum_fixes_2
Fix: Refund-path / Maturity checks.
2026-05-30 15:16:23 +00:00
gerlofvanek 0de77c8d97 Update: HTLC - CSV / CLTV 2026-05-29 22:01:32 +02:00
gerlofvanek 283662e659 Fix: Median time one call. 2026-05-29 19:15:34 +02:00
gerlofvanek 221d962c12 Fix: Refund-path / Maturity checks. 2026-05-29 18:36:05 +02:00
nahuhh 31f8bc0f12 firo: bump to v0.14.16.0 [mandatory]
"[update] prior to block 1,329,000 (approximately 22 June 2026)."
2026-05-29 13:30:40 +00:00
nahuhh a473347a67 xmr: bump to v0.18.5.0 2026-05-29 13:30:40 +00:00
nahuhh b676d0be2e decred: bump to v2.1.5 2026-05-29 13:30:40 +00:00
tecnovert 5099b9ebaa test: raise default waitForServer time 2026-05-29 15:22:48 +02:00
tecnovert 19f13d9d96 Merge pull request #481 from tecnovert/refactor
Refactor
2026-05-29 12:54:54 +00:00
tecnovert 27f9f8c13a doc: update release notes 2026-05-29 14:23:32 +02:00
tecnovert 248b8046b1 test: wait longer, add startup_delay option 2026-05-29 14:02:07 +02:00
tecnovert 6b4b97376b test: print xmr daemon logs on ci failure 2026-05-29 11:26:11 +02:00
tecnovert e8ebfd34d0 refactor: black 2026-05-28 01:55:36 +02:00
tecnovert 24c8e8b2dd refactor: remove duplicate method 2026-05-27 23:51:51 +02:00
tecnovert 7bf3dce974 Merge pull request #477 from kewde/patch-1
fix: public key validation
2026-05-27 11:12:28 +00:00
tecnovert b6e922e3a8 Merge pull request #478 from kewde/patch-2
fix: use main address for XMR & WOW
2026-05-27 11:11:26 +00:00
kewde 59be986aa4 fix: use main address for XMR & WOW 2026-05-26 14:12:19 +02:00
kewde 25dd3809e9 fix: public key validation
https://github.com/basicswap/coincurve/blob/2bf23f173f411a60c66ba973231fadab772bfed2/src/coincurve/dleag.py#L63
2026-05-26 14:04:43 +02:00
tecnovert be1dbaeeaa build, guix: update packed version 2026-05-08 20:45:39 +02:00
tecnovert 3b76adeedb build: raise version to 0.16.2 2026-05-08 20:30:19 +02:00
tecnovert ae6691e7ab Merge pull request #467 from tecnovert/refactor
refactor: simplify getAddressInfo
2026-05-08 18:26:13 +00:00
tecnovert 8482533b37 Merge pull request #471 from gerlofvanek/fix_balances
Fix: Wallet balance overwrite on WebSocket updates.
2026-05-08 18:18:17 +00:00
gerlofvanek 8fe0913fda BLACK 2026-05-08 19:59:42 +02:00
gerlofvanek 9244a9fed8 Fix: Wallet balance overwrite on WebSocket updates. 2026-05-08 19:54:22 +02:00
tecnovert bfc58955da Merge pull request #469 from gerlofvanek/fix_tests
Fixes: PIVX/FIRO and test_pivx.py
2026-05-08 17:45:41 +00:00
tecnovert f77b7dc363 Merge pull request #470 from tecnovert/ci
test: show log on failure
2026-05-08 17:45:23 +00:00
tecnovert a6b5906a6d test: add balance check to test_swap_direction.py 2026-05-08 19:22:41 +02:00
tecnovert 568eab1f31 test: show log on failure 2026-05-08 18:31:27 +02:00
gerlofvanek 1b86df9b60 Fix: PIVX/Firo bug and test_pivx.py 2026-05-08 17:10:27 +02:00
tecnovert 680fc7ce35 build: raise version to 0.16.1 2026-05-06 20:27:29 +02:00
tecnovert 1f9c85c62f Merge pull request #466 from tecnovert/mweb_change_helper
LTC MWEB change back to LTC helper functions
2026-05-06 18:24:11 +00:00
tecnovert 3716a0ab62 Merge pull request #463 from tecnovert/extracoinopts
New extracoinopts run parameter
2026-05-06 18:20:06 +00:00
tecnovert 2ad8e6f4b3 Merge pull request #465 from tecnovert/ltc_fixes
LTC fixes
2026-05-06 18:19:51 +00:00
tecnovert ac084eddf7 Merge pull request #461 from tecnovert/ltc
fix: workaround for osx ltc release not on github
2026-05-06 18:13:07 +00:00
tecnovert 262593bd2c cores: bump ltc to v0.21.5.5 2026-05-06 20:11:21 +02:00
tecnovert 9f17ee709a Merge pull request #468 from tecnovert/actions
test: raise github actions plugin versions
2026-05-05 15:56:25 +00:00
tecnovert e29eb4af76 test: raise github actions plugin versions 2026-05-05 11:05:26 +02:00
tecnovert 6ebbd98aec feat: add helper functions to convert MWEB change in LTC wallet 2026-05-04 21:11:21 +02:00
tecnovert c8e7c02fe2 refactor: reduce wallet_manager imports 2026-05-04 19:39:03 +02:00
tecnovert 57a1a6505e refactor: simplify getAddressInfo 2026-05-04 19:24:25 +02:00
tecnovert bdb7f9bb5a feat: add fallback urls to downloadRelease 2026-05-04 14:56:35 +02:00
tecnovert f626e400ff fix: better log format in prepare script 2026-05-04 14:54:23 +02:00
tecnovert 2bacbcabd0 Merge pull request #462 from nahuhh/dependabot_actions
dependabot: use for github actions
2026-05-04 12:06:17 +00:00
tecnovert 2b33ed3d93 Merge pull request #464 from tecnovert/ci
test: remove cirrus ci
2026-05-04 11:59:37 +00:00
tecnovert c4e7de2873 fix: ltc, deduplicate MWEB wallet creation 2026-05-03 18:56:57 +02:00
tecnovert 9caae399d2 fix: convert coin variant tickers 2026-05-02 22:59:35 +02:00
tecnovert fd2e442839 fix: ltc, filter out mweb addresses in getUnspentsByAddr 2026-05-02 22:55:32 +02:00
tecnovert dfa11ed32f fix: ltc, deduplicate checkWallets 2026-05-02 21:47:50 +02:00
tecnovert c4f00dfa5b test: remove cirrus ci 2026-05-02 10:46:43 +02:00
tecnovert b5226c0e1c feat: add "extracoinopts" option 2026-05-02 10:24:51 +02:00
tecnovert 842e44e41b doc: log if db upgrade was forced 2026-05-02 10:24:47 +02:00
nahuhh c298cf3963 dependabot: use for github actions 2026-04-29 12:49:43 +00:00
tecnovert e06c4638d3 fix: workaround for osx ltc release not on github 2026-04-29 12:24:35 +02:00
tecnovert 6dcf0df8aa build, guix: update packed version 2026-04-28 20:22:40 +02:00
tecnovert 2c13314bdd build: raise version to 0.16.0 2026-04-28 19:57:59 +02:00
tecnovert 60eb0b295b Merge pull request #451 from nahuhh/mweb_copy_addr
wallet: fix copyable mweb address
2026-04-28 17:54:40 +00:00
tecnovert 2c1d5c60b2 Merge pull request #450 from gerlofvanek/segfault
Fix: Segfault + various fixes, Bump GUI: v3.5.0
2026-04-28 17:42:48 +00:00
tecnovert 47cd052c9f Merge pull request #452 from nahuhh/corrupt_xmr
xmr: add "input stream error" to corrupt wallet errors
2026-04-28 17:37:21 +00:00
tecnovert 6a8ab745e1 Merge pull request #455 from nahuhh/ltc_2154
ltc: bump to v0.21.5.4 [required]
2026-04-28 17:37:09 +00:00
gerlofvanek c5e703dfb3 GUI: v3.5.0 2026-04-28 10:10:24 +02:00
gerlofvanek ff6d1ad0ba Fix electrum deposit addresses jumping past gap limit. 2026-04-28 09:44:17 +02:00
gerlofvanek 1d80f479c0 Fix CI test 2026-04-28 09:34:01 +02:00
gerlofvanek f2fff7292b Fix: Deposit address gap limit for electrum wallet. 2026-04-28 01:19:00 +02:00
gerlofvanek f84c46376e Fix: Pre-fund State 2026-04-27 18:55:44 +02:00
gerlofvanek a3e6d0cf17 Fix: Extended private key for electrum. 2026-04-27 18:43:31 +02:00
gerlofvanek fe0de84054 Fixing commented issue. 2026-04-27 18:32:44 +02:00
nahuhh dfd4bb5b65 ltc: bump to v0.21.5.4 [required]
"This release includes important security updates.
All node operators and wallet users are strongly
encouraged to upgrade ASAP."

"This corrects MWEB input/output accounting going
forward and is a required upgrade for all node operators,
miners, and wallet users."
2026-04-26 04:45:28 +00:00
gerlofvanek aeff117fdc Fix warning when locked for electrum. 2026-04-26 04:25:54 +02:00
gerlofvanek 0dc5284e51 Fix: Waiting on Electrum Server + re-check the seed after error. 2026-04-25 19:45:27 +02:00
nahuhh b8f41b26c0 xmr: add "input stream error" to corrupt wallet errors 2026-04-25 10:25:17 +00:00
gerlofvanek 0c0fb8360e Fix: Segfault + log spam and various fixes. 2026-04-24 21:02:37 +02:00
nahuhh d8d457e283 wallet: fix copyable mweb address 2026-04-24 02:21:43 +00:00
muscleman a8d953d8e0 js: always set currentCoin = coinSymbol (#417)
* always set currentCoin = coinSymbol

* genericize functionality remove hardcoded wow logic
2026-04-18 20:39:47 +02:00
tecnovert 19d7d0a5b4 test: change more wallet names 2026-04-08 22:23:18 +02:00
tecnovert 67b290db27 Merge pull request #442 from tecnovert/set_wallet_name
refactor: where possible use "bsx_wallet" name for new installs
2026-04-08 20:07:03 +00:00
tecnovert 6df5c4687d Merge pull request #441 from tecnovert/wallet_name
fix: remove workaround for blank wallet name
2026-04-08 20:06:21 +00:00
tecnovert 7f125a2fe1 Merge pull request #443 from tecnovert/backports
refactor: backports
2026-04-08 20:06:00 +00:00
tecnovert 9b4a853c65 Merge pull request #445 from gerlofvanek/wallet_tables
Cleanup / Wallet tables use own schema.
2026-04-08 20:05:41 +00:00
gerlofvanek 953ef6b4ae Remove extra_tables. 2026-04-08 20:52:05 +02:00
gerlofvanek 614d29c31c Cleanup / Wallet tables use own schema. 2026-04-08 19:55:33 +02:00
tecnovert 360d356cbf refactor: backports 2026-04-08 09:47:27 +02:00
tecnovert 3ff67f0766 fix: enable multiwallet for BCH 2026-04-08 00:39:52 +02:00
tecnovert 25b7ecfc42 tests: change wallet name 2026-04-07 23:23:02 +02:00
tecnovert da248239d4 refactor: where possible use "bsx_wallet" name for new installs 2026-04-07 23:23:02 +02:00
tecnovert 0061b347f5 fix: remove workaround for blank wallet name 2026-04-07 22:54:41 +02:00
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
tecnovert 9d716d25cb guix: update packed version 2026-01-01 01:28:36 +02:00
tecnovert 2259e964b1 build: raise version to 0.15.2 2026-01-01 01:14:19 +02:00
tecnovert dc289a18ee dcr: fix wallet creation and test for v2.1.2 2026-01-01 01:12:27 +02:00
tecnovert b3591a2bb1 Merge pull request #409 from nahuhh/decred_212
decred: v2.1.2
2025-12-31 06:52:18 +00:00
nahuhh 7e892ee9af decred: v2.1.2 2025-12-26 00:20:46 +00:00
tecnovert 69f607d28f Merge pull request #408 from tecnovert/backports
Backports
2025-12-25 18:55:12 +00:00
tecnovert e1bca8e384 backports 2025-12-25 13:18:48 +02:00
tecnovert 6fd324ec9f refactor: split watched classes into new file 2025-12-25 13:13:46 +02:00
tecnovert b6af5ee93d fix delay checking for expired bids and offers 2025-12-25 13:10:38 +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
tecnovert 289c66e2e5 Merge pull request #407 from tecnovert/coincurve
Update coincurve to basicswap_v0.3
2025-12-02 07:35:07 +00:00
tecnovert bcec90feca build(deps): use rebased coincurve version 2025-11-26 11:02:36 +02:00
tecnovert 543a8e32a4 Merge pull request #403 from basicswap/dependabot/pip/dev/pyzmq-27.1.0
build(deps): bump pyzmq from 26.2.1 to 27.1.0
2025-11-26 08:29:08 +00:00
tecnovert c8be53068b Merge pull request #405 from nahuhh/monero_1844
xmr: bump to v0.18.4.4
2025-11-25 09:37:48 +00:00
tecnovert 0af7e79929 prepare: set Particl version to 27.2.3.0 2025-11-23 00:38:13 +02:00
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
nahuhh 529402cff6 xmr: bump to v0.18.4.4 2025-11-19 14:20:52 +00:00
dependabot[bot] 9a863a00b6 build(deps): bump pyzmq from 26.2.1 to 27.1.0
Bumps [pyzmq](https://github.com/zeromq/pyzmq) from 26.2.1 to 27.1.0.
- [Release notes](https://github.com/zeromq/pyzmq/releases)
- [Commits](https://github.com/zeromq/pyzmq/compare/v26.2.1...v27.1.0)

---
updated-dependencies:
- dependency-name: pyzmq
  dependency-version: 27.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 07:16:09 +00:00
117 changed files with 17381 additions and 2272 deletions
-45
View File
@@ -1,45 +0,0 @@
container:
image: python
lint_task:
setup_script:
- pip install flake8 codespell
script:
- flake8 --version
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
test_task:
environment:
- TEST_RELOAD_PATH: $HOME/test_basicswap1
- TEST_DIR: $HOME/test_basicswap2
- BIN_DIR: /tmp/cached_bin
- PARTICL_BINDIR: ${BIN_DIR}/particl
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
- XMR_BINDIR: ${BIN_DIR}/monero
setup_script:
- apt-get update
- apt-get install -y python3-pip pkg-config
- pip install tox pytest
- pip install .
bins_cache:
folder: /tmp/cached_bin
reupload_on_changes: false
fingerprint_script:
- basicswap-prepare -v
populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
script:
- cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}"
- mkdir -p "${DATADIRS}/bin"
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
- mkdir -p "${TEST_RELOAD_PATH}/bin"
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
- # tox
- pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'
+8
View File
@@ -9,3 +9,11 @@ updates:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"
# Set update schedule for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"
+23 -9
View File
@@ -30,9 +30,9 @@ jobs:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -45,15 +45,13 @@ jobs:
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
sudo apt-get update
sudo apt-get install -y firefox
sudo apt-get install -y firefox gnupg
fi
python -m pip install --upgrade pip
pip install python-gnupg
pip install -e .[dev]
pip install -r requirements.txt --require-hashes
pip install .[dev]
- name: Install
run: |
pip install .
# Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt
@@ -62,7 +60,7 @@ jobs:
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Run codespell
run: |
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
codespell
- name: Run black
run: |
black --check --diff --exclude="contrib" .
@@ -71,7 +69,7 @@ jobs:
pytest tests/basicswap/test_other.py
- name: Cache coin cores
id: cache-cores
uses: actions/cache@v3
uses: actions/cache@v5
env:
cache-name: cache-cores
with:
@@ -94,15 +92,26 @@ jobs:
export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero"
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx or test_03_a_follower_recover_a_lock_tx or test_11_fee_validation"
- name: Run test_encrypted_xmr_reload
id: test_encrypted_xmr_reload
run: |
export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Print log files on failure
if: ${{ failure() && steps.test_encrypted_xmr_reload.conclusion == 'failure' }}
run: |
for i in 0 1 2; do
for logname in core_stderr core_stdout wallet_stderr wallet_stdout; do
echo "=== client${i} ${logname}.log ==="
cat /tmp/test_basicswap/client${i}/monero/${logname}.log || true
done
done
- name: Run selenium tests
id: selenium_tests
run: |
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
@@ -128,3 +137,8 @@ jobs:
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID
- name: Print log file on failure
if: ${{ failure() && steps.selenium_tests.conclusion == 'failure' }}
run: |
echo "=== SELENIUM BACKGROUND LOG ==="
cat /tmp/log.txt
+1 -1
View File
@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.15.1"
__version__ = "0.16.3"
+75 -3
View File
@@ -5,10 +5,12 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import logging
import os
import random
import shlex
import shutil
import socket
import socks
import subprocess
@@ -55,7 +57,7 @@ class BaseApp(DBMethods):
self.settings = settings
self.coin_clients = {}
self.coin_interfaces = {}
self.mxDB = threading.Lock()
self.mxDB = threading.RLock()
self.debug = self.settings.get("debug", False)
self.delay_event = threading.Event()
self.chainstate_delay_event = threading.Event()
@@ -156,6 +158,71 @@ class BaseApp(DBMethods):
except Exception:
return {}
def getElectrumAddressIndex(self, coin_name: str) -> tuple:
try:
chain_settings = self.settings["chainclients"].get(coin_name, {})
ext_idx = chain_settings.get("electrum_address_index", 0)
int_idx = chain_settings.get("electrum_internal_address_index", 0)
return (ext_idx, int_idx)
except Exception:
return (0, 0)
def updateElectrumAddressIndex(
self, coin_name: str, ext_idx: int, int_idx: int
) -> None:
try:
if coin_name not in self.settings["chainclients"]:
self.log.debug(
f"updateElectrumAddressIndex: {coin_name} not in chainclients"
)
return
chain_settings = self.settings["chainclients"][coin_name]
current_ext = chain_settings.get("electrum_address_index", 0)
current_int = chain_settings.get("electrum_internal_address_index", 0)
if ext_idx <= current_ext and int_idx <= current_int:
return
if ext_idx > current_ext:
chain_settings["electrum_address_index"] = ext_idx
if int_idx > current_int:
chain_settings["electrum_internal_address_index"] = int_idx
self.log.debug(
f"Persisting electrum address index for {coin_name}: ext={ext_idx}, int={int_idx}"
)
self._saveSettings()
except Exception as e:
self.log.warning(
f"Failed to update electrum address index for {coin_name}: {e}"
)
def _normalizeSettingsPaths(self, settings: dict) -> dict:
if "chainclients" in settings:
for coin_name, cc in settings["chainclients"].items():
for path_key in ("datadir", "bindir", "walletsdir"):
if path_key in cc and isinstance(cc[path_key], str):
cc[path_key] = os.path.normpath(cc[path_key])
return settings
def _saveSettings(self) -> None:
from basicswap import config as cfg
self._normalizeSettingsPaths(self.settings)
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new"
try:
if os.path.exists(settings_path):
shutil.copyfile(settings_path, settings_path + ".last")
with open(settings_path_new, "w") as fp:
json.dump(self.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path)
self.log.debug(f"Settings saved to {settings_path}")
except Exception as e:
self.log.warning(f"Failed to save settings: {e}")
def setDaemonPID(self, name, pid) -> None:
if isinstance(name, Coins):
self.coin_clients[name]["pid"] = pid
@@ -298,8 +365,10 @@ class BaseApp(DBMethods):
self.log.warning(f"Setting mocktime to {new_offset}")
self.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
value: int = self.settings.get(name, default_v)
def get_clamped_int_from(
self, settings: dict, name: str, default_v: int, min_v: int, max_v
) -> int:
value: int = settings.get(name, default_v)
if value < min_v:
self.log.warning(f"Setting {name} to {min_v}")
value = min_v
@@ -308,6 +377,9 @@ class BaseApp(DBMethods):
value = max_v
return value
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
return self.get_clamped_int_from(self.settings, name, default_v, min_v, max_v)
def get_delay_event_seconds(self):
if self.min_delay_event == self.max_delay_event:
return self.min_delay_event
+3247 -351
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
BCH_MERCY = auto()
PTX_PRE_FUNDED = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
@@ -212,6 +214,10 @@ class EventLogTypes(IntEnum):
BCH_MERCY_TX_FOUND = auto()
LOCK_TX_A_IN_MEMPOOL = auto()
LOCK_TX_A_CONFLICTS = auto()
LOCK_TX_B_RPC_ERROR = auto()
LOCK_TX_A_SPEND_TX_SEEN = auto()
LOCK_TX_B_SPEND_TX_SEEN = auto()
LOCK_TX_B_REFUND_TX_SEEN = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -236,8 +242,11 @@ class DebugTypes(IntEnum):
OFFER_LOCK_2_VALUE_INC = auto()
BID_STOP_AFTER_COIN_B_LOCK = auto()
BID_DONT_SPEND_COIN_B_LOCK = auto()
WAIT_FOR_COIN_B_LOCK_BEFORE_PREREFUND = auto()
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
BID_DONT_SPEND_COIN_A_LOCK = auto()
DONT_SEND_COIN_B_LOCK = auto()
DONT_RELEASE_COIN_A_LOCK = auto()
class NotificationTypes(IntEnum):
@@ -247,6 +256,7 @@ class NotificationTypes(IntEnum):
BID_ACCEPTED = auto()
SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto()
SWEEP_COMPLETED = auto()
class ConnectionRequestTypes(IntEnum):
@@ -458,6 +468,8 @@ def describeEventEntry(event_type, event_msg):
return "Failed to publish lock tx B refund"
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return "Detected invalid lock Tx B"
if event_type == EventLogTypes.LOCK_TX_B_RPC_ERROR:
return "Temporary RPC error checking lock tx B: " + event_msg
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return "Lock tx A pre-refund tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
@@ -498,6 +510,12 @@ def describeEventEntry(event_type, event_msg):
return "BCH mercy tx found"
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
return "Lock tx B mercy tx published"
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_SEEN:
return "Lock tx A spend tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_SEEN:
return "Lock tx B spend tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_SEEN:
return "Lock tx B refund tx seen in chain"
def getVoutByAddress(txjs, p2sh):
@@ -628,6 +646,7 @@ def canTimeoutBidState(state):
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
BidStates.BID_REQUEST_ACCEPTED,
)
+190 -51
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -29,6 +29,7 @@ import urllib.parse
import zipfile
import zmq
from typing import List
from urllib.request import urlopen
import basicswap.config as cfg
@@ -51,26 +52,26 @@ from basicswap.bin.run import (
# Coin clients
PARTICL_REPO = os.getenv("PARTICL_REPO", "tecnovert")
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.2.0")
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.3.0")
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "29.3")
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.5.5")
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1")
DCR_VERSION = os.getenv("DCR_VERSION", "2.1.5")
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.3")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.5.0")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = (
"df28b670cb3a174d7763dd6d22fb4ef20597d0ac" # Lock hashes.txt to monero version
"5e8d74229b742b54173010e3a676215b6f2fd1d7" # Lock hashes.txt to monero version
)
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -82,16 +83,16 @@ WOW_SITE_COMMIT = (
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3")
DASH_VERSION = os.getenv("DASH_VERSION", "23.1.2")
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.16.1")
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "")
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", "")
DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1")
@@ -185,11 +186,13 @@ else:
BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH)
FILE_EXT = os.getenv("FILE_EXT", FILE_EXT)
logger = logging.getLogger()
logger = logging.getLogger("prepare")
LOG_LEVEL = logging.DEBUG
logger.propagate = False
logger.level = LOG_LEVEL
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(levelname)s : %(message)s"))
logger.addHandler(handler)
logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
@@ -458,33 +461,47 @@ def getRemoteFileLength(url: str) -> (int, bool):
popConnectionParameters()
def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None:
"""If file exists at path compare it's size to the content length at the url
and attempt to resume download if file size is below expected.
"""
resume_from: int = 0
def downloadRelease(
url_in: str | List[str], path: str, extra_opts, timeout: int = 10
) -> None:
# If file exists at path compare it's size to the content length at the url
# and attempt to resume download if file size is below expected.
if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
elif extra_opts.get("verify_release_file_size", True):
file_size = os.stat(path).st_size
remote_file_length, can_resume = getRemoteFileLength(url)
if file_size < remote_file_length:
logger.warning(
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download."
)
if can_resume:
resume_from = file_size
release_filename: str = os.path.basename(path)
urls = (
url_in
if isinstance(url_in, list)
else [
url_in,
]
)
for url in urls:
try:
resume_from: int = 0
if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
elif extra_opts.get("verify_release_file_size", True):
file_size = os.stat(path).st_size
remote_file_length, can_resume = getRemoteFileLength(url)
if file_size < remote_file_length:
logger.warning(
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download."
)
if can_resume:
resume_from = file_size
else:
logger.warning("Download can not be resumed, restarting.")
else:
return
else:
logger.warning("Download can not be resumed, restarting.")
else:
return
else:
# File exists and size check is disabled
return
return downloadFile(url, path, timeout, resume_from)
# File exists and size check is disabled
return
return downloadFile(url, path, timeout, resume_from)
except Exception as e:
logger.warning(f"Failed to download {release_filename} from {url}")
logger.debug(f"Download error {e}")
raise RuntimeError(f"Failed to download {release_filename}.")
def downloadFile(url: str, path: str, timeout: int = 5, resume_from: int = 0) -> None:
@@ -925,9 +942,10 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename,
)
elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
version + version_tag, release_filename
)
release_url = [
f"https://github.com/litecoin-project/litecoin/releases/download/v{version}{version_tag}/{release_filename}",
f"https://download.litecoin.org/litecoin-{version}{version_tag}/{os_name}/{release_filename}",
]
assert_filename = "{}-core-{}-{}-build.assert".format(
coin, os_name, ".".join(version.split(".")[:2])
)
@@ -1249,6 +1267,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write("rpc-bind-ip={}\n".format(COINS_RPCBIND_IP))
fp.write(f"wallet-dir={config_datadir}\n")
fp.write("log-file={}\n".format(os.path.join(config_datadir, "wallet.log")))
fp.write("max-log-files=5\n")
fp.write(
"rpc-login={}:{}\n".format(
core_settings["walletrpcuser"], core_settings["walletrpcpassword"]
@@ -1748,6 +1767,13 @@ def printHelp():
)
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(
"--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 = []
for coin_name in known_coins.keys():
@@ -1757,15 +1783,15 @@ def printHelp():
def finalise_daemon(d):
logging.info("Interrupting {}".format(d.handle.pid))
logging.info(f"Interrupting {d.name} {d.handle.pid}")
try:
d.handle.send_signal(signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT)
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:
logging.info(f"Error {e} for process {d.handle.pid}")
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp:
fp.close()
logging.info(f"Error stopping {d.name}, process {d.handle.pid}: {e}")
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
@@ -1834,6 +1860,7 @@ def initialise_wallets(
daemons = []
daemon_args = ["-noconnect", "-nodnsseed"]
generated_mnemonic: bool = False
extended_keys = {}
coins_failed_to_initialise = []
@@ -1955,6 +1982,11 @@ def initialise_wallets(
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
createDCRWallet(args, hex_seed, logger, threading.Event())
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)
# Create wallet if it doesn't exist yet
wallets = swap_client.callcoinrpc(c, "listwallets")
@@ -2052,7 +2084,11 @@ def initialise_wallets(
c = swap_client.getCoinIdFromName(coin_name)
if c in (Coins.PART,):
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_
swap_client.waitForDaemonRPC(c)
try:
@@ -2082,6 +2118,25 @@ def initialise_wallets(
except Exception as e: # noqa: F841
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)
coin_settings = settings["chainclients"].get(coin_name, {})
is_electrum = coin_settings.get("connection_type") == "electrum"
can_export = (
hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum()
)
if can_export or (is_electrum and hasattr(ci, "getAccountKey")):
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:
if swap_client:
swap_client.finalise()
@@ -2113,6 +2168,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):
if not os.path.exists(config_path):
@@ -2279,6 +2346,9 @@ def main():
tor_control_password = None
client_auth_pwd_value = None
disable_client_auth_flag = False
light_mode = False
coin_modes = {}
electrum_servers = {}
extra_opts = {}
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
@@ -2433,6 +2503,31 @@ def main():
if name == "disable-client-auth":
disable_client_auth_flag = True
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:
exitWithError("Unknown argument {}".format(v))
exitWithError("Unknown argument {}".format(v))
@@ -2791,13 +2886,45 @@ 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():
coin_id = getCoinIdFromName(coin_name)
coin_params = chainparams[coin_id]
if coin_settings.get("core_type_group", "") == "xmr":
default_name = "swap_wallet"
default_name: str = "swap_wallet"
use_name: str = default_name
else:
default_name = "wallet.dat"
default_name: str = "wallet.dat"
use_name: str = (
"wallet.dat"
if coin_id in (Coins.NAV, Coins.FIRO, Coins.DCR)
else "bsx_wallet"
)
if coin_name == "litecoin":
set_name: str = getWalletName(
@@ -2806,7 +2933,7 @@ def main():
if set_name != "mweb":
coin_settings["mweb_wallet_name"] = set_name
set_name: str = getWalletName(coin_params, default_name)
set_name: str = getWalletName(coin_params, use_name)
if set_name != default_name:
coin_settings["wallet_name"] = set_name
@@ -3001,7 +3128,7 @@ def main():
)
if particl_wallet_mnemonic != "none":
initialise_wallets(
extended_keys = initialise_wallets(
None,
{
add_coin,
@@ -3013,6 +3140,18 @@ def main():
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)
finally:
if "particl_daemon" in extra_opts:
+91 -77
View File
@@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
import json
import logging
import os
@@ -22,6 +23,7 @@ from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
from typing import Set
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
@@ -36,22 +38,25 @@ def signal_handler(sig, frame):
os.write(
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
)
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
try:
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
logger.info(f"AMM signal shutdown: {msg}")
else:
logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}")
amm_status = get_amm_status()
if amm_status == "running":
logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
logger.info(f"AMM signal shutdown: {msg}")
else:
logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}")
swap_client.stopRunning()
swap_client.stopRunning()
except NameError:
pass
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
@@ -344,7 +349,7 @@ def mainLoop(daemons, update: bool = True):
def runClient(
data_dir: str,
chain: str,
start_only_coins: bool,
start_only_coins: Set[str],
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
@@ -388,39 +393,46 @@ def runClient(
# Settings may have been modified
settings = swap_client.settings
base_coin_opts = []
if "extra_coin_opts" in extra_opts:
if len(start_only_coins) == 0:
raise ValueError('"extracoinopts" can only be used with "startonlycoins"')
base_coin_opts += extra_opts["extra_coin_opts"]
try:
# Try start daemons
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
if len(start_only_coins) > 0:
swap_client.log.warning('Not starting networks as "startonlycoin" is set')
else:
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
)
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
@@ -457,10 +469,18 @@ def runClient(
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"]
)
opts = [
wallet_opts = [
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon",
"--daemon-address",
daemon_addr,
]
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
wallet_opts += [
"--daemon-login",
daemon_rpcuser + ":" + daemon_rpcpass,
]
proxy_log_str = ""
proxy_host, proxy_port = swap_client.getXMRWalletProxy(
@@ -468,7 +488,7 @@ def runClient(
)
if proxy_host:
proxy_log_str = " through proxy"
opts += [
wallet_opts += [
"--proxy",
f"{proxy_host}:{proxy_port}",
"--daemon-ssl-allow-any-cert",
@@ -482,19 +502,11 @@ def runClient(
)
)
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
opts.append("--daemon-login")
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
opts.append(
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
)
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
startXmrWalletDaemon(
v["datadir"], v["bindir"], filename, wallet_opts
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
@@ -503,9 +515,8 @@ def runClient(
if c == "decred":
appdata = v["datadir"]
extra_opts = [
f'--appdata="{appdata}"',
]
coin_opts = copy.deepcopy(base_coin_opts)
coin_opts.append(f'--appdata="{appdata}"')
use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
@@ -523,7 +534,7 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -534,12 +545,13 @@ def runClient(
swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_opts = [f'--appdata="{appdata}"']
wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "":
# Only set when in startonlycoin mode
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"')
wallet_opts.append(f'--pass="{wallet_pwd}"')
extra_config = {
"add_datadir": False,
"stdout_to_file": True,
@@ -552,13 +564,12 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=wallet_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
continue # /decred
if v["manage_daemon"] is True:
@@ -568,7 +579,7 @@ def runClient(
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs(
coin_opts = copy.deepcopy(base_coin_opts) + getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
extra_config = {"coin_name": c}
@@ -577,7 +588,7 @@ def runClient(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -608,13 +619,6 @@ def runClient(
except Exception as e: # noqa: F841
traceback.print_exc()
if swap_client.ws_server:
try:
swap_client.log.info("Stopping websocket server.")
swap_client.ws_server.shutdown_gracefully()
except Exception as e: # noqa: F841
traceback.print_exc()
swap_client.finalise()
closed_pids = []
@@ -625,7 +629,7 @@ def runClient(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
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:
try:
d.handle.wait(timeout=120)
@@ -634,10 +638,12 @@ def runClient(
fp.close()
closed_pids.append(d.handle.pid)
except Exception as e:
swap_client.log.error(f"Error: {e}")
swap_client.log.error(
f"Waiting for {d.name} {d.handle.pid} to shutdown: {e}"
)
fail_code: int = swap_client.fail_code
del swap_client
swap_client = None
if os.path.exists(pids_path):
with open(pids_path) as fd:
@@ -681,6 +687,9 @@ def printHelp():
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print(
"--extracoinopts Extra options to pass to coin daemon, can only be used with --startonlycoin."
)
print("--logprefix Specify log prefix.")
print(
"--forcedbupgrade Recheck database against schema regardless of version."
@@ -745,6 +754,11 @@ def main():
ensure_coin_valid(coin)
start_only_coins.add(coin)
continue
if name == "extracoinopts":
options["extra_coin_opts"] = []
for opt in [s.lower() for s in s[1].split(",")]:
options["extra_coin_opts"].append(opt)
continue
logger.warning(f"Unknown argument {v}")
+14 -4
View File
@@ -242,7 +242,7 @@ chainparams = {
"pubkey_address": 0x0E91,
"script_address": 0x0E6C,
"key_prefix": 0x2307,
"bip44": 1,
"bip44": 115,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
@@ -552,21 +552,31 @@ chainparams = {
name_map = {}
ticker_map = {}
variant_ticker_map = {}
for c, params in chainparams.items():
name_map[params["name"].lower()] = c
ticker_map[params["ticker"].lower()] = c
# Add coin variants, eg: LTC_MWEB, PART_ANON
for c in Coins:
if c.name.lower() in ticker_map:
continue
variant_ticker_map[c.name.lower()] = c
def getCoinIdFromTicker(ticker: str) -> str:
def getCoinIdFromTicker(ticker: str, inc_variant: bool = False) -> str:
lc_ticker: str = ticker.lower()
try:
return ticker_map[ticker.lower()]
if inc_variant and lc_ticker in variant_ticker_map:
return variant_ticker_map[lc_ticker]
return ticker_map[lc_ticker]
except Exception:
raise ValueError(f"Unknown coin {ticker}")
def getCoinIdFromName(name: str) -> str:
def getCoinIdFromName(name: str) -> Coins:
try:
return name_map[name.lower()]
except Exception:
+82 -68
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -12,9 +12,8 @@ import time
from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 32
CURRENT_DB_DATA_VERSION = 7
CURRENT_DB_VERSION = 34
CURRENT_DB_DATA_VERSION = 8
class Concepts(IntEnum):
@@ -76,10 +75,16 @@ class Table:
__sqlite3_table__ = True
def __init__(self, **kwargs):
init_all_columns: bool = True
for name, value in kwargs.items():
if name == "_init_all_columns":
init_all_columns = value
continue
if not hasattr(self, name):
raise ValueError(f"Unknown attribute {name}")
setattr(self, name, value)
if init_all_columns is False:
return
# Init any unset columns to None
for mc in inspect.getmembers(self):
mc_name, mc_obj = mc
@@ -135,6 +140,20 @@ class Index:
self.column_3 = column_3
class StateRows:
state = Column("integer")
state_time = Column("integer") # Timestamp of last state change
states = Column("blob") # Packed states and times
def setState(self, new_state, state_time=None):
now = int(time.time()) if state_time is None else state_time
self.state = new_state
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class DBKVInt(Table):
__tablename__ = "kv_int"
@@ -149,7 +168,7 @@ class DBKVString(Table):
value = Column("string")
class Offer(Table):
class Offer(Table, StateRows):
__tablename__ = "offers"
offer_id = Column("blob", primary_key=True)
@@ -197,19 +216,8 @@ class Offer(Table):
bid_reversed = Column("bool")
smsg_payload_version = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
def setState(self, new_state):
now = int(time.time())
self.state = new_state
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class Bid(Table):
class Bid(Table, StateRows):
__tablename__ = "bids"
bid_id = Column("blob", primary_key=True)
@@ -244,11 +252,7 @@ class Bid(Table):
participate_txn_refund = Column("blob")
in_progress = Column("integer")
state = Column("integer")
state_time = Column("integer") # Timestamp of last state change
states = Column("blob") # Packed states and times
state_note = Column("string")
was_sent = Column("bool") # Sent by node
was_received = Column("bool")
contract_count = Column("integer")
@@ -287,25 +291,13 @@ class Bid(Table):
if self.isSet("participate_tx"):
self.participate_tx.setState(new_state)
def setState(self, new_state, state_note=None):
now = int(time.time())
self.state = new_state
self.state_time = now
if self.isSet("state_note"):
self.state_note = state_note
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
def getLockTXBVout(self):
if self.isSet("xmr_b_lock_tx"):
return self.xmr_b_lock_tx.vout
return None
class SwapTx(Table):
class SwapTx(Table, StateRows):
__tablename__ = "transactions"
bid_id = Column("blob")
@@ -328,21 +320,8 @@ class SwapTx(Table):
block_height = Column("integer")
block_time = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
primary_key = PrimaryKeyConstraint("bid_id", "tx_type")
def setState(self, new_state):
if self.state == new_state:
return
self.state = new_state
now: int = int(time.time())
if self.isSet("states") is False:
self.states = pack_state(new_state, now)
else:
self.states += pack_state(new_state, now)
class PrefundedTx(Table):
__tablename__ = "prefunded_transactions"
@@ -420,8 +399,9 @@ class XmrOffer(Table):
swap_id = Column("integer", primary_key=True, autoincrement=True)
offer_id = Column("blob")
a_fee_rate = Column("integer") # Chain a fee rate
b_fee_rate = Column("integer") # Chain b fee rate
# TODO: rename to from/to - values are not switched for reverse swaps
a_fee_rate = Column("integer") # Chain from fee rate
b_fee_rate = Column("integer") # Chain to fee rate
# Delay before the chain a lock refund tx can be mined
lock_time_1 = Column("integer")
@@ -792,8 +772,9 @@ class NetworkPortal(Table):
created_at = Column("integer")
def extract_schema() -> dict:
g = globals().copy()
def extract_schema(input_globals: dict = None) -> dict:
g = (input_globals if input_globals else globals()).copy()
tables = {}
for name, obj in g.items():
if not inspect.isclass(obj):
@@ -914,7 +895,10 @@ def create_table(c, table_name, table) -> None:
def create_db_(con, log) -> None:
from .db_wallet import extract_wallet_schema
db_schema = extract_schema()
db_schema.update(extract_wallet_schema())
c = con.cursor()
for table_name, table in db_schema.items():
create_table(c, table_name, table)
@@ -932,42 +916,63 @@ def create_db(db_path: str, log) -> None:
class DBMethods:
_db_lock_depth = 0
def _db_lock_held(self) -> bool:
if hasattr(self.mxDB, "_is_owned"):
return self.mxDB._is_owned()
return self.mxDB.locked()
def openDB(self, cursor=None):
if cursor:
# assert(self._thread_debug == threading.get_ident())
assert self.mxDB.locked()
assert self._db_lock_held()
return cursor
if self._db_lock_held():
self._db_lock_depth += 1
return self._db_con.cursor()
self.mxDB.acquire()
# self._thread_debug = threading.get_ident()
self._db_lock_depth = 1
self._db_con = sqlite3.connect(self.sqlite_file)
self._db_con.execute("PRAGMA busy_timeout = 30000")
return self._db_con.cursor()
def getNewDBCursor(self):
assert self.mxDB.locked()
assert self._db_lock_held()
return self._db_con.cursor()
def commitDB(self):
assert self.mxDB.locked()
assert self._db_lock_held()
self._db_con.commit()
def rollbackDB(self):
assert self.mxDB.locked()
assert self._db_lock_held()
self._db_con.rollback()
def closeDBCursor(self, cursor):
assert self.mxDB.locked()
assert self._db_lock_held()
if cursor:
cursor.close()
def closeDB(self, cursor, commit=True):
assert self.mxDB.locked()
assert self._db_lock_held()
if self._db_lock_depth > 1:
if commit:
self._db_con.commit()
cursor.close()
self._db_lock_depth -= 1
return
if commit:
self._db_con.commit()
cursor.close()
self._db_con.close()
self._db_lock_depth = 0
self.mxDB.release()
def setIntKV(self, str_key: str, int_val: int, cursor=None) -> None:
@@ -1057,9 +1062,9 @@ class DBMethods:
)
finally:
if cursor is None:
self.closeDB(use_cursor, commit=False)
self.closeDB(use_cursor, commit=True)
def add(self, obj, cursor, upsert: bool = False):
def add(self, obj, cursor, upsert: bool = False, columns_list=None):
if cursor is None:
raise ValueError("Cursor is null")
if not hasattr(obj, "__tablename__"):
@@ -1072,7 +1077,8 @@ class DBMethods:
# See if the instance overwrote any class methods
for mc in inspect.getmembers(obj.__class__):
mc_name, mc_obj = mc
if columns_list is not None and mc_name not in columns_list:
continue
if not hasattr(mc_obj, "__sqlite3_column__"):
continue
@@ -1113,6 +1119,7 @@ class DBMethods:
order_by={},
query_suffix=None,
extra_query_data={},
columns_list=None,
):
if cursor is None:
raise ValueError("Cursor is null")
@@ -1125,6 +1132,8 @@ class DBMethods:
for mc in inspect.getmembers(table_class):
mc_name, mc_obj = mc
if columns_list is not None and mc_name not in columns_list:
continue
if not hasattr(mc_obj, "__sqlite3_column__"):
continue
if len(columns) > 0:
@@ -1193,6 +1202,7 @@ class DBMethods:
order_by={},
query_suffix=None,
extra_query_data={},
columns_list=None,
):
return firstOrNone(
self.query(
@@ -1202,10 +1212,11 @@ class DBMethods:
order_by,
query_suffix,
extra_query_data,
columns_list,
)
)
def updateDB(self, obj, cursor, constraints=[]):
def updateDB(self, obj, cursor, constraints=[], columns_list=None):
if cursor is None:
raise ValueError("Cursor is null")
if not hasattr(obj, "__tablename__"):
@@ -1215,9 +1226,10 @@ class DBMethods:
query: str = f"UPDATE {table_name} SET "
values = {}
constraint_values = {}
set_columns = []
for mc in inspect.getmembers(obj.__class__):
mc_name, mc_obj = mc
if not hasattr(mc_obj, "__sqlite3_column__"):
continue
@@ -1227,17 +1239,19 @@ class DBMethods:
continue
if mc_name in constraints:
values[mc_name] = m_obj
constraint_values[mc_name] = m_obj
continue
if columns_list is not None and mc_name not in columns_list:
continue
if len(values) > 0:
query += ", "
query += f"{mc_name} = :{mc_name}"
set_columns.append(f"{mc_name} = :{mc_name}")
values[mc_name] = m_obj
query += ", ".join(set_columns)
query += " WHERE 1=1 "
for ck in constraints:
query += f" AND {ck} = :{ck} "
values.update(constraint_values)
cursor.execute(query, values)
+111 -7
View File
@@ -18,6 +18,8 @@ from .db import (
extract_schema,
)
from .db_wallet import extract_wallet_schema
from .basicswap_util import (
BidStates,
canAcceptBidState,
@@ -129,6 +131,14 @@ def upgradeDatabaseData(self, data_version):
"state_id": int(state),
},
)
if data_version > 0 and data_version < 8:
cursor.execute(
"UPDATE bidstates SET can_timeout = :can_timeout WHERE state_id = :state_id",
{
"can_timeout": 1,
"state_id": int(BidStates.BID_REQUEST_ACCEPTED),
},
)
if data_version > 0 and data_version < 4:
for state in (
BidStates.BID_REQUEST_SENT,
@@ -152,12 +162,106 @@ def upgradeDatabaseData(self, data_version):
self.closeDB(cursor, commit=False)
def upgradeDatabase(self, db_version):
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
def upgradeDatabaseFromSchema(self, cursor, expect_schema):
have_tables = {}
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
tables = cursor.execute(query).fetchall()
for table in tables:
table_name = table[0]
if table_name in ("sqlite_sequence",):
continue
have_table = {}
have_columns = {}
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
for column in columns:
cid, name, data_type, notnull, default_value, primary_key = column
have_columns[name] = {"type": data_type, "primary_key": primary_key}
have_table["columns"] = have_columns
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
indices = cursor.fetchall()
for index in indices:
seq, index_name, unique, origin, partial = index
if origin == "pk": # Created by a PRIMARY KEY constraint
continue
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
index_info = cursor.fetchall()
add_index = {"index_name": index_name}
for index_columns in index_info:
seqno, cid, name = index_columns
if origin == "u": # Created by a UNIQUE constraint
have_columns[name]["unique"] = 1
else:
if "column_1" not in add_index:
add_index["column_1"] = name
elif "column_2" not in add_index:
add_index["column_2"] = name
elif "column_3" not in add_index:
add_index["column_3"] = name
else:
raise RuntimeError("Add more index columns.")
if origin == "c":
if "indices" not in table:
have_table["indices"] = []
have_table["indices"].append(add_index)
have_tables[table_name] = have_table
for table_name, table in expect_schema.items():
if table_name not in have_tables:
self.log.info(f"Creating table {table_name}.")
create_table(cursor, table_name, table)
continue
have_table = have_tables[table_name]
have_columns = have_table["columns"]
for colname, column in table["columns"].items():
if colname not in have_columns:
col_type = column["type"]
self.log.info(f"Adding column {colname} to table {table_name}.")
cursor.execute(
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
)
indices = table.get("indices", [])
have_indices = have_table.get("indices", [])
for index in indices:
index_name = index["index_name"]
if not any(
have_idx.get("index_name") == index_name for have_idx in have_indices
):
self.log.info(f"Adding index {index_name} to table {table_name}.")
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
cursor.execute(query)
def upgradeDatabase(self, db_version: int):
upgrade_forced: bool = False
if db_version < CURRENT_DB_VERSION:
pass
elif self._force_db_upgrade is True:
upgrade_forced = True
else:
return
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}"
+ (" (forced)" if upgrade_forced else "")
+ "."
)
# db_version, tablename, oldcolumnname, newcolumnname
@@ -174,10 +278,10 @@ def upgradeDatabase(self, db_version):
]
expect_schema = extract_schema()
expect_schema.update(extract_wallet_schema())
have_tables = {}
try:
cursor = self.openDB()
for rename_column in rename_columns:
dbv, table_name, colname_from, colname_to = rename_column
if db_version < dbv:
@@ -207,7 +311,7 @@ def upgradeDatabase(self, db_version):
for index in indices:
seq, index_name, unique, origin, partial = index
if origin == "pk": # Created by a PRIMARY KEY constraint
if origin == "pk":
continue
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
@@ -216,7 +320,7 @@ def upgradeDatabase(self, db_version):
add_index = {"index_name": index_name}
for index_columns in index_info:
seqno, cid, name = index_columns
if origin == "u": # Created by a UNIQUE constraint
if origin == "u":
have_columns[name]["unique"] = 1
else:
if "column_1" not in add_index:
@@ -228,7 +332,7 @@ def upgradeDatabase(self, db_version):
else:
raise RuntimeError("Add more index columns.")
if origin == "c":
if "indices" not in table:
if "indices" not in have_table:
have_table["indices"] = []
have_table["indices"].append(add_index)
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from .db import Column, Index, Table, UniqueConstraint, extract_schema
class WalletAddress(Table):
__tablename__ = "wallet_addresses"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
derivation_index = Column("integer")
is_internal = Column("bool")
derivation_path = Column("string")
address = Column("string")
scripthash = Column("string")
pubkey = Column("blob")
is_funded = Column("bool")
cached_balance = Column("integer")
cached_balance_time = Column("integer")
first_seen_height = Column("integer")
created_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "derivation_index", "is_internal")
__index_address__ = Index("idx_wallet_address", "address")
__index_scripthash__ = Index("idx_wallet_scripthash", "scripthash")
__index_funded__ = Index("idx_wallet_funded", "coin_type", "is_funded")
class WalletState(Table):
__tablename__ = "wallet_state"
coin_type = Column("integer", primary_key=True)
last_external_index = Column("integer")
last_internal_index = Column("integer")
derivation_path_type = Column("string")
last_sync_height = Column("integer")
migration_complete = Column("bool")
created_at = Column("integer")
updated_at = Column("integer")
class WalletWatchOnly(Table):
__tablename__ = "wallet_watch_only"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
address = Column("string")
scripthash = Column("string")
label = Column("string")
source = Column("string")
is_funded = Column("bool")
cached_balance = Column("integer")
cached_balance_time = Column("integer")
private_key_encrypted = Column("blob")
created_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "address")
__index_watch_address__ = Index("idx_watch_address", "address")
__index_watch_scripthash__ = Index("idx_watch_scripthash", "scripthash")
class WalletLockedUTXO(Table):
__tablename__ = "wallet_locked_utxos"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
vout = Column("integer")
value = Column("integer")
address = Column("string")
bid_id = Column("blob")
locked_at = Column("integer")
expires_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid", "vout")
__index_locked_coin__ = Index("idx_locked_coin", "coin_type")
__index_locked_bid__ = Index("idx_locked_bid", "bid_id")
class WalletTxCache(Table):
__tablename__ = "wallet_tx_cache"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
block_height = Column("integer")
confirmations = Column("integer")
tx_data = Column("blob")
cached_at = Column("integer")
expires_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid")
__index_tx_cache__ = Index("idx_tx_cache", "coin_type", "txid")
class WalletPendingTx(Table):
__tablename__ = "wallet_pending_txs"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_type = Column("integer")
txid = Column("string")
tx_type = Column("string")
amount = Column("integer")
fee = Column("integer")
addresses = Column("string")
bid_id = Column("blob")
first_seen = Column("integer")
confirmed_at = Column("integer")
__unique_1__ = UniqueConstraint("coin_type", "txid")
__index_pending_coin__ = Index("idx_pending_coin", "coin_type", "confirmed_at")
def extract_wallet_schema() -> dict:
return extract_schema(input_globals=globals())
-1
View File
@@ -7,7 +7,6 @@
import json
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
+67 -3
View File
@@ -6,8 +6,10 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import gzip
import json
import shlex
import hashlib
import secrets
import traceback
import threading
@@ -19,6 +21,7 @@ from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
from datetime import datetime, timedelta, timezone
from email.utils import formatdate, parsedate_to_datetime
from http.cookies import SimpleCookie
from . import __version__
@@ -210,7 +213,7 @@ class HttpHandler(BaseHTTPRequestHandler):
status_code=200,
version=__version__,
extra_headers=None,
):
) -> bytes:
swap_client = self.server.swap_client
if swap_client.ws_server:
args_dict["ws_port"] = swap_client.ws_server.client_port
@@ -802,7 +805,6 @@ class HttpHandler(BaseHTTPRequestHandler):
if page == "static":
try:
static_path = os.path.join(os.path.dirname(__file__), "static")
content = None
mime_type = ""
filepath = ""
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
@@ -835,9 +837,71 @@ class HttpHandler(BaseHTTPRequestHandler):
if mime_type == "" or not filepath:
raise ValueError("Unknown file type or path")
file_stat = os.stat(filepath)
mtime = file_stat.st_mtime
file_size = file_stat.st_size
etag_hash = hashlib.md5(f"{file_size}-{mtime}".encode()).hexdigest()
etag = f'"{etag_hash}"'
last_modified = formatdate(mtime, usegmt=True)
if_none_match = self.headers.get("If-None-Match")
if if_none_match:
if if_none_match.strip() == "*" or etag in [
t.strip() for t in if_none_match.split(",")
]:
self.send_response(304)
self.send_header("ETag", etag)
self.send_header("Cache-Control", "public")
self.end_headers()
return b""
if_modified_since = self.headers.get("If-Modified-Since")
if if_modified_since and not if_none_match:
try:
ims_time = parsedate_to_datetime(if_modified_since)
file_time = datetime.fromtimestamp(int(mtime), tz=timezone.utc)
if file_time <= ims_time:
self.send_response(304)
self.send_header("Last-Modified", last_modified)
self.send_header("Cache-Control", "public")
self.end_headers()
return b""
except (TypeError, ValueError):
pass
is_lib = len(url_split) > 4 and url_split[3] == "libs"
if is_lib:
cache_control = "public, max-age=31536000, immutable"
elif url_split[2] in ("css", "js"):
cache_control = "public, max-age=3600, must-revalidate"
elif url_split[2] in ("images", "sequence_diagrams"):
cache_control = "public, max-age=86400"
else:
cache_control = "public, max-age=3600"
with open(filepath, "rb") as fp:
content = fp.read()
self.putHeaders(status_code, mime_type)
extra_headers = [
("Cache-Control", cache_control),
("Last-Modified", last_modified),
("ETag", etag),
]
is_compressible = mime_type in (
"text/css; charset=utf-8",
"application/javascript",
"image/svg+xml",
)
accept_encoding = self.headers.get("Accept-Encoding", "")
if is_compressible and "gzip" in accept_encoding:
content = gzip.compress(content)
extra_headers.append(("Content-Encoding", "gzip"))
extra_headers.append(("Vary", "Accept-Encoding"))
extra_headers.append(("Content-Length", str(len(content))))
self.putHeaders(status_code, mime_type, extra_headers=extra_headers)
return content
except FileNotFoundError:
+17 -15
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Copyright (c) 2025-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import threading
from enum import IntEnum
from typing import List
from basicswap.chainparams import (
chainparams,
@@ -30,6 +30,7 @@ from basicswap.util.ecc import (
)
from coincurve.dleag import verify_secp256k1_point
from coincurve.keys import (
PrivateKey,
PublicKey,
)
@@ -48,7 +49,7 @@ class CoinInterface:
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
def __init__(self, network, **kwargs):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
@@ -179,17 +180,26 @@ class CoinInterface:
class AdaptorSigInterface:
def getScriptLockTxDummyWitness(self, script: bytes):
def getP2WPKHDummyWitness(self) -> List[bytes]:
return [bytes(72), bytes(33)]
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
return [b"", bytes(72), bytes(72), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), b"", bytes(len(script))]
def getLockRefundVout(self, lock_refund_tx_data: bytes, vbkv: bytes):
return 0
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@staticmethod
def curve_type():
return Curves.secp256k1
@@ -215,20 +225,12 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
return PrivateKey(ka).add(kb).secret
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
+48 -18
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -10,7 +10,6 @@ from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, C
from basicswap.util import b2i, ensure, i2b
from basicswap.util.script import decodePushData, decodeScriptNum
from .btc import BTCInterface, ensure_op, findOutput
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
from basicswap.util.crypto import hash160, sha256
@@ -72,14 +71,14 @@ class BCHInterface(BTCInterface):
# TODO: BCH Watchonly: Remove when BCH watchonly works.
return True
def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
self.swap_client = swap_client
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.rpc_wallet_watch = self.rpc_wallet
self.swap_client = swap_client
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
@@ -148,7 +147,9 @@ class BCHInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True])
self.rpc_wallet(
"importaddress", [lock_tx_dest.hex(), "bid lock", False, True]
)
return address
@@ -157,18 +158,35 @@ class BCHInterface(BTCInterface):
def createRawFundedTransaction(
self,
addr_to: str,
addr_to: str | bytes,
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if isinstance(addr_to, bytes):
# addr_to is script_pubkey
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vout.append(self.txoType()(amount, addr_to))
txn = tx.serialize_without_witness().hex()
else:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = {
"lockUnspents": lock_unspents,
# 'conf_target': self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -220,6 +238,16 @@ class BCHInterface(BTCInterface):
)
return pay_fee
def getBLockTxo(
self,
chain_b_lock_txid: bytes,
lock_tx_vout: int,
script_pk: bytes,
) -> (int, int):
txout = self.rpc("gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, True])
actual_value = self.make_int(txout["value"])
return lock_tx_vout, actual_value
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
@@ -274,7 +302,7 @@ class BCHInterface(BTCInterface):
found_vout = try_vout
break
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e}")
return None
if found_vout is None:
@@ -287,13 +315,14 @@ class BCHInterface(BTCInterface):
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
block_height = self.getChainHeight() - (confirmations - 1)
rv = {
"txid": txid.hex(),
"depth": confirmations,
"index": found_vout,
"height": block_height,
"value": self.make_int(txout["value"]),
}
return rv
@@ -508,6 +537,7 @@ class BCHInterface(BTCInterface):
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue
@@ -1126,7 +1156,7 @@ class BCHInterface(BTCInterface):
refund_output_value = refund_swipe_tx.vout[0].nValue
refund_output_script = refund_swipe_tx.vout[0].scriptPubKey
# mercy transaction size consisting of one input of freshly received funds,
# Mercy transaction size consisting of one input of freshly received funds,
# one op_return with mercy information, a dust output to the leader and change back to the follower
tx_size = 275
dust_limit = 546
+2181 -145
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
@@ -24,8 +23,13 @@ class DASHInterface(BTCInterface):
def coin_type():
return Coins.DASH
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._wallet_passphrase = ""
self._have_checked_seed = False
@@ -132,7 +136,7 @@ class DASHInterface(BTCInterface):
self.unlockWallet(old_password, check_seed=False)
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [new_password])
self.rpc_wallet("encryptwallet", [new_password], timeout=120)
if check_seed is False or seed_id_before == "Not found":
return
@@ -156,4 +160,6 @@ class DASHInterface(BTCInterface):
if self.isWalletEncrypted():
raise ValueError("Old password must be set")
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
self.rpc_wallet(
"walletpassphrasechange", [old_password, new_password], timeout=120
)
+216 -39
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,16 +12,15 @@ import logging
import random
import traceback
from typing import List
from typing import List, Optional
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import (
CScriptNum,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.interface.btc import (
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
@@ -82,7 +80,6 @@ from coincurve.ecdsaotves import (
ecdsaotves_rec_enc_key,
)
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000F
@@ -182,12 +179,16 @@ def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
return sig, pk
class DCRInterface(Secp256k1Interface):
class DCRInterface(FeeValidator, Secp256k1Interface):
@staticmethod
def coin_type():
return Coins.DCR
@staticmethod
def useBackend() -> bool:
return False
@staticmethod
def exp() -> int:
return 8
@@ -255,13 +256,13 @@ class DCRInterface(Secp256k1Interface):
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
super().__init__(coin_settings=coin_settings, network=network, **kwargs)
self._rpc_host = coin_settings.get("rpchost", "127.0.0.1")
self._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"]
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
if "walletrpcport" in coin_settings:
self._walletrpcport = coin_settings["walletrpcport"]
@@ -364,7 +365,9 @@ class DCRInterface(Secp256k1Interface):
# Read initial pwd from settings
settings = self._sc.getChainClientSettings(self.coin_type())
old_password = settings["wallet_pwd"]
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
self.rpc_wallet(
"walletpassphrasechange", [old_password, new_password], timeout=120
)
# Lock wallet to match other coins
self.rpc_wallet("walletlock")
@@ -378,7 +381,7 @@ class DCRInterface(Secp256k1Interface):
self._log.info("unlockWallet - {}".format(self.ticker()))
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
@@ -406,14 +409,19 @@ class DCRInterface(Secp256k1Interface):
# Adjust verificationprogress to consider blocks wallet has synced
wallet_blocks = self.rpc_wallet("getinfo")["blocks"]
synced_ind = bci["verificationprogress"]
wallet_synced_ind = wallet_blocks / bci["headers"]
if bci["headers"] < 1:
wallet_synced_ind = 0
else:
wallet_synced_ind = wallet_blocks / bci["headers"]
if wallet_synced_ind < synced_ind:
bci["verificationprogress"] = wallet_synced_ind
return bci
def getBlockHeader(self, block_hash: str) -> dict:
return self.rpc("getblockheader", [block_hash])
def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet("getinfo")
wi = self.rpc_wallet("walletinfo")
balances = self.rpc_wallet("getbalance")
@@ -588,7 +596,7 @@ class DCRInterface(Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate:
self._log.debug(
"Fee rate override used for %s: %f", self.coin_name(), override_feerate
f"Fee rate override used for {self.coin_name()}: {override_feerate}"
)
return override_feerate, "override_feerate"
@@ -629,6 +637,15 @@ class DCRInterface(Secp256k1Interface):
# TODO: filter errors
return None
def listWalletTransactions(self, count=100, skip=0, include_watchonly=True):
try:
return self.rpc_wallet(
"listtransactions", ["*", count, skip, include_watchonly]
)
except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}")
return []
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
@@ -840,12 +857,17 @@ class DCRInterface(Secp256k1Interface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string)
float_amount = float(self.format_amount(amount))
txn = self.rpc("createrawtransaction", [[], {addr_to: float_amount}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = feerate
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
@@ -898,7 +920,7 @@ class DCRInterface(Secp256k1Interface):
found_vout = try_vout
break
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e})
return None
if found_vout is None:
@@ -911,13 +933,14 @@ class DCRInterface(Secp256k1Interface):
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
block_height = self.getChainHeight() - (confirmations - 1)
rv = {
"txid": txid.hex(),
"depth": confirmations,
"index": found_vout,
"height": block_height,
"value": self.make_int(txout["value"]),
}
return rv
@@ -978,6 +1001,10 @@ class DCRInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex()
def ensureFunds(self, amount: int) -> None:
if self.getSpendableBalance() < amount:
raise ValueError("Balance too low")
def verifyRawTransaction(self, tx_hex: str, prevouts):
inputs_valid: bool = True
validscripts: int = 0
@@ -1052,7 +1079,12 @@ class DCRInterface(Secp256k1Interface):
def describeTx(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex])
def fundTx(self, tx: bytes, feerate) -> bytes:
def decodeRawTransaction(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex])
def fundTx(
self, tx: bytes, feerate: int, lock_unspents: bool = True, subfee: bool = False
) -> bytes:
feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled
options = {
@@ -1125,6 +1157,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1176,6 +1209,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1227,6 +1261,7 @@ class DCRInterface(Secp256k1Interface):
script_lock_refund
)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1311,6 +1346,7 @@ class DCRInterface(Secp256k1Interface):
assert fee_paid > 0
size = len(tx.serialize()) + add_witness_bytes
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1372,6 +1408,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1382,7 +1419,7 @@ class DCRInterface(Secp256k1Interface):
)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError("Bad fee rate, expected: {}".format(feerate))
raise ValueError(f"Bad fee rate, expected: {feerate}")
return True
@@ -1444,6 +1481,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1451,7 +1489,7 @@ class DCRInterface(Secp256k1Interface):
)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError("Bad fee rate, expected: {}".format(feerate))
raise ValueError(f"Bad fee rate, expected: {feerate}")
return txid, locked_coin, locked_n
@@ -1505,6 +1543,7 @@ class DCRInterface(Secp256k1Interface):
prevout_script
)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1512,7 +1551,7 @@ class DCRInterface(Secp256k1Interface):
)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError("Bad fee rate, expected: {}".format(feerate))
raise ValueError(f"Bad fee rate, expected: {feerate}")
return True
@@ -1720,15 +1759,19 @@ class DCRInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, script_pk))
return tx.serialize()
def publishBLockTx(
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
) -> bytes:
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate)
script_pk = self.getPkDest(Kbs)
funded_tx = self.loadTx(b_lock_tx)
lock_vout = findOutput(funded_tx, script_pk)
b_lock_tx = self.signTxWithWallet(b_lock_tx)
return bytes.fromhex(self.publishTx(b_lock_tx))
txid = bytes.fromhex(self.publishTx(b_lock_tx))
return txid, lock_vout
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
witness_bytes = 115
@@ -1751,17 +1794,16 @@ class DCRInterface(Secp256k1Interface):
spend_actual_balance: bool = False,
lock_tx_vout=None,
) -> bytes:
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
locked_n = lock_tx_vout
self._log.info(
f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n"
)
Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs)
if locked_n is None:
self._log.debug(
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}"
)
# When refunding a lock tx, it should be in the wallet as a sent tx
locked_n = None
actual_value = None
try:
wtx = self.rpc_wallet(
"gettransaction",
[
@@ -1770,8 +1812,45 @@ class DCRInterface(Secp256k1Interface):
)
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 {self._log.id(chain_b_lock_txid)}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
)
for i, out in enumerate(lock_tx.vout):
self._log.debug(
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
)
except Exception as e: # noqa: F841
txout = self.rpc(
"gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, 0, True]
)
actual_value = self.make_int(txout["value"])
locked_n = lock_tx_vout
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 {self._log.id(chain_b_lock_txid)}"
)
ensure(locked_n is not None, "Output not found in tx")
spend_value = cb_swap_value
if spend_actual_balance and actual_value is not None:
if actual_value != cb_swap_value:
self._log.warning(
f"spendBLockTx: Spending actual balance {actual_value}, "
f"not expected swap value {cb_swap_value}."
)
spend_value = actual_value
pkh_to = self.decodeAddress(address_to)
tx = CTransaction()
@@ -1780,10 +1859,10 @@ class DCRInterface(Secp256k1Interface):
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to)))
tx.vout.append(self.txoType()(spend_value, self.getPubkeyHashDest(pkh_to)))
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
tx.vout[0].value = cb_swap_value - pay_fee
tx.vout[0].value = spend_value - pay_fee
b_lock_spend_tx = tx.serialize()
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
@@ -1794,14 +1873,14 @@ class DCRInterface(Secp256k1Interface):
try:
txout = self.rpc("gettxout", [txid_hex, 0, 0, True])
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e}"))
return None
confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"]
)
if confirmations >= self.blocks_confirmed:
block_height = self.getChainHeight() - confirmations # TODO: Better way?
block_height = self.getChainHeight() - (confirmations - 1)
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
@@ -1816,3 +1895,101 @@ class DCRInterface(Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool:
return "locks on inputs not met" in err_str
def getChainMedianTime(self) -> int:
bestblockhash = self.rpc("getbestblockhash")
bestblockheader = self.rpc(
"getblockheader",
[
bestblockhash,
],
)
return bestblockheader["mediantime"]
def getTxLocktime(self, tx_data: bytes) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.locktime
def getTxInSequence(self, tx_data: bytes, vout: int) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.vin[vout].sequence
def isCsvLockMature(
self,
lock_type: int,
encoded_sequence: int,
parent_block_height: Optional[int],
parent_block_time: Optional[int],
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if parent_block_height is None or parent_block_height < 1:
return False
lock_value: int = self.decodeSequence(encoded_sequence)
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= parent_block_height + lock_value
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
if parent_block_time is None or parent_block_time < 1:
return False
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= parent_block_time + lock_value
raise ValueError(f"Unknown lock type {lock_type}")
def isAbsLockTimeMature(
self,
nlocktime: int,
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if nlocktime == 0:
return True
if nlocktime < 500000000:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= nlocktime
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= nlocktime
def getTxOutInfo(
self, txid: bytes, n: int, include_mempool: bool = False
) -> dict():
try:
txout = self.rpc("gettxout", [txid.hex(), n, 0, include_mempool])
confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"]
)
if confirmations < 1:
return None
chain_tip_height: int = 0
if "bestblock" in txout:
bestheader_info = self.getBlockHeader(txout["bestblock"])
chain_tip_height = bestheader_info["height"]
else:
chain_tip_height = self.getChainHeight()
if confirmations == 1:
header_info = bestheader_info
else:
block_height: int = chain_tip_height - (confirmations - 1)
header_info = self.getBlockHeaderFromHeight(block_height)
block_hash: bytes = bytes.fromhex(header_info["hash"])
return {
"block_hash": block_hash,
"block_height": header_info["height"],
"block_time": header_info["time"],
}
except Exception as e: # noqa: F841
# self._log.warning(f"gettxout {e}")
return None
def is_transient_error(self, ex) -> bool:
str_error: str = str(ex).lower()
if "no information for transaction" in str_error:
return True
return super().is_transient_error(ex)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
+5 -4
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# 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.
@@ -9,10 +10,10 @@ import traceback
from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1", timeout=None):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
x = Jsonrpc(url)
x = Jsonrpc(url, timeout=timeout if timeout else 10)
x.__handler = None
v = x.json_request(method, params)
x.close()
@@ -41,7 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
auth = auth
host = host
def rpc_func(method, params=None):
return callrpc(port, auth, method, params, host)
def rpc_func(method, params=None, timeout=None):
return callrpc(port, auth, method, params, host, timeout=timeout)
return rpc_func
+9 -1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -12,7 +13,7 @@ import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info("Creating DCR wallet")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
pipe_r, pipe_w = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt":
str_args = " ".join(args)
@@ -35,6 +36,13 @@ def createDCRWallet(args, hex_seed, logging, delay_event):
response = b"y\n"
elif "Enter existing wallet seed" in buf:
response = (hex_seed + "\n").encode("utf-8")
elif "Do you have a wallet birthday we should rescan from" in buf:
response = b"no\n"
elif (
"Do you have an additional account to import from an extended public key"
in buf
):
response = b"no\n"
elif "Seed input successful" in buf:
pass
elif "Upgrading database from version" in buf:
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The BasicSwap developers
@@ -32,8 +31,13 @@ class DOGEInterface(BTCInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 192
def __init__(self, coin_settings, network, swap_client=None):
super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
def getScriptDest(self, script: bytearray) -> bytearray:
# P2SH
File diff suppressed because it is too large Load Diff
+112 -11
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -38,8 +37,13 @@ class FIROInterface(BTCInterface):
def coin_type():
return Coins.FIRO
def __init__(self, coin_settings, network, swap_client=None):
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -64,7 +68,7 @@ class FIROInterface(BTCInterface):
# Firo shuts down after encryptwallet
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
self.rpc_wallet("encryptwallet", [password])
self.rpc_wallet("encryptwallet", [password], timeout=120)
if check_seed is False or seed_id_before == "Not found":
return
@@ -102,6 +106,100 @@ class FIROInterface(BTCInterface):
return addr_info["ismine"]
return addr_info["ismine"] or addr_info["iswatchonly"]
def getNewSparkAddress(self) -> str:
try:
return self.rpc_wallet("getnewsparkaddress")[0]
except Exception as e:
self._log.error(f"getnewsparkaddress failed: {str(e)}")
raise
def getNewStealthAddress(self):
"""Get a new Spark address (alias for consistency with other coins)."""
return self.getNewSparkAddress()
def getWalletInfo(self):
"""Get wallet info including Spark balance."""
rv = super(FIROInterface, self).getWalletInfo()
try:
spark_balance_info = self.rpc("getsparkbalance")
# getsparkbalance returns amounts in atomic units (satoshis)
# Field names: availableBalance, unconfirmedBalance, fullBalance
confirmed = spark_balance_info.get("availableBalance", 0)
unconfirmed = spark_balance_info.get("unconfirmedBalance", 0)
full_balance = spark_balance_info.get("fullBalance", 0)
# Values are already in atomic units, keep as integers
# basicswap.py will format them using format_amount
rv["spark_balance"] = confirmed if confirmed else 0
rv["spark_unconfirmed"] = unconfirmed if unconfirmed else 0
immature = full_balance - confirmed - unconfirmed
rv["spark_immature"] = immature if immature > 0 else 0
except Exception as e:
self._log.warning(f"getsparkbalance failed: {str(e)}")
rv["spark_balance"] = 0
rv["spark_unconfirmed"] = 0
rv["spark_immature"] = 0
return rv
def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance()
if spendable_balance < value_sats:
raise ValueError("Balance too low")
address = self.getNewAddress(self._use_segwit, "create_utxo")
return (
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
address,
)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
"""Withdraw coins, supporting both transparent and Spark transactions.
Args:
value: Amount to withdraw
type_from: "plain" for transparent, "spark" for Spark
addr_to: Destination address
subfee: Whether to subtract fee from amount
"""
type_to = "spark" if addr_to.startswith("sm1") else "plain"
if "spark" in (type_from, type_to):
# RPC format: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}}
# RPC wrapper will serialize this as: {"method": "spendspark", "params": [{...}], ...}
try:
if type_from == "spark":
# Construct params: dict where address is the key, wrapped in array for RPC
params = [
{"address": addr_to, "amount": value, "subtractfee": subfee}
]
result = self.rpc_wallet("spendspark", params)
else:
# Use automintspark to perform a plain -> spark tx of full balance
balance = self.rpc_wallet("getbalance")
if str(balance) == str(value):
result = self.rpc_wallet("automintspark")
else:
# subfee param is available on plain -> spark transactions
mint_params = {"amount": value}
if subfee:
mint_params["subfee"] = True
params = [{addr_to: mint_params}]
result = self.rpc_wallet("mintspark", params)
# spendspark returns a txid string directly, in a result dict, or as an array
if isinstance(result, list) and len(result) > 0:
return result[0]
if isinstance(result, dict):
return result.get("txid", result.get("tx", ""))
return result
except Exception as e:
self._log.error(f"spark tx failed: {str(e)}")
raise
else:
# Use standard sendtoaddress for transparent transactions
params = [addr_to, value, "", "", subfee]
return self.rpc_wallet("sendtoaddress", params)
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest)
@@ -178,6 +276,8 @@ class FIROInterface(BTCInterface):
if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if rv["index"] is not None and rv["index"] >= 0:
rv["value"] = self.make_int(tx_obj["vout"][rv["index"]]["value"])
if return_txid:
rv["txid"] = txid.hex()
@@ -206,11 +306,16 @@ class FIROInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
@@ -252,10 +357,6 @@ class FIROInterface(BTCInterface):
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getWalletSeedID(self):
return self.rpc("getwalletinfo")["hdmasterkeyid"]
@@ -271,7 +372,7 @@ class FIROInterface(BTCInterface):
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc(
"signrawtransaction",
+239 -23
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
@@ -16,8 +15,13 @@ class LTCInterface(BTCInterface):
def coin_type():
return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet_mweb = make_rpc_func(
self._rpcport,
@@ -27,12 +31,21 @@ class LTCInterface(BTCInterface):
)
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
def getNewStealthAddress(self, label=""):
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
return self.getNewMwebAddress(False, label)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
if self.useBackend():
if type_from == "mweb":
raise ValueError("MWEB withdrawals not supported in electrum mode")
return self._withdrawCoinElectrum(value, addr_to, subfee)
params = [addr_to, value, "", "", subfee, True, self._conf_target]
if type_from == "mweb":
return self.rpc_wallet_mweb("sendtoaddress", params)
@@ -53,14 +66,27 @@ class LTCInterface(BTCInterface):
def getWalletInfo(self):
rv = super(LTCInterface, self).getWalletInfo()
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
rv["mweb_balance"] = mweb_info["balance"]
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
rv["mweb_immature"] = mweb_info["immature_balance"]
if not self.useBackend():
try:
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
rv["mweb_balance"] = mweb_info["balance"]
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
rv["mweb_immature"] = mweb_info["immature_balance"]
except Exception:
pass
return rv
def getUnspentsByAddr(self):
unspent_addr = dict()
if self.useBackend():
wm = self.getWalletManager()
if wm:
addresses = wm.getAllAddresses(self.coin_type())
if addresses:
return self._backend.getBalance(addresses)
return unspent_addr
unspent = self.rpc_wallet("listunspent")
for u in unspent:
if u.get("spendable", False) is False:
@@ -69,9 +95,14 @@ class LTCInterface(BTCInterface):
continue
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
if self.using_segwit():
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
continue
@@ -81,19 +112,170 @@ class LTCInterface(BTCInterface):
else:
if not desc.startswith("pkh"):
continue
unspent_addr[u["address"]] = unspent_addr.get(
u["address"], 0
unspent_addr[utxo_address] = unspent_addr.get(
utxo_address, 0
) + self.make_int(u["amount"], r=1)
return unspent_addr
def getMWEBBalance(self) -> int:
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
value: int = 0
unspent = self.rpc_wallet(
"listunspent",
[
0,
],
)
for u in unspent:
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
value += self.make_int(u["amount"], r=1)
return value
def convertMWEBBalance(self):
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
self._log.info(f"convertMWEBBalance - {self.ticker()}")
locked_before = self.rpc_wallet("listlockunspent")
lock_utxos = []
try:
# Hack: mark all the other utxos as unspendable, alternative is to use a mweb_transfer wallet
utxos = self.rpc_wallet("listunspent")
mweb_amount: int = 0
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
mweb_amount += self.make_int(utxo["amount"], r=1)
continue
utxo_op = {"txid": utxo["txid"], "vout": utxo["vout"]}
if utxo_op in locked_before:
continue
lock_utxos.append(utxo_op)
if mweb_amount == 0:
raise ValueError("No MWEB outputs to convert")
self.rpc_wallet("lockunspent", [False, lock_utxos])
subfee_to_mweb: bool = True
convert_value = self.format_amount(mweb_amount)
plain_addr: str = self.rpc_wallet("getnewaddress", ["transfer", "bech32"])
# Double check generated address is owned by this wallet
if not self.isAddressMine(plain_addr):
raise ValueError("Generated address not owned by wallet!")
params = [
plain_addr,
convert_value,
"",
"",
subfee_to_mweb,
True,
self._conf_target,
]
txid = self.rpc_wallet("sendtoaddress", params)
self._log.info(f"MWEB in plain converted in txid: {self._log.id(txid)}")
return txid
finally:
self.rpc_wallet("lockunspent", [True, lock_utxos])
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):
def interface_type(self) -> int:
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
@@ -127,24 +309,46 @@ class LTCInterfaceMWEB(LTCInterface):
def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock
self._log.info("init_wallet - {}".format(self.ticker()))
wallet_name: str = self._rpc_wallet
self._log.info(f"init_wallet - {self.ticker()}")
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
wallets = self.rpc("listwallets")
if wallet_name not in wallets:
try:
self.rpc("loadwallet", [wallet_name])
self._log.debug(f'Loaded existing wallet "{wallet_name}".')
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 "{wallet_name}" for {self.coin_name()}.'
)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
wallet_name,
False,
True,
password,
False,
self._use_descriptors,
],
)
else:
raise
if password is not None:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
if self.getWalletSeedID() == "Not found":
self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"])
self.rpc("loadwallet", ["mweb"])
self.rpc("unloadwallet", [wallet_name])
self.rpc("loadwallet", [wallet_name])
if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
@@ -152,10 +356,22 @@ class LTCInterfaceMWEB(LTCInterface):
return
self._log.info("unlockWallet - {}".format(self.ticker()))
if self.useBackend():
return
if not self.has_mweb_wallet():
self.init_wallet(password)
else:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
try:
seed_id = self.getWalletSeedID()
needs_seed_init = seed_id == "Not found"
except Exception as e:
self._log.debug(f"getWalletSeedID failed: {e}")
needs_seed_init = True
if needs_seed_init:
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
self._sc.initialiseWallet(self.interface_type())
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
self._sc.checkWalletSeed(self.interface_type())
+22 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
@@ -73,8 +72,13 @@ class NAVInterface(BTCInterface):
def txoType():
return CTxOut
def __init__(self, coin_settings, network, swap_client=None):
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -311,11 +315,16 @@ class NAVInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
@@ -605,6 +614,8 @@ class NAVInterface(BTCInterface):
if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if rv["index"] is not None and rv["index"] >= 0:
rv["value"] = self.make_int(tx_obj["vout"][rv["index"]]["value"])
if return_txid:
rv["txid"] = txid.hex()
@@ -751,7 +762,13 @@ class NAVInterface(BTCInterface):
return tx.serialize()
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True):
def fundTx(
self,
tx_hex: str,
feerate: int,
lock_unspents: bool = True,
subfee: bool = False,
):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert
+47 -12
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -81,8 +80,17 @@ class PARTInterface(BTCInterface):
def txoType():
return CTxOutPart
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
@staticmethod
def defaultMaxFeeRate() -> int:
return PARTInterface.COIN() // 2
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.setAnonTxRingSize(int(coin_settings.get("anon_tx_ring_size", 12)))
def use_tx_vsize(self) -> bool:
@@ -137,7 +145,7 @@ class PARTInterface(BTCInterface):
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
return [bytes(72), bytes(33)]
return self.getP2WPKHDummyWitness()
raise ValueError("Unknown script type")
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
@@ -146,9 +154,16 @@ class PARTInterface(BTCInterface):
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
def getWitnessStackSerialisedLength(self, witness_stack) -> int:
length: int = getCompactSizeLen(len(witness_stack))
for e in witness_stack:
length += getWitnessElementLen(len(e))
length: int = 0
if len(witness_stack) > 0 and isinstance(witness_stack[0], list):
for input_stack in witness_stack:
length += getCompactSizeLen(len(input_stack))
for e in input_stack:
length += getWitnessElementLen(len(e))
else:
length += getCompactSizeLen(len(witness_stack))
for e in witness_stack:
length += getWitnessElementLen(len(e))
return length
def getWalletRestoreHeight(self) -> int:
@@ -651,7 +666,7 @@ class PARTInterfaceBlind(PARTInterface):
ensure(
self.compareFeeRates(fee_rate_paid, feerate),
"Bad fee rate, expected: {}".format(feerate),
f"Bad fee rate, expected: {feerate}",
)
return (
@@ -727,7 +742,7 @@ class PARTInterfaceBlind(PARTInterface):
fee_rate_paid = fee_paid * 1000 // vsize
ensure(
self.compareFeeRates(fee_rate_paid, feerate),
"Bad fee rate, expected: {}".format(feerate),
f"Bad fee rate, expected: {feerate}",
)
return True
@@ -951,7 +966,7 @@ class PARTInterfaceBlind(PARTInterface):
fee_rate_paid = fee_paid * 1000 // vsize
self._log.info("vsize, feerate: %ld, %ld", vsize, fee_rate_paid)
if not self.compareFeeRates(fee_rate_paid, feerate):
raise ValueError("Bad fee rate, expected: {}".format(feerate))
raise ValueError(f"Bad fee rate, expected: {feerate}")
return True
@@ -1224,6 +1239,7 @@ class PARTInterfaceBlind(PARTInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# Estimate lock tx size / fee
@@ -1263,9 +1279,17 @@ class PARTInterfaceBlind(PARTInterface):
}
}
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = {
"lockUnspents": lock_unspents,
"conf_target": self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -1275,6 +1299,17 @@ class PARTInterfaceBlind(PARTInterface):
"fundrawtransactionfrom", ["blind", tx_hex, {}, outputs_info, options]
)["hex"]
def getLockRefundVout(self, lock_refund_tx_data: bytes, vkbv: bytes):
lock_refund_tx_obj = self.rpc(
"decoderawtransaction", [lock_refund_tx_data.hex()]
)
# Nonce is derived from vkbv
nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock refund tx to spend
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
return spend_n
class PARTInterfaceAnon(PARTInterface):
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
@@ -10,8 +9,13 @@ from basicswap.contrib.test_framework.messages import CTxOut
class PassthroughBTCInterface(BTCInterface):
def __init__(self, coin_settings, network):
super().__init__(coin_settings, network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.txoType = CTxOut
self._network = network
self.blocks_confirmed = coin_settings["blocks_confirmed"]
+31 -28
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
@@ -12,7 +11,7 @@ from .btc import BTCInterface
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
from .contrib.pivx_test_framework.messages import CTransaction
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP,
@@ -27,8 +26,13 @@ class PIVXInterface(BTCInterface):
def coin_type():
return Coins.PIVX
def __init__(self, coin_settings, network, swap_client=None):
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -40,7 +44,7 @@ class PIVXInterface(BTCInterface):
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [password])
self.rpc_wallet("encryptwallet", [password], timeout=120)
if check_seed is False or seed_id_before == "Not found":
return
@@ -74,11 +78,16 @@ class PIVXInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
@@ -100,29 +109,13 @@ class PIVXInterface(BTCInterface):
return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
block = self.rpc("getblock", [block_hash, True])
tx_rv = []
for tx in decoded_block.vtx:
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
for txid_str in block["tx"]:
tx_dec = self.rpc("getrawtransaction", [txid_str, True])
tx_rv.append(tx_dec)
block_rv = {
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
block["tx"] = tx_rv
return block
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee]
@@ -150,7 +143,7 @@ class PIVXInterface(BTCInterface):
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc(
"signrawtransaction",
@@ -177,3 +170,13 @@ class PIVXInterface(BTCInterface):
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
def getChainMedianTime(self) -> int:
bestblockhash = self.rpc("getbestblockhash")
bestblockheader = self.rpc(
"getblockheader",
[
bestblockhash,
],
)
return bestblockheader["mediantime"]
+111
View File
@@ -0,0 +1,111 @@
# -*- 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.
from basicswap.contrib.test_framework.messages import COIN
class FeeValidator:
@staticmethod
def defaultMaxFeeRate() -> int:
return COIN // 10
def makeIntFromSetting(
self, settings: dict, setting_name: str, default: int
) -> int:
# Return make_int(setting), or already integer default
if setting_name in settings:
return self.make_int(settings[setting_name])
return default
def __init__(self, **kwargs):
default_low_fee_conf_target: int = 24
default_low_fee_rate: int = 0
default_high_estimated_feerate_multiplier: float = 4.0
default_high_fee_rate: int = self.defaultMaxFeeRate()
if self._sc:
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
settings = self._sc.settings
default_low_fee_conf_target = int(
settings.get("low_fee_conf_target", default_low_fee_conf_target)
)
default_low_fee_rate = self.makeIntFromSetting(
settings, "low_feerate", default_low_fee_rate
)
default_high_estimated_feerate_multiplier = float(
settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
default_high_fee_rate = self.makeIntFromSetting(
settings, "high_feerate", default_high_fee_rate
)
else:
if kwargs.get("network") != "regtest":
raise ValueError("swapclient unset")
chain_client_settings = {}
self._low_fee_conf_target = int(
chain_client_settings.get(
"low_fee_conf_target", default_low_fee_conf_target
)
)
self._low_feerate = self.makeIntFromSetting(
chain_client_settings, "low_feerate", default_low_fee_rate
)
# Set below 1.0 to disable estimating the max feerate and use max_feerate
self._high_estimated_feerate_multiplier = float(
chain_client_settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
self._high_feerate = self.makeIntFromSetting(
chain_client_settings, "high_feerate", default_high_fee_rate
)
super().__init__(**kwargs)
def validateFeeRate(self, feerate: int) -> None:
if self._low_feerate > 0:
min_feerate_src = "set_value"
min_feerate = self._low_feerate
else:
min_feerate, min_feerate_src = self.get_fee_rate(self._low_fee_conf_target)
min_feerate = self.make_int(min_feerate)
if self._high_estimated_feerate_multiplier >= 1.0:
max_feerate, max_feerate_src = self.get_fee_rate()
max_feerate = int(
self.make_int(max_feerate) * self._high_estimated_feerate_multiplier
)
else:
max_feerate_src = "set_value"
max_feerate = self._high_feerate
if max_feerate_src in ("estimatesmartfee", "electrum"):
if max_feerate > self._high_feerate:
max_feerate_src = "clamped_to_set_value"
max_feerate = self._high_feerate
self._log.debug(
f"Verify {self.ticker()} fee rate {feerate}, min {min_feerate} {min_feerate_src}, max {max_feerate} {max_feerate_src}"
)
if feerate < min_feerate:
err_msg: str = (
f"Fee rate too low, {feerate} < {min_feerate}, {min_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
if feerate > max_feerate:
err_msg: str = (
f"Fee rate too high, {feerate} > {max_feerate}, {max_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
+36 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
@@ -34,7 +33,6 @@ from basicswap.rpc_xmr import make_xmr_rpc_func, make_xmr_rpc2_func
from basicswap.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface
ed25519_l = 2**252 + 27742317777372353535851937790883648493
@@ -94,13 +92,21 @@ class XMRInterface(CoinInterface):
"failed to get output distribution",
"request-sent",
"idle",
"busy",
"responsenotready",
"connection",
]
):
return True
return super().is_transient_error(ex)
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._addr_prefix = self.chainparams_network()["address_prefix"]
@@ -225,6 +231,7 @@ class XMRInterface(CoinInterface):
"invalid signature",
"std::bad_alloc",
"basic_string::_M_replace_aux",
"input stream error",
)
):
self._log.error(f"{self.coin_name()} wallet is corrupt.")
@@ -832,3 +839,28 @@ 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 []
def validateFeeRate(self, fee_rate: int) -> None:
pass # Fee rate isn't used
+419 -13
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -79,9 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
txid_hex = swap_client.withdrawParticl(
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")
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):
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
else:
@@ -127,7 +129,6 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = []
@@ -135,7 +136,7 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
if v["connection_type"] in ("rpc", "electrum"):
balance = "0.0"
if k in wallets:
@@ -168,8 +169,51 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
"balance": balance,
"pending": pending,
"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)
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if k == Coins.PART:
for field in ("blind_balance", "anon_balance"):
if field in w:
raw = w[field]
if isinstance(raw, float):
coin_entry[field] = f"{raw:.8f}".rstrip(
"0"
).rstrip(".")
elif isinstance(raw, int):
coin_entry[field] = str(raw)
else:
coin_entry[field] = raw
elif k == Coins.LTC:
if "mweb_balance" in w:
coin_entry["mweb_balance"] = w["mweb_balance"]
elif k == Coins.FIRO:
if "spark_balance" in w:
coin_entry["spark_balance"] = w["spark_balance"]
coins_with_balances.append(coin_entry)
if k == Coins.PART:
@@ -267,7 +311,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus()
if len(url_split) > 3:
ticker_str = url_split[3]
coin_type = getCoinIdFromTicker(ticker_str)
coin_type = getCoinIdFromTicker(ticker_str, inc_variant=True)
if len(url_split) > 4:
cmd = url_split[4]
@@ -293,6 +337,9 @@ def js_wallets(self, url_split, post_string, is_json):
elif cmd == "reseed":
swap_client.reseedWallet(coin_type)
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":
if coin_type != Coins.PART:
raise ValueError("Invalid coin for command")
@@ -306,6 +353,43 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
)
elif cmd == "mwebbalance":
# mweb outputs left behind when sending LTC -> MWEB
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
ci = swap_client.ci(coin_type)
return bytes(json.dumps(ci.format_amount(ci.getMWEBBalance())), "UTF-8")
elif cmd == "convertmweb":
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
return bytes(
json.dumps(swap_client.ci(coin_type).convertMWEBBalance()), "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")
if coin_type == Coins.LTC_MWEB:
@@ -599,8 +683,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
)
if have_data_entry(post_data, "debugind"):
main_debug_ind: bool = toBool(
get_data_entry_or(post_data, "maindebugind", True)
)
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()}
@@ -618,8 +707,13 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
elif have_data_entry(post_data, "abandon"):
swap_client.abandonBid(bid_id)
elif have_data_entry(post_data, "debugind"):
main_debug_ind: bool = toBool(
get_data_entry_or(post_data, "maindebugind", True)
)
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"):
@@ -628,7 +722,9 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
with_events = True
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 have_data_entry(post_data, "chainbkeysplit"):
@@ -1208,10 +1304,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
"current_seed_id": wallet_seed_id,
}
)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
rv.update(
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
if hasattr(ci, "getAccountKey"):
try:
rv.update({"account_key": ci.getAccountKey(seed_key, extkey_prefix)})
except Exception as e:
rv.update({"account_key_error": str(e)})
return bytes(
json.dumps(rv),
@@ -1526,6 +1624,74 @@ 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)
if all_txs and coin_id not in (Coins.XMR, Coins.WOW):
all_txs = list(reversed(all_txs))
elif not all_txs:
all_txs = []
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:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
@@ -1569,10 +1735,247 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
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",
)
def js_getsubfeebidtx(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
offer_id = bytes.fromhex(get_data_entry(post_data, "offer_id"))
offer = swap_client.getOffer(offer_id)
ensure(offer, "Offer not found.")
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
amount_to: int = inputAmount(get_data_entry(post_data, "amount_to"), ci_to)
bid_rate: int = ci_to.make_int(get_data_entry(post_data, "bid_rate"), r=1)
prefunded_data = swap_client.createSubfeeBidTx(offer_id, amount_to, bid_rate)
return bytes(
json.dumps(
{
"amount_from": ci_from.format_amount(prefunded_data["amount_from"]),
"amount_to": ci_to.format_amount(prefunded_data["amount_to"]),
"bid_tx": prefunded_data["bid_tx"].hex(),
}
),
"UTF-8",
)
endpoints = {
"coins": js_coins,
"walletbalances": js_walletbalances,
"wallets": js_wallets,
"wallettransactions": js_wallettransactions,
"offers": js_offers,
"sentoffers": js_sentoffers,
"bids": js_bids,
@@ -1602,6 +2005,9 @@ endpoints = {
"coinvolume": js_coinvolume,
"coinhistory": js_coinhistory,
"messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo,
"getsubfeebidtx": js_getsubfeebidtx,
}
-1
View File
@@ -23,7 +23,6 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
from basicswap.util.integer import encode_varint, decode_varint
NPBW_INT = 0
NPBW_BYTES = 2
-1
View File
@@ -39,7 +39,6 @@ from basicswap.contrib.rfc6979 import (
rfc6979_hmac_sha256_generate,
)
START_TOKEN = 0xABCD
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
+1 -1
View File
@@ -53,7 +53,7 @@ def initSimplexClient(args, logger, delay_event):
# TODO: Must be a better way?
logger.info("Initialising Simplex client")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
pipe_r, pipe_w = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt":
str_args = " ".join(args)
+2 -5
View File
@@ -15,9 +15,6 @@ from basicswap.interface.btc import (
class ProtocolInterface:
swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError("base class")
def getMockScript(self) -> bytearray:
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
@@ -29,7 +26,7 @@ class ProtocolInterface:
else ci.get_p2sh_script_pubkey(script)
)
def getMockAddrTo(self, ci):
def getMockScriptAddr(self, ci):
script = self.getMockScript()
return (
ci.encodeScriptDest(ci.getScriptDest(script))
@@ -38,5 +35,5 @@ class ProtocolInterface:
)
def findMockVout(self, ci, itx_decoded):
mock_addr = self.getMockAddrTo(ci)
mock_addr = self.getMockScriptAddr(ci)
return find_vout_for_address_from_txobj(itx_decoded, mock_addr)
+11 -8
View File
@@ -130,10 +130,7 @@ def redeemITx(self, bid_id: bytes, cursor):
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
self.log.debug(
"Submitted initiate redeem txn %s to %s chain for bid %s",
txid,
ci_from.coin_name(),
bid_id.hex(),
f"Submitted initiate redeem txn {self.logIDT(txid)} to {ci_from.coin_name()} chain for bid {self.logIDB(bid_id)}"
)
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
@@ -141,12 +138,18 @@ def redeemITx(self, bid_id: bytes, cursor):
class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
def getFundedInitiateTxTemplate(
self,
ci,
amount: int,
sub_fee: bool,
feerate: int = None,
lock_unspents: bool = False,
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
addr_to, amount, sub_fee, lock_unspents=lock_unspents, feerate=feerate
)
return bytes.fromhex(funded_tx)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
+119 -27
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,6 +11,7 @@ from basicswap.util import (
ensure,
)
from basicswap.interface.base import Curves
from basicswap.interface.btc import findOutput
from basicswap.chainparams import (
Coins,
)
@@ -49,11 +50,11 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
try:
use_cursor = self.openDB(cursor)
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
ensure(bid, "Bid not found: {}.".format(bid_id.hex()))
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex()))
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(xmr_swap, f"Adaptor-sig swap not found: {self.log.id(bid_id)}.")
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex()))
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex()))
ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(bid.offer_id)}.")
# The no-script coin is always the follower
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
@@ -105,7 +106,10 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
address_to = self.getReceiveAddressFromPool(
base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor
)
amount = bid.amount_to
amount: int = bid.amount_to
chain_b_fee_rate: int = (
xmr_offer.a_fee_rate if reverse_bid else xmr_offer.b_fee_rate
)
lock_tx_vout = bid.getLockTXBVout()
txid = ci_follower.spendBLockTx(
xmr_swap.b_lock_tx_id,
@@ -113,13 +117,13 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
xmr_swap.vkbv,
vkbs,
amount,
xmr_offer.b_fee_rate,
chain_b_fee_rate,
bid.chain_b_height_start,
spend_actual_balance=True,
lock_tx_vout=lock_tx_vout,
)
self.log.debug(
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
f"Submitted lock B spend txn {self.logIDT(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
)
self.logBidEvent(
bid.bid_id,
@@ -191,17 +195,11 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10):
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
kbsf, "proof kbsf owned for swap"
)
pk_recovered: bytes = ci_to.verifySigAndRecover(
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
)
if pk_recovered == xmr_swap.pkbsf:
break
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
assert pk_recovered == xmr_swap.pkbsf
xmr_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, "proof kbsf owned for swap")
pk_recovered: bytes = ci_to.verifySigAndRecover(
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
)
ensure(pk_recovered == xmr_swap.pkbsf, "kbsf recovered pubkey mismatch")
xmr_swap.pkasf = xmr_swap.pkbsf
else:
raise ValueError("Unknown curve")
@@ -209,9 +207,12 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP
_mock_key: bytes = bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
)
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there
# Fallthrough to ci if genScriptLockTxScript is implemented there
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
@@ -220,20 +221,78 @@ class XmrSwapInterface(ProtocolInterface):
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
def getMockScriptAddr(self, ci):
script = self.getMockScript()
if ci.coin_type() == Coins.PART:
# Use btc-segwit address to match createSCLockTx()
# _use_segwit is false for Particl
return ci.encode_p2wsh(ci.getScriptDest(script))
return (
ci.encodeScriptDest(ci.getScriptDest(script))
if ci._use_segwit
else ci.encode_p2sh(script)
)
def getMockScriptScriptPubkey(self, ci) -> bytearray:
script = self.getMockScript()
if ci.coin_type() == Coins.PART:
# Use btc-segwit address to match createSCLockTx()
# _use_segwit is false for Particl
return ci.getScriptDest(script)
return (
ci.getScriptDest(script)
if ci._use_segwit
else ci.get_p2sh_script_pubkey(script)
)
def getFundedInitiateTxTemplate(
self,
ci,
amount: int,
sub_fee: bool,
feerate: int = None,
lock_unspents: bool = False,
) -> bytes:
if ci.coin_type() == Coins.BCH:
# Workaround, BCH getScriptDest() uses OP_HASH256
script: bytes = self.getMockScript()
addr_to: bytes = ci.getScriptDest(script)
else:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=lock_unspents, feerate=feerate
)
return bytes.fromhex(funded_tx)
def getMockITxSwapValue(self, ci, tx_data: bytes) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockITxSwapVout(self, ci, tx_obj) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script)
if ci.coin_type() == Coins.BCH:
mock_script: bytes = self.getMockScript()
mock_txo_script: bytes = ci.getScriptDest(mock_script)
else:
mock_txo_script: bytes = self.getMockScriptScriptPubkey(ci)
real_txo_script: bytes = ci.getScriptDest(script)
found: int = 0
ctx = ci.loadTx(mock_tx)
ctx = ci.loadTx(mock_tx, allow_witness=False)
for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script
@@ -246,3 +305,36 @@ class XmrSwapInterface(ProtocolInterface):
ctx.nLockTime = 0
return ctx.serialize()
def getMockPubkey(self, ci) -> bytes:
return ci.getPubkey(self._mock_key)
def getMockPTxSwapValue(self, ci, tx_data: bytes) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockPTxSwapVout(self, ci, tx_obj) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockPTx(self, ci, tx_data: bytes, kbv: bytes, Kbs: bytes) -> bytes:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
tx_obj.vout[lock_vout].scriptPubKey = ci.getPkDest(Kbs)
return tx_obj.serialize()
+24 -5
View File
@@ -152,15 +152,17 @@ class Jsonrpc:
pass
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
def callrpc(
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
):
if _use_rpc_pooling:
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
return callrpc_pooled(rpc_port, auth, method, params, wallet, host, timeout)
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet)
x = Jsonrpc(url)
x = Jsonrpc(url, timeout=timeout if timeout else 10)
v = x.json_request(method, params)
x.close()
@@ -174,7 +176,9 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
return r["result"]
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
def callrpc_pooled(
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
):
from .rpc_pool import get_rpc_pool
import http.client
import socket
@@ -183,6 +187,20 @@ def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0
if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet)
if timeout:
try:
conn = Jsonrpc(url, timeout=timeout)
v = conn.json_request(method, params)
r = json.loads(v.decode("utf-8"))
conn.close()
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
return r["result"]
except ValueError:
raise
except Exception as ex:
raise ValueError(f"RPC server error: {ex}, method: {method}")
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
pool = get_rpc_pool(url, max_connections)
@@ -247,7 +265,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
wallet = wallet
host = host
def rpc_func(method, params=None, wallet_override=None):
def rpc_func(method, params=None, wallet_override=None, timeout=None):
return callrpc(
port,
auth,
@@ -255,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
params,
wallet if wallet_override is None else wallet_override,
host,
timeout=timeout,
)
return rpc_func
+19
View File
@@ -1,3 +1,22 @@
(function() {
const originalFetch = window.fetch;
window.fetch = function(url, options) {
return originalFetch.apply(this, arguments).then(function(response) {
if (response.status === 401) {
const urlStr = typeof url === 'string' ? url : (url && url.url) || '';
if (urlStr.startsWith('/json/') || urlStr.startsWith('/json')) {
window.location.href = '/login';
return new Response(JSON.stringify({error: 'Session expired'}), {
status: 401,
headers: {'Content-Type': 'application/json'}
});
}
}
return response;
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
+173 -30
View File
@@ -2,15 +2,46 @@
'use strict';
const EventHandlers = {
confirmPopup: function(action = 'proceed', coinName = '') {
const message = action === 'Accept'
? 'Are you sure you want to accept this bid?'
: coinName
? `Are you sure you want to ${action} ${coinName}?`
: 'Are you sure you want to proceed?';
return confirm(message);
showConfirmModal: function(title, message, callback) {
const modal = document.getElementById('confirmModal');
if (!modal) {
if (callback) callback();
return;
}
const titleEl = document.getElementById('confirmTitle');
const messageEl = document.getElementById('confirmMessage');
const yesBtn = document.getElementById('confirmYes');
const noBtn = document.getElementById('confirmNo');
const bidDetails = document.getElementById('bidDetailsSection');
if (titleEl) titleEl.textContent = title;
if (messageEl) {
messageEl.textContent = message;
messageEl.classList.remove('hidden');
}
if (bidDetails) bidDetails.classList.add('hidden');
modal.classList.remove('hidden');
const newYesBtn = yesBtn.cloneNode(true);
yesBtn.parentNode.replaceChild(newYesBtn, yesBtn);
newYesBtn.addEventListener('click', function() {
modal.classList.add('hidden');
if (callback) callback();
});
const newNoBtn = noBtn.cloneNode(true);
noBtn.parentNode.replaceChild(newNoBtn, noBtn);
newNoBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
},
confirmMWEBChangeConvert: function() {
return confirm('Confirm MWEB change conversion: This will create a tx sending all spendable MWEB outputs in the plain LTC wallet to LTC.');
},
confirmReseed: function() {
@@ -18,7 +49,6 @@
},
confirmWithdrawal: function() {
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
return window.WalletPage.confirmWithdrawal();
}
@@ -34,7 +64,7 @@
},
fillDonationAddress: function(address, coinType) {
let addressInput = null;
addressInput = window.DOMCache
@@ -67,14 +97,36 @@
return;
}
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
let coinFromId;
if (inputId === 'add-amm-amount') {
coinFromId = 'add-amm-coin-from';
} else if (inputId === 'edit-amm-amount') {
coinFromId = 'edit-amm-coin-from';
} else {
const form = amountInput.closest('form') || amountInput.closest('.modal-content') || amountInput.closest('[id*="modal"]');
const select = form?.querySelector('select[id*="coin-from"]');
coinFromId = select?.id;
}
const coinFromSelect = coinFromId ? document.getElementById(coinFromId) : null;
if (!coinFromSelect) {
console.error('EventHandlers: Coin-from dropdown not found for:', inputId);
return;
}
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption) {
console.error('EventHandlers: No option selected in coin-from dropdown');
return;
}
const balance = parseFloat(selectedOption.getAttribute('data-balance') || '0');
if (balance > 0) {
const calculatedAmount = balance * percent;
amountInput.value = calculatedAmount.toFixed(8);
} else {
console.warn('EventHandlers: No balance found for AMM amount calculation');
console.warn('EventHandlers: No balance found for selected coin');
}
},
@@ -131,19 +183,64 @@
}
},
hideConfirmModal: function() {
if (window.DOMCache) {
window.DOMCache.hide('confirmModal');
} else {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.style.display = 'none';
setBidAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: Bid amount input not found:', inputId);
return;
}
const haveBalance = amountInput.getAttribute('haveamount');
if (!haveBalance) {
console.error('EventHandlers: Balance not found for bid');
return;
}
const floatBalance = parseFloat(haveBalance);
if (isNaN(floatBalance)) {
alert('Invalid bid balance');
return;
}
const maxAmount = amountInput.getAttribute('max');
if (!maxAmount) {
console.error('EventHandlers: Max amount not found for bid');
return;
}
const floatMax = parseFloat(maxAmount);
if (isNaN(floatMax) || floatMax <= 0) {
alert('Invalid bid max amount');
return;
}
const coinExp = amountInput.getAttribute('exp');
if (!coinExp) {
console.error('EventHandlers: Coin exp not found for bid');
return;
}
let calculatedAmount = maxAmount * percent;
if (floatBalance < calculatedAmount) {
calculatedAmount = floatBalance;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
checkbox.checked = true;
}
}
amountInput.value = calculatedAmount.toFixed(coinExp);
window.updateBidParams('sending');
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
modal.style.display = '';
}
},
lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates();
} else {
@@ -187,17 +284,43 @@
},
initialize: function() {
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm]');
if (target) {
if (target.dataset.confirmHandled) {
delete target.dataset.confirmHandled;
return;
}
e.preventDefault();
e.stopPropagation();
const action = target.getAttribute('data-confirm-action') || 'proceed';
const coinName = target.getAttribute('data-confirm-coin') || '';
if (!this.confirmPopup(action, coinName)) {
e.preventDefault();
return false;
}
const message = action === 'Accept'
? 'Are you sure you want to accept this bid?'
: coinName
? `Are you sure you want to ${action} ${coinName}?`
: 'Are you sure you want to proceed?';
const title = `Confirm ${action}`;
this.showConfirmModal(title, message, function() {
target.dataset.confirmHandled = 'true';
if (target.form) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = target.name;
hiddenInput.value = target.value;
target.form.appendChild(hiddenInput);
target.form.submit();
} else {
target.click();
}
});
}
});
@@ -211,6 +334,16 @@
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-mweb-change-convert]');
if (target) {
if (!this.confirmMWEBChangeConvert()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]');
if (target) {
@@ -261,6 +394,16 @@
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-bid-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-bid-amount'));
const inputId = target.getAttribute('data-input-id');
this.setBidAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]');
if (target) {
@@ -326,15 +469,15 @@
}
window.EventHandlers = EventHandlers;
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.setBidAmount = EventHandlers.setBidAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
@@ -250,6 +250,7 @@ function ensureToastContainer() {
'new_bid': 'bg-green-500',
'bid_accepted': 'bg-purple-500',
'swap_completed': 'bg-green-600',
'sweep_completed': 'bg-orange-500',
'balance_change': 'bg-yellow-500',
'update_available': 'bg-blue-600',
'success': 'bg-blue-500'
@@ -609,7 +610,7 @@ function ensureToastContainer() {
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
cursorStyle = 'cursor-pointer';
} else if (options.coinSymbol) {
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol.toLowerCase()}'"`;
cursorStyle = 'cursor-pointer';
} else if (options.releaseUrl) {
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
@@ -735,6 +736,18 @@ function ensureToastContainer() {
shouldShowToast = config.showUpdateNotifications;
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':
if (data.coin && config.showBalanceChanges) {
this.handleBalanceUpdate(data);
+18 -1
View File
@@ -23,6 +23,11 @@
types: ['default'],
hasSubfee: false,
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);
},
@@ -188,7 +204,8 @@
balance: balance,
blind: balance2,
anon: balance3,
mweb: balance2
mweb: balance2,
spark: balance2
};
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: 'redemption', order: 11.5, label: 'Refunding' }, // 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 state = {
dentities: new Map(),
identities: new Map(),
currentPage: 1,
wsConnected: false,
jsonData: [],
+74 -26
View File
@@ -4,11 +4,13 @@
const OfferPage = {
xhr_rates: null,
xhr_bid_params: null,
xhr_bid_prefund: null,
init: function() {
this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest();
this.xhr_bid_prefund = new XMLHttpRequest();
this.setupXHRHandlers();
this.setupEventListeners();
this.handleBidsPageAddress();
@@ -33,7 +35,20 @@
if (bidAmountSendInput) {
bidAmountSendInput.value = obj['amount_to'];
}
this.updateModalValues();
}
};
this.xhr_bid_prefund.onload = () => {
if (this.xhr_bid_prefund.status == 200) {
const obj = JSON.parse(this.xhr_bid_prefund.response);
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountInput) {
bidAmountInput.value = obj['amount_from'];
}
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = obj['bid_tx'];
}
}
};
},
@@ -41,12 +56,29 @@
setupEventListeners: function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
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) {
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"]');
@@ -54,16 +86,6 @@
mainCancelBtn.onclick = this.handleCancelClick.bind(this);
}
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
}
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
@@ -95,7 +117,7 @@
const bidRateInput = document.getElementById('bid_rate');
const validMinsInput = document.querySelector('input[name="validmins"]');
const amtVar = document.getElementById('amt_var')?.value === 'True';
if (bidAmountSendInput) {
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
}
@@ -112,8 +134,7 @@
if (!amtVar) {
this.updateBidParams('rate');
}
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
@@ -139,6 +160,7 @@
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate');
const bidSubfee = document.getElementById('subfee_bid');
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
@@ -154,7 +176,7 @@
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'sending') {
} else if (value_changed === 'sending' || value_changed === 'subfee') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
@@ -170,11 +192,31 @@
this.validateAmountsAfterChange();
if (bidSubfee && bidSubfee.checked) {
bidAmountInput.readOnly = true;
const offer_id = document.getElementById('offer_id')?.value || '';
if (!offer_id) {
console.log("offer_id not found!");
return;
}
this.xhr_bid_prefund.open('POST', '/json/getsubfeebidtx');
this.xhr_bid_prefund.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
const data = { offer_id: offer_id, amount_to: bidAmountSendInput.value , bid_rate: rate};
this.xhr_bid_prefund.overrideMimeType("application/json");
this.xhr_bid_prefund.send(JSON.stringify(data));
return;
}
bidAmountInput.readOnly = false;
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = "";
}
this.xhr_bid_params.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_bid_params.overrideMimeType("application/json");
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues();
},
validateAmountsAfterChange: function() {
@@ -236,6 +278,11 @@
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false;
}
let subfee = false;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
subfee = checkbox.checked;
}
const coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || '';
@@ -256,7 +303,12 @@
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
if (modalSendCurrency) {
modalSendCurrency.textContent = ` ${tlaTo}`;
if (subfee) {
modalSendCurrency.textContent += ` (incl fee)`;
}
}
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins;
@@ -275,10 +327,6 @@
return false;
},
updateModalValues: function() {
},
handleBidsPageAddress: function() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
+19 -31
View File
@@ -477,6 +477,7 @@ const ui = {
const chartModule = {
chart: null,
currentCoin: 'BTC',
hasChartData: false,
loadStartTime: 0,
chartRefs: new WeakMap(),
pendingAnimationFrame: null,
@@ -865,6 +866,7 @@ destroyChart: function() {
console.warn(`No price data available for ${coinSymbol}`);
chartModule.hideChartLoader();
chartModule.showNoDataMessage(coinSymbol);
chartModule.hasChartData = false;
return;
}
@@ -901,16 +903,12 @@ destroyChart: function() {
if (chartData.length > 0 && chartModule.chart) {
chartModule.chart.data.datasets[0].data = chartData;
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
if (coinSymbol === 'WOW') {
chartModule.chart.options.scales.x.time.unit = 'hour';
} else {
const resolution = window.config.chartConfig.resolutions[window.config.currentResolution];
chartModule.chart.options.scales.x.time.unit =
resolution && resolution.interval === 'hourly' ? 'hour' :
window.config.currentResolution === 'year' ? 'month' : 'day';
}
const resolution = window.config.chartConfig.resolutions[window.config.currentResolution];
chartModule.chart.options.scales.x.time.unit =
resolution && resolution.interval === 'hourly' ? 'hour' :
window.config.currentResolution === 'year' ? 'month' : 'day';
chartModule.chart.update('active');
chartModule.currentCoin = coinSymbol;
chartModule.hasChartData = true;
const loadTime = Date.now() - chartModule.loadStartTime;
ui.updateLoadTimeAndCache(loadTime, cachedData);
}
@@ -925,9 +923,12 @@ destroyChart: function() {
chartModule.chart.data.datasets[0].data = [];
chartModule.chart.update('active');
}
chartModule.hasChartData = false;
}
} finally {
chartModule.currentCoin = coinSymbol;
chartModule.hideChartLoader();
app.updateResolutionButtons();
}
},
@@ -1125,7 +1126,6 @@ const app = {
}
await chartModule.updateChart(defaultCoin);
app.updateResolutionButtons(defaultCoin);
const chartTitle = document.getElementById('chart-title');
if (chartTitle) {
@@ -1233,11 +1233,8 @@ const app = {
}
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
if (chartModule.chart) {
if (coin.symbol === 'WOW') {
window.config.currentResolution = 'day';
}
window.config.currentResolution = 'day';
chartModule.updateChart(coin.symbol);
app.updateResolutionButtons(coin.symbol);
}
});
}
@@ -1614,20 +1611,14 @@ refreshAllData: async function() {
}
},
updateResolutionButtons: function(coinSymbol) {
updateResolutionButtons: function() {
const resolutionButtons = document.querySelectorAll('.resolution-button');
resolutionButtons.forEach(button => {
const resolution = button.id.split('-')[1];
if (coinSymbol === 'WOW') {
if (resolution === 'day') {
button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.add('active');
button.disabled = false;
} else {
button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.remove('active');
button.disabled = true;
}
if (!chartModule.hasChartData) {
button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.disabled = true;
} else {
button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.toggle('active', resolution === window.config.currentResolution);
@@ -1659,12 +1650,9 @@ resolutionButtons.forEach(button => {
button.addEventListener('click', () => {
const resolution = button.id.split('-')[1];
const currentCoin = chartModule.currentCoin;
if (currentCoin !== 'WOW' || resolution === 'day') {
window.config.currentResolution = resolution;
chartModule.updateChart(currentCoin, true);
app.updateResolutionButtons(currentCoin);
}
window.config.currentResolution = resolution;
chartModule.updateChart(currentCoin, true);
app.updateResolutionButtons();
});
});
+569
View File
@@ -6,11 +6,15 @@
confirmCallback: null,
triggerElement: null,
originalConnectionTypes: {},
init: function() {
this.setupTabs();
this.setupCoinHeaders();
this.setupConfirmModal();
this.setupNotificationSettings();
this.setupMigrationIndicator();
this.setupServerDiscovery();
},
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() {
const confirmYesBtn = document.getElementById('confirmYes');
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() {
};
+284 -4
View File
@@ -13,6 +13,7 @@
this.setupWithdrawalConfirmation();
this.setupTransactionDisplay();
this.setupWebSocketUpdates();
this.setupTransactionPagination();
},
setupAddressCopy: function() {
@@ -340,13 +341,292 @@
},
handleBalanceUpdate: function(balanceData) {
console.log('Balance updated:', balanceData);
if (!balanceData || !Array.isArray(balanceData)) return;
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][data-balance-type]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) {
const balanceType = element.getAttribute('data-balance-type');
const value = coinData[balanceType];
if (value !== undefined) {
const ticker = coinData.ticker || coinId.toUpperCase();
const newBalance = balanceType === 'est_fee' ? value : `${value} ${ticker}`;
if (element.textContent !== newBalance) {
element.textContent = newBalance;
console.log(`Updated ${balanceType}: ${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) {
console.log('Swap event:', eventData);
if (window.BalanceUpdatesManager) {
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);
}
};
+90 -2
View File
@@ -75,9 +75,28 @@
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else if (coinData.name === 'Litecoin MWEB') {
this.updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
this.removePendingBalance('Litecoin', 'MWEB Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'MWEB Pending:', coinData);
}
} else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.mweb_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'MWEB Balance:', coinData.mweb_balance, coinData.ticker || coinData.name);
}
if (coinData.spark_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Spark Balance:', coinData.spark_balance, coinData.ticker || coinData.name);
}
if (coinData.blind_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Blind Balance:', coinData.blind_balance, coinData.ticker || coinData.name);
}
if (coinData.anon_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Anon Balance:', coinData.anon_balance, coinData.ticker || coinData.name);
}
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData);
@@ -86,6 +105,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) {
@@ -102,12 +187,13 @@
const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) {
const cleanBalance = balance.toString().replace(/^\+/, '');
if (isPending) {
const cleanBalance = balance.toString().replace(/^\+/, '');
element.textContent = `+${cleanBalance} ${ticker}`;
} else {
element.textContent = `${balance} ${ticker}`;
}
element.setAttribute('data-original-value', `${cleanBalance} ${ticker}`);
}
}
}
@@ -139,6 +225,7 @@
if (pendingSpan) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${coinData.ticker || coinData.name}`);
}
let initialUSD = '$0.00';
@@ -218,7 +305,7 @@
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-white, .dark\\:bg-gray-500');
return element.closest('.bg-gray-50, .dark\\:bg-gray-500');
}
}
return null;
@@ -330,6 +417,7 @@
if (pendingSpan) {
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
pendingSpan.setAttribute('data-original-value', `+${cleanPending} ${ticker}`);
}
}
}
+12 -2
View File
@@ -525,14 +525,14 @@
</div>
{% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" 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>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" 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>
{% endif %}
</div>
@@ -689,6 +689,16 @@ document.addEventListener('DOMContentLoaded', function() {
overrideButtonConfirm(acceptBidBtn, 'Accept');
});
</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>
{% include 'footer.html' %}
</body>
+12 -2
View File
@@ -801,14 +801,14 @@
</div>
{% if data.can_abandon == true %}
<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>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" 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>
{% endif %}
</div>
@@ -965,6 +965,16 @@ document.addEventListener('DOMContentLoaded', function() {
overrideButtonConfirm(acceptBidBtn, 'Accept');
});
</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>
{% include 'footer.html' %}
</body>
+2 -2
View File
@@ -24,9 +24,9 @@
<div class="w-full md:w-1/2 mb-6 md:mb-0">
<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">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.5.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }}
</div>
+14 -1
View File
@@ -224,7 +224,7 @@
</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">Chain A local fee rate</td>
<td class="py-3 px-6">{{ data.a_fee_rate_verify }} (Fee source: {{ data.a_fee_rate_verify_src }}{% if data.a_fee_warn == true %} WARNING {% endif %})</td>
<td class="py-3 px-6">{{ data.a_fee_rate_verify }} (Fee source: {{ data.a_fee_rate_verify_src }}{% if data.a_fee_warn == "high" %} WARNING - HIGH {% elif data.a_fee_warn == "low" %} WARNING - LOW {% elif data.a_fee_warn is defined %} WARNING {% endif %})</td>
</tr>
{% endif %}
</table>
@@ -419,11 +419,22 @@
name="bid_amount_send"
value=""
max="{{ data.amt_to }}"
haveamount="{{ data.coin_to_balance }}"
exp="{{ data.coin_to_exp }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');">
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
max {{ data.amt_to }} ({{ data.tla_to }})
</div>
</div>
<div class="mt-2 flex space-x-2">
<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" data-set-bid-amount="1" data-input-id="bid_amount_send">max</button>
{% if data.bid_can_subfee == true %}
<label>
<input type="checkbox" name="subfee_bid" id="subfee_bid" value="sfb" onchange="updateBidParams('subfee');"/>
<span for="subfee_bid">Subfee</span>
</label>
{% endif %}
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -721,6 +732,8 @@
<input type="hidden" id="coin_to_name" value="{{ data.coin_to }}">
<input type="hidden" id="tla_from" value="{{ data.tla_from }}">
<input type="hidden" id="tla_to" value="{{ data.tla_to }}">
<input type="hidden" id="offer_id" value="{{ offer_id }}">
<input type="hidden" name="prefunded_bid_tx" id="prefunded_bid_tx" value="{{ data.prefunded_bid_tx }}">
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<p id="rates_display"></p>
+223 -5
View File
@@ -105,17 +105,31 @@
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
Connection
</h4>
<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>
<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">
{{ c.connection_type }}
</div>
{% endif %}
</div>
{% if c.manage_daemon is defined %}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Manage Daemon</label>
@@ -138,12 +152,169 @@
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% 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') %}
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
@@ -656,6 +827,53 @@
</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>
{% include 'footer.html' %}
{% include 'footer.html' %}
+1 -1
View File
@@ -101,7 +101,7 @@
</svg>
</button>
</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">
<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>
+318 -20
View File
@@ -35,6 +35,8 @@
</section>
{% endif %}
{% if w.havedata %}
{% if w.error %}
<section class="py-4 px-6" id="messages_error" role="alert">
@@ -82,6 +84,36 @@
</section>
{% 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>
<form method="post" autocomplete="off">
<div class="px-6 py-0 h-full overflow-hidden">
@@ -106,18 +138,21 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
{% endif %}
{% 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>
</tr>
{% if w.cid == '1' %} {# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.blind_unconfirmed %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
@@ -127,7 +162,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Anon"> </span>Anon Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.anon_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.anon_pending }} {{ w.ticker }}</span>
@@ -139,15 +174,53 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
{% 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 }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
{% endif %}
{% endif %}
</td>
</tr>
{% if w.mweb_in_plain %}
<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 in Plain Balance: </td>
<td class="py-3 px-6 bold">
<span>{{ w.mweb_in_plain }} {{ w.ticker }}</span>
</td>
<td class="py-3 px-6 bold">
<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="convertmweb_{{ w.cid }}" value="Convert" data-confirm-mweb-change-convert> Convert </button>
</td>
</tr>
{% endif %}
{% 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 }}" data-balance-type="spark_balance">{{ 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 }}" data-balance-type="spark_balance">{{ 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>
{% endif %}
{# / LTC #}
{# / FIRO #}
{% if w.locked_utxos %}
<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>
@@ -163,6 +236,19 @@
<td class="py-3 px-6 bold">{{ w.name }} Version:</td>
<td class="py-3 px-6">{{ w.version }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">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">
<td class="py-3 px-6 bold">Blockheight:</td>
<td class="py-3 px-6">{{ w.blocks }}
@@ -178,8 +264,70 @@
{% endif %}
<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">{{ 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>
{% 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 %}
<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>
@@ -200,6 +348,7 @@
<td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
@@ -286,8 +435,8 @@
</div>
</div>
</div>
{% if w.cid in '1, 3, 6, 9' %}
{# PART | LTC | XMR | WOW | #}
{% if w.cid in '1, 3, 6, 9, 13' %}
{# PART | LTC | XMR | WOW | FIRO #}
<div class="w-full md:w-1/2 p-3 flex justify-center items-center">
<div class="h-full">
<div class="flex flex-wrap -m-3">
@@ -319,8 +468,9 @@
</div>
{# / PART #}
{% elif w.cid == '3' %}
{# LTC #}
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div>
{# LTC - MWEB not available in light mode #}
{% if not is_electrum_mode %}
<div id="qrcode-mweb" class="qrcode" data-qrcode data-address="{{ w.mweb_address }}"> </div>
</div>
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
@@ -333,7 +483,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>
</div>
</div>
{% endif %}
{# / 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 %}
</div>
</div>
@@ -349,10 +516,6 @@
</div>
</section>
<!-- Address copy functionality handled by external JS -->
<section class="p-6">
<div class="lg:container mx-auto">
<div class="flex items-center">
@@ -384,7 +547,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -393,7 +556,29 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
{% 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 }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</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 }}" data-balance-type="spark_balance">{{ 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 }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -402,14 +587,14 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Blind Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Anon Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -487,6 +672,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>
{# / 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 %}
<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>
@@ -541,7 +734,7 @@
</td>
</tr>
{# / 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">
<td class="py-3 px-6 bold">Type From:</td>
<td class="py-3 px-6">
@@ -553,8 +746,21 @@
</div>
</td>
</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 %}
{# / LTC #}
{# / FIRO #}
{% if w.cid not in '6,9' %} {# Not XMR WOW #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Rate:</td>
@@ -563,7 +769,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Estimate:</td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.est_fee }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="est_fee">{{ w.est_fee }}</span>
(<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
</td>
</tr>
@@ -593,6 +799,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>
{# / XMR | WOW #}
{% elif w.show_utxo_groups %}
{% elif is_electrum_mode %}
{# Hide UTXO Groups button in electrum/lite wallet mode #}
{% 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>
{% endif %}
@@ -680,6 +888,96 @@
<input type="hidden" name="formid" value="{{ form_id }}">
</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 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">
@@ -688,7 +986,7 @@
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<div class="flex justify-center gap-4">
<button type="button" id="confirmYes"
<button type="button" id="confirmYes"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Confirm
</button>
+78 -6
View File
@@ -48,14 +48,24 @@
<div class="px-6 mb-6">
<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>
{% 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>
<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 %}
</div>
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Balance:</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 coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</div>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</div>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
@@ -71,10 +81,16 @@
<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 %}
{% 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 #}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind 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.blind_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 }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4>
@@ -92,7 +108,7 @@
{% endif %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon 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.anon_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 }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon USD value:</h4>
@@ -110,10 +126,10 @@
</div>
{% endif %}
{% 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">
<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 }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4>
@@ -132,6 +148,28 @@
{% endif %}
{% endif %}
{# / 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 }}" data-balance-type="spark_balance">{{ 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">
<div class="flex mb-2 justify-between items-center">
<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>
<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>
{% 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">
<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>
@@ -179,6 +250,7 @@
</div>
</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
class WatchedOutput: # Watch for spends
__slots__ = ("bid_id", "txid_hex", "vout", "tx_type", "swap_type")
def __init__(self, bid_id: bytes, txid_hex: str, vout, tx_type, swap_type):
self.bid_id = bid_id
self.txid_hex = txid_hex
self.vout = vout
self.tx_type = tx_type
self.swap_type = swap_type
class WatchedScript: # Watch for txns containing outputs
__slots__ = ("bid_id", "script", "tx_type", "swap_type")
def __init__(self, bid_id: bytes, script: bytes, tx_type, swap_type):
self.bid_id = bid_id
self.script = script
self.tx_type = tx_type
self.swap_type = swap_type
class WatchedTransaction:
__slots__ = (
"bid_id",
"coin_type",
"txid_hex",
"tx_type",
"swap_type",
"block_hash",
"depth",
)
# TODO
# Watch for presence in mempool (getrawtransaction)
def __init__(
self, bid_id: bytes, coin_type: int, txid_hex: str, tx_type, swap_type
):
self.bid_id = bid_id
self.coin_type = coin_type
self.txid_hex = txid_hex
self.tx_type = tx_type
self.swap_type = swap_type
self.block_hash = None
self.depth = -1
+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_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:
json.dump(swap_client.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path)
+79 -18
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2022-2026 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@
import traceback
import time
from typing import List
from urllib import parse
from .util import (
getCoinType,
@@ -184,14 +185,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
parsed_data["swap_type"] = page_data["swap_type"]
swap_type = swap_type_from_string(parsed_data["swap_type"])
elif (
parsed_data["coin_from"] in swap_client.adaptor_swap_only_coins
or parsed_data["coin_to"] in swap_client.adaptor_swap_only_coins
parsed_data["coin_from"] in swap_client.coins_without_segwit
and parsed_data["coin_to"] in swap_client.coins_without_segwit
):
parsed_data["swap_type"] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
else:
parsed_data["swap_type"] = strSwapType(SwapTypes.SELLER_FIRST)
swap_type = SwapTypes.SELLER_FIRST
else:
parsed_data["swap_type"] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
if swap_type == SwapTypes.XMR_SWAP:
page_data["swap_style"] = "xmr"
@@ -218,6 +219,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_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"]
else:
page_data["fee_from_extra"] = 0
if have_data_entry(form_data, "fee_to_conf"):
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
@@ -226,6 +229,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_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"]
else:
page_data["fee_to_extra"] = 0
if have_data_entry(form_data, "check_offer"):
page_data["check_offer"] = True
@@ -249,6 +254,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
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:
if len(errors) == 0 and page_data["swap_style"] == "xmr":
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
@@ -342,7 +355,15 @@ def postNewOfferFromParsed(swap_client, parsed_data):
lock_type = TxLockTypes.ABS_LOCK_TIME
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:
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
if "from_fee_multiplier_percent" in parsed_data:
@@ -393,7 +414,7 @@ def postNewOfferFromParsed(swap_client, parsed_data):
parsed_data["amt_bid_min"],
swap_type,
lock_type=lock_type,
lock_value=parsed_data["lock_seconds"],
lock_value=lock_value,
addr_send_from=parsed_data["addr_from"],
extra_options=extra_options,
)
@@ -479,7 +500,7 @@ def page_newoffer(self, url_split, post_string):
"debug_ui": swap_client.debug_ui,
"automation_strat_id": -1,
"amt_bid_min": format_amount(1, 3),
"swap_type": strSwapType(SwapTypes.SELLER_FIRST),
"swap_type": strSwapType(SwapTypes.XMR_SWAP),
}
post_data = parse.parse_qs(post_string)
@@ -563,7 +584,7 @@ def page_newoffer(self, url_split, post_string):
)
def page_offer(self, url_split, post_string):
def page_offer(self, url_split: List[str], post_string: str) -> bytes:
ensure(len(url_split) > 2, "Offer ID not specified")
offer_id = decode_offer_id(url_split[2])
server = self.server
@@ -654,6 +675,11 @@ def page_offer(self, url_split, post_string):
amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, "debugind", -1))
if have_data_entry(form_data, "subfee_bid"):
extra_options["prefunded_tx"] = bytes.fromhex(
get_data_entry(form_data, "prefunded_bid_tx")
)
sent_bid_id = swap_client.postBid(
offer_id,
amount_from,
@@ -748,24 +774,30 @@ def page_offer(self, url_split, post_string):
ci_leader = ci_to if reverse_bid else ci_from
if xmr_offer:
int_fee_rate_now, fee_source = ci_leader.get_fee_rate()
a_fee_rate_now, fee_source = ci_leader.get_fee_rate()
a_fee_rate_now = ci_leader.make_int(a_fee_rate_now)
data["xmr_type"] = True
data["a_fee_rate"] = ci_leader.format_amount(xmr_offer.a_fee_rate)
data["a_fee_rate_verify"] = ci_leader.format_amount(
int_fee_rate_now, conv_int=True
chain_a_fee_rate: int = (
xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
)
data["xmr_type"] = True
data["a_fee_rate"] = ci_leader.format_amount(chain_a_fee_rate)
data["a_fee_rate_verify"] = ci_leader.format_amount(a_fee_rate_now)
data["a_fee_rate_verify_src"] = fee_source
data["a_fee_warn"] = xmr_offer.a_fee_rate < int_fee_rate_now
from_fee_rate = xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
warning_threshold: float = 1.2
if chain_a_fee_rate * warning_threshold < a_fee_rate_now:
data["a_fee_warn"] = "low"
elif chain_a_fee_rate > a_fee_rate_now * warning_threshold:
data["a_fee_warn"] = "high"
lock_spend_tx_vsize = (
ci_from.xmr_swap_b_lock_spend_tx_vsize()
if reverse_bid
else ci_from.xmr_swap_a_lock_spend_tx_vsize()
)
lock_spend_tx_fee = ci_from.make_int(
from_fee_rate * lock_spend_tx_vsize / 1000, r=1
chain_a_fee_rate * lock_spend_tx_vsize / 1000, r=1
)
data["amt_from_lock_spend_tx_fee"] = ci_from.format_amount(
lock_spend_tx_fee // ci_from.COIN()
@@ -797,6 +829,33 @@ def page_offer(self, url_split, post_string):
)
data["amt_swapped"] = ci_from.format_amount(amt_swapped)
if show_bid_form:
coin_to_id = int(ci_to.coin_type())
wallet_coin_to_id = coin_to_id
if coin_to_id in (Coins.PART_ANON, Coins.PART_BLIND):
wallet_coin_to_id = Coins.PART
swap_client.updateWalletsInfo(only_coin=wallet_coin_to_id)
coin_to_wallet = swap_client.getCachedWalletsInfo(
{"coin_id": wallet_coin_to_id}
)[wallet_coin_to_id]
if coin_to_id == Coins.PART_ANON:
balance_key = "anon_balance"
elif coin_to_id == Coins.PART_BLIND:
balance_key = "blind_balance"
else:
balance_key = "balance"
data["coin_to_balance"] = coin_to_wallet[balance_key]
bid_can_subfee: bool = True
if offer.swap_type != SwapTypes.XMR_SWAP:
bid_can_subfee = False
if coin_to_id in (Coins.XMR, Coins.WOW):
bid_can_subfee = False
if offer.amount_negotiable is False:
bid_can_subfee = False
data["bid_can_subfee"] = bid_can_subfee
template = server.env.get_template("offer.html")
return self.render_template(
template,
@@ -815,7 +874,9 @@ def page_offer(self, url_split, post_string):
)
def format_timestamp(timestamp, with_ago=True, is_expired=False):
def format_timestamp(
timestamp: int, with_ago: bool = True, is_expired: bool = False
) -> str:
current_time = int(time.time())
if is_expired:
+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."
)
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
for name, c in swap_client.settings["chainclients"].items():
if have_data_entry(form_data, "apply_" + 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(
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(
name, data
settings_changed, suggest_reboot, migration_message = (
swap_client.editSettings(name, data)
)
if migration_message:
messages.append(migration_message)
if settings_changed is True:
messages.append("Settings applied.")
if suggest_reboot is True:
@@ -156,19 +220,71 @@ def page_settings(self, url_split, post_string):
display_name = getCoinName(swap_client.getCoinIdFromName(name))
messages.append(display_name + " disabled, shutting down.")
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:
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
except Exception as e:
err_messages.append(str(e))
chains_formatted = []
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
sorted_names = sorted(swap_client.settings["chainclients"].keys())
from basicswap.interface.electrumx import (
DEFAULT_ELECTRUM_SERVERS,
DEFAULT_ONION_SERVERS,
)
for name in sorted_names:
c = swap_client.settings["chainclients"][name]
try:
display_name = getCoinName(swap_client.getCoinIdFromName(name))
except Exception:
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(
{
"name": name,
@@ -176,6 +292,10 @@ def page_settings(self, url_split, post_string):
"lookups": c.get("chain_lookups", "local"),
"manage_daemon": c.get("manage_daemon", "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"):
@@ -203,6 +323,14 @@ def page_settings(self, url_split, post_string):
else:
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 = {
"debug": swap_client.debug,
"debug_ui": swap_client.debug_ui,
+203 -7
View File
@@ -32,11 +32,15 @@ DONATION_ADDRESSES = {
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 = {
"name": ci.coin_name(),
"version": w.get("version", "?"),
"ticker": ci.ticker_mainnet(),
"cid": str(int(ci.coin_type())),
"cid": str(int(coin_id)),
"balance": w.get("balance", "?"),
"blocks": w.get("blocks", "?"),
"synced": w.get("synced", "?"),
@@ -45,6 +49,7 @@ def format_wallet_data(swap_client, ci, w):
"locked": w.get("locked", "?"),
"updating": w.get("updating", "?"),
"havedata": True,
"connection_type": connection_type,
}
if "wallet_blocks" in w:
@@ -70,6 +75,9 @@ def format_wallet_data(swap_client, ci, w):
if pending > 0.0:
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:
wf["stealth_address"] = w.get("stealth_address", "?")
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_balance"] = w.get("mweb_balance", "?")
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)
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):
server = self.server
swap_client = server.swap_client
@@ -131,6 +225,7 @@ def page_wallets(self, url_split, post_string):
"err_messages": err_messages,
"wallets": wallets_formatted,
"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
withdrawal_successful: bool = False
force_refresh: bool = False
tx_filters = {
"page_no": 1,
"limit": 30,
"offset": 0,
}
form_data = self.checkForm(post_string, "wallet", err_messages)
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))
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
@@ -161,8 +273,13 @@ def page_wallet(self, url_split, post_string):
swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True
elif have_data_entry(form_data, "convertmweb_" + cid):
txid = swap_client.ci(coin_id).convertMWEBBalance()
messages.append(f"Converted MWEB change to LTC in tx: {txid}")
elif have_data_entry(form_data, "newmwebaddr_" + cid):
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):
try:
swap_client.reseedWallet(coin_id)
@@ -170,6 +287,22 @@ def page_wallet(self, url_split, post_string):
except Exception as ex:
err_messages.append("Reseed failed " + str(ex))
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:
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
page_data["wd_subfee_" + cid] = subfee
@@ -208,14 +341,21 @@ def page_wallet(self, url_split, post_string):
page_data["wd_type_to_" + cid] = type_to
except Exception as e: # noqa: F841
err_messages.append("Missing type")
elif coin_id == Coins.LTC:
elif coin_id in (Coins.LTC, Coins.FIRO):
try:
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
0
].decode("utf-8")
page_data["wd_type_from_" + cid] = type_from
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:
ci = swap_client.ci(coin_id)
@@ -230,9 +370,9 @@ def page_wallet(self, url_split, post_string):
value, ticker, type_from, type_to, address, txid
)
)
elif coin_id == Coins.LTC:
txid = swap_client.withdrawLTC(
type_from, value, address, subfee
elif coin_id in (Coins.LTC, Coins.FIRO):
txid = swap_client.withdrawCoinExtended(
coin_id, type_from, value, address, subfee
)
messages.append(
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
@@ -305,8 +445,12 @@ def page_wallet(self, url_split, post_string):
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
is_electrum_mode = (
swap_client.coin_clients.get(coin_id, {}).get("connection_type") == "electrum"
)
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})
wallet_data = {}
@@ -328,6 +472,18 @@ def page_wallet(self, url_split, post_string):
cid = str(int(coin_id))
wallet_data = format_wallet_data(swap_client, ci, w)
wallet_data["is_electrum_mode"] = (
getattr(ci, "_connection_type", "rpc") == "electrum"
)
if hasattr(ci, "getAccountKey") and k not in (Coins.XMR, Coins.WOW):
try:
chain = swap_client.chain
zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC
seed_key = swap_client.getWalletKey(k, 1)
wallet_data["account_key"] = ci.getAccountKey(seed_key, zprv_prefix)
except Exception:
pass
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
@@ -342,6 +498,8 @@ def page_wallet(self, url_split, post_string):
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
elif k == Coins.LTC:
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:
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
@@ -370,6 +528,10 @@ def page_wallet(self, url_split, post_string):
// page_data["fee_estimate"]["sum_weight"]
)
if k == Coins.LTC and ci.useBackend() is False:
mweb_value: int = ci.getMWEBBalance()
if mweb_value > 0:
wallet_data["mweb_in_plain"] = ci.format_amount(mweb_value)
if show_utxo_groups:
utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr()
@@ -400,6 +562,33 @@ def page_wallet(self, url_split, post_string):
"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)
if all_txs and coin_id not in (Coins.XMR, Coins.WOW):
all_txs = list(reversed(all_txs))
elif not all_txs:
all_txs = []
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")
return self.render_template(
template,
@@ -411,5 +600,12 @@ def page_wallet(self, url_split, post_string):
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
"donation_info": donation_info,
"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 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -251,7 +251,7 @@ def describeBid(
elif bid.state == BidStates.BID_ABANDONED:
state_description = "Bid abandoned"
elif bid.state == BidStates.BID_ERROR:
state_description = bid.state_note
state_description = "Bid error"
elif offer.swap_type == SwapTypes.XMR_SWAP:
if bid.state == BidStates.BID_SENT:
state_description = "Waiting for offerer to accept"
@@ -331,6 +331,7 @@ def describeBid(
"ticker_from": ci_from.ticker(),
"ticker_to": ci_to.ticker(),
"bid_state": strBidState(bid.state),
"bid_state_ind": int(bid.state),
"state_description": state_description,
"itx_state": strTxState(bid.getITxState()),
"ptx_state": strTxState(bid.getPTxState()),
@@ -343,6 +344,8 @@ def describeBid(
if for_api
else format_timestamp(bid.created_at, with_seconds=True)
),
"created_at_timestamp": bid.created_at,
"state_time_timestamp": getLastStateTimestamp(bid),
"expired_at": (
bid.expire_at
if for_api
@@ -623,6 +626,14 @@ def listOldBidStates(bid):
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):
if c == Coins.PART_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():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
if v["connection_type"] in ("rpc", "electrum"):
coins.append((int(k), getCoinName(k)))
if split_from:
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():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
if v["connection_type"] in ("rpc", "electrum"):
balance = "0.0"
if k in wallets:
@@ -735,10 +746,23 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
if wallet_info["stealth_address"] != "?":
if not ci.isAddressMine(wallet_info["stealth_address"]):
ci._log.error(
"Unowned stealth address: {}".format(wallet_info["stealth_address"])
ci._log.warning(
"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 (
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 wallet_info["deposit_address"] != "Refresh necessary":
if not ci.isAddressMine(wallet_info["deposit_address"]):
ci._log.error(
"Unowned deposit address: {}".format(wallet_info["deposit_address"])
ci._log.warning(
"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 (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
):
+5 -3
View File
@@ -10,7 +10,6 @@ import json
import time
import decimal
COIN = 100000000
@@ -190,11 +189,14 @@ def format_amount(i: int, display_scale: int, scale: int = None) -> str:
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"
if with_seconds:
str_format += ":%S"
str_format += " %z"
if with_timezone:
str_format += " %z"
return time.strftime(str_format, time.localtime(value))
+18
View File
@@ -45,3 +45,21 @@ class BSXLogger(logging.Logger):
def info_s(self, msg, *args, **kwargs):
if self.safe_logs is False:
self.info(msg, *args, **kwargs)
class BSXLogAdapter(logging.LoggerAdapter):
def __init__(self, logger, prefix):
super().__init__(logger, {})
self.prefix = prefix
def process(self, msg, kwargs):
return f"{self.prefix} {msg}", kwargs
def addr(self, addr: str) -> str:
return self.logger.addr(addr)
def id(self, concept_id: bytes, prefix: str = "") -> str:
return self.logger.id(concept_id, prefix)
def info_s(self, msg, *args, **kwargs):
return self.logger.info_s(msg, *args, **kwargs)
-1
View File
@@ -25,7 +25,6 @@ from basicswap.contrib.test_framework.messages import (
uint256_from_str,
)
AES_BLOCK_SIZE = 16
+823
View File
@@ -0,0 +1,823 @@
# -*- 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
last_activity = getattr(self._server, "_last_activity", 0)
most_recent = max(height_time, last_activity)
is_synced = height > 0 and (now - most_recent) < 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
+7
View File
@@ -0,0 +1,7 @@
# LTC Notes
## MWEB
Sending LTC -> MWEB generates MWEB change outputs in the plain LTC wallet that BSX can't use.
A temporary convenience function is provided to convert those MWEB outputs back to plain LTC.
+6
View File
@@ -140,6 +140,12 @@ Observe progress with
tail -f /tmp/firo.log
Alternatively --extracoinopts can be used with --startonlycoin
docker-compose run --rm swapclient \
basicswap-run --datadir=/coindata --startonlycoin=litecoin --extracoinopts="-reindex"
## Start a subset of the configured coins using docker
docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero
+29
View File
@@ -1,3 +1,32 @@
0.16.3
==============
- Automatic fee validation.
- Prevent sending bids to offers
- Reject received offers, and
- Prevent sending offers where the chain feerates are out of range.
- Valid feerate range is the node's estimated feerate for confirmation in 24 blocks to 4x the estimated feerate.
- The minimum feerate confirmation can be adjusted with the "low_fee_conf_target" setting.
- If "low_feerate" is set above 0 it is used instead of the dynamic feerate with "low_fee_conf_target".
- The maximum feerate multiplier can be adjusted with the "high_estimated_feerate_multiplier" setting.
- If "high_estimated_feerate_multiplier" is set below 1.0 the max feerate can be set with the "high_feerate" setting.
- New setting "startup_delay":
- Adjusts the time waited for coin daemons to start between "startup_tries".
- Valid as a base setting and can be overridden per coin with chainclients settings.
- Add subfee bids.
- Enables a user to create a bid specifying the amount before the lock tx fee.
- Currently only works when the coin to is not XMR like.
- Set Adaptor sig bid type as default where possible.
- UI:
- offer page:
- Fixed feerate from other chain displayed for reversed swaps.
- Added warning text for fee above 1.2 x local estimate.
- Added subfee bid option.
- Increase DCR fee estimate by 1 byte.
- Waits for the refund and refund spend txn locks to expire before trying to submit them.
- Fixed bug where initiate tx amount was not checked for secret hash swaps.
0.14.5
==============
+2 -2
View File
@@ -79,13 +79,13 @@ def main():
continue
if coin_name in ("monero", "wownero"):
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:
for line in fp_in:
fp.write(line)
fpp.write(line)
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:
for line in fp_in:
fp.write(line)
+5 -5
View File
@@ -90,7 +90,7 @@
(define python-coincurve-basicswap
(package
(name "python-coincurve-basicswap")
(version "basicswap_v0.2")
(version "basicswap_v0.3")
(source
(origin
(method git-fetch)
@@ -101,7 +101,7 @@
(file-name
(git-file-name name version))
(sha256
(base32 "1vm9cvwr0z02zc0mp7l8qj9vhg8kmfrzysiwzg91zkgmccza9ryc"))))
(base32 "08bc8175v4d479lgavkcclc0kkh3icxm9i0i26wqd1g3bv0is8cm"))))
(build-system pyproject-build-system)
(arguments
`(#:phases
@@ -135,15 +135,15 @@
(define-public basicswap
(package
(name "basicswap")
(version "0.15.1")
(version "0.16.2")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/basicswap/basicswap")
(commit "0bc9d3a5db40f54d79e2ab18be58b6bbc20740d1")))
(commit "ced017ab3a3234c68d3d8f773cf9ceb187a39adb")))
(sha256
(base32
"1x6c6hynvbayq4cyv9s6vwgsgdmhm7r1av6iy7pax103lj20habf"))
"0manck3zlf05by08b825ynqk7q1byzgy7p3i8chpg413mqkx7q5r"))
(file-name (git-file-name name version))))
(build-system pyproject-build-system)
+8
View File
@@ -48,3 +48,11 @@ allow-direct-references = true
[tool.ruff]
exclude = ["basicswap/contrib","basicswap/interface/contrib"]
[tool.codespell]
check-filenames = true
disable-colors = true
quiet-level = 7
dictionary = "tests/lint/spelling.extra_dictionary.txt,-"
ignore-words = "tests/lint/spelling.ignore-words.txt"
skip = ".git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static"
+3 -3
View File
@@ -1,7 +1,7 @@
pyzmq==26.2.1
python-gnupg==0.5.5
pyzmq==27.1.0
python-gnupg==0.5.6
Jinja2==3.1.6
pycryptodome==3.23.0
PySocks==1.7.1
websocket-client==1.9.0
coincurve@https://github.com/basicswap/coincurve/archive/refs/tags/basicswap_v0.2.zip
coincurve@https://github.com/basicswap/coincurve/archive/refs/tags/basicswap_v0.3.zip
+190 -192
View File
@@ -4,81 +4,8 @@
#
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
#
asn1crypto==1.5.1 \
--hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \
--hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67
# via coincurve
cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
# via coincurve
coincurve @ https://github.com/basicswap/coincurve/archive/refs/tags/basicswap_v0.2.zip \
--hash=sha256:c309deef22c929c9ab5b3adf7adbda940bffcea6c6ec7c66202d6c3d4e3ceb79
coincurve @ https://github.com/basicswap/coincurve/archive/refs/tags/basicswap_v0.3.zip \
--hash=sha256:41e40a335994112938984097b089142acceb24324d76a7e35d8b9d14c2cfa8b5
# via -r requirements.in
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
@@ -147,10 +74,6 @@ markupsafe==3.0.2 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
--hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
# via jinja2
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
pycryptodome==3.23.0 \
--hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \
--hash=sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c \
@@ -199,120 +122,195 @@ pysocks==1.7.1 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via -r requirements.in
python-gnupg==0.5.5 \
--hash=sha256:3fdcaf76f60a1b948ff8e37dc398d03cf9ce7427065d583082b92da7a4ff5a63 \
--hash=sha256:51fa7b8831ff0914bc73d74c59b99c613de7247b91294323c39733bb85ac3fc1
python-gnupg==0.5.6 \
--hash=sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac \
--hash=sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a
# via -r requirements.in
pyzmq==26.2.1 \
--hash=sha256:000760e374d6f9d1a3478a42ed0c98604de68c9e94507e5452951e598ebecfba \
--hash=sha256:004837cb958988c75d8042f5dac19a881f3d9b3b75b2f574055e22573745f841 \
--hash=sha256:0250c94561f388db51fd0213cdccbd0b9ef50fd3c57ce1ac937bf3034d92d72e \
--hash=sha256:03719e424150c6395b9513f53a5faadcc1ce4b92abdf68987f55900462ac7eec \
--hash=sha256:0995fd3530f2e89d6b69a2202e340bbada3191014352af978fa795cb7a446331 \
--hash=sha256:099b56ef464bc355b14381f13355542e452619abb4c1e57a534b15a106bf8e23 \
--hash=sha256:09dac387ce62d69bec3f06d51610ca1d660e7849eb45f68e38e7f5cf1f49cbcb \
--hash=sha256:0b2007f28ce1b8acebdf4812c1aab997a22e57d6a73b5f318b708ef9bcabbe95 \
--hash=sha256:0b6a93d684278ad865fc0b9e89fe33f6ea72d36da0e842143891278ff7fd89c3 \
--hash=sha256:0f50db737d688e96ad2a083ad2b453e22865e7e19c7f17d17df416e91ddf67eb \
--hash=sha256:100a826a029c8ef3d77a1d4c97cbd6e867057b5806a7276f2bac1179f893d3bf \
--hash=sha256:1238c2448c58b9c8d6565579393148414a42488a5f916b3f322742e561f6ae0d \
--hash=sha256:160194d1034902937359c26ccfa4e276abffc94937e73add99d9471e9f555dd6 \
--hash=sha256:17d72a74e5e9ff3829deb72897a175333d3ef5b5413948cae3cf7ebf0b02ecca \
--hash=sha256:17f88622b848805d3f6427ce1ad5a2aa3cf61f12a97e684dab2979802024d460 \
--hash=sha256:1c6ae0e95d0a4b0cfe30f648a18e764352d5415279bdf34424decb33e79935b8 \
--hash=sha256:1c84c1297ff9f1cd2440da4d57237cb74be21fdfe7d01a10810acba04e79371a \
--hash=sha256:1fd4b3efc6f62199886440d5e27dd3ccbcb98dfddf330e7396f1ff421bfbb3c2 \
--hash=sha256:25e720dba5b3a3bb2ad0ad5d33440babd1b03438a7a5220511d0c8fa677e102e \
--hash=sha256:269c14904da971cb5f013100d1aaedb27c0a246728c341d5d61ddd03f463f2f3 \
--hash=sha256:290c96f479504439b6129a94cefd67a174b68ace8a8e3f551b2239a64cfa131a \
--hash=sha256:2d88ba221a07fc2c5581565f1d0fe8038c15711ae79b80d9462e080a1ac30435 \
--hash=sha256:2e1eb9d2bfdf5b4e21165b553a81b2c3bd5be06eeddcc4e08e9692156d21f1f6 \
--hash=sha256:31fff709fef3b991cfe7189d2cfe0c413a1d0e82800a182cfa0c2e3668cd450f \
--hash=sha256:361edfa350e3be1f987e592e834594422338d7174364763b7d3de5b0995b16f3 \
--hash=sha256:36d4e7307db7c847fe37413f333027d31c11d5e6b3bacbb5022661ac635942ba \
--hash=sha256:36ee4297d9e4b34b5dc1dd7ab5d5ea2cbba8511517ef44104d2915a917a56dc8 \
--hash=sha256:380816d298aed32b1a97b4973a4865ef3be402a2e760204509b52b6de79d755d \
--hash=sha256:3ef584f13820d2629326fe20cc04069c21c5557d84c26e277cfa6235e523b10f \
--hash=sha256:3fe6e28a8856aea808715f7a4fc11f682b9d29cac5d6262dd8fe4f98edc12d53 \
--hash=sha256:44dba28c34ce527cf687156c81f82bf1e51f047838d5964f6840fd87dfecf9fe \
--hash=sha256:45fad32448fd214fbe60030aa92f97e64a7140b624290834cc9b27b3a11f9473 \
--hash=sha256:46d4ebafc27081a7f73a0f151d0c38d4291656aa134344ec1f3d0199ebfbb6d4 \
--hash=sha256:49135bb327fca159262d8fd14aa1f4a919fe071b04ed08db4c7c37d2f0647162 \
--hash=sha256:4a98898fdce380c51cc3e38ebc9aa33ae1e078193f4dc641c047f88b8c690c9a \
--hash=sha256:4eb3197f694dfb0ee6af29ef14a35f30ae94ff67c02076eef8125e2d98963cd0 \
--hash=sha256:51431f6b2750eb9b9d2b2952d3cc9b15d0215e1b8f37b7a3239744d9b487325d \
--hash=sha256:574b285150afdbf0a0424dddf7ef9a0d183988eb8d22feacb7160f7515e032cb \
--hash=sha256:57dd4d91b38fa4348e237a9388b4423b24ce9c1695bbd4ba5a3eada491e09399 \
--hash=sha256:59660e15c797a3b7a571c39f8e0b62a1f385f98ae277dfe95ca7eaf05b5a0f12 \
--hash=sha256:5b4fc44f5360784cc02392f14235049665caaf7c0fe0b04d313e763d3338e463 \
--hash=sha256:632a09c6d8af17b678d84df442e9c3ad8e4949c109e48a72f805b22506c4afa7 \
--hash=sha256:637536c07d2fb6a354988b2dd1d00d02eb5dd443f4bbee021ba30881af1c28aa \
--hash=sha256:651726f37fcbce9f8dd2a6dab0f024807929780621890a4dc0c75432636871be \
--hash=sha256:6991ee6c43e0480deb1b45d0c7c2bac124a6540cba7db4c36345e8e092da47ce \
--hash=sha256:6d75fcb00a1537f8b0c0bb05322bc7e35966148ffc3e0362f0369e44a4a1de99 \
--hash=sha256:70b3a46ecd9296e725ccafc17d732bfc3cdab850b54bd913f843a0a54dfb2c04 \
--hash=sha256:786dd8a81b969c2081b31b17b326d3a499ddd1856e06d6d79ad41011a25148da \
--hash=sha256:7c6160fe513654e65665332740f63de29ce0d165e053c0c14a161fa60dd0da01 \
--hash=sha256:7ebdd96bd637fd426d60e86a29ec14b8c1ab64b8d972f6a020baf08a30d1cf46 \
--hash=sha256:7f18ce33f422d119b13c1363ed4cce245b342b2c5cbbb76753eabf6aa6f69c7d \
--hash=sha256:80a00370a2ef2159c310e662c7c0f2d030f437f35f478bb8b2f70abd07e26b24 \
--hash=sha256:817fcd3344d2a0b28622722b98500ae9c8bfee0f825b8450932ff19c0b15bebd \
--hash=sha256:8531ed35dfd1dd2af95f5d02afd6545e8650eedbf8c3d244a554cf47d8924459 \
--hash=sha256:866c12b7c90dd3a86983df7855c6f12f9407c8684db6aa3890fc8027462bda82 \
--hash=sha256:88812b3b257f80444a986b3596e5ea5c4d4ed4276d2b85c153a6fbc5ca457ae7 \
--hash=sha256:8b0f5bab40a16e708e78a0c6ee2425d27e1a5d8135c7a203b4e977cee37eb4aa \
--hash=sha256:8bacc1a10c150d58e8a9ee2b2037a70f8d903107e0f0b6e079bf494f2d09c091 \
--hash=sha256:8ec8e3aea6146b761d6c57fcf8f81fcb19f187afecc19bf1701a48db9617a217 \
--hash=sha256:8eddb3784aed95d07065bcf94d07e8c04024fdb6b2386f08c197dfe6b3528fda \
--hash=sha256:9027a7fcf690f1a3635dc9e55e38a0d6602dbbc0548935d08d46d2e7ec91f454 \
--hash=sha256:90dc731d8e3e91bcd456aa7407d2eba7ac6f7860e89f3766baabb521f2c1de4a \
--hash=sha256:91e2bfb8e9a29f709d51b208dd5f441dc98eb412c8fe75c24ea464734ccdb48e \
--hash=sha256:95f5728b367a042df146cec4340d75359ec6237beebf4a8f5cf74657c65b9257 \
--hash=sha256:95f7b01b3f275504011cf4cf21c6b885c8d627ce0867a7e83af1382ebab7b3ff \
--hash=sha256:97cbb368fd0debdbeb6ba5966aa28e9a1ae3396c7386d15569a6ca4be4572b99 \
--hash=sha256:9ec6abfb701437142ce9544bd6a236addaf803a32628d2260eb3dbd9a60e2891 \
--hash=sha256:9fbdb90b85c7624c304f72ec7854659a3bd901e1c0ffb2363163779181edeb68 \
--hash=sha256:a003200b6cd64e89b5725ff7e284a93ab24fd54bbac8b4fa46b1ed57be693c27 \
--hash=sha256:a0741edbd0adfe5f30bba6c5223b78c131b5aa4a00a223d631e5ef36e26e6d13 \
--hash=sha256:a23948554c692df95daed595fdd3b76b420a4939d7a8a28d6d7dea9711878641 \
--hash=sha256:a4bffcadfd40660f26d1b3315a6029fd4f8f5bf31a74160b151f5c577b2dc81b \
--hash=sha256:a6549ecb0041dafa55b5932dcbb6c68293e0bd5980b5b99f5ebb05f9a3b8a8f3 \
--hash=sha256:a7ad34a2921e8f76716dc7205c9bf46a53817e22b9eec2e8a3e08ee4f4a72468 \
--hash=sha256:abf7b5942c6b0dafcc2823ddd9154f419147e24f8df5b41ca8ea40a6db90615c \
--hash=sha256:b314268e716487bfb86fcd6f84ebbe3e5bec5fac75fdf42bc7d90fdb33f618ad \
--hash=sha256:baa1da72aecf6a490b51fba7a51f1ce298a1e0e86d0daef8265c8f8f9848eb77 \
--hash=sha256:bd8fdee945b877aa3bffc6a5a8816deb048dab0544f9df3731ecd0e54d8c84c9 \
--hash=sha256:bdbc78ae2065042de48a65f1421b8af6b76a0386bb487b41955818c3c1ce7bed \
--hash=sha256:c059883840e634a21c5b31d9b9a0e2b48f991b94d60a811092bc37992715146a \
--hash=sha256:c1bb37849e2294d519117dd99b613c5177934e5c04a5bb05dd573fa42026567e \
--hash=sha256:c2a9cb17fd83b7a3a3009901aca828feaf20aa2451a8a487b035455a86549c09 \
--hash=sha256:c7154d228502e18f30f150b7ce94f0789d6b689f75261b623f0fdc1eec642aab \
--hash=sha256:cdb69710e462a38e6039cf17259d328f86383a06c20482cc154327968712273c \
--hash=sha256:ceb0d78b7ef106708a7e2c2914afe68efffc0051dc6a731b0dbacd8b4aee6d68 \
--hash=sha256:d14f50d61a89b0925e4d97a0beba6053eb98c426c5815d949a43544f05a0c7ec \
--hash=sha256:d51a7bfe01a48e1064131f3416a5439872c533d756396be2b39e3977b41430f9 \
--hash=sha256:d9da0289d8201c8a29fd158aaa0dfe2f2e14a181fd45e2dc1fbf969a62c1d594 \
--hash=sha256:e5e33b1491555843ba98d5209439500556ef55b6ab635f3a01148545498355e5 \
--hash=sha256:e76ad4729c2f1cf74b6eb1bdd05f6aba6175999340bd51e6caee49a435a13bf5 \
--hash=sha256:e7eeaef81530d0b74ad0d29eec9997f1c9230c2f27242b8d17e0ee67662c8f6e \
--hash=sha256:e8e47050412f0ad3a9b2287779758073cbf10e460d9f345002d4779e43bb0136 \
--hash=sha256:ed038a921df836d2f538e509a59cb638df3e70ca0fcd70d0bf389dfcdf784d2a \
--hash=sha256:edb550616f567cd5603b53bb52a5f842c0171b78852e6fc7e392b02c2a1504bb \
--hash=sha256:ee7152f32c88e0e1b5b17beb9f0e2b14454235795ef68c0c120b6d3d23d12833 \
--hash=sha256:eeb37f65350d5c5870517f02f8bbb2ac0fbec7b416c0f4875219fef305a89a45 \
--hash=sha256:ef29630fde6022471d287c15c0a2484aba188adbfb978702624ba7a54ddfa6c1 \
--hash=sha256:ef5479fac31df4b304e96400fc67ff08231873ee3537544aa08c30f9d22fce38 \
--hash=sha256:f0019cc804ac667fb8c8eaecdb66e6d4a68acf2e155d5c7d6381a5645bd93ae4 \
--hash=sha256:f0f19c2097fffb1d5b07893d75c9ee693e9cbc809235cf3f2267f0ef6b015f24 \
--hash=sha256:f19dae58b616ac56b96f2e2290f2d18730a898a171f447f491cc059b073ca1fa \
--hash=sha256:f1f31661a80cc46aba381bed475a9135b213ba23ca7ff6797251af31510920ce \
--hash=sha256:f2c307fbe86e18ab3c885b7e01de942145f539165c3360e2af0f094dd440acd9 \
--hash=sha256:f32718ee37c07932cc336096dc7403525301fd626349b6eff8470fe0f996d8d7 \
--hash=sha256:f39d1227e8256d19899d953e6e19ed2ccb689102e6d85e024da5acf410f301eb \
--hash=sha256:f5eeeb82feec1fc5cbafa5ee9022e87ffdb3a8c48afa035b356fcd20fc7f533f \
--hash=sha256:f92a002462154c176dac63a8f1f6582ab56eb394ef4914d65a9417f5d9fde218 \
--hash=sha256:f9ba5def063243793dec6603ad1392f735255cbc7202a3a484c14f99ec290705 \
--hash=sha256:fc409c18884eaf9ddde516d53af4f2db64a8bc7d81b1a0c274b8aa4e929958e8
pyzmq==27.1.0 \
--hash=sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d \
--hash=sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d \
--hash=sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8 \
--hash=sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8 \
--hash=sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e \
--hash=sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e \
--hash=sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba \
--hash=sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba \
--hash=sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581 \
--hash=sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581 \
--hash=sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05 \
--hash=sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05 \
--hash=sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386 \
--hash=sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386 \
--hash=sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db \
--hash=sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db \
--hash=sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28 \
--hash=sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28 \
--hash=sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e \
--hash=sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e \
--hash=sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea \
--hash=sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea \
--hash=sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b \
--hash=sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b \
--hash=sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066 \
--hash=sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066 \
--hash=sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97 \
--hash=sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97 \
--hash=sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0 \
--hash=sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0 \
--hash=sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113 \
--hash=sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113 \
--hash=sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92 \
--hash=sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92 \
--hash=sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3 \
--hash=sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3 \
--hash=sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86 \
--hash=sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86 \
--hash=sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd \
--hash=sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd \
--hash=sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc \
--hash=sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc \
--hash=sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233 \
--hash=sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233 \
--hash=sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345 \
--hash=sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345 \
--hash=sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31 \
--hash=sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31 \
--hash=sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74 \
--hash=sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74 \
--hash=sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc \
--hash=sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc \
--hash=sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429 \
--hash=sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429 \
--hash=sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381 \
--hash=sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381 \
--hash=sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172 \
--hash=sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172 \
--hash=sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b \
--hash=sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b \
--hash=sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556 \
--hash=sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556 \
--hash=sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4 \
--hash=sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4 \
--hash=sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27 \
--hash=sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27 \
--hash=sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c \
--hash=sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c \
--hash=sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd \
--hash=sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd \
--hash=sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e \
--hash=sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e \
--hash=sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526 \
--hash=sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526 \
--hash=sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e \
--hash=sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e \
--hash=sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f \
--hash=sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f \
--hash=sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128 \
--hash=sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128 \
--hash=sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96 \
--hash=sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96 \
--hash=sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f \
--hash=sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f \
--hash=sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027 \
--hash=sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027 \
--hash=sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa \
--hash=sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa \
--hash=sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f \
--hash=sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f \
--hash=sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50 \
--hash=sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50 \
--hash=sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c \
--hash=sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c \
--hash=sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2 \
--hash=sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2 \
--hash=sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146 \
--hash=sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146 \
--hash=sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d \
--hash=sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d \
--hash=sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97 \
--hash=sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97 \
--hash=sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5 \
--hash=sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5 \
--hash=sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098 \
--hash=sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098 \
--hash=sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb \
--hash=sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb \
--hash=sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32 \
--hash=sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32 \
--hash=sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62 \
--hash=sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62 \
--hash=sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf \
--hash=sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf \
--hash=sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312 \
--hash=sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312 \
--hash=sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda \
--hash=sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda \
--hash=sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540 \
--hash=sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540 \
--hash=sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604 \
--hash=sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604 \
--hash=sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0 \
--hash=sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0 \
--hash=sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db \
--hash=sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db \
--hash=sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78 \
--hash=sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78 \
--hash=sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f \
--hash=sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f \
--hash=sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef \
--hash=sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef \
--hash=sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2 \
--hash=sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2 \
--hash=sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78 \
--hash=sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78 \
--hash=sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b \
--hash=sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b \
--hash=sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f \
--hash=sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f \
--hash=sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6 \
--hash=sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6 \
--hash=sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39 \
--hash=sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39 \
--hash=sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f \
--hash=sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f \
--hash=sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355 \
--hash=sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355 \
--hash=sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a \
--hash=sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a \
--hash=sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a \
--hash=sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a \
--hash=sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856 \
--hash=sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856 \
--hash=sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9 \
--hash=sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9 \
--hash=sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968 \
--hash=sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968 \
--hash=sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7 \
--hash=sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7 \
--hash=sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1 \
--hash=sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1 \
--hash=sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266 \
--hash=sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266 \
--hash=sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831 \
--hash=sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831 \
--hash=sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f \
--hash=sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f \
--hash=sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7 \
--hash=sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7 \
--hash=sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394 \
--hash=sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394 \
--hash=sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07 \
--hash=sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07 \
--hash=sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496 \
--hash=sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496 \
--hash=sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90 \
--hash=sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90 \
--hash=sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271 \
--hash=sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271 \
--hash=sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6 \
--hash=sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6 \
--hash=sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9 \
--hash=sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9
# via -r requirements.in
websocket-client==1.9.0 \
--hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \
+19 -6
View File
@@ -729,7 +729,25 @@ def process_offers(args, config, script_state) -> None:
matching_sent_offers.append(offer)
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(
"Revoking offer {}, wallet from balance below minimum".format(
offer_id
@@ -1169,11 +1187,6 @@ def process_offers(args, config, script_state) -> None:
)
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:
print(
"Creating offer for: {} at rate: {}".format(
+69 -5
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
@@ -15,12 +14,12 @@ import subprocess
from urllib.request import urlopen
from .util import read_json_api
from basicswap.basicswap import Coins
from basicswap.rpc import callrpc
from basicswap.util import toBool
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.bin.prepare import downloadPIVXParams
TEST_HTTP_HOST = os.getenv(
"TEST_HTTP_HOST", "127.0.0.1"
) # Set to 0.0.0.0 when used in docker
@@ -94,7 +93,7 @@ def prepareDataDir(
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
fp.write("txindex=1\n")
fp.write("wallet=wallet.dat\n")
fp.write("wallet=bsx_wallet\n")
fp.write("findpeers=0\n")
@@ -125,6 +124,65 @@ def prepareDataDir(
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,
f"wallets/{coin_ticker.lower()}/withdraw",
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):
try:
if "bip9_softforks" in ro:
@@ -177,11 +235,17 @@ def wait_for_bid(
)
if isinstance(state, (list, tuple)):
if bid[5] in state:
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return
else:
continue
elif state is not None and state != bid[5]:
continue
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return
else:
if i > 0 and i % 10 == 0:
@@ -230,7 +294,7 @@ def wait_for_event(
def wait_for_offer(delay_event, swap_client, offer_id, wait_for=20):
logging.info("wait_for_offer %s", offer_id.hex())
logging.info(f"wait_for_offer {offer_id.hex()}")
for i in range(wait_for):
if delay_event.is_set():
raise ValueError("Test stopped.")
+23 -25
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -56,7 +55,6 @@ from tests.basicswap.extended.test_doge import (
import basicswap.config as cfg
import basicswap.bin.run as runSystem
TEST_PATH = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap1"))
PARTICL_PORT_BASE = int(os.getenv("PARTICL_PORT_BASE", BASE_PORT))
@@ -139,6 +137,7 @@ def run_prepare(
use_rpcauth=False,
extra_settings={},
port_ofs=0,
extra_args=[],
):
config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME)
@@ -180,7 +179,7 @@ def run_prepare(
"-noextractover",
"-noreleasesizecheck",
"-xmrrestoreheight=0",
]
] + extra_args
if mnemonic_in:
testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
@@ -531,9 +530,7 @@ def run_prepare(
for opt in EXTRA_CONFIG_JSON.get("doge{}".format(node_id), []):
fp.write(opt + "\n")
with open(config_path) as fs:
settings = json.load(fs)
settings["startup_delay"] = 1
settings["min_delay_event"] = 1
settings["max_delay_event"] = 4
settings["min_delay_event_short"] = 1
@@ -585,7 +582,7 @@ def prepare_nodes(
class TestBase(unittest.TestCase):
def setUpClass(cls):
super(TestBase, cls).setUpClass()
super().setUpClass()
cls.delay_event = threading.Event()
signal.signal(
@@ -622,6 +619,18 @@ class TestBase(unittest.TestCase):
raise ValueError(f"wait_for_particl_height failed http_port: {http_port}")
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 XmrTestBase(TestBase):
@classmethod
def setUpClass(cls):
@@ -632,27 +641,17 @@ class XmrTestBase(TestBase):
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):
multiprocessing.set_start_method("spawn")
self.delay_event.clear()
for i in range(3):
self.processes.append(
multiprocessing.Process(target=self.run_thread, args=(i,))
multiprocessing.Process(target=run_process, args=(i,))
)
self.processes[-1].start()
waitForServer(self.delay_event, 12701)
waitForServer(self.delay_event, 12701, 60)
def waitForMainAddress():
for i in range(20):
@@ -664,13 +663,12 @@ class XmrTestBase(TestBase):
)
return wallets["XMR"]["main_address"]
except Exception as e:
print("Waiting for main address {}".format(str(e)))
print(f"Waiting for main address {e}")
self.delay_event.wait(1)
raise ValueError("waitForMainAddress timedout")
xmr_addr1 = waitForMainAddress()
num_blocks = 100
num_blocks: int = 100
xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "":
@@ -682,7 +680,7 @@ class XmrTestBase(TestBase):
]
< num_blocks
):
logging.info("Mining {} Monero blocks to {}.".format(num_blocks, xmr_addr1))
logging.info(f"Mining {num_blocks} Monero blocks to {xmr_addr1}.")
callrpc_xmr(
XMR_BASE_RPC_PORT + 1,
"generateblocks",
+21 -19
View File
@@ -65,7 +65,6 @@ from tests.basicswap.common import (
)
from basicswap.bin.run import startDaemon
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
@@ -112,7 +111,7 @@ def prepareOtherDir(datadir, nodeId, conf_file="dash.conf"):
fp.write("acceptnonstdtxn=0\n")
if conf_file == "bitcoin.conf":
fp.write("wallet=wallet.dat\n")
fp.write("wallet=bsx_wallet\n")
def prepareDir(datadir, nodeId, network_key, network_pubkey):
@@ -137,7 +136,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("zmqpubsmsg=tcp://127.0.0.1:" + str(BASE_ZMQ_PORT + nodeId) + "\n")
fp.write("wallet=wallet.dat\n")
fp.write("wallet=bsx_wallet\n")
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
@@ -176,6 +175,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing
"wallet_name": "bsx_wallet",
},
"dash": {
"connection_type": "rpc",
@@ -185,6 +185,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"bindir": DASH_BINDIR,
"use_csv": True,
"use_segwit": False,
"wallet_name": "bsx_wallet",
},
"bitcoin": {
"connection_type": "rpc",
@@ -193,6 +194,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True,
"wallet_name": "bsx_wallet",
},
},
"check_progress_seconds": 2,
@@ -286,7 +288,7 @@ class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
super().setUpClass()
k = PrivateKey()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret)
@@ -313,7 +315,7 @@ class Test(unittest.TestCase):
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"-wallet=bsx_wallet -legacy create",
"bitcoin-wallet",
)
except Exception:
@@ -321,7 +323,7 @@ class Test(unittest.TestCase):
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=wallet.dat create",
"-wallet=bsx_wallet create",
"bitcoin-wallet",
)
cls.daemons.append(startDaemon(btc_data_dir, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
@@ -333,7 +335,7 @@ class Test(unittest.TestCase):
if os.path.exists(os.path.join(DASH_BINDIR, 'dash-wallet')):
logging.info('Creating DASH wallet.')
callrpc_cli(DASH_BINDIR, dash_data_dir, 'regtest', '-wallet=wallet.dat create', 'dash-wallet')
callrpc_cli(DASH_BINDIR, dash_data_dir, 'regtest', '-wallet=bsx_wallet create', 'dash-wallet')
"""
cls.daemons.append(startDaemon(dash_data_dir, DASH_BINDIR, DASHD))
logging.info("Started %s %d", DASHD, cls.daemons[-1].handle.pid)
@@ -346,7 +348,7 @@ class Test(unittest.TestCase):
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"-wallet=bsx_wallet -legacy create",
"particl-wallet",
)
except Exception:
@@ -354,7 +356,7 @@ class Test(unittest.TestCase):
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat create",
"-wallet=bsx_wallet create",
"particl-wallet",
)
cls.daemons.append(startDaemon(data_dir, cfg.PARTICL_BINDIR, cfg.PARTICLD))
@@ -410,15 +412,15 @@ class Test(unittest.TestCase):
waitForRPC(dashRpc, delay_event, rpc_command="getblockchaininfo")
if len(dashRpc("listwallets")) < 1:
dashRpc("createwallet wallet.dat")
dashRpc("createwallet bsx_wallet")
sc.start()
waitForRPC(dashRpc, delay_event)
num_blocks = 500
logging.info("Mining %d dash blocks", num_blocks)
logging.info(f"Mining {num_blocks} dash blocks")
cls.dash_addr = dashRpc("getnewaddress mining_addr")
dashRpc("generatetoaddress {} {}".format(num_blocks, cls.dash_addr))
dashRpc(f"generatetoaddress {num_blocks} {cls.dash_addr}")
ro = dashRpc("getblockchaininfo")
try:
@@ -432,8 +434,8 @@ class Test(unittest.TestCase):
waitForRPC(btcRpc, delay_event)
cls.btc_addr = btcRpc("getnewaddress mining_addr bech32")
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr)
btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr))
logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}")
btcRpc(f"generatetoaddress {num_blocks} {cls.btc_addr}")
ro = btcRpc("getblockchaininfo")
checkForks(ro)
@@ -450,7 +452,7 @@ class Test(unittest.TestCase):
# Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3
logging.info("Waiting for Particl chain height %d", num_blocks)
logging.info(f"Waiting for Particl chain height {num_blocks}")
for i in range(60):
particl_blocks = cls.swap_clients[0].callrpc("getblockcount")
print("particl_blocks", particl_blocks)
@@ -474,7 +476,7 @@ class Test(unittest.TestCase):
cls.swap_clients.clear()
cls.daemons.clear()
super(Test, cls).tearDownClass()
super().tearDownClass()
def test_02_part_dash(self):
logging.info("---------- Test PART to DASH")
@@ -684,9 +686,9 @@ class Test(unittest.TestCase):
offer_id = swap_clients[0].postOffer(
Coins.DASH,
Coins.BTC,
0.001 * COIN,
0.01 * COIN,
1.0 * COIN,
0.001 * COIN,
0.01 * COIN,
SwapTypes.SELLER_FIRST,
)
@@ -710,7 +712,7 @@ class Test(unittest.TestCase):
del swap_clients[0].getChainClientSettings(Coins.DASH)["override_feerate"]
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")
addr = dashRpc('getnewaddress "Withdrawal test"')
+27 -38
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -75,24 +75,6 @@ def make_rpc_func(node_id, base_rpc_port):
return rpc_func
def wait_for_dcr_height(http_port, num_blocks=3):
logging.info("Waiting for DCR chain height %d", num_blocks)
for i in range(60):
if test_delay_event.is_set():
raise ValueError("Test stopped.")
try:
wallet = read_json_api(http_port, "wallets/dcr")
decred_blocks = wallet["blocks"]
print("decred_blocks", decred_blocks)
if decred_blocks >= num_blocks:
return
except Exception as e:
print("Error reading wallets", str(e))
test_delay_event.wait(1)
raise ValueError(f"wait_for_decred_blocks failed http_port: {http_port}")
def run_test_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f"---------- Test {coin_from.name} to {coin_to.name}")
@@ -689,7 +671,7 @@ def prepareDCDDataDir(datadir, node_id, conf_file, dir_prefix, num_nodes=3):
"noseeders=1\n",
"nodnsseed=1\n",
"nodiscoverip=1\n",
"miningaddr=SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH\n",
"miningaddr=SsppG7KLiH52NC7iJmUVGVq89FLS83E5vho\n",
]
for i in range(0, num_nodes):
@@ -725,7 +707,8 @@ class Test(BaseTest):
dcr_daemons = []
start_ltc_nodes = False
start_xmr_nodes = True
dcr_mining_addr = "SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH"
# Addresses differ after 2.1.2, simnet bip44id changed from 1 to 115
dcr_mining_addr = "SsppG7KLiH52NC7iJmUVGVq89FLS83E5vho"
extra_wait_time = 0
max_fee: int = 10000
@@ -739,7 +722,8 @@ class Test(BaseTest):
def prepareExtraCoins(cls):
ci0 = cls.swap_clients[0].ci(cls.test_coin)
if not cls.restore_instance:
assert ci0.rpc_wallet("getnewaddress") == cls.dcr_mining_addr
dcr_mining_addr = ci0.rpc_wallet("getnewaddress")
assert dcr_mining_addr == cls.dcr_mining_addr
cls.dcr_ticket_account = ci0.rpc_wallet(
"getaccount",
[
@@ -763,20 +747,20 @@ class Test(BaseTest):
@classmethod
def tearDownClass(cls):
logging.info("Finalising Decred Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.dcr_daemons)
cls.dcr_daemons.clear()
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin)
num_passed: int = 0
for i in range(30):
try:
ci0.rpc_wallet("purchaseticket", [cls.dcr_ticket_account, 0.1, 0])
ci0.rpc_wallet("purchaseticket", [cls.dcr_ticket_account, 0, 1])
num_passed += 1
if num_passed >= 5:
break
@@ -876,15 +860,16 @@ class Test(BaseTest):
"use_csv": True,
"use_segwit": True,
"blocks_confirmed": 1,
"min_relay_fee": 0.00001,
}
def test_0001_decred_address(self):
logging.info("---------- Test {}".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name}")
coin_settings = {"rpcport": 0, "rpcauth": "none"}
coin_settings.update(REQUIRED_SETTINGS)
ci = DCRInterface(coin_settings, "mainnet")
ci = DCRInterface(coin_settings, "mainnet", self.swap_clients[0])
k = ci.getNewRandomKey()
K = ci.getPubkey(k)
@@ -902,17 +887,17 @@ class Test(BaseTest):
masterpubkey = loop_ci.rpc_wallet("getmasterpubkey")
masterpubkey_data = loop_ci.decode_address(masterpubkey)[4:]
seed_hash = loop_ci.getSeedHash(root_key)
seed_hash: bytes = loop_ci.getSeedHash(root_key)
if i == 0:
assert (
masterpubkey
== "spubVV1z2AFYjVZvzM45FSaWMPRqyUoUwyW78wfANdjdNG6JGCXrr8AbRvUgYb3Lm1iun9CgHew1KswdePryNLKEnBSQ82AjNpYdQgzXPUme9c6"
== "spubVUjNdu1HtDuQYHjVLTgdK3JKtC7JQoCUkhkoVn3rJt6kYctRksn4vTGsdV3obeZLHaB1YobsLENYKHjtey67LFZJdjJyAvHuRqgFRpaSmfn"
)
if i < 2:
assert seed_hash == hash160(masterpubkey_data)
assert hash160(masterpubkey_data) == seed_hash
def test_001_segwit(self):
logging.info("---------- Test {} segwit".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} segwit")
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin)
@@ -970,7 +955,7 @@ class Test(BaseTest):
assert f_decoded["txid"] == ctx.TxHash().hex()
def test_003_signature_hash(self):
logging.info("---------- Test {} signature_hash".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} signature_hash")
# Test that signing a transaction manually produces the same result when signed with the wallet
swap_clients = self.swap_clients
@@ -1045,7 +1030,7 @@ class Test(BaseTest):
assert len(sent_txid) == 64
def test_004_csv(self):
logging.info("---------- Test {} csv".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} csv")
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin)
@@ -1159,7 +1144,7 @@ class Test(BaseTest):
assert sent_spend_txid is not None
def test_005_watchonly(self):
logging.info("---------- Test {} watchonly".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} watchonly")
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin)
@@ -1259,7 +1244,7 @@ class Test(BaseTest):
assert found_txid is not None
def test_008_gettxout(self):
logging.info("---------- Test {} gettxout".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} gettxout")
ci0 = self.swap_clients[0].ci(self.test_coin)
@@ -1371,7 +1356,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount
def test_009_wallet_encryption(self):
logging.info("---------- Test {} wallet encryption".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} wallet encryption")
for coin in ("part", "dcr", "xmr"):
jsw = read_json_api(1800, f"wallets/{coin}")
@@ -1410,7 +1395,7 @@ class Test(BaseTest):
assert jsw["locked"] is False
def test_010_txn_size(self):
logging.info("---------- Test {} txn size".format(self.test_coin.name))
logging.info(f"---------- Test {self.test_coin.name} txn size")
swap_clients = self.swap_clients
ci = swap_clients[0].ci(self.test_coin)
@@ -1487,7 +1472,11 @@ class Test(BaseTest):
v = ci.getNewRandomKey()
s = ci.getNewRandomKey()
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)
addr_out = ci.getNewAddress(True)
+10 -4
View File
@@ -141,7 +141,7 @@ class Test(TestFunctions):
dogeRpc = make_rpc_func(i, base_rpc_port=DOGE_BASE_RPC_PORT)
waitForRPC(dogeRpc, test_delay_event, rpc_command="getblockchaininfo")
if len(dogeRpc("listwallets")) < 1:
dogeRpc("createwallet", ["wallet.dat", False, True, "", False, False])
dogeRpc("createwallet", ["bsx_wallet", False, True, "", False, False])
wif_prefix: int = 239
wif = toWIF(wif_prefix, bytes.fromhex(cls.doge_seeds[i]), False)
dogeRpc("sethdseed", [True, wif])
@@ -179,6 +179,7 @@ class Test(TestFunctions):
@classmethod
def prepareExtraCoins(cls):
super().prepareExtraCoins()
if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.doge_addr = (
@@ -232,7 +233,7 @@ class Test(TestFunctions):
@classmethod
def tearDownClass(cls):
logging.info("Finalising DOGE Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.doge_daemons)
cls.doge_daemons.clear()
@@ -251,11 +252,12 @@ class Test(TestFunctions):
"use_segwit": False,
"blocks_confirmed": 1,
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
"wallet_name": "bsx_wallet",
}
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
if cls.pause_chain:
return
ci0 = cls.swap_clients[0].ci(cls.test_coin)
@@ -412,7 +414,11 @@ class Test(TestFunctions):
v = ci.getNewRandomKey()
s = ci.getNewRandomKey()
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)
addr_out = ci.getNewAddress(False)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -12,7 +12,7 @@ mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export PYTHONPATH=$(pwd)
export TEST_COINS_LIST='bitcoin,dogecoin'
python tests/basicswap/extended/test_doge.py
python tests/basicswap/extended/test_doge_with_prepare.py
"""
@@ -27,14 +27,11 @@ from tests.basicswap.extended.test_xmr_persistent import (
BaseTestWithPrepare,
UI_PORT,
)
from tests.basicswap.extended.test_scripts import (
wait_for_offers,
)
from tests.basicswap.util import (
read_json_api,
wait_for_offers,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
@@ -51,11 +48,11 @@ def wait_for_bid(
bid = read_json_api(UI_PORT + node_id, f"bids/{bid_id}")
if "state" not in bid:
if "bid_state" not in bid:
continue
if state is None:
return
if bid["state"].lower() == state.lower():
if bid["bid_state"].lower() == state.lower():
return
raise ValueError("wait_for_bid failed")
@@ -102,8 +99,9 @@ def prepare_balance(
class DOGETest(BaseTestWithPrepare):
def test_a(self):
__test__ = True
def test_a(self):
amount_from = 10.0
offer_json = {
"coin_from": "btc",
@@ -115,10 +113,8 @@ class DOGETest(BaseTestWithPrepare):
"automation_strat_id": 1,
}
offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"]
logging.debug(f"offer_id {offer_id}")
prepare_balance(self.delay_event, 1, 0, "DOGE", 1000.0)
wait_for_offers(self.delay_event, 1, 1, offer_id)
post_json = {"offer_id": offer_id, "amount_from": amount_from}
@@ -32,6 +32,7 @@ from tests.basicswap.common import (
waitForNumSwapping,
)
from tests.basicswap.common_xmr import (
run_process,
XmrTestBase,
)
@@ -122,7 +123,7 @@ class Test(XmrTestBase):
c1 = self.processes[1]
c1.terminate()
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()
waitForServer(self.delay_event, 12701)
+29 -15
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -104,7 +104,7 @@ def prepareDataDir(
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("fallbackfee=0.01\n")
fp.write("fallbackfee=0.0002\n")
fp.write("acceptnonstdtxn=0\n")
"""
@@ -160,7 +160,7 @@ class Test(BaseTest):
FIRO_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat create",
"-wallet=bsx_wallet create",
"firo-wallet",
)
@@ -199,7 +199,7 @@ class Test(BaseTest):
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)
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(
0,
"generatetoaddress",
@@ -230,7 +230,7 @@ class Test(BaseTest):
0, "getblockcount", base_rpc_port=FIRO_BASE_RPC_PORT
)
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(
0,
"generatetoaddress",
@@ -286,7 +286,7 @@ class Test(BaseTest):
self.callnoderpc("generatetoaddress", [num_blocks, self.firo_addr])
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:
@@ -339,7 +339,7 @@ class Test(BaseTest):
assert txid_with_scriptsig == tx_signed_decoded["txid"]
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]
# Run initialiseWallet to set 'main_wallet_seedid_'
@@ -349,7 +349,7 @@ class Test(BaseTest):
assert swap_client.checkWalletSeed(self.test_coin_from) is True
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]
@@ -428,7 +428,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount
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")
addr = self.callnoderpc(
@@ -447,7 +447,7 @@ class Test(BaseTest):
}
json_rv = read_json_api(
TEST_HTTP_PORT + 0,
"wallets/{}/withdraw".format(self.test_coin_from.name.lower()),
f"wallets/{self.test_coin_from.name.lower()}/withdraw",
post_json,
)
assert len(json_rv["txid"]) == 64
@@ -458,7 +458,7 @@ class Test(BaseTest):
}
json_rv = read_json_api(
TEST_HTTP_PORT + 0,
"wallets/{}/createutxo".format(self.test_coin_from.name.lower()),
f"wallets/{self.test_coin_from.name.lower()}/createutxo",
post_json,
)
assert len(json_rv["txid"]) == 64
@@ -473,6 +473,14 @@ class Test(BaseTest):
ci_from = swap_clients[0].ci(coin_from)
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)
rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer(
@@ -506,9 +514,7 @@ class Test(BaseTest):
coin_from = Coins.BTC
coin_to = Coins.FIRO
logging.info(
"---------- Test {} to {} follower recovers coin b lock tx".format(
coin_from.name, coin_to.name
)
f"---------- Test {coin_from.name} to {coin_to.name} follower recovers coin b lock tx"
)
swap_clients = self.swap_clients
@@ -568,6 +574,14 @@ class Test(BaseTest):
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)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
@@ -592,7 +606,7 @@ class Test(BaseTest):
)
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:
logging.warning("Skipping test")
return
+1 -4
View File
@@ -29,18 +29,15 @@ import unittest
from tests.basicswap.util import (
read_json_api,
waitForServer,
UI_PORT,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
ELECTRUM_PATH = os.getenv("ELECTRUM_PATH")
ELECTRUM_DATADIR = os.getenv("ELECTRUM_DATADIR")
+2 -3
View File
@@ -58,7 +58,6 @@ from tests.basicswap.common import (
from basicswap.bin.run import startDaemon
logger = logging.getLogger()
NUM_NODES = 3
@@ -216,7 +215,7 @@ class Test(unittest.TestCase):
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"-wallet=bsx_wallet -legacy create",
"particl-wallet",
)
@@ -286,7 +285,7 @@ class Test(unittest.TestCase):
cfg.BITCOIN_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"-wallet=bsx_wallet -legacy create",
"bitcoin-wallet",
)
+1 -2
View File
@@ -40,7 +40,6 @@ from tests.basicswap.extended.test_dcr import (
run_test_itx_refund,
)
logger = logging.getLogger("BSX Tests")
if not len(logger.handlers):
@@ -161,7 +160,7 @@ class TestNMC(BasicSwapTest):
if len(nmc_rpc("listwallets")) < 1:
nmc_rpc(
"createwallet",
["wallet.dat", False, True, "", False, NMC_USE_DESCRIPTORS],
["bsx_wallet", False, True, "", False, NMC_USE_DESCRIPTORS],
)
if NMC_USE_DESCRIPTORS:
nmc_rpc(
+179 -402
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,22 +11,14 @@ basicswap]$ python tests/basicswap/extended/test_pivx.py
"""
import json
import logging
import os
import random
import shutil
import signal
import sys
import threading
import time
import unittest
from coincurve.keys import PrivateKey
import basicswap.config as cfg
from basicswap.basicswap import (
BasicSwap,
Coins,
SwapTypes,
BidStates,
@@ -39,45 +31,36 @@ from basicswap.util import (
from basicswap.basicswap_util import (
TxLockTypes,
)
from basicswap.util.address import (
toWIF,
)
from tests.basicswap.util import (
read_json_api,
)
from tests.basicswap.common import (
callrpc_cli,
checkForks,
stopDaemons,
wait_for_bid,
wait_for_offer,
wait_for_balance,
wait_for_unspent,
wait_for_in_progress,
wait_for_bid_tx_state,
TEST_HTTP_HOST,
TEST_HTTP_PORT,
BASE_PORT,
BASE_RPC_PORT,
BASE_ZMQ_PORT,
PREFIX_SECRET_KEY_REGTEST,
waitForRPC,
make_rpc_func,
)
from tests.basicswap.test_xmr import (
BaseTest,
test_delay_event as delay_event,
callnoderpc,
)
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.bin.run import startDaemon
from basicswap.bin.prepare import downloadPIVXParams
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
NUM_NODES = 3
PIVX_NODE = 3
BTC_NODE = 4
delay_event = threading.Event()
stop_test = False
PIVX_BINDIR = os.path.expanduser(
os.getenv("PIVX_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "pivx"))
@@ -86,392 +69,173 @@ PIVXD = os.getenv("PIVXD", "pivxd" + cfg.bin_suffix)
PIVX_CLI = os.getenv("PIVX_CLI", "pivx-cli" + cfg.bin_suffix)
PIVX_TX = os.getenv("PIVX_TX", "pivx-tx" + cfg.bin_suffix)
def prepareOtherDir(datadir, nodeId, conf_file="pivx.conf"):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, conf_file)
with open(filePath, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(BASE_PORT + nodeId) + "\n")
fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n")
fp.write("daemon=0\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("findpeers=0\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
if conf_file == "pivx.conf":
params_dir = os.path.join(datadir, "pivx-params")
downloadPIVXParams(params_dir)
fp.write(f"paramsdir={params_dir}\n")
if conf_file == "bitcoin.conf":
fp.write("wallet=wallet.dat\n")
PIVX_BASE_PORT = 34832
PIVX_BASE_RPC_PORT = 35832
PIVX_BASE_ZMQ_PORT = 36832
def prepareDir(datadir, nodeId, network_key, network_pubkey):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, "particl.conf")
with open(filePath, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(BASE_PORT + nodeId) + "\n")
fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n")
fp.write("daemon=0\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("findpeers=0\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("zmqpubsmsg=tcp://127.0.0.1:" + str(BASE_ZMQ_PORT + nodeId) + "\n")
fp.write("wallet=wallet.dat\n")
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
fp.write("minstakeinterval=5\n")
fp.write("smsgsregtestadjust=0\n")
for i in range(0, NUM_NODES):
if nodeId == i:
continue
fp.write("addnode=127.0.0.1:%d\n" % (BASE_PORT + i))
if nodeId < 2:
fp.write("spentindex=1\n")
fp.write("txindex=1\n")
basicswap_dir = os.path.join(datadir, str(nodeId), "basicswap")
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
pivxdatadir = os.path.join(datadir, str(PIVX_NODE))
btcdatadir = os.path.join(datadir, str(BTC_NODE))
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
settings = {
"debug": True,
"zmqhost": "tcp://127.0.0.1",
"zmqport": BASE_ZMQ_PORT + nodeId,
"htmlhost": TEST_HTTP_HOST,
"htmlport": TEST_HTTP_PORT + nodeId,
"network_key": network_key,
"network_pubkey": network_pubkey,
"chainclients": {
"particl": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + nodeId,
"datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing
},
"pivx": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + PIVX_NODE,
"datadir": pivxdatadir,
"bindir": PIVX_BINDIR,
"use_csv": False,
"use_segwit": False,
},
"bitcoin": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + BTC_NODE,
"datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True,
},
},
"check_progress_seconds": 2,
"check_watched_seconds": 4,
"check_expired_seconds": 60,
"check_events_seconds": 1,
"check_xmr_swaps_seconds": 1,
"min_delay_event": 1,
"max_delay_event": 3,
"min_delay_event_short": 1,
"max_delay_event_short": 3,
"min_delay_retry": 2,
"max_delay_retry": 10,
"restrict_unknown_seed_wallets": False,
"check_updates": False,
}
with open(settings_path, "w") as fp:
json.dump(settings, fp, indent=4)
def partRpc(cmd, node_id=0):
return callrpc_cli(
cfg.PARTICL_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(node_id)),
"regtest",
cmd,
cfg.PARTICL_CLI,
)
def btcRpc(cmd):
return callrpc_cli(
cfg.BITCOIN_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)),
"regtest",
cmd,
cfg.BITCOIN_CLI,
)
def pivxRpc(cmd):
def pivxCli(cmd, node_id=0):
return callrpc_cli(
PIVX_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)),
os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(node_id)),
"regtest",
cmd,
PIVX_CLI,
)
def signal_handler(sig, frame):
global stop_test
os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8"))
stop_test = True
delay_event.set()
def prepareDataDir(
datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3
):
node_dir = os.path.join(datadir, dir_prefix + str(node_id))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
cfg_file_path = os.path.join(node_dir, conf_file)
if os.path.exists(cfg_file_path):
return
with open(cfg_file_path, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(base_p2p_port + node_id) + "\n")
fp.write("rpcport=" + str(base_rpc_port + node_id) + "\n")
def run_coins_loop(cls):
while not stop_test:
try:
pivxRpc("generatetoaddress 1 {}".format(cls.pivx_addr))
btcRpc("generatetoaddress 1 {}".format(cls.btc_addr))
except Exception as e:
logging.warning("run_coins_loop " + str(e))
time.sleep(1.0)
def run_loop(self):
while not stop_test:
for c in self.swap_clients:
c.update()
time.sleep(1)
def make_part_cli_rpc_func(node_id):
node_id = node_id
def rpc_func(method, params=None, wallet=None):
cmd = method
if params:
for p in params:
cmd += ' "' + p + '"'
return partRpc(cmd, node_id)
return rpc_func
class Test(unittest.TestCase):
test_coin_from = Coins.PIVX
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
k = PrivateKey()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret)
cls.network_pubkey = k.public_key.format().hex()
if os.path.isdir(cfg.TEST_DATADIRS):
logging.info("Removing " + cfg.TEST_DATADIRS)
for name in os.listdir(cfg.TEST_DATADIRS):
if name == "pivx-params":
continue
fullpath = os.path.join(cfg.TEST_DATADIRS, name)
if os.path.isdir(fullpath):
shutil.rmtree(fullpath)
else:
os.remove(fullpath)
for i in range(NUM_NODES):
prepareDir(cfg.TEST_DATADIRS, i, cls.network_key, cls.network_pubkey)
prepareOtherDir(cfg.TEST_DATADIRS, PIVX_NODE)
prepareOtherDir(cfg.TEST_DATADIRS, BTC_NODE, "bitcoin.conf")
cls.daemons = []
cls.swap_clients = []
btc_data_dir = os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE))
if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet")):
try:
callrpc_cli(
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"bitcoin-wallet",
)
except Exception:
callrpc_cli(
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=wallet.dat create",
"bitcoin-wallet",
)
cls.daemons.append(startDaemon(btc_data_dir, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
logging.info("Started %s %d", cfg.BITCOIND, cls.daemons[-1].handle.pid)
cls.daemons.append(
startDaemon(
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), PIVX_BINDIR, PIVXD
salt = generate_salt(16)
fp.write(
"rpcauth={}:{}${}\n".format(
"test" + str(node_id),
salt,
password_to_hmac(salt, "test_pass" + str(node_id)),
)
)
logging.info("Started %s %d", PIVXD, cls.daemons[-1].handle.pid)
for i in range(NUM_NODES):
data_dir = os.path.join(cfg.TEST_DATADIRS, str(i))
if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, "particl-wallet")):
try:
callrpc_cli(
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat -legacy create",
"particl-wallet",
)
except Exception:
callrpc_cli(
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=wallet.dat create",
"particl-wallet",
)
cls.daemons.append(startDaemon(data_dir, cfg.PARTICL_BINDIR, cfg.PARTICLD))
logging.info("Started %s %d", cfg.PARTICLD, cls.daemons[-1].handle.pid)
fp.write("daemon=0\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("findpeers=0\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
for i in range(NUM_NODES):
rpc = make_part_cli_rpc_func(i)
waitForRPC(rpc, delay_event)
if i == 0:
rpc(
"extkeyimportmaster",
[
"abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb"
],
)
elif i == 1:
rpc(
"extkeyimportmaster",
[
"pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic",
"",
"true",
],
)
rpc("getnewextaddress", ["lblExtTest"])
rpc("rescanblockchain")
else:
rpc("extkeyimportmaster", [rpc("mnemonic", ["new"])["master"]])
rpc(
"walletsettings",
[
"stakingoptions",
json.dumps(
{"stakecombinethreshold": 100, "stakesplitthreshold": 200}
).replace('"', '\\"'),
],
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
params_dir = os.path.join(datadir, "pivx-params")
downloadPIVXParams(params_dir)
fp.write(f"paramsdir={params_dir}\n")
for i in range(0, num_nodes):
if node_id == i:
continue
fp.write("addnode=127.0.0.1:{}\n".format(base_p2p_port + i))
return node_dir
class Test(BaseTest):
__test__ = True
test_coin_from = Coins.PIVX
pivx_daemons = []
pivx_addr = None
start_ltc_nodes = False
start_xmr_nodes = False
@classmethod
def prepareExtraDataDir(cls, i):
extra_opts = []
if not cls.restore_instance:
prepareDataDir(
cfg.TEST_DATADIRS,
i,
"pivx.conf",
"pivx_",
base_p2p_port=PIVX_BASE_PORT,
base_rpc_port=PIVX_BASE_RPC_PORT,
)
rpc("reservebalance", ["false"])
basicswap_dir = os.path.join(
os.path.join(cfg.TEST_DATADIRS, str(i)), "basicswap"
cls.pivx_daemons.append(
startDaemon(
os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(i)),
PIVX_BINDIR,
PIVXD,
opts=extra_opts,
)
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
with open(settings_path) as fs:
settings = json.load(fs)
sc = BasicSwap(
basicswap_dir, settings, "regtest", log_name="BasicSwap{}".format(i)
)
logging.info("Started %s %d", PIVXD, cls.pivx_daemons[-1].handle.pid)
waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT), delay_event)
@classmethod
def addPIDInfo(cls, sc, i):
sc.setDaemonPID(Coins.PIVX, cls.pivx_daemons[i].handle.pid)
@classmethod
def prepareExtraCoins(cls):
if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.pivx_addr = (
cls.swap_clients[0]
.ci(Coins.PIVX)
.pubkey_to_address(void_block_rewards_pubkey)
)
cls.swap_clients.append(sc)
sc.setDaemonPID(Coins.BTC, cls.daemons[0].handle.pid)
sc.setDaemonPID(Coins.PIVX, cls.daemons[1].handle.pid)
sc.setDaemonPID(Coins.PART, cls.daemons[2 + i].handle.pid)
sc.start()
else:
num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351.
logging.info(f"Mining {num_blocks} pivx blocks")
cls.pivx_addr = pivxCli("getnewaddress mining_addr")
pivxCli(f"generatetoaddress {num_blocks} {cls.pivx_addr}")
waitForRPC(pivxRpc, delay_event)
num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351.
logging.info("Mining %d pivx blocks", num_blocks)
cls.pivx_addr = pivxRpc("getnewaddress mining_addr")
pivxRpc("generatetoaddress {} {}".format(num_blocks, cls.pivx_addr))
ro = pivxRpc("getblockchaininfo")
try:
assert ro["bip9_softforks"]["csv"]["status"] == "active"
except Exception:
logging.info("pivx: csv is not active")
try:
assert ro["bip9_softforks"]["segwit"]["status"] == "active"
except Exception:
logging.info("pivx: segwit is not active")
waitForRPC(btcRpc, delay_event)
cls.btc_addr = btcRpc("getnewaddress mining_addr bech32")
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr)
btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr))
ro = btcRpc("getblockchaininfo")
checkForks(ro)
signal.signal(signal.SIGINT, signal_handler)
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
cls.update_thread.start()
cls.coins_update_thread = threading.Thread(target=run_coins_loop, args=(cls,))
cls.coins_update_thread.start()
# Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3
logging.info("Waiting for Particl chain height %d", num_blocks)
for i in range(60):
particl_blocks = cls.swap_clients[0].callrpc("getblockcount")
print("particl_blocks", particl_blocks)
if particl_blocks >= num_blocks:
break
delay_event.wait(1)
assert particl_blocks >= num_blocks
ro = pivxCli("getblockchaininfo")
try:
assert ro["bip9_softforks"]["csv"]["status"] == "active"
except Exception:
logging.info("pivx: csv is not active")
try:
assert ro["bip9_softforks"]["segwit"]["status"] == "active"
except Exception:
logging.info("pivx: segwit is not active")
@classmethod
def tearDownClass(cls):
global stop_test
logging.info("Finalising")
stop_test = True
cls.update_thread.join()
cls.coins_update_thread.join()
for c in cls.swap_clients:
c.finalise()
logging.info("Finalising PIVX Test")
super().tearDownClass()
stopDaemons(cls.daemons)
cls.swap_clients.clear()
cls.daemons.clear()
stopDaemons(cls.pivx_daemons)
cls.pivx_daemons.clear()
super(Test, cls).tearDownClass()
@classmethod
def addCoinSettings(cls, settings, datadir, node_id):
settings["chainclients"]["pivx"] = {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": PIVX_BASE_RPC_PORT + node_id,
"rpcuser": "test" + str(node_id),
"rpcpassword": "test_pass" + str(node_id),
"datadir": os.path.join(datadir, "pivx_" + str(node_id)),
"bindir": PIVX_BINDIR,
"use_csv": False,
"use_segwit": False,
"wallet_name": "",
}
@classmethod
def coins_loop(cls):
super().coins_loop()
callnoderpc(
0, "generatetoaddress", [1, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT
)
@classmethod
def prepareBalances(cls):
super().prepareBalances()
cls.prepare_balance(
cls,
Coins.PIVX,
10000.0,
1801,
1800,
)
def test_02_part_pivx(self):
logging.info("---------- Test PART to PIVX")
@@ -498,7 +262,7 @@ class Test(unittest.TestCase):
wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
)
wait_for_bid(
delay_event,
@@ -506,7 +270,7 @@ class Test(unittest.TestCase):
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=60,
wait_for=80,
)
js_0 = read_json_api(1800)
@@ -546,7 +310,7 @@ class Test(unittest.TestCase):
wait_for=60,
)
wait_for_bid(
delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60
delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
)
js_0 = read_json_api(1800)
@@ -578,7 +342,7 @@ class Test(unittest.TestCase):
wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
)
wait_for_bid(
delay_event,
@@ -715,7 +479,7 @@ class Test(unittest.TestCase):
logging.info("---------- Test {} wallet".format(self.test_coin_from.name))
logging.info("Test withdrawal")
addr = pivxRpc('getnewaddress "Withdrawal test"')
addr = pivxCli('getnewaddress "Withdrawal test"')
wallets = read_json_api(TEST_HTTP_PORT + 0, "wallets")
assert float(wallets[self.test_coin_from.name]["balance"]) > 100
@@ -745,22 +509,32 @@ class Test(unittest.TestCase):
def test_09_v3_tx(self):
logging.info("---------- Test PIVX v3 txns")
generate_addr = pivxRpc('getnewaddress "generate test"')
pivx_addr = pivxRpc('getnewaddress "Sapling test"')
pivx_sapling_addr = pivxRpc('getnewshieldaddress "shield addr"')
generate_addr = pivxCli('getnewaddress "generate test"')
pivx_addr = pivxCli('getnewaddress "Sapling test"')
pivx_sapling_addr = pivxCli('getnewshieldaddress "shield addr"')
pivxRpc(f'sendtoaddress "{pivx_addr}" 6.0')
pivxRpc(f'generatetoaddress 1 "{generate_addr}"')
pivxCli(f'sendtoaddress "{pivx_addr}" 6.0')
pivxCli(f'generatetoaddress 1 "{generate_addr}"')
txid = pivxRpc(
txid = pivxCli(
'shieldsendmany "{}" "[{{\\"address\\": \\"{}\\", \\"amount\\": 1}}]"'.format(
pivx_addr, pivx_sapling_addr
)
)
rtx = pivxRpc(f'getrawtransaction "{txid}" true')
rtx = pivxCli(f'getrawtransaction "{txid}" true')
assert rtx["version"] == 3
block_hash = pivxRpc(f'generatetoaddress 1 "{generate_addr}"')[0]
block_hash = None
for i in range(15):
rtx = pivxCli(f'getrawtransaction "{txid}" true')
if "blockhash" in rtx:
block_hash = rtx["blockhash"]
logging.info(f"Shielded tx confirmed in block {block_hash} after {i}s")
break
if i == 5:
pivxCli(f'generatetoaddress 1 "{generate_addr}"')
delay_event.wait(1)
assert block_hash is not None, "Shielded tx was not confirmed"
ci = self.swap_clients[0].ci(Coins.PIVX)
block = ci.getBlockWithTxns(block_hash)
@@ -837,14 +611,17 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9)
itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True)
addr_to = pi.getMockScriptAddr(ci_from)
funded_tx = ci_from.createRawFundedTransaction(
addr_to, swap_value, True, lock_unspents=True
)
itx = bytes.fromhex(funded_tx)
itx_decoded = ci_from.describeTx(itx.hex())
n = pi.findMockVout(ci_from, itx_decoded)
value_after_subfee = ci_from.make_int(itx_decoded["vout"][n]["value"])
assert value_after_subfee < swap_value
swap_value = value_after_subfee
wait_for_unspent(delay_event, ci_from, swap_value)
extra_options = {"prefunded_itx": itx}
rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1)
+3 -18
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -36,19 +36,16 @@ from tests.basicswap.common import (
from tests.basicswap.util import (
read_json_api,
waitForServer,
wait_for_offers,
UI_PORT,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
class HttpHandler(BaseHTTPRequestHandler):
def js_response(self, url_split, post_string, is_json):
@@ -132,18 +129,6 @@ def clear_offers(delay_event, node_id) -> None:
raise ValueError("clear_offers failed")
def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None:
logging.info(f"Waiting for {num_offers} offers on node {node_id}")
for i in range(20):
delay_event.wait(1)
offers = read_json_api(
UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}"
)
if len(offers) >= num_offers:
return
raise ValueError("wait_for_offers failed")
def wait_for_bids(delay_event, node_id, num_bids, offer_id=None) -> None:
logging.info(f"Waiting for {num_bids} bids on node {node_id}")
for i in range(20):
@@ -313,7 +313,7 @@ class Test(unittest.TestCase):
ltc_datadir = os.path.join(test_path, "litecoin")
rv = json.loads(
callcoincli(
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat"
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="bsx_wallet"
)
)
assert "unlocked_until" in rv
@@ -474,7 +474,7 @@ class Test(unittest.TestCase):
ltc_datadir = os.path.join(test_path, "litecoin")
rv = json.loads(
callcoincli(
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat"
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="bsx_wallet"
)
)
assert "unlocked_until" in rv
@@ -653,7 +653,7 @@ class Test(unittest.TestCase):
logging.info("Check both LTC wallets are encrypted and mweb seeds match.")
rv = json.loads(
callcoincli(
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat"
ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="bsx_wallet"
)
)
assert "unlocked_until" in rv
@@ -769,7 +769,7 @@ class Test(unittest.TestCase):
)
walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet")
walletpath = os.path.join(walletdir, "wallet.dat")
walletpath = os.path.join(walletdir, "bsx_wallet")
db = berkeleydb.db.DB()
db.open(
@@ -812,10 +812,10 @@ class Test(unittest.TestCase):
],
)
bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp")
bkp_path = os.path.join(walletdir, "bsx_wallet" + ".bkp")
for i in range(1000):
if os.path.exists(bkp_path):
bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}")
bkp_path = os.path.join(walletdir, "bsx_wallet" + f".bkp{i}")
assert os.path.exists(bkp_path) is False
if os.path.isfile(walletpath):
@@ -940,7 +940,7 @@ class Test(unittest.TestCase):
)
logging.info(f"Looking for hdchain for {seedid_bytes.hex()}")
walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet2")
walletpath = os.path.join(walletdir, "wallet.dat")
walletpath = os.path.join(walletdir, "bsx_wallet")
found_hdchain = False
max_key_count = 4000000 # arbitrary
with open(walletpath, "rb") as fp:
@@ -1134,7 +1134,7 @@ class Test(unittest.TestCase):
)
walletdir = os.path.join(test_path, "regtest", "wallets", "descr_wallet")
walletpath = os.path.join(walletdir, "wallet.dat")
walletpath = os.path.join(walletdir, "bsx_wallet")
orig_active_descriptors = []
with sqlite3.connect(walletpath) as conn:
@@ -1234,10 +1234,10 @@ class Test(unittest.TestCase):
"descr_wallet",
],
)
bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp")
bkp_path = os.path.join(walletdir, "bsx_wallet" + ".bkp")
for i in range(1000):
if os.path.exists(bkp_path):
bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}")
bkp_path = os.path.join(walletdir, "bsx_wallet" + f".bkp{i}")
assert os.path.exists(bkp_path) is False
if os.path.isfile(walletpath):
+13 -12
View File
@@ -45,6 +45,18 @@ if not len(logger.handlers):
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):
@classmethod
def setUpClass(cls):
@@ -64,24 +76,13 @@ class Test(unittest.TestCase):
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):
update_thread = None
processes = []
time.sleep(5)
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()
try:
+16 -15
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):
@classmethod
def setUpClass(cls):
@@ -112,17 +124,6 @@ class Test(TestBase):
for i in range(3):
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):
self.delay_event.set()
if self.update_thread:
@@ -136,7 +137,7 @@ class Test(TestBase):
processes = []
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()
try:
@@ -153,7 +154,7 @@ class Test(TestBase):
num_blocks = 431
self.ltc_addr = callltcnoderpc(
1, "getnewaddress", ["mining_addr", "bech32"], wallet="wallet.dat"
1, "getnewaddress", ["mining_addr", "bech32"], wallet="bsx_wallet"
)
logging.info("Mining %d Litecoin blocks to %s", num_blocks, self.ltc_addr)
callltcnoderpc(1, "generatetoaddress", [num_blocks, self.ltc_addr])
@@ -161,7 +162,7 @@ class Test(TestBase):
mweb_addr = callltcnoderpc(
1, "getnewaddress", ["mweb_addr", "mweb"], wallet="mweb"
)
callltcnoderpc(1, "sendtoaddress", [mweb_addr, 1], wallet="wallet.dat")
callltcnoderpc(1, "sendtoaddress", [mweb_addr, 1], wallet="bsx_wallet")
num_blocks = 69
callltcnoderpc(1, "generatetoaddress", [num_blocks, self.ltc_addr])
@@ -201,7 +202,7 @@ class Test(TestBase):
logging.info("Starting a new node on the same mnemonic as the first")
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()
waitForServer(self.delay_event, 12703)
+4 -4
View File
@@ -5,9 +5,9 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
import logging
import os
import time
from basicswap.basicswap import (
Coins,
@@ -120,14 +120,14 @@ class Test(BaseTest):
@classmethod
def tearDownClass(cls):
logging.info("Finalising Wownero Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.wow_daemons)
cls.wow_daemons.clear()
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
if cls.wow_addr is not None:
callrpc_xmr(
@@ -162,7 +162,7 @@ class Test(BaseTest):
startXmrWalletDaemon(node_dir, WOW_BINDIR, WOW_WALLET_RPC, opts=opts)
)
cls.wow_wallet_auth.append(("test{0}".format(i), "test_pass{0}".format(i)))
cls.wow_wallet_auth.append((f"test{i}", f"test_pass{i}"))
waitForWOWNode(i, auth=cls.wow_wallet_auth[i])
+78 -71
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with
export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
"""
import json
@@ -58,6 +59,8 @@ from tests.basicswap.util import (
make_boolean,
read_json_api,
waitForServer,
PORT_OFS,
UI_PORT,
)
from tests.basicswap.common_xmr import (
prepare_nodes,
@@ -72,9 +75,6 @@ import basicswap.bin.run as runSystem
test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent"))
RESET_TEST = make_boolean(os.getenv("RESET_TEST", "true"))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
PARTICL_RPC_PORT_BASE = int(os.getenv("PARTICL_RPC_PORT_BASE", BASE_RPC_PORT))
BITCOIN_RPC_PORT_BASE = int(os.getenv("BITCOIN_RPC_PORT_BASE", BTC_BASE_RPC_PORT))
LITECOIN_RPC_PORT_BASE = int(os.getenv("LITECOIN_RPC_PORT_BASE", LTC_BASE_RPC_PORT))
@@ -124,7 +124,7 @@ def callbtcrpc(
node_id,
method,
params=[],
wallet="wallet.dat",
wallet="bsx_wallet",
base_rpc_port=BITCOIN_RPC_PORT_BASE + PORT_OFS,
):
auth = "test_btc_{0}:test_btc_pwd_{0}".format(node_id)
@@ -153,7 +153,7 @@ def callnmcrpc(
node_id,
method,
params=[],
wallet="wallet.dat",
wallet="bsx_wallet",
base_rpc_port=NAMECOIN_RPC_PORT_BASE + PORT_OFS,
):
auth = "test_nmc_{0}:test_nmc_pwd_{0}".format(node_id)
@@ -209,7 +209,7 @@ def updateThread(cls):
calldogerpc(0, "generatetoaddress", [1, cls.doge_addr])
except Exception as 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):
@@ -228,7 +228,7 @@ def updateThreadXMR(cls):
)
except Exception as 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):
@@ -247,7 +247,7 @@ def updateThreadDCR(cls):
if "double spend" in str(e):
pass
else:
logging.warning("updateThreadDCR purchaseticket {}".format(e))
logging.warning(f"updateThreadDCR purchaseticket {e}")
cls.delay_event.wait(0.5)
try:
if num_passed >= 5:
@@ -259,10 +259,10 @@ def updateThreadDCR(cls):
],
)
except Exception as e:
logging.warning("updateThreadDCR generate {}".format(e))
logging.warning(f"updateThreadDCR generate {e}")
except Exception as 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):
@@ -270,8 +270,8 @@ def signal_handler(self, sig, frame):
self.delay_event.set()
def run_thread(self, client_id):
client_path = os.path.join(test_path, "client{}".format(client_id))
def run_process(client_id):
client_path = os.path.join(test_path, f"client{client_id}")
testargs = [
"basicswap-run",
"-datadir=" + client_path,
@@ -283,16 +283,14 @@ def run_thread(self, client_id):
def start_processes(self):
multiprocessing.set_start_method("spawn")
self.delay_event.clear()
for i in range(NUM_NODES):
self.processes.append(
multiprocessing.Process(
target=run_thread,
args=(
self,
i,
),
target=run_process,
args=(i,),
)
)
self.processes[-1].start()
@@ -300,15 +298,24 @@ def start_processes(self):
for i in range(NUM_NODES):
waitForServer(self.delay_event, UI_PORT + i)
wallets = read_json_api(UI_PORT + 1, "wallets")
if "monero" in self.test_coins_list:
try:
for i in range(8):
wallets = read_json_api(UI_PORT + 1, "wallets")
if "XMR" in wallets and "main_address" in wallets["XMR"]:
break
logging.info("Waiting for wallets output")
self.delay_event.wait(1.0)
self.xmr_addr = wallets["XMR"]["main_address"]
except Exception as e:
logging.error("{} - wallets json: {}".format(str(e), json.dumps(wallets)))
raise
if "monero" in TEST_COINS_LIST:
xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "":
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
self.xmr_addr = wallets["XMR"]["main_address"]
num_blocks = 100
num_blocks: int = 100
if (
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count"
@@ -323,10 +330,11 @@ def start_processes(self):
auth=xmr_auth,
)
logging.info(
"XMR blocks: %d",
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count"
],
"XMR blocks: {}".format(
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count"
]
)
)
self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
@@ -336,9 +344,9 @@ def start_processes(self):
callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
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(
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
0, "getnewaddress", ["mining_addr"], wallet="bsx_wallet"
)
num_blocks: int = 431
have_blocks: int = callltcrpc(0, "getblockcount")
@@ -348,7 +356,7 @@ def start_processes(self):
0,
"generatetoaddress",
[num_blocks - have_blocks, self.ltc_addr],
wallet="wallet.dat",
wallet="bsx_wallet",
)
# https://github.com/litecoin-project/litecoin/issues/807
@@ -356,7 +364,7 @@ def start_processes(self):
mweb_addr = callltcrpc(
0, "getnewaddress", ["mweb_addr", "mweb"], wallet="mweb"
)
callltcrpc(0, "sendtoaddress", [mweb_addr, 1.0], wallet="wallet.dat")
callltcrpc(0, "sendtoaddress", [mweb_addr, 1.0], wallet="bsx_wallet")
num_blocks = 69
have_blocks: int = callltcrpc(0, "getblockcount")
@@ -364,10 +372,10 @@ def start_processes(self):
0,
"generatetoaddress",
[500 - have_blocks, self.ltc_addr],
wallet="wallet.dat",
wallet="bsx_wallet",
)
if "decred" in TEST_COINS_LIST:
if "decred" in self.test_coins_list:
if RESET_TEST:
_ = calldcrrpc(0, "getnewaddress")
# assert (addr == self.dcr_addr)
@@ -397,15 +405,13 @@ def start_processes(self):
self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,))
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"])
num_blocks: int = 200
have_blocks: int = callfirorpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Firo blocks to %s",
num_blocks - have_blocks,
self.firo_addr,
f"Mining {num_blocks - have_blocks} Firo blocks to {self.firo_addr}"
)
callfirorpc(
0,
@@ -413,40 +419,36 @@ def start_processes(self):
[num_blocks - have_blocks, self.firo_addr],
)
if "bitcoincash" in TEST_COINS_LIST:
if "bitcoincash" in self.test_coins_list:
self.bch_addr = callbchrpc(
0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
0, "getnewaddress", ["mining_addr"], wallet="bsx_wallet"
)
num_blocks: int = 200
have_blocks: int = callbchrpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Bitcoincash blocks to %s",
num_blocks - have_blocks,
self.bch_addr,
f"Mining {num_blocks - have_blocks} Bitcoincash blocks to {self.bch_addr}"
)
callbchrpc(
0,
"generatetoaddress",
[num_blocks - have_blocks, self.bch_addr],
wallet="wallet.dat",
wallet="bsx_wallet",
)
if "dogecoin" in TEST_COINS_LIST:
if "dogecoin" in self.test_coins_list:
self.doge_addr = calldogerpc(0, "getnewaddress", ["mining_addr"])
num_blocks: int = 200
have_blocks: int = calldogerpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Dogecoin blocks to %s",
num_blocks - have_blocks,
self.doge_addr,
f"Mining {num_blocks - have_blocks} Dogecoin blocks to {self.doge_addr}"
)
calldogerpc(
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"])
num_blocks: int = 500
have_blocks: int = callnmcrpc(0, "getblockcount")
@@ -539,35 +541,49 @@ class BaseTestWithPrepare(unittest.TestCase):
firo_addr = None
bch_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
def setUpClass(cls):
super(BaseTestWithPrepare, cls).setUpClass()
cls.addClassCleanup(
cls.finalise
) # tearDownClass is not run if setUpClass fails
super().setUpClass()
random.seed(time.time())
if os.path.exists(test_path) and not RESET_TEST:
logging.info(f"Continuing with existing directory: {test_path}")
else:
logging.info(f"Preparing {NUM_NODES} nodes.")
prepare_nodes(
NUM_NODES,
TEST_COINS_LIST,
True,
{"min_sequence_lock_seconds": 60},
PORT_OFS,
)
cls.setupNodes()
for i in range(NUM_NODES):
modifyConfig(test_path, i)
cls.modifyConfig(test_path, i)
signal.signal(
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
def tearDownClass(cls):
def finalise(cls):
logging.info("Stopping test")
cls.delay_event.set()
if cls.update_thread:
@@ -585,18 +601,9 @@ class BaseTestWithPrepare(unittest.TestCase):
cls.update_thread_dcr = None
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):
def test_persistent(self):
while not self.delay_event.is_set():
logging.info("Looping indefinitely, ctrl+c to exit.")
self.delay_event.wait(10)

Some files were not shown because too many files have changed in this diff Show More