190 Commits

Author SHA1 Message Date
tecnovert
53b06859fc Merge pull request #385 from gerlofvanek/update-fix
Fix Update notification.
2025-10-21 15:11:08 +00:00
gerlofvanek
7755b4c505 Fix enable/disable Update notification settings. 2025-10-21 08:56:47 +02:00
gerlofvanek
95da26211b Fix Update notification. 2025-10-21 08:21:55 +02:00
tecnovert
15b2030b92 guix: update packed version 2025-10-17 19:19:19 +02:00
Gerlof van Ek
336e92fff6 Merge pull request #382 from gerlofvanek/version-4
GUI v3.3.1
2025-10-17 17:49:41 +02:00
gerlofvanek
fd4fa37b9d GUI v3.3.1 2025-10-17 15:00:33 +02:00
tecnovert
005abee85d Merge pull request #381 from tecnovert/tests
tests: Disable checking for updates
2025-10-17 11:31:44 +00:00
tecnovert
c6d5f47cea tests: Disable checking for updates
Override with CHECK_FOR_BSX_UPDATES.
2025-10-17 12:31:21 +02:00
tecnovert
d16dc9e124 build: raise version to 0.15.0 2025-10-15 21:24:17 +02:00
tecnovert
a9953c5ffe Merge pull request #380 from tecnovert/refactor
refactor: deduplicate threads array
2025-10-15 19:15:48 +00:00
tecnovert
19fd15b9dc refactor: deduplicate threads array 2025-10-15 20:40:52 +02:00
tecnovert
3794b58021 Merge pull request #378 from gerlofvanek/refactor-2
Refactor + Optimizations
2025-10-15 18:37:32 +00:00
tecnovert
2d1ff4f8bf Merge pull request #379 from basicswap/dependabot/pip/dev/websocket-client-1.9.0
build(deps): bump websocket-client from 1.8.0 to 1.9.0
2025-10-15 17:13:20 +00:00
gerlofvanek
6a8c90a04b Fixed identity tooltips on bids page + removed bottleneck offers page. 2025-10-15 18:28:44 +02:00
tecnovert
e9704510f9 Merge pull request #377 from nahuhh/stray_period
run: remove trailing period on webgui url
2025-10-15 16:20:02 +00:00
gerlofvanek
14a1b0dd7d Update test_settings.py 2025-10-15 12:42:05 +02:00
gerlofvanek
de501f4bb5 Removed CryptoCompare + Added background thread for price fetching. 2025-10-15 12:13:08 +02:00
gerlofvanek
4c1c5cd1a6 Fix keep WebSockets alive. 2025-10-14 13:06:59 +02:00
gerlofvanek
1a9c153306 Fix getwalletinfo + various fixes. 2025-10-13 19:37:36 +02:00
dependabot[bot]
0a3afd4a5a build(deps): bump websocket-client from 1.8.0 to 1.9.0
Bumps [websocket-client](https://github.com/websocket-client/websocket-client) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/websocket-client/websocket-client/releases)
- [Changelog](https://github.com/websocket-client/websocket-client/blob/master/ChangeLog)
- [Commits](https://github.com/websocket-client/websocket-client/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: websocket-client
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 07:47:09 +00:00
Gerlof van Ek
3dbc5f329c Merge pull request #376 from nahuhh/monero_v0.18.4.3
xmr: bump to v0.18.4.3
2025-10-10 23:04:16 +02:00
gerlofvanek
eb46a4fcc5 Fix pricechart if no price/historical data available. 2025-10-10 12:26:36 +02:00
gerlofvanek
73d486d6f0 Refactor + Optimizations 2025-10-10 11:08:23 +02:00
nahuhh
cc6fbb9685 run: remove trailing period on webgui url 2025-10-10 02:25:55 +00:00
nahuhh
4ad8a3f07c xmr: bump to v0.18.4.3 2025-10-10 01:25:38 +00:00
tecnovert
2f7e425da9 Merge pull request #374 from tecnovert/sign
btc: grind for low-r value signatures to match core
2025-10-03 16:18:34 +00:00
tecnovert
a6c2251146 btc: grind for low-r value signatures to match core 2025-10-03 10:36:29 +02:00
tecnovert
071675d359 Merge pull request #373 from tecnovert/refactor
refactor: remove unused code
2025-10-02 21:42:38 +00:00
tecnovert
9cc731d313 Merge pull request #372 from nahuhh/firo_emerg_release
firo: v0.14.14.3 emergency release
2025-10-02 21:29:53 +00:00
tecnovert
4e152d5a2b refactor: remove unused code 2025-10-02 23:21:30 +02:00
nahuhh
26392eafb4 firo: v0.14.14.3 emergency release 2025-10-02 18:32:33 +00:00
tecnovert
c27ea87e9f Merge pull request #370 from tecnovert/xmr_retry
xmr: retry on transient error.
2025-10-02 18:29:48 +00:00
tecnovert
b35f74c659 Merge pull request #369 from tecnovert/prepare
prepare: start Particl daemon only once.
2025-10-02 18:26:49 +00:00
tecnovert
93e5ce0ab9 Merge pull request #371 from tecnovert/createoffers
scripts: remove default values occluding error
2025-10-02 18:24:50 +00:00
tecnovert
292a3713c0 scripts: remove default values occluding error
fix test
2025-10-02 16:10:01 +02:00
tecnovert
add3a1d83e prepare: reuse Particl daemon when adding coins. 2025-10-01 22:18:57 +02:00
tecnovert
a4cc20022e xmr: retry on transient error.
alternative to #368
2025-10-01 21:44:57 +02:00
tecnovert
390fb71aa7 Merge pull request #364 from nahuhh/automation_strat_max_concurrent
automation: set max concurrent incoming bids to 1
2025-10-01 18:25:27 +00:00
tecnovert
91dbe6bf0e Merge pull request #367 from basicswap/dependabot/pip/dev/black-25.9.0
build(deps): bump black from 25.1.0 to 25.9.0
2025-10-01 18:24:49 +00:00
tecnovert
fda2d1f578 Merge pull request #368 from nahuhh/xmr_transient
xmr: add `request-sent`, `idle`, and `output distribution` to transie…
2025-10-01 18:23:08 +00:00
tecnovert
7e53af3616 Merge pull request #362 from nahuhh/init_logging
init: adjust node startup log timing and types
2025-10-01 18:21:07 +00:00
nahuhh
6172785e2e xmr: add request-sent, idle, and output distribution to transient errors 2025-10-01 16:05:19 +00:00
nahuhh
ad472cf16f init: adjust daemon startup log timing and types 2025-10-01 15:56:40 +00:00
tecnovert
9d6e566c3b backports 2025-09-29 09:55:50 +02:00
tecnovert
911ca189bc Merge pull request #366 from tecnovert/expire_accepted
timeout bids before the script coin lock tx is mined.
2025-09-25 07:48:21 +00:00
dependabot[bot]
f309256a7f build(deps): bump black from 25.1.0 to 25.9.0
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 07:24:56 +00:00
tecnovert
4ebb6d6441 timeout bids before the script coin lock tx is mined. 2025-09-14 03:37:02 +02:00
nahuhh
42c40244a1 automation: set max concurrent incoming bids to 1 2025-09-11 13:45:25 +00:00
Gerlof van Ek
918bf60200 Merge pull request #360 from gerlofvanek/version-3
GUI v3.3.0
2025-08-30 21:30:23 +02:00
Gerlof van Ek
19b8e89836 Merge pull request #359 from gerlofvanek/pricechart_fix
Fix BTC chart loads even if BTC is not enabled
2025-08-30 21:30:12 +02:00
Gerlof van Ek
d117938bb0 Merge pull request #358 from gerlofvanek/update_notification
Show notification when new release of BSX
2025-08-30 21:29:58 +02:00
Gerlof van Ek
ab827833a6 Merge pull request #357 from gerlofvanek/password
Fix small bug with changepassword required.
2025-08-30 21:29:43 +02:00
Gerlof van Ek
a5a727a9ac Merge pull request #354 from nahuhh/monero_1842
xmr: bump to v0.18.4.2
2025-08-30 21:29:30 +02:00
gerlofvanek
c160ba5114 GUI v3.3.0 2025-08-29 21:28:06 +02:00
gerlofvanek
30226c37af Fix BTC chart loads even if BTC is not enabled 2025-08-29 21:21:50 +02:00
gerlofvanek
43f9ae8acf Show notification when new release of BSX 2025-08-29 21:07:47 +02:00
gerlofvanek
4c9aa7b777 Fix small bug with changepassword required. 2025-08-29 20:13:19 +02:00
nahuhh
84b6850a0b xmr: bump to v0.18.4.2 2025-08-26 16:28:59 +00:00
tecnovert
ba8168938f network: add ttl to smsgEncrypt 2025-08-22 15:17:34 +02:00
Gerlof van Ek
ed69a36d5d Merge pull request #351 from gerlofvanek/fixes-25
GUI: Updated toasts and added notifications history + Various fixes.
2025-08-15 22:51:52 +02:00
gerlofvanek
672747cc7d GUI: Updated toasts and added notifications history + Various fixes. 2025-08-13 10:39:14 +02:00
tecnovert
a2239c0a5b Merge pull request #350 from basicswap/dependabot/pip/dev/python-gnupg-0.5.5
build(deps): bump python-gnupg from 0.5.4 to 0.5.5
2025-08-11 15:42:10 +00:00
dependabot[bot]
667851c24a build(deps): bump python-gnupg from 0.5.4 to 0.5.5
Bumps [python-gnupg](https://github.com/vsajip/python-gnupg) from 0.5.4 to 0.5.5.
- [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.4...0.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 11:16:18 +00:00
tecnovert
bae6aac12a Fix backwards compatibility. 2025-08-09 18:01:34 +02:00
tecnovert
6fce77f34a Fix force_db_upgrade setting. 2025-08-09 11:39:41 +02:00
tecnovert
e3f51a7ac3 Merge pull request #346 from CrynTox/dev
fix: allow relative paths in inputs
2025-08-06 17:32:56 +00:00
tecnovert
7ee1931176 prepare: Set Particl version to 27.2.2.0
Fixes zmq missing curve functions.
Fix signmessage for v23.
2025-08-06 15:12:20 +02:00
tecnovert
a171bbb48a prepare: Set Particl version to 27.2.1.0 2025-08-05 23:09:06 +02:00
tecnovert
72481337e1 Fix verifyMessage parameter order.
i Please enter the commit message for your changes. Lines starting
2025-08-05 21:19:01 +02:00
tecnovert
cd147da7dd network: Fix selection when no networks are specified. 2025-08-05 13:29:56 +02:00
tecnovert
aa26111665 prepare: Enable network logging category. 2025-08-05 13:29:44 +02:00
tecnovert
235a8f6830 Explicitly set messagemagic string for Particl v27 2025-08-05 00:25:11 +02:00
tecnovert
9cc4734bda Make backwards compatible with smsg payload version 1 2025-08-04 21:29:14 +02:00
tecnovert
11bbc9b128 Set smsg_payload_version automatically if unset. 2025-08-04 21:29:13 +02:00
tecnovert
4fa61e8e49 Allow lock-tx nLockTime to be > chain height + 2 2025-08-04 21:29:13 +02:00
tecnovert
dd2e8d1b59 Rename smsg_plaintext_version. 2025-08-04 21:29:13 +02:00
tecnovert
4b010cfee0 network: Don't raise an error if multiple networks are active.
For testing pre smsg-plaintext-version2
2025-08-03 22:30:05 +02:00
CrynTox
0174715dd2 fix: allow relative paths in inputs 2025-08-03 22:45:00 +03:00
tecnovert
1ea8b80bdc network: Automatically set direct simplex mode per bid. 2025-08-03 20:23:55 +02:00
tecnovert
6b218773dc Merge pull request #344 from gerlofvanek/simplex
Fix: Simplex SQLite duplicate server entries.
2025-08-01 17:43:23 +00:00
tecnovert
fafbd0defe prepare: Add deprecatedrpc line to particl.conf 2025-08-01 19:28:46 +02:00
gerlofvanek
e68fc6509b Fix: Simplex SQLite duplicate server entries. 2025-08-01 16:33:32 +02:00
tecnovert
55bad836a9 prepare: Add an env var to switch core repositories. 2025-07-31 10:50:31 +02:00
tecnovert
4ba2b877dd Store pubkeys in BSX if possible. 2025-07-31 10:50:23 +02:00
tecnovert
f932a41b1a api: Add "message_nets" field to bids and offers. 2025-07-31 00:09:38 +02:00
tecnovert
fea7130835 zmq: Use persistent client keypair. 2025-07-31 00:09:37 +02:00
tecnovert
6d4200f871 zmq: Use recv_multipart and set server keypair in prepare script. 2025-07-31 00:09:37 +02:00
tecnovert
53fc673e71 tests: Fix test_reload. 2025-07-31 00:09:37 +02:00
tecnovert
6e614ff76d Keep compatible with Particl 32.2.7
Only call smsggetdifficulty when smsg_plaintext_version >= 2
2025-07-31 00:09:37 +02:00
tecnovert
355da5ee90 Add network portal data to database. 2025-07-31 00:09:37 +02:00
tecnovert
d0ebed93d8 net: Fix add_to_outbox parameter. 2025-07-31 00:09:37 +02:00
tecnovert
10d6b13930 net: Manage SMSG pubkeys in BSX. 2025-07-31 00:09:36 +02:00
tecnovert
e73e084a6d net: Add network portals to allow swaps between networks. 2025-07-31 00:09:33 +02:00
tecnovert
1e0a7c7395 Merge pull request #340 from tecnovert/change_keypaths
Change default key derivation paths.
2025-07-30 22:08:28 +00:00
Gerlof van Ek
b6e9118797 ZMQ remove time.sleep(0.1) (#341) 2025-07-28 21:43:25 +02:00
Gerlof van Ek
02ceb89d14 Fix: Rate tolerance. (#339)
* Fix: Rate tolerance.

* Fix GUI Rate tolerance.

* Fix: json/rate

* Fix: Mismatch

* Fix: Use backend handle calc.

* Cleanup

* Fix: format_amount

* Add test.
2025-07-28 21:43:06 +02:00
tecnovert
d92fa0c61d Change default key derivation paths.
To allow account keys to be imported into electrum.
Only applies when using descriptor wallets.
To match keys from legacy (sethdseed) wallets set the {COIN}_USE_LEGACY_KEY_PATHS environment variable before prepare.py.
2025-07-26 01:54:34 +02:00
tecnovert
dc692209ca Merge pull request #337 from nahuhh/monero_1841
xmr: v0.18.4.1
2025-07-25 23:35:22 +00:00
nahuhh
56ec500797 xmr: v0.18.4.1 2025-07-25 20:39:57 +00:00
Gerlof van Ek
faf76e3269 Merge pull request #333 from nahuhh/pr/pre-commit
dev: add pre-commit hooks
2025-07-23 23:54:24 +02:00
Gerlof van Ek
e19a99b113 Merge pull request #338 from nahuhh/amm_table
amm: icon beside amount & consistent size of add/edit
2025-07-23 23:54:09 +02:00
Gerlof van Ek
27220d5d36 Merge pull request #335 from nahuhh/pr/wow_dedup
wow: cleanup some dups in prepare.py and basicswap.py
2025-07-23 23:53:56 +02:00
nahuhh
ba1678ad26 lint: eslints 2025-07-23 20:42:03 +00:00
nahuhh
11f1454627 amm: icon beside amount & consistent size of add/edit 2025-07-23 20:08:03 +00:00
nahuhh
90a162f0ea wow: reuse threadPollXMRChainstate & monerod_proxy_config 2025-07-22 21:50:16 +00:00
Gerlof van Ek
96faa26c5b Merge pull request #336 from nahuhh/pr/dash_2213
dash: 22.1.3
2025-07-22 23:46:11 +02:00
Gerlof van Ek
a5cc83157d GUI: Dynamic balances (WS) + Better Notifications (Toasts) + various fixes. (#332)
* GUI: Dynamic balances (WS) + various fixes.

* BLACK + FLAKE8

* Clean-up.

* Fix refresh intervals + Fix pending balance.

* Fix amounts scientific notation (1e-8)

* Better Notifications (Toasts)

* Removed duplicated code + Balance skip if the chain is still syncing.

* Fix MWEB doesnt show as pending + Various fixes.

* Fix: USD values are off with part blind.

* Fix: Percentage change buttons on wallet page.

* Cleanup debug on wallet page.

* Use ZMQ for part balances.

* Fix ZMQ config.

* Fix PART price in chart.
2025-07-22 23:45:45 +02:00
nahuhh
bf5396dd17 dash: 22.1.3 2025-07-15 19:17:26 +00:00
Gerlof van Ek
d6ef4f2edb Merge pull request #334 from nahuhh/pr/wow_confs
wow: coins unlock after 4 confirmations
2025-07-12 17:40:42 +02:00
nahuhh
221a06ba44 wow: coins unlock after 4 confirmations 2025-07-12 04:04:58 +00:00
nahuhh
5cecef676d dev: add pre-commit hooks 2025-07-11 19:33:31 +00:00
Gerlof van Ek
d45e0bcd85 Merge pull request #331 from nahuhh/devel/percentage_rates
js(offers): use same rates for buying and selling
2025-07-07 09:22:04 +02:00
Gerlof van Ek
3e3b8c1cfe Merge pull request #330 from nahuhh/devel/minrate_null
AMM: bug fixes
2025-07-07 09:21:39 +02:00
nahuhh
f2c73f6238 js(offers): use same rates for buying and selling 2025-07-05 07:58:43 +00:00
nahuhh
94b972502e refactor(js/amm): use float for amount_step instead of string 2025-07-05 04:55:29 +00:00
nahuhh
543a820a12 AMM: bug fixes
- skip offer if amount field missing
- write amount line if missing
- set minrate to 0 if missing or null
2025-07-05 04:55:21 +00:00
tecnovert
266bbd1807 guix: Update packed version. 2025-06-30 19:51:48 +02:00
tecnovert
8c06508e7c Raise version to 0.14.6 2025-06-30 19:25:14 +02:00
tecnovert
6489b80666 Merge pull request #328 from tecnovert/non_segwit_utxos
prepare: Set changetype=bech32 in BTC and LTC .conf files.
2025-06-23 23:20:38 +00:00
Gerlof van Ek
bc71ec8246 Merge pull request #329 from gerlofvanek/fixes-19
Fix: Offers - when table is updated, the filters aren't applied.
2025-06-24 00:39:47 +02:00
gerlofvanek
2b945f3e3a Fix: Offers - when table is updated, the filters aren't applied. 2025-06-23 23:15:42 +02:00
Gerlof van Ek
6e5b8fb0ad GUI: Multi-select coin filtering / Various fixes. (#327)
* GUI: Multi-select coin filtering / Various fixes.

* Use coin-manager / clean-up.

* Fix BCH in filters + fix UX with bid pages modals when amount is empty.

* Fix amount not empty.

* Abandon Bid under debug_ui
2025-06-23 22:12:34 +02:00
tecnovert
f031d41a38 prepare: Set changetype=bech32 in BTC and LTC .conf files.
Rewrite .conf files to add changetype at startup if possible.
Add combine_non_segwit_prevouts function to coin interface.
Add option to list non-segwit UTXOs and combine_non_segwit_prevouts to gui.
Add test for changetype and combine_non_segwit_prevouts.
2025-06-21 01:24:02 +02:00
tecnovert
1797ab055b Merge pull request #319 from tecnovert/multinet
network: Start adding simplex to prepare.py.
2025-06-20 17:00:06 +00:00
Gerlof van Ek
bd4ecc5306 Merge pull request #316 from nahuhh/pr/mantissa
utils: round inputAmount to avoid mantissa err
2025-06-20 15:44:49 +02:00
Gerlof van Ek
b3dfae4289 Merge pull request #324 from gerlofvanek/fixes-13
GUI: Various fixes + Fix bid modal.
2025-06-20 14:54:10 +02:00
Gerlof van Ek
7bfd79812f Merge pull request #322 from nahuhh/pr/amm_swap-type
amm: sort+restrict adaptor & secret hash
2025-06-20 14:53:16 +02:00
Gerlof van Ek
94d02ff1cc Merge pull request #321 from tecnovert/load_wallet
Try and load missing wallets.
2025-06-20 14:52:58 +02:00
gerlofvanek
0e19f4139c Fix bid modal show no value when amounts are empty. 2025-06-18 17:10:18 +02:00
gerlofvanek
dd53c8e76d Update test_swap_direction.py 2025-06-18 15:40:32 +02:00
gerlofvanek
6ad9cb24fe GUI: Various fixes. 2025-06-18 14:43:11 +02:00
nahuhh
1c11767d1e amm: sort+restrict adaptor & secret hash 2025-06-16 04:03:56 +00:00
tecnovert
b19edd6771 Try and load missing wallets. 2025-06-14 20:59:19 +02:00
nahuhh
740924632e utils: round inputAmount to avoid mantissa err 2025-06-14 15:32:48 +00:00
Gerlof van Ek
0e6f37a479 Merge pull request #320 from gerlofvanek/chart-2
GUI: Fix autorefresh enabled/disabled.
2025-06-14 16:13:06 +02:00
gerlofvanek
d1fb11e92a GUI: Fix autorefresh enabled/disabled. 2025-06-14 13:45:22 +02:00
tecnovert
ff149e988c network: Start adding simplex to prepare.py.
Group link must still be specified.
2025-06-14 00:13:29 +02:00
Gerlof van Ek
45b4ac8ca0 GUI: Settings / Changepassword page updates + Various fixes. (#318)
* GUI: Settings page updates / fixes.

* Fix Enabled/Disabled logic.

* GUI: Changepassword add warning / + disabled coins check / Various Fixes.
2025-06-13 14:46:16 +02:00
Gerlof van Ek
125fbb43db GUI: Settings page update + Various Fixes. (#315)
* Better settings page + Various Fixes.

* Fix selenium test for test_settings.py

* Fix + BLACK

* Small fix.

* Fix settings.html + Small fix on tests.

* Fix default state.

* Fix selenium charts.

* Fix switch back tab (tests)

* fix XMR (tests)

* Add Enabled Coins in setting.
2025-06-13 12:11:05 +02:00
Gerlof van Ek
b3c946d056 AMM: use_balance_bidding + (USD) price fix + Various fixes. (#314)
* AMM: use_balance_bidding + BCH (USD) price fix + Various fixes.

* AMM: Fixed NMC, DOGE, DCR (USD) price.
2025-06-13 12:10:49 +02:00
Gerlof van Ek
4055b7d6c8 GUI: unlock/changepassword page update + Various fixes. (#313)
* Better unlock page + Various fixes.

* Better changepassword page + Various fixes.

* Small styling fix.
2025-06-13 12:10:32 +02:00
tecnovert
aa9b1c0eb9 Merge pull request #309 from basicswap/dependabot/pip/dev/pycryptodome-3.23.0
build(deps): bump pycryptodome from 3.21.0 to 3.23.0
2025-06-08 19:47:17 +00:00
tecnovert
0c40f14855 Merge pull request #311 from tecnovert/multinet
Multinet
2025-06-08 19:46:57 +00:00
tecnovert
1a42e5e123 net: Workaround error specifying server to simplex-chat twice. 2025-06-08 20:32:31 +02:00
tecnovert
bc20fecc82 net: Update response format for SimpleX Chat v6.3.4 2025-06-08 20:32:31 +02:00
tecnovert
7f6077815a Detect and log if processes end unexpectedly. 2025-06-08 20:32:31 +02:00
tecnovert
69acf00e0d Add socks-proxy option for simplex-chat. 2025-06-08 20:32:30 +02:00
tecnovert
f918652b6c tests: Install python-gnupg first. 2025-06-08 20:32:30 +02:00
tecnovert
fea19c00f2 network: Use simplex direct chats by default. 2025-06-08 20:32:30 +02:00
tecnovert
f269881990 Start Simplex client in run.py 2025-06-08 20:32:30 +02:00
tecnovert
c6f8e5e2ba Set dleag_size per bid 2025-06-08 20:32:30 +02:00
tecnovert
4f47267598 Add setting for max_transient_errors. 2025-06-08 20:32:30 +02:00
tecnovert
3faf947588 Sync DB schema to table definitions. 2025-06-08 20:32:30 +02:00
tecnovert
f3adc17bb8 network: Use Simplex direct chats. 2025-06-08 20:32:29 +02:00
tecnovert
b57ff3497a Merge pull request #312 from tecnovert/ui
ui: Combine bid sent and received fields.
2025-06-08 18:31:47 +00:00
tecnovert
df4a6af6a0 ui: Don't reset the swap type if it's a valid option. 2025-06-08 20:29:05 +02:00
tecnovert
7ba2daf671 ui: Combine bid sent and received fields. 2025-06-08 20:29:05 +02:00
Gerlof van Ek
d08e09061f AMM (#310)
* AMM

* LINT + Fixes

* Remove unused global variables.

* BLACK

* BLACK

* AMM - Various Fixes/Features/Bug Fixes.

* FLAKE

* FLAKE

* BLACK

* Small fix

* Fix

* Auto-start option AMM + Various fixes/bugs/styling.

* Updated createoffers.py

* BLACK

* AMM Styling

* Update bid_xmr template confirm model.

* Fixed bug with Create Default Configuration + Added confirm modal.

* Fix: Better redirect.

* Fixed adjust_rates_based_on_market + Removed debug / extra logging + Various fixes.

* GUI v3.2.2

* Fix sub-header your-offers count when created offers by AMM.

* Fix math.

* Added USD prices + Add offers/bids checkbox enabled always checked.

* Donation page.

* Updated header.html + Typo.

* Update on createoffer.py + BLACK

* AMM: html updates.

* AMM: Add all, minrate, and static options.

* AMM: Amount step default 0.001

* Fix global settings.

* Update createoffers.py

* Fixed bug with autostart when save global settings + Various layout fixes.

* Fixed bug with autostart with add/edit  + Added new option Orderbook (Auto-Accept)

* Fixed debug + New feature attempt bids first.

* Fix: Orderbook (Auto-Accept)

* Added bidding strategy:  Only bid on auto-accept offers (best rates from auto-accept only)

* Fix: with_extra_info

* Small fix automation_strat_id

* Various fixes.

* Final fixes
2025-06-08 17:43:01 +02:00
dependabot[bot]
f7a4798014 build(deps): bump pycryptodome from 3.21.0 to 3.23.0
Bumps [pycryptodome](https://github.com/Legrandin/pycryptodome) from 3.21.0 to 3.23.0.
- [Release notes](https://github.com/Legrandin/pycryptodome/releases)
- [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst)
- [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.21.0...v3.23.0)

---
updated-dependencies:
- dependency-name: pycryptodome
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 08:04:10 +00:00
tecnovert
13847e129b Fix adaptor sig swaps to Firo. 2025-05-13 22:09:16 +02:00
tecnovert
f6914d7c30 tests: Disable test_13_locked_xmr when xmr version >= 0.18.4. 2025-05-12 00:21:37 +02:00
tecnovert
2a8ac051fc Fix prefunded ITX. 2025-05-11 14:29:21 +02:00
tecnovert
3ea7a219d1 tests: Fix LTC wallet creation. 2025-05-11 14:29:18 +02:00
tecnovert
80915d9865 Deduplicate getP2SHScriptForHash 2025-05-11 14:29:02 +02:00
tecnovert
38302d2d79 Merge pull request #307 from gerlofvanek/fixes-17
Fix: Price Tiles / (JS) Better memory cleanup/manager / Improved header / ALL tab/table on bids page + Various fixes.
2025-05-10 19:41:53 +00:00
gerlofvanek
e7b47486f5 ALL tab/table on bids page. + Fix bids export. 2025-05-09 20:43:31 +02:00
gerlofvanek
b3c0ad7e9c Add clickable bid counters in the header that navigate to the sent/received tabs + small fix. 2025-05-08 22:31:39 +02:00
gerlofvanek
ece9d7fb4b Removed repeating console.log(s) 2025-05-08 21:09:24 +02:00
gerlofvanek
868b2475c1 Fix: Better memory/tooltip/clean-up managers, various fixes. 2025-05-08 21:01:02 +02:00
tecnovert
27c3b93ff9 Raise version to 0.14.5 2025-05-06 19:18:02 +02:00
tecnovert
7df2f1b290 Add check for minimum sqlite version. 2025-05-05 00:38:02 +02:00
gerlofvanek
d57a148ff4 Fix: Price Tiles volume/btc display + Better memory clean / tooltip manager. 2025-05-04 19:51:22 +02:00
tecnovert
aa898a9601 Merge pull request #306 from tecnovert/sip
UI: Improve swaps in progress.
2025-05-02 09:31:03 +00:00
tecnovert
ec5ea4ca3c Merge pull request #304 from nahuhh/pr_bids-patch
bids: cosmetic fixes
2025-05-02 09:30:51 +00:00
tecnovert
ed18b36da6 Merge pull request #303 from justanwar/firo_v0141401_hardfork
firo: v0.14.14.1 hardfork 2025-05-28 mandatory
2025-05-02 09:30:36 +00:00
tecnovert
058270ec7a tests: Add bid and active pages to test_swap_direction.py 2025-05-01 00:25:39 +02:00
tecnovert
2818afc933 ui: Swap send/receive for sent bids in active.html. 2025-04-29 20:10:15 +02:00
tecnovert
48bfdb7462 Fix js_active amounts for reverse bids. 2025-04-29 19:48:49 +02:00
nahuhh
e14b9b7e6e bids: adjust / consolidate colors 2025-04-24 23:19:19 +00:00
nahuhh
a87180f2ef header: bid totals too high 2025-04-24 23:19:19 +00:00
nahuhh
66d763e8ea bids: fix hidden details column 2025-04-24 23:19:12 +00:00
justanwar
061a09f3fb firo: v0.14.14.1 hardfork 2025-05-28 mandatory 2025-04-21 21:35:53 +08:00
tecnovert
e7af4f9005 Merge pull request #301 from gerlofvanek/smallfix
Fix the rate/amount variable toggles on order creation.
2025-04-19 05:56:50 +00:00
tecnovert
a22274b06d Merge pull request #299 from nahuhh/patch-1
cosmetic issue & some lints
2025-04-19 05:56:26 +00:00
gerlofvanek
3b2b666c75 Fix the rate/amount variable toggles on order creation. 2025-04-18 19:59:41 +02:00
nahuhh
ec092eaa6e cleanup 2025-04-17 02:44:42 +00:00
tecnovert
b605bd4bc3 tests: Limit infinite loop in ci. 2025-04-16 22:11:24 +02:00
tecnovert
934aab9d8a Allow starting with a subset of configured coins. 2025-04-16 20:20:43 +02:00
tecnovert
21c0a534f2 Timeout waiting for mutex on shutdown. 2025-04-16 20:20:43 +02:00
Cryptoguard
b293b5daee Update install.md 2025-04-16 12:47:24 -04:00
167 changed files with 28637 additions and 8744 deletions

3
.djlintrc Normal file
View File

@@ -0,0 +1,3 @@
{
"indent": 2
}

View File

@@ -38,6 +38,7 @@ jobs:
sudo apt-get install -y firefox
fi
python -m pip install --upgrade pip
pip install python-gnupg
pip install -e .[dev]
pip install -r requirements.txt --require-hashes
- name: Install
@@ -99,13 +100,21 @@ jobs:
export PYTHONPATH=$(pwd)
python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & TEST_NETWORK_PID=$!
echo "Starting test_xmr_persistent, PID $TEST_NETWORK_PID"
i=0
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
do
tail -n 1 /tmp/log.txt
sleep 2
((++i))
if [ $i -ge 60 ]; then
echo "Timed out waiting for test_xmr_persistent, PID $TEST_NETWORK_PID"
kill $TEST_NETWORK_PID
(exit 1) # Fail test
break
fi
done
echo "Running test_settings.py"
python tests/basicswap/selenium/test_settings.py
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
kill -9 $TEST_NETWORK_PID
kill $TEST_NETWORK_PID

2
.gitignore vendored
View File

@@ -1,5 +1,6 @@
old/
build/
venv/
*.pyc
__pycache__
/dist/
@@ -10,6 +11,7 @@ __pycache__
.eggs
.ruff_cache
.pytest_cache
.vectorcode
*~
# geckodriver.log

40
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,40 @@
repos:
# Common hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
args: ["--assume-in-merge"]
- id: check-yaml
- id: detect-private-key
- id: end-of-file-fixer
- id: trailing-whitespace
args: ["--markdown-linebreak-ext=md"]
# Black - Python formatter
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
exclude: (basicswap/contrib|basicswap/interface/contrib)/
# Flake8 - Lint Python
- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
args: ["--ignore=E203,E501,W503", "--exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py"]
# ESLint - Lint Javascript and fix issues where possible
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.30.1
hooks:
- id: eslint
#args: ["--fix"]
# djLint - Lint HTML
#- repo: https://github.com/djlint/djlint
# rev: v1.36.4
# hooks:
# - id: djlint

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.14.4"
__version__ = "0.15.0"

View File

@@ -31,6 +31,7 @@ from .util import (
)
from .util.logging import (
BSXLogger,
LogCategories as LC,
)
from .chainparams import (
Coins,
@@ -43,7 +44,7 @@ def getaddrinfo_tor(*args):
class BaseApp(DBMethods):
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
def __init__(self, data_dir, settings, chain, log_name="BasicSwap", **kwargs):
self.fp = None
self.log_name = log_name
self.fail_code = 0
@@ -61,7 +62,7 @@ class BaseApp(DBMethods):
self._network = None
self.prepareLogging()
self.log.info("Network: {}".format(self.chain))
self.log.info(f"Network: {self.chain}")
self.use_tor_proxy = self.settings.get("use_tor", False)
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
@@ -71,6 +72,32 @@ class BaseApp(DBMethods):
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
self._force_db_upgrade = False
self._enabled_log_categories = set()
for category in self.settings.get("enabled_log_categories", []):
category = category.lower()
if category == "net":
self._enabled_log_categories.add(LC.NET)
else:
self.log.warning(
f'Unknown entry "{category}" in "enabled_log_categories"'
)
if len(self._enabled_log_categories) > 0:
self.log.info(
"Enabled logging categories: {}".format(
",".join(sorted([c.name for c in self._enabled_log_categories]))
)
)
super().__init__(
data_dir=data_dir,
settings=settings,
chain=chain,
log_name=log_name,
**kwargs,
)
def __del__(self):
if self.fp:
@@ -78,7 +105,14 @@ class BaseApp(DBMethods):
def stopRunning(self, with_code=0):
self.fail_code = with_code
with self.mxDB:
# Wait for lock to shutdown gracefully.
if self.mxDB.acquire(timeout=5):
self.chainstate_delay_event.set()
self.delay_event.set()
self.mxDB.release()
else:
# Waiting for lock timed out, stop anyway
self.chainstate_delay_event.set()
self.delay_event.set()
@@ -143,7 +177,7 @@ class BaseApp(DBMethods):
for c, params in chainparams.items():
if coin_name.lower() == params["name"].lower():
return c
raise ValueError("Unknown coin: {}".format(coin_name))
raise ValueError(f"Unknown coin: {coin_name}")
def callrpc(self, method, params=[], wallet=None):
cc = self.coin_clients[Coins.PART]
@@ -228,11 +262,16 @@ class BaseApp(DBMethods):
request = urllib.request.Request(url, headers=headers)
return opener.open(request, timeout=timeout).read()
def logException(self, message) -> None:
def logException(self, message: str) -> None:
self.log.error(message)
if self.debug:
self.log.error(traceback.format_exc())
def logD(self, log_category: int, message: str) -> None:
if log_category not in self._enabled_log_categories:
return
self.log.debug("(" + LC(log_category).name + ") " + message)
def torControl(self, query):
try:
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,16 @@ class KeyTypes(IntEnum):
KAF = 6
class MessageNetworks(IntEnum):
SMSG = auto()
SIMPLEX = auto()
class MessageNetworkLinkTypes(IntEnum):
RECEIVED_ON = auto()
SENT_ON = auto()
class MessageTypes(IntEnum):
OFFER = auto()
BID = auto()
@@ -53,12 +63,18 @@ class MessageTypes(IntEnum):
ADS_BID_LF = auto()
ADS_BID_ACCEPT_FL = auto()
CONNECT_REQ = auto()
PORTAL_OFFER = auto()
PORTAL_SEND = auto()
class AddressTypes(IntEnum):
OFFER = auto()
BID = auto()
RECV_OFFER = auto()
SEND_OFFER = auto()
PORTAL_LOCAL = auto()
PORTAL = auto()
class SwapTypes(IntEnum):
@@ -111,6 +127,7 @@ class BidStates(IntEnum):
BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
CONNECT_REQ_SENT = 34
class TxStates(IntEnum):
@@ -193,6 +210,8 @@ class EventLogTypes(IntEnum):
LOCK_TX_B_IN_MEMPOOL = auto()
BCH_MERCY_TX_PUBLISHED = auto()
BCH_MERCY_TX_FOUND = auto()
LOCK_TX_A_IN_MEMPOOL = auto()
LOCK_TX_A_CONFLICTS = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -226,6 +245,12 @@ class NotificationTypes(IntEnum):
OFFER_RECEIVED = auto()
BID_RECEIVED = auto()
BID_ACCEPTED = auto()
SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto()
class ConnectionRequestTypes(IntEnum):
BID = 1
class AutomationOverrideOptions(IntEnum):
@@ -339,6 +364,8 @@ def strBidState(state):
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
if state == BidStates.CONNECT_REQ_SENT:
return "Connect request sent"
return "Unknown" + " " + str(state)
@@ -381,15 +408,14 @@ def strTxType(tx_type):
def strAddressType(addr_type):
if addr_type == AddressTypes.OFFER:
return "Offer"
if addr_type == AddressTypes.BID:
return "Bid"
if addr_type == AddressTypes.RECV_OFFER:
return "Offer recv"
if addr_type == AddressTypes.SEND_OFFER:
return "Offer send"
return "Unknown"
return {
AddressTypes.OFFER: "Offer",
AddressTypes.BID: "Bid",
AddressTypes.RECV_OFFER: "Offer recv",
AddressTypes.SEND_OFFER: "Offer send",
AddressTypes.PORTAL_LOCAL: "Portal (local)",
AddressTypes.PORTAL: "Portal",
}.get(addr_type, "Unknown")
def getLockName(lock_type):
@@ -412,6 +438,10 @@ def describeEventEntry(event_type, event_msg):
return "Lock tx B published"
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
return "Failed to publish lock tx B spend: " + event_msg
if event_type == EventLogTypes.LOCK_TX_A_IN_MEMPOOL:
return "Lock tx A seen in mempool"
if event_type == EventLogTypes.LOCK_TX_A_CONFLICTS:
return "Lock tx A conflicting txn/s"
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
return "Lock tx A seen in chain"
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
@@ -581,6 +611,26 @@ def canAcceptBidState(state):
)
def canExpireBidState(state):
return state in (
BidStates.BID_SENT,
BidStates.BID_RECEIVING,
BidStates.BID_RECEIVED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
BidStates.BID_REQUEST_SENT,
)
def canTimeoutBidState(state):
return state in (
BidStates.BID_ACCEPTED,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
)
def isActiveBidState(state):
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
return True

View File

@@ -6,6 +6,7 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import base64
import contextlib
import gnupg
import hashlib
@@ -26,6 +27,7 @@ import threading
import time
import urllib.parse
import zipfile
import zmq
from urllib.request import urlopen
@@ -47,8 +49,9 @@ from basicswap.bin.run import (
getWalletBinName,
)
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
# Coin clients
PARTICL_REPO = os.getenv("PARTICL_REPO", "tecnovert")
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.2.0")
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
@@ -64,10 +67,10 @@ DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.0")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.3")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = (
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version
"df28b670cb3a174d7763dd6d22fb4ef20597d0ac" # Lock hashes.txt to monero version
)
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -79,10 +82,10 @@ WOW_SITE_COMMIT = (
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "22.0.0")
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3")
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.0")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.3")
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
@@ -94,12 +97,6 @@ BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1")
DOGECOIN_VERSION_TAG = os.getenv("DOGECOIN_VERSION_TAG", "")
GUIX_SSL_CERT_DIR = None
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", "false"))
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false"))
known_coins = {
"particl": (PARTICL_VERSION, PARTICL_VERSION_TAG, ("tecnovert",)),
@@ -121,6 +118,21 @@ disabled_coins = [
"navcoin",
]
# Network clients
SIMPLEX_CHAT_VERSION = os.getenv("SIMPLEX_CHAT_VERSION", "6.3.5")
SIMPLEX_WS_PORT = int(os.getenv("SIMPLEX_WS_PORT", 5225))
SIMPLEX_SERVER_ADDRESS = os.getenv(
"SIMPLEX_CHAT_VERSION",
"smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im",
)
SIMPLEX_SERVER_SOCKS_PROXY = os.getenv("SIMPLEX_SERVER_SOCKS_PROXY", "127.0.0.1:9150")
SIMPLEX_GROUP_LINK = os.getenv("SIMPLEX_GROUP_LINK", None)
known_networks = ["smsg", "simplex"]
disabled_networks = []
expected_key_ids = {
"tecnovert": ("8E517DC12EC1CC37F6423A8A13F13651C9CF0D6B",),
"thrasher": ("59CAF0E96F23F53747945FD4FE3348877809386C",),
@@ -145,8 +157,15 @@ expected_key_ids = {
),
"decred_release": ("F516ADB7A069852C7C28A02D6D897EDF518A031D",),
"Calin_Culianu": ("D465135F97D0047E18E99DC321810A542031C02C",),
"SimpleX_Chat": ("FB44AF81A45BDE327319797C85107E357D4A17FC",),
}
GUIX_SSL_CERT_DIR = None
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", False))
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", False))
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
if USE_PLATFORM == "Darwin":
BIN_ARCH = "osx64"
@@ -173,11 +192,11 @@ if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", "false"))
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", "false"))
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", "false"))
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", False))
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", False))
BSX_UPDATE_UNMANAGED = toBool(
os.getenv("BSX_UPDATE_UNMANAGED", "true")
os.getenv("BSX_UPDATE_UNMANAGED", True)
) # Disable updating unmanaged coin cores.
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
@@ -302,10 +321,8 @@ def setTorrcVars():
)
TEST_TOR_PROXY = toBool(
os.getenv("TEST_TOR_PROXY", "true")
) # Expects a known exit node
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", "false"))
TEST_TOR_PROXY = toBool(os.getenv("TEST_TOR_PROXY", True)) # Expects a known exit node
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", False))
BITCOIN_FASTSYNC_URL = os.getenv(
"BITCOIN_FASTSYNC_URL",
@@ -322,6 +339,8 @@ BITCOIN_FASTSYNC_SIG_URL = os.getenv(
# Encrypt new wallets with this password, must match the Particl wallet password when adding coins
WALLET_ENCRYPTION_PWD = os.getenv("WALLET_ENCRYPTION_PWD", "")
CHECK_FOR_BSX_UPDATES = toBool(os.getenv("CHECK_FOR_BSX_UPDATES", True))
use_tor_proxy: bool = False
with_coins_changed: bool = False
@@ -340,21 +359,6 @@ monero_wallet_rpc_proxy_config = [
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
wownerod_proxy_config = [
f"proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}",
"proxy-allow-dns-leaks=0",
"no-igd=1", # Disable UPnP port mapping
"hide-my-port=1", # Don't share the p2p port
"p2p-bind-ip=127.0.0.1", # Don't broadcast ip
"in-peers=0", # Changes "error" in log to "incoming connections disabled"
"out-peers=24",
f"tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16", # Outgoing tx relay to onion
]
wownero_wallet_rpc_proxy_config = [
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout()
default_socket_getaddrinfo = socket.getaddrinfo
@@ -400,6 +404,12 @@ def getDescriptorWalletOption(coin_params):
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
def getLegacyKeyPathOption(coin_params):
ticker: str = coin_params["ticker"]
default_option: bool = False
return toBool(os.getenv(ticker + "_USE_LEGACY_KEY_PATHS", default_option))
def getKnownVersion(coin_name: str) -> str:
version, version_tag, _ = known_coins[coin_name]
return version + version_tag
@@ -597,7 +607,7 @@ def downloadPIVXParams(output_dir):
downloadFile(url, path)
file_hash = getFileHash(path)
logger.info("%s hash: %s", k, file_hash)
logger.info(f"{k} hash: {file_hash}")
assert file_hash == v
finally:
popConnectionParameters()
@@ -624,9 +634,21 @@ def ensureValidSignatureBy(result, signing_key_name):
logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).")
def ensureFileHashInFile(release_hash, assert_path):
with (
open(assert_path, "rb", 0) as fp,
mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s,
):
if s.find(bytes(release_hash, "utf-8")) == -1:
raise ValueError(
f"Error: Release hash {release_hash} not found in assert file."
)
logger.info("Found release hash in assert file.")
def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts={}):
version, version_tag, signers = version_data
logger.info("extractCore %s v%s%s", coin, version, version_tag)
logger.info(f"Extracting core {coin} v{version}{version_tag}")
extract_core_overwrite = extra_opts.get("extract_core_overwrite", True)
if coin in ("monero", "firo", "wownero"):
@@ -655,9 +677,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
)
except Exception as e:
logging.warning(
"Unable to set file permissions: %s, for %s",
str(e),
out_path,
f"Unable to set file permissions: {e}, for {out_path}"
)
break
return
@@ -686,9 +706,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
except Exception as e:
logging.warning(
"Unable to set file permissions: %s, for %s",
str(e),
out_path,
f"Unable to set file permissions: {e}, for {out_path}"
)
return
@@ -731,9 +749,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
except Exception as e:
logging.warning(
"Unable to set file permissions: %s, for %s",
str(e),
out_path,
f"Unable to set file permissions: {e}, for {out_path}"
)
else:
with tarfile.open(release_path) as ft:
@@ -749,15 +765,13 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
except Exception as e:
logging.warning(
"Unable to set file permissions: %s, for %s",
str(e),
out_path,
f"Unable to set file permissions: {e}, for {out_path}"
)
def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
version, version_tag, signers = version_data
logger.info("prepareCore %s v%s%s", coin, version, version_tag)
logger.info(f"Prepare core {coin} v{version}{version_tag}")
bin_dir = os.path.expanduser(settings["chainclients"][coin]["bindir"])
if not os.path.exists(bin_dir):
@@ -795,11 +809,9 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
release_path = os.path.join(bin_dir, release_filename)
downloadRelease(release_url, release_path, extra_opts)
assert_filename = "monero-{}-hashes.txt".format(version)
# assert_url = 'https://www.getmonero.org/downloads/hashes.txt'
assert_url = "https://raw.githubusercontent.com/monero-project/monero-site/{}/downloads/hashes.txt".format(
XMR_SITE_COMMIT
)
assert_filename = f"monero-{version}-hashes.txt"
# Get the hashes file as of XMR_SITE_COMMIT
assert_url = f"https://raw.githubusercontent.com/monero-project/monero-site/{XMR_SITE_COMMIT}/downloads/hashes.txt"
assert_path = os.path.join(bin_dir, assert_filename)
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
@@ -876,7 +888,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
downloadFile(assert_sig_url, assert_sig_path)
else:
major_version = int(version.split(".")[0])
use_guix: bool = coin in ("dash",) or major_version >= 22
arch_name = BIN_ARCH
if os_name == "osx" and use_guix:
@@ -897,22 +908,22 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
coin, version + version_tag, arch_name, filename_extra, FILE_EXT
)
if coin == "particl":
release_url = "https://github.com/particl/particl-core/releases/download/v{}/{}".format(
version + version_tag, release_filename
release_url = (
"https://github.com/{}/particl-core/releases/download/v{}/{}".format(
PARTICL_REPO, version + version_tag, release_filename
)
)
assert_filename = "{}-{}-{}-build.assert".format(coin, os_name, version)
if use_guix:
assert_url = f"https://raw.githubusercontent.com/particl/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
assert_url = f"https://raw.githubusercontent.com/{PARTICL_REPO}/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
else:
assert_url = (
"https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s"
% (
assert_url = "https://raw.githubusercontent.com/{}/gitian.sigs/master/{}-{}/{}/{}".format(
PARTICL_REPO,
version + version_tag,
os_dir_name,
signing_key_name,
assert_filename,
)
)
elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
version + version_tag, release_filename
@@ -920,9 +931,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename = "{}-core-{}-{}-build.assert".format(
coin, os_name, ".".join(version.split(".")[:2])
)
assert_url = (
"https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s"
% (version, os_dir_name, signing_key_name, assert_filename)
assert_url = "https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/{}-{}/{}/{}".format(
version, os_dir_name, signing_key_name, assert_filename
)
elif coin == "dogecoin":
release_url = (
@@ -955,10 +965,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
version, release_filename
)
assert_filename = "SHA256SUMS.{}.asc.Calin_Culianu".format(version)
assert_url = (
"https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/%s/%s"
% (version, assert_filename)
)
assert_url = f"https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/{version}/{assert_filename}"
elif coin == "namecoin":
release_url = f"https://www.namecoin.org/files/namecoin-core/namecoin-core-{version}/{release_filename}"
signing_key = "Rose%20Turing"
@@ -974,15 +981,12 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename = "{}-{}-{}-build.assert".format(
coin, os_name, version.rsplit(".", 1)[0]
)
assert_url = (
"https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/%s-%s/%s/%s"
% (
assert_url = "https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/{}-{}/{}/{}".format(
version + version_tag,
os_dir_name,
signing_key_name.capitalize(),
assert_filename,
)
)
elif coin == "dash":
release_filename = "{}-{}-{}.{}".format(
"dashcore", version + version_tag, arch_name, FILE_EXT
@@ -1011,9 +1015,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
version + version_tag, release_filename
)
)
assert_url = (
"https://github.com/firoorg/firo/releases/download/v%s/SHA256SUMS"
% (version + version_tag)
assert_url = "https://github.com/firoorg/firo/releases/download/v{}/SHA256SUMS".format(
version + version_tag
)
elif coin == "navcoin":
release_filename = "{}-{}-{}.{}".format(coin, version, BIN_ARCH, FILE_EXT)
@@ -1054,15 +1057,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
release_hash: str = getFileHash(release_path)
logger.info(f"{release_filename} hash: {release_hash}")
with (
open(assert_path, "rb", 0) as fp,
mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s,
):
if s.find(bytes(release_hash, "utf-8")) == -1:
raise ValueError(
f"Error: Release hash {release_hash} not found in assert file."
)
logger.info("Found release hash in assert file.")
ensureFileHashInFile(release_hash, assert_path)
if SKIP_GPG_VALIDATION:
logger.warning(
@@ -1221,15 +1216,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if coin == "monero":
if XMR_RPC_USER != "":
fp.write(f"rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n")
if tor_control_password is not None:
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
if coin == "wownero":
if WOW_RPC_USER != "":
fp.write(f"rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n")
if tor_control_password is not None:
for opt_line in wownerod_proxy_config:
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
wallets_dir = core_settings.get("walletsdir", data_dir)
@@ -1313,7 +1304,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
)
wallet_conf_path = os.path.join(data_dir, wallet_conf_filename)
if os.path.exists(wallet_conf_path):
exitWithError("{} exists".format(wallet_conf_path))
exitWithError(f"{wallet_conf_path} exists")
with open(wallet_conf_path, "w") as fp:
if chain != "mainnet":
fp.write(chainname + "=1\n")
@@ -1340,7 +1331,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
core_conf_name: str = core_settings.get("config_filename", coin + ".conf")
core_conf_path = os.path.join(data_dir, core_conf_name)
if os.path.exists(core_conf_path):
exitWithError("{} exists".format(core_conf_path))
exitWithError(f"{core_conf_path} exists")
with open(core_conf_path, "w") as fp:
if chain != "mainnet":
if coin in ("navcoin",):
@@ -1354,7 +1345,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
elif chain == "regtest":
fp.write("[regtest]\n\n")
else:
logger.warning("Unknown chain %s", chain)
logger.warning(f"Unknown chain {chain}")
if COINS_RPCBIND_IP != "127.0.0.1":
fp.write("rpcallowip=127.0.0.1\n")
@@ -1376,10 +1367,25 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
salt = generate_salt(16)
if coin == "particl":
fp.write("deprecatedrpc=create_bdb\n")
fp.write("debugexclude=libevent\n")
if chain == "mainnet":
fp.write("rpcdoccheck=0\n")
fp.write(
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
)
fp.write(
"zmqpubhashwtx=tcp://{}:{}\n".format(
COINS_RPCBIND_IP, settings["zmqport"]
)
)
zmqsecret = extra_opts.get("zmqsecret", None)
if zmqsecret:
try:
_ = base64.b64decode(zmqsecret)
except Exception as e: # noqa: F841
raise ValueError("zmqsecret must be base64 encoded")
fp.write(f"serverkeyzmq={zmqsecret}\n")
fp.write("spentindex=1\n")
fp.write("txindex=1\n")
fp.write("staking=0\n")
@@ -1391,6 +1397,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
)
elif coin == "litecoin":
fp.write("prune=4000\n")
fp.write("changetype=bech32\n")
if LTC_RPC_USER != "":
fp.write(
"rpcauth={}:{}${}\n".format(
@@ -1408,6 +1415,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
elif coin == "bitcoin":
fp.write("deprecatedrpc=create_bdb\n")
fp.write("prune=2000\n")
fp.write("changetype=bech32\n")
fp.write("fallbackfee=0.0002\n")
if BTC_RPC_USER != "":
fp.write(
@@ -1478,10 +1486,10 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
)
)
else:
logger.warning("Unknown coin %s", coin)
logger.warning(f"Unknown coin {coin}")
if coin == "bitcoin" and extra_opts.get("use_btc_fastsync", False) is True:
logger.info("Initialising BTC chain with fastsync %s", BITCOIN_FASTSYNC_FILE)
logger.info(f"Initialising BTC chain with fastsync {BITCOIN_FASTSYNC_FILE}")
base_dir = extra_opts["data_dir"]
for dirname in ("blocks", "chainstate"):
@@ -1558,27 +1566,18 @@ def modify_tor_config(
# Disable tor first
for line in fp_in:
skip_line: bool = False
if coin == "monero":
if coin in ("wownero", "monero"):
for opt_line in monerod_proxy_config:
setting: str = opt_line[0 : opt_line.find("=") + 1]
if line.startswith(setting):
skip_line = True
break
if coin == "wownero":
for opt_line in wownerod_proxy_config:
setting: str = opt_line[0 : opt_line.find("=") + 1]
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
if coin == "monero":
if coin in ("wownero", "monero"):
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
if coin == "wownero":
for opt_line in wownerod_proxy_config:
fp.write(opt_line + "\n")
with open(wallet_conf_path, "w") as fp:
with open(wallet_conf_path + ".last") as fp_in:
@@ -1690,6 +1689,8 @@ def printHelp():
)
print("--preparebinonly Don't prepare settings or datadirs.")
print("--nocores Don't download and extract any coin clients.")
print("--addnetwork Add network.")
print("--disablenetwork Remove network.")
print("--usecontainers Expect each core to run in a unique container.")
print("--portoffset=n Raise all ports by n.")
print(
@@ -1767,7 +1768,7 @@ def finalise_daemon(d):
fp.close()
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
swap_client = None
daemons = []
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
@@ -1780,7 +1781,7 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
coin_name = "particl"
coin_settings = settings["chainclients"][coin_name]
daemon_args += getCoreBinArgs(c, coin_settings, prepare=True)
extra_config = {"stdout_to_file": True}
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
if coin_settings["manage_daemon"]:
filename: str = getCoreBinName(c, coin_settings, coin_name + "d")
daemons.append(
@@ -1804,10 +1805,12 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
"Must set WALLET_ENCRYPTION_PWD to add coin when Particl wallet is encrypted"
)
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
extra_opts["particl_daemon"] = daemons[-1]
finally:
if swap_client:
swap_client.finalise()
del swap_client
if "particl_daemon" not in extra_opts:
for d in daemons:
finalise_daemon(d)
@@ -1902,7 +1905,12 @@ def initialise_wallets(
)
]
extra_config = {"stdout_to_file": True}
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
if c == Coins.PART and "particl_daemon" in extra_opts:
daemons.append(extra_opts["particl_daemon"])
del extra_opts["particl_daemon"]
else:
daemons.append(
startDaemon(
coin_settings["datadir"],
@@ -2152,7 +2160,6 @@ def load_config(config_path):
def save_config(config_path, settings, add_options: bool = True) -> None:
if add_options is True:
# Add to config file only if manually set
if os.getenv("BSX_DOCKER_MODE"):
@@ -2232,11 +2239,18 @@ def check_btc_fastsync_data(base_dir, sync_filename):
ensureValidSignatureBy(verified, "nicolasdorier")
def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None:
if coin not in known_coins:
exitWithError(f"Unknown coin {coin.capitalize()}")
if test_disabled and not OVERRIDE_DISABLED_COINS and coin in disabled_coins:
exitWithError(f"{coin.capitalize()} is disabled")
def ensure_coin_valid(coin_name: str, test_disabled: bool = True) -> None:
if coin_name not in known_coins:
exitWithError(f"Unknown coin {coin_name.capitalize()}")
if test_disabled and not OVERRIDE_DISABLED_COINS and coin_name in disabled_coins:
exitWithError(f"{coin_name.capitalize()} is disabled")
def ensure_network_valid(network_name: str, test_disabled: bool = True) -> None:
if network_name not in known_networks:
exitWithError(f"Unknown network {network_name.capitalize()}")
if test_disabled and network_name in disabled_networks:
exitWithError(f"{network_name.capitalize()} is disabled")
def main():
@@ -2345,10 +2359,10 @@ def main():
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1].strip('"'))
data_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
continue
if name == "bindir":
bin_dir = os.path.expanduser(s[1].strip('"'))
bin_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
continue
if name == "portoffset":
port_offset = int(s[1])
@@ -2379,6 +2393,16 @@ def main():
disable_coin = s[1].strip().lower()
ensure_coin_valid(disable_coin, test_disabled=False)
continue
if name == "addnetwork":
network_name = s[1].strip().lower()
ensure_network_valid(network_name)
extra_opts["addnetwork"] = network_name
continue
if name == "disablenetwork":
network_name = s[1].strip().lower()
ensure_network_valid(network_name, test_disabled=False)
extra_opts["disablenetwork"] = network_name
continue
if name == "htmlhost":
htmlhost = s[1].strip('"')
continue
@@ -2395,7 +2419,9 @@ def main():
extra_opts["walletrestoretime"] = int(s[1])
continue
if name == "keysdirpath":
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
extra_opts["keysdirpath"] = os.path.abspath(
os.path.expanduser(s[1].strip('"'))
)
continue
if name == "trustremotenode":
extra_opts["trust_remote_node"] = toBool(s[1])
@@ -2793,6 +2819,8 @@ def main():
coin_settings["watch_wallet_name"] = getWalletName(
coin_params, "bsx_watch", prefix_override=f"{ticker}_WATCH"
)
if getLegacyKeyPathOption(coin_params) is True:
coin_settings["use_legacy_key_paths"] = True
if PART_RPC_USER != "":
chainclients["particl"]["rpcuser"] = PART_RPC_USER
@@ -2843,10 +2871,10 @@ def main():
settings = load_config(config_path)
init_coins = settings["chainclients"].keys()
logger.info("Active coins: %s", ", ".join(init_coins))
logger.info("Active coins: {}".format(", ".join(init_coins)))
if with_coins_changed:
init_coins = with_coins
logger.info("Initialising coins: %s", ", ".join(init_coins))
logger.info("Initialising coins: {}".format(", ".join(init_coins)))
initialise_wallets(
particl_wallet_mnemonic,
init_coins,
@@ -2903,7 +2931,7 @@ def main():
if "particl" in disable_coin:
exitWithError("Cannot disable Particl (required for operation)")
logger.info("Disabling coin: %s", disable_coin)
logger.info(f"Disabling coin: {disable_coin}")
settings = load_config(config_path)
if disable_coin not in settings["chainclients"]:
@@ -2928,7 +2956,7 @@ def main():
extra_opts["tor_control_password"] = tor_control_password
if add_coin != "":
logger.info("Adding coin: %s", add_coin)
logger.info(f"Adding coin: {add_coin}")
settings = load_config(config_path)
if add_coin in settings["chainclients"]:
@@ -2937,7 +2965,7 @@ def main():
coin_settings["connection_type"] == "none"
and coin_settings["manage_daemon"] is False
):
logger.info("Enabling coin: %s", add_coin)
logger.info(f"Enabling coin: {add_coin}")
coin_settings["connection_type"] = "rpc"
coin_settings["manage_daemon"] = True
if "manage_wallet_daemon" in coin_settings:
@@ -2945,21 +2973,27 @@ def main():
save_config(config_path, settings)
logger.info("Done.")
return 0
exitWithError("{} is already in the settings file".format(add_coin))
exitWithError(f"{add_coin} is already in the settings file")
if tor_control_password is None and settings.get("use_tor", False):
extra_opts["tor_control_password"] = settings.get(
"tor_control_password", None
)
try:
if particl_wallet_mnemonic != "none":
# Ensure Particl wallet is unencrypted or correct password is supplied
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
# Keep daemon running to use in initialise_wallets
test_particl_encryption(
data_dir, settings, chain, use_tor_proxy, extra_opts
)
settings["chainclients"][add_coin] = chainclients[add_coin]
if not no_cores:
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
prepareCore(
add_coin, known_coins[add_coin], settings, data_dir, extra_opts
)
if not (prepare_bin_only or upgrade_cores):
prepareDataDir(
@@ -2980,10 +3014,144 @@ def main():
)
save_config(config_path, settings)
finally:
if "particl_daemon" in extra_opts:
finalise_daemon(extra_opts["particl_daemon"])
del extra_opts["particl_daemon"]
logger.info(f"Done. Coin {add_coin} successfully added.")
return 0
if "addnetwork" in extra_opts:
network_name = extra_opts["addnetwork"]
logger.info(f"Adding network: {network_name}")
settings = load_config(config_path)
network_config_list = settings.get("networks", [])
if len(network_config_list) < 1:
network_config_list = [{"type": "smsg", "enabled": True}]
network_enabled: bool = False
if network_name == "simplex":
if SIMPLEX_GROUP_LINK is None:
raise ValueError("SIMPLEX_GROUP_LINK must be set.")
simplex_chat_bin_dir = os.path.join(bin_dir, "simplex")
simplex_chat_client_path = os.path.join(
simplex_chat_bin_dir, "simplex-chat"
)
simplex_chat_release_dir = os.path.join(
simplex_chat_bin_dir, SIMPLEX_CHAT_VERSION
)
if not os.path.exists(simplex_chat_release_dir):
os.makedirs(simplex_chat_release_dir)
if USE_PLATFORM == "Linux":
simplex_chat_release_file = "simplex-chat-ubuntu-24_04-x86-64"
elif USE_PLATFORM == "Darwin":
simplex_chat_release_file = "simplex-chat-macos-x86-64"
elif USE_PLATFORM == "Windows":
simplex_chat_release_file = "simplex-chat-windows-x86-64"
else:
raise ValueError(f"Unknown platform {USE_PLATFORM}")
simplex_chat_release_url = f"https://github.com/simplex-chat/simplex-chat/releases/download/v{SIMPLEX_CHAT_VERSION}/{simplex_chat_release_file}"
simplex_chat_release_path = os.path.join(
simplex_chat_release_dir, simplex_chat_release_file
)
downloadRelease(
simplex_chat_release_url, simplex_chat_release_path, extra_opts
)
assert_filename = "_sha256sums"
assert_path = os.path.join(simplex_chat_release_dir, assert_filename)
assert_url = f"https://github.com/simplex-chat/simplex-chat/releases/download/v{SIMPLEX_CHAT_VERSION}/_sha256sums"
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
release_hash: str = getFileHash(simplex_chat_release_path)
logger.info(f"{simplex_chat_release_file} hash: {release_hash}")
ensureFileHashInFile(release_hash, assert_path)
assert_sig_filename = assert_filename + ".asc"
assert_sig_url = assert_url + ".asc"
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
if not os.path.exists(assert_sig_path):
downloadFile(assert_sig_url, assert_sig_path)
gpg = gnupg.GPG()
pubkey_filename = "SimpleX_Chat.pgp"
pubkeyurls = []
if not havePubkey(gpg, expected_key_ids["SimpleX_Chat"][0]):
importPubkey(gpg, pubkey_filename, pubkeyurls)
with open(assert_sig_path, "rb") as fp:
verified = gpg.verify_file(fp, assert_path)
ensureValidSignatureBy(verified, "SimpleX_Chat")
shutil.copyfile(simplex_chat_release_path, simplex_chat_client_path)
simplex_settings = {
"type": "simplex",
"server_address": SIMPLEX_SERVER_ADDRESS,
"client_path": simplex_chat_client_path,
"ws_port": SIMPLEX_WS_PORT,
"group_link": SIMPLEX_GROUP_LINK,
"enabled": True,
}
if SIMPLEX_SERVER_SOCKS_PROXY is not None:
simplex_settings["socks_proxy_override"] = SIMPLEX_SERVER_SOCKS_PROXY
found_network: bool = False
for network in network_config_list:
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
found_network = True
if network.get("enabled", False) is True:
logger.warning(f"Network {network_type} is already active.")
network = simplex_settings
else:
# TODO: Allow multiple active networks
network["enabled"] = False
logger.info(f"Disabling network {network_type}.")
if found_network is False:
network_config_list.append(simplex_settings)
elif network_name == "smsg":
found_network: bool = False
for network in network_config_list:
network_type: str = network.get("type", "unknown")
if network_type == "smsg":
found_network = True
if network.get("enabled", False) is True:
logger.warning(f"Network {network_type} is already active.")
else:
network["enabled"] = True
else:
# TODO: Allow multiple active networks
network["enabled"] = False
logger.info(f"Disabling network {network_type}.")
if found_network is False:
network_config_list.append({"type": "smsg", "enabled": True})
else:
raise ValueError(f"Unknown network {network_name}")
settings["networks"] = network_config_list
save_config(config_path, settings)
if network_enabled:
logger.info(f"Done. Network {network_name} successfully added.")
else:
logger.info("Done.")
return 0
if "disablenetwork" in extra_opts:
network_name = extra_opts["disablenetwork"]
logger.info(f"Disable network: {network_name}")
settings = load_config(config_path)
network_config_list = settings.get("networks", [])
if len(network_config_list) < 1:
network_config_list = [{"type": "smsg", "enabled": True}]
logger.info(f"Done. Network {network_name} successfully disabled.")
return 0
logger.info(
"With coins: "
+ (", ".join(with_coins))
@@ -3074,6 +3242,10 @@ def main():
for c in with_coins:
withchainclients[c] = chainclients[c]
zmq_server_pubkey, zmq_server_key = zmq.curve_keypair()
zmq_client_pubkey, zmq_client_key = zmq.curve_keypair()
extra_opts["zmqsecret"] = base64.b64encode(zmq_server_key).decode("utf-8")
settings = {
"debug": True,
"zmqhost": f"tcp://{PART_RPC_HOST}",
@@ -3089,6 +3261,12 @@ def main():
"check_watched_seconds": 60,
"check_expired_seconds": 60,
"wallet_update_timeout": 10, # Seconds to wait for wallet page update
"zmq_client_key": base64.b64encode(zmq_client_key).decode("utf-8"),
"zmq_client_pubkey": base64.b64encode(zmq_client_pubkey).decode("utf-8"),
"zmq_server_pubkey": base64.b64encode(zmq_server_pubkey).decode("utf-8"),
"enabled_log_categories": [
"net",
],
}
wshost: str = extra_opts.get("wshost", htmlhost)
@@ -3096,6 +3274,11 @@ def main():
settings["wshost"] = wshost
settings["wsport"] = UI_WS_PORT + port_offset
if "CHECK_FOR_BSX_UPDATES" in os.environ:
settings["check_updates"] = CHECK_FOR_BSX_UPDATES
elif BSX_TEST_MODE is True:
settings["check_updates"] = False
if use_tor_proxy:
tor_control_password = generate_salt(24)
addTorSettings(settings, tor_control_password)

View File

@@ -17,12 +17,11 @@ import traceback
import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
@@ -33,41 +32,81 @@ logger = initial_logger
swap_client = None
class Daemon:
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v["name"]:
return True
return False
def signal_handler(sig, frame):
os.write(
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
)
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
logger.info(f"AMM signal shutdown: {msg}")
else:
logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}")
swap_client.stopRunning()
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
try:
datadir = part_settings.get("datadir")
if not datadir:
return
config_path = os.path.join(datadir, "particl.conf")
if not os.path.exists(config_path):
return
with open(config_path, "r") as f:
config_content = f.read()
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
zmq_port = swap_settings.get("zmqport", 14792)
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
if "zmqpubhashwtx=" not in config_content:
with open(config_path, "a") as f:
f.write(f"{expected_line}\n")
elif expected_line not in config_content:
lines = config_content.split("\n")
updated_lines = []
for line in lines:
if line.startswith("zmqpubhashwtx="):
updated_lines.append(expected_line)
else:
updated_lines.append(line)
with open(config_path, "w") as f:
f.write("\n".join(updated_lines))
except Exception as e:
logger.debug(f"Error checking PART ZMQ config: {e}")
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
coin_name = extra_config.get("coin_name", "")
# Rewrite litecoin.conf
# TODO: Remove
needs_rewrite: bool = False
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
needs_rewrite: bool = False
add_changetype: bool = True
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if line.endswith("=onion"):
needs_rewrite = True
break
@@ -83,6 +122,29 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
fp_to.write(line.strip()[:-6] + "\n")
else:
fp_to.write(line)
if add_changetype:
fp_to.write("changetype=bech32\n")
add_changetype = False
if add_changetype:
logger.info("Adding changetype to litecoin.conf")
with open(ltc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
# Rewrite bitcoin.conf
# TODO: Remove
btc_conf_path = os.path.join(datadir_path, "bitcoin.conf")
if coin_name == "bitcoin" and os.path.exists(btc_conf_path):
add_changetype: bool = True
with open(btc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if add_changetype:
logger.info("Adding changetype to bitcoin.conf")
with open(btc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
args = [
daemon_bin,
@@ -91,7 +153,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
if add_datadir:
args.append("-datadir=" + datadir_path)
args += opts
logger.info("Starting node {}".format(daemon_bin))
logger.info(f"Starting node {daemon_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
opened_files = []
@@ -122,6 +184,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
cwd=datadir_path,
),
opened_files,
os.path.basename(daemon_bin),
)
@@ -137,7 +200,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
"--non-interactive",
"--config-file=" + os.path.join(datadir_path, config_filename),
] + opts
logger.info("Starting node {}".format(daemon_bin))
logger.info(f"Starting node {daemon_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -152,6 +215,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
cwd=datadir_path,
),
[file_stdout, file_stderr],
os.path.basename(daemon_bin),
)
@@ -200,7 +264,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
):
fp_to.write(line)
logger.info("Starting wallet daemon {}".format(wallet_bin))
logger.info(f"Starting wallet daemon {wallet_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
@@ -215,28 +279,10 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
cwd=data_dir,
),
[wallet_stdout, wallet_stderr],
os.path.basename(wallet_bin),
)
def ws_new_client(client, server):
if swap_client:
swap_client.log.debug(f'ws_new_client {client["id"]}')
def ws_client_left(client, server):
if client is None:
return
if swap_client:
swap_client.log.debug(f'ws_client_left {client["id"]}')
def ws_message_received(client, server, message):
if len(message) > 200:
message = message[:200] + ".."
if swap_client:
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get(
"core_binname", chainparams[coin_id].get("core_binname", default_name)
@@ -275,13 +321,36 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
return extra_args
def mainLoop(daemons, update: bool = True):
while not swap_client.delay_event.wait(0.5):
if update:
swap_client.update()
else:
pass
for daemon in daemons:
if daemon.running is False:
continue
poll = daemon.handle.poll()
if poll is None:
pass # Process is running
else:
daemon.running = False
swap_client.log.error(
f"Process {daemon.handle.pid} for {daemon.name} terminated unexpectedly returning {poll}."
)
def runClient(
data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
data_dir: str,
chain: str,
start_only_coins: bool,
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
global swap_client, logger
daemons = []
pids = []
threads = []
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
pids_path = os.path.join(data_dir, ".pids")
@@ -302,30 +371,76 @@ def runClient(
with open(settings_path) as fs:
settings = json.load(fs)
swap_client = BasicSwap(data_dir, settings, chain, log_name=log_prefix)
swap_client = BasicSwap(
data_dir, settings, chain, log_name=log_prefix, extra_opts=extra_opts
)
logger = swap_client.log
if os.path.exists(pids_path):
with open(pids_path) as fd:
for ln in fd:
# TODO: try close
logger.warning("Found pid for daemon {} ".format(ln.strip()))
logger.warning("Found pid for daemon {}".format(ln.strip()))
# Ensure daemons are stopped
swap_client.stopDaemons()
# Settings may have been modified
settings = swap_client.settings
try:
# Try start daemons
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
)
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
continue
if (
len(swap_client.with_coins_override) > 0
and c not in swap_client.with_coins_override
) or c in swap_client.without_coins_override:
if v.get("manage_daemon", False) or v.get(
"manage_wallet_daemon", False
):
logger.warning(
f"Not starting coin {c.capitalize()}, disabled by arguments."
)
continue
try:
coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id)
except Exception as e: # noqa: F841
logger.warning("Not starting unknown coin: {}".format(c))
logger.warning(f"Not starting unknown coin: {c}")
continue
if c in ("monero", "wownero"):
if v["manage_daemon"] is True:
@@ -334,7 +449,7 @@ def runClient(
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -382,7 +497,7 @@ def runClient(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
)
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
continue # /monero
@@ -401,6 +516,7 @@ def runClient(
"stdout_to_file": True,
"stdout_filename": "dcrd_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -412,7 +528,7 @@ def runClient(
)
)
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -429,6 +545,7 @@ def runClient(
"stdout_to_file": True,
"stdout_filename": "dcrwallet_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -445,19 +562,29 @@ def runClient(
continue # /decred
if v["manage_daemon"] is True:
if c == "particl" and swap_client._zmq_queue_enabled:
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
extra_config = {"coin_name": c}
daemons.append(
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
startDaemon(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
pids.append((c, pid))
swap_client.setDaemonPID(c, pid)
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if len(pids) > 0:
with open(pids_path, "w") as fd:
for p in pids:
@@ -471,47 +598,12 @@ def runClient(
logger.info(
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
)
while not swap_client.delay_event.wait(0.5):
pass
mainLoop(daemons, update=False)
else:
swap_client.start()
if "htmlhost" in settings:
swap_client.log.info(
"Starting http server at http://%s:%d."
% (settings["htmlhost"], settings["htmlport"])
)
allow_cors = (
settings["allowcors"]
if "allowcors" in settings
else cfg.DEFAULT_ALLOW_CORS
)
thread_http = HttpThread(
settings["htmlhost"],
settings["htmlport"],
allow_cors,
swap_client,
)
threads.append(thread_http)
thread_http.start()
if "wshost" in settings:
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
swap_client.log.info(f"Starting ws server at {ws_url}.")
swap_client.ws_server = WebsocketServer(
host=settings["wshost"], port=settings["wsport"]
)
swap_client.ws_server.client_port = settings.get(
"wsclientport", settings["wsport"]
)
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
logger.info("Exit with Ctrl + c.")
while not swap_client.delay_event.wait(0.5):
swap_client.update()
mainLoop(daemons)
except Exception as e: # noqa: F841
traceback.print_exc()
@@ -524,23 +616,16 @@ def runClient(
traceback.print_exc()
swap_client.finalise()
swap_client.log.info("Stopping HTTP threads.")
for t in threads:
try:
t.stop()
t.join()
except Exception as e: # noqa: F841
traceback.print_exc()
closed_pids = []
for d in daemons:
swap_client.log.info(f"Interrupting {d.handle.pid}")
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}")
try:
d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
except Exception as e:
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
for d in daemons:
try:
d.handle.wait(timeout=120)
@@ -576,6 +661,11 @@ def printVersion():
)
def ensure_coin_valid(coin: str) -> bool:
if isKnownCoinName(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
def printHelp():
print("Usage: basicswap-run ")
print("\n--help, -h Print help.")
@@ -586,10 +676,15 @@ def printHelp():
print("--mainnet Run in mainnet mode.")
print("--testnet Run in testnet mode.")
print("--regtest Run in regtest mode.")
print("--withcoin= Run only with coin/s.")
print("--withoutcoin= Run without coin/s.")
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print("--logprefix Specify log prefix.")
print(
"--forcedbupgrade Recheck database against schema regardless of version."
)
def main():
@@ -597,6 +692,9 @@ def main():
chain = "mainnet"
start_only_coins = set()
log_prefix: str = "BasicSwap"
options = dict()
with_coins = set()
without_coins = set()
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != "-":
@@ -620,18 +718,31 @@ def main():
if name in ("mainnet", "testnet", "regtest"):
chain = name
continue
if name in ("withcoin", "withcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
ensure_coin_valid(coin)
with_coins.add(coin)
continue
if name in ("withoutcoin", "withoutcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
if coin == "particl":
raise ValueError("Particl is required.")
ensure_coin_valid(coin)
without_coins.add(coin)
continue
if name == "forcedbupgrade":
options["force_db_upgrade"] = True
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1])
data_dir = os.path.abspath(os.path.expanduser(s[1]))
continue
if name == "logprefix":
log_prefix = s[1]
continue
if name == "startonlycoin":
for coin in [s.lower() for s in s[1].split(",")]:
if is_known_coin(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
ensure_coin_valid(coin)
start_only_coins.add(coin)
continue
@@ -650,8 +761,14 @@ def main():
if not os.path.exists(data_dir):
os.makedirs(data_dir)
if len(with_coins) > 0:
with_coins.add("particl")
options["with_coins"] = with_coins
if len(without_coins) > 0:
options["without_coins"] = without_coins
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix)
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix, options)
print("Done.")
return fail_code

View File

@@ -571,3 +571,7 @@ def getCoinIdFromName(name: str) -> str:
return name_map[name.lower()]
except Exception:
raise ValueError(f"Unknown coin {name}")
def isKnownCoinName(name: str) -> bool:
return params["name"].lower() in name_map

View File

@@ -9,6 +9,8 @@ import os
CONFIG_FILENAME = "basicswap.json"
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
DEFAULT_ALLOW_CORS = False
DEFAULT_RPC_POOL_ENABLED = True
DEFAULT_RPC_POOL_MAX_CONNECTIONS = 5
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
DEFAULT_TEST_BINDIR = os.path.expanduser(
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))

View File

@@ -1,356 +0,0 @@
# ed25519.py - Optimized version of the reference implementation of Ed25519
#
# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to>
# 2013 by Donald Stufft <donald@stufft.io>
# 2013 by Alex Gaynor <alex.gaynor@gmail.com>
# 2013 by Greg Price <price@mit.edu>
#
# To the extent possible under law, the author(s) have dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication along
# with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
"""
NB: This code is not safe for use with secret keys or secret data.
The only safe use of this code is for verifying signatures on public messages.
Functions for computing the public key of a secret key and for signing
a message are included, namely publickey_unsafe and signature_unsafe,
for testing purposes only.
The root of the problem is that Python's long-integer arithmetic is
not designed for use in cryptography. Specifically, it may take more
or less time to execute an operation depending on the values of the
inputs, and its memory access patterns may also depend on the inputs.
This opens it to timing and cache side-channel attacks which can
disclose data to an attacker. We rely on Python's long-integer
arithmetic, so we cannot handle secrets without risking their disclosure.
"""
import hashlib
import operator
import sys
__version__ = "1.0.dev0"
# Useful for very coarse version differentiation.
PY3 = sys.version_info[0] == 3
if PY3:
indexbytes = operator.getitem
intlist2bytes = bytes
int2byte = operator.methodcaller("to_bytes", 1, "big")
else:
int2byte = chr
range = xrange
def indexbytes(buf, i):
return ord(buf[i])
def intlist2bytes(l):
return b"".join(chr(c) for c in l)
b = 256
q = 2 ** 255 - 19
l = 2 ** 252 + 27742317777372353535851937790883648493
def H(m):
return hashlib.sha512(m).digest()
def pow2(x, p):
"""== pow(x, 2**p, q)"""
while p > 0:
x = x * x % q
p -= 1
return x
def inv(z):
"""$= z^{-1} \mod q$, for z != 0"""
# Adapted from curve25519_athlon.c in djb's Curve25519.
z2 = z * z % q # 2
z9 = pow2(z2, 2) * z % q # 9
z11 = z9 * z2 % q # 11
z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0
z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0
z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ...
z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0
return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2
d = -121665 * inv(121666) % q
I = pow(2, (q - 1) // 4, q)
def xrecover(y, sign=0):
xx = (y * y - 1) * inv(d * y * y + 1)
x = pow(xx, (q + 3) // 8, q)
if (x * x - xx) % q != 0:
x = (x * I) % q
if x % 2 != sign:
x = q-x
return x
By = 4 * inv(5)
Bx = xrecover(By)
B = (Bx % q, By % q, 1, (Bx * By) % q)
ident = (0, 1, 1, 0)
def edwards_add(P, Q):
# This is formula sequence 'addition-add-2008-hwcd-3' from
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
(x1, y1, z1, t1) = P
(x2, y2, z2, t2) = Q
a = (y1-x1)*(y2-x2) % q
b = (y1+x1)*(y2+x2) % q
c = t1*2*d*t2 % q
dd = z1*2*z2 % q
e = b - a
f = dd - c
g = dd + c
h = b + a
x3 = e*f
y3 = g*h
t3 = e*h
z3 = f*g
return (x3 % q, y3 % q, z3 % q, t3 % q)
def edwards_sub(P, Q):
# This is formula sequence 'addition-add-2008-hwcd-3' from
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
(x1, y1, z1, t1) = P
(x2, y2, z2, t2) = Q
# https://eprint.iacr.org/2008/522.pdf
# The negative of (X:Y:Z)is (X:Y:Z)
#x2 = q-x2
"""
doesn't work
x2 = q-x2
t2 = (x2*y2) % q
"""
zi = inv(z2)
x2 = q-((x2 * zi) % q)
y2 = (y2 * zi) % q
z2 = 1
t2 = (x2*y2) % q
a = (y1-x1)*(y2-x2) % q
b = (y1+x1)*(y2+x2) % q
c = t1*2*d*t2 % q
dd = z1*2*z2 % q
e = b - a
f = dd - c
g = dd + c
h = b + a
x3 = e*f
y3 = g*h
t3 = e*h
z3 = f*g
return (x3 % q, y3 % q, z3 % q, t3 % q)
def edwards_double(P):
# This is formula sequence 'dbl-2008-hwcd' from
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
(x1, y1, z1, t1) = P
a = x1*x1 % q
b = y1*y1 % q
c = 2*z1*z1 % q
# dd = -a
e = ((x1+y1)*(x1+y1) - a - b) % q
g = -a + b # dd + b
f = g - c
h = -a - b # dd - b
x3 = e*f
y3 = g*h
t3 = e*h
z3 = f*g
return (x3 % q, y3 % q, z3 % q, t3 % q)
def scalarmult(P, e):
if e == 0:
return ident
Q = scalarmult(P, e // 2)
Q = edwards_double(Q)
if e & 1:
Q = edwards_add(Q, P)
return Q
# Bpow[i] == scalarmult(B, 2**i)
Bpow = []
def make_Bpow():
P = B
for i in range(253):
Bpow.append(P)
P = edwards_double(P)
make_Bpow()
def scalarmult_B(e):
"""
Implements scalarmult(B, e) more efficiently.
"""
# scalarmult(B, l) is the identity
e = e % l
P = ident
for i in range(253):
if e & 1:
P = edwards_add(P, Bpow[i])
e = e // 2
assert e == 0, e
return P
def encodeint(y):
bits = [(y >> i) & 1 for i in range(b)]
return b''.join([
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
for i in range(b//8)
])
def encodepoint(P):
(x, y, z, t) = P
zi = inv(z)
x = (x * zi) % q
y = (y * zi) % q
bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
return b''.join([
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
for i in range(b // 8)
])
def bit(h, i):
return (indexbytes(h, i // 8) >> (i % 8)) & 1
def publickey_unsafe(sk):
"""
Not safe to use with secret keys or secret data.
See module docstring. This function should be used for testing only.
"""
h = H(sk)
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
A = scalarmult_B(a)
return encodepoint(A)
def Hint(m):
h = H(m)
return sum(2 ** i * bit(h, i) for i in range(2 * b))
def signature_unsafe(m, sk, pk):
"""
Not safe to use with secret keys or secret data.
See module docstring. This function should be used for testing only.
"""
h = H(sk)
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
r = Hint(
intlist2bytes([indexbytes(h, j) for j in range(b // 8, b // 4)]) + m
)
R = scalarmult_B(r)
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
return encodepoint(R) + encodeint(S)
def isoncurve(P):
(x, y, z, t) = P
return (z % q != 0 and
x*y % q == z*t % q and
(y*y - x*x - z*z - d*t*t) % q == 0)
def decodeint(s):
return sum(2 ** i * bit(s, i) for i in range(0, b))
def decodepoint(s):
y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
x = xrecover(y)
if x & 1 != bit(s, b-1):
x = q - x
P = (x, y, 1, (x*y) % q)
if not isoncurve(P):
raise ValueError("decoding point that is not on curve")
return P
class SignatureMismatch(Exception):
pass
def checkvalid(s, m, pk):
"""
Not safe to use when any argument is secret.
See module docstring. This function should be used only for
verifying public signatures of public messages.
"""
if len(s) != b // 4:
raise ValueError("signature length is wrong")
if len(pk) != b // 8:
raise ValueError("public-key length is wrong")
R = decodepoint(s[:b // 8])
A = decodepoint(pk)
S = decodeint(s[b // 8:b // 4])
h = Hint(encodepoint(R) + pk + m)
(x1, y1, z1, t1) = P = scalarmult_B(S)
(x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h))
if (not isoncurve(P) or not isoncurve(Q) or
(x1*z2 - x2*z1) % q != 0 or (y1*z2 - y2*z1) % q != 0):
raise SignatureMismatch("signature does not pass verification")
def is_identity(P):
return True if P[0] == 0 else False
def edwards_negated(P):
(x, y, z, t) = P
zi = inv(z)
x = q - ((x * zi) % q)
y = (y * zi) % q
z = 1
t = (x * y) % q
return (x, y, z, t)

View File

@@ -1,486 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Implementation of elliptic curves, for cryptographic applications.
#
# This module doesn't provide any way to choose a random elliptic
# curve, nor to verify that an elliptic curve was chosen randomly,
# because one can simply use NIST's standard curves.
#
# Notes from X9.62-1998 (draft):
# Nomenclature:
# - Q is a public key.
# The "Elliptic Curve Domain Parameters" include:
# - q is the "field size", which in our case equals p.
# - p is a big prime.
# - G is a point of prime order (5.1.1.1).
# - n is the order of G (5.1.1.1).
# Public-key validation (5.2.2):
# - Verify that Q is not the point at infinity.
# - Verify that X_Q and Y_Q are in [0,p-1].
# - Verify that Q is on the curve.
# - Verify that nQ is the point at infinity.
# Signature generation (5.3):
# - Pick random k from [1,n-1].
# Signature checking (5.4.2):
# - Verify that r and s are in [1,n-1].
#
# Version of 2008.11.25.
#
# Revision history:
# 2005.12.31 - Initial version.
# 2008.11.25 - Change CurveFp.is_on to contains_point.
#
# Written in 2005 by Peter Pearson and placed in the public domain.
def inverse_mod(a, m):
"""Inverse of a mod m."""
if a < 0 or m <= a:
a = a % m
# From Ferguson and Schneier, roughly:
c, d = a, m
uc, vc, ud, vd = 1, 0, 0, 1
while c != 0:
q, c, d = divmod(d, c) + (c,)
uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc
# At this point, d is the GCD, and ud*a+vd*m = d.
# If d == 1, this means that ud is a inverse.
assert d == 1
if ud > 0:
return ud
else:
return ud + m
def modular_sqrt(a, p):
# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
""" Find a quadratic residue (mod p) of 'a'. p
must be an odd prime.
Solve the congruence of the form:
x^2 = a (mod p)
And returns x. Note that p - x is also a root.
0 is returned is no square root exists for
these a and p.
The Tonelli-Shanks algorithm is used (except
for some simple cases in which the solution
is known from an identity). This algorithm
runs in polynomial time (unless the
generalized Riemann hypothesis is false).
"""
# Simple cases
#
if legendre_symbol(a, p) != 1:
return 0
elif a == 0:
return 0
elif p == 2:
return p
elif p % 4 == 3:
return pow(a, (p + 1) // 4, p)
# Partition p-1 to s * 2^e for an odd s (i.e.
# reduce all the powers of 2 from p-1)
#
s = p - 1
e = 0
while s % 2 == 0:
s /= 2
e += 1
# Find some 'n' with a legendre symbol n|p = -1.
# Shouldn't take long.
#
n = 2
while legendre_symbol(n, p) != -1:
n += 1
# Here be dragons!
# Read the paper "Square roots from 1; 24, 51,
# 10 to Dan Shanks" by Ezra Brown for more
# information
#
# x is a guess of the square root that gets better
# with each iteration.
# b is the "fudge factor" - by how much we're off
# with the guess. The invariant x^2 = ab (mod p)
# is maintained throughout the loop.
# g is used for successive powers of n to update
# both a and b
# r is the exponent - decreases with each update
#
x = pow(a, (s + 1) // 2, p)
b = pow(a, s, p)
g = pow(n, s, p)
r = e
while True:
t = b
m = 0
for m in range(r):
if t == 1:
break
t = pow(t, 2, p)
if m == 0:
return x
gs = pow(g, 2 ** (r - m - 1), p)
g = (gs * gs) % p
x = (x * gs) % p
b = (b * g) % p
r = m
def legendre_symbol(a, p):
""" Compute the Legendre symbol a|p using
Euler's criterion. p is a prime, a is
relatively prime to p (if p divides
a, then a|p = 0)
Returns 1 if a has a square root modulo
p, -1 otherwise.
"""
ls = pow(a, (p - 1) // 2, p)
return -1 if ls == p - 1 else ls
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
See http://en.wikipedia.org/wiki/Jacobi_symbol
For our application k is always prime, so this is the same as the Legendre symbol."""
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
n %= k
t = 0
while n != 0:
while n & 1 == 0:
n >>= 1
r = k & 7
t ^= (r == 3 or r == 5)
n, k = k, n
t ^= (n & k & 3 == 3)
n = n % k
if k == 1:
return -1 if t else 1
return 0
class CurveFp(object):
"""Elliptic Curve over the field of integers modulo a prime."""
def __init__(self, p, a, b):
"""The curve of points satisfying y^2 = x^3 + a*x + b (mod p)."""
self.__p = p
self.__a = a
self.__b = b
def p(self):
return self.__p
def a(self):
return self.__a
def b(self):
return self.__b
def contains_point(self, x, y):
"""Is the point (x,y) on this curve?"""
return (y * y - (x * x * x + self.__a * x + self.__b)) % self.__p == 0
class Point(object):
""" A point on an elliptic curve. Altering x and y is forbidding,
but they can be read by the x() and y() methods."""
def __init__(self, curve, x, y, order=None):
"""curve, x, y, order; order (optional) is the order of this point."""
self.__curve = curve
self.__x = x
self.__y = y
self.__order = order
# self.curve is allowed to be None only for INFINITY:
if self.__curve:
assert self.__curve.contains_point(x, y)
if order:
assert self * order == INFINITY
def __eq__(self, other):
"""Return 1 if the points are identical, 0 otherwise."""
if self.__curve == other.__curve \
and self.__x == other.__x \
and self.__y == other.__y:
return 1
else:
return 0
def __add__(self, other):
"""Add one point to another point."""
# X9.62 B.3:
if other == INFINITY:
return self
if self == INFINITY:
return other
assert self.__curve == other.__curve
if self.__x == other.__x:
if (self.__y + other.__y) % self.__curve.p() == 0:
return INFINITY
else:
return self.double()
p = self.__curve.p()
l = ((other.__y - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
x3 = (l * l - self.__x - other.__x) % p
y3 = (l * (self.__x - x3) - self.__y) % p
return Point(self.__curve, x3, y3)
def __sub__(self, other):
#The inverse of a point P=(xP,yP) is its reflexion across the x-axis : P=(xP,yP).
#If you want to compute QP, just replace yP by yP in the usual formula for point addition.
# X9.62 B.3:
if other == INFINITY:
return self
if self == INFINITY:
return other
assert self.__curve == other.__curve
p = self.__curve.p()
#opi = inverse_mod(other.__y, p)
opi = -other.__y % p
#print(opi)
#print(-other.__y % p)
if self.__x == other.__x:
if (self.__y + opi) % self.__curve.p() == 0:
return INFINITY
else:
return self.double
l = ((opi - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
x3 = (l * l - self.__x - other.__x) % p
y3 = (l * (self.__x - x3) - self.__y) % p
return Point(self.__curve, x3, y3)
def __mul__(self, e):
if self.__order:
e %= self.__order
if e == 0 or self == INFINITY:
return INFINITY
result, q = INFINITY, self
while e:
if e & 1:
result += q
e, q = e >> 1, q.double()
return result
"""
def __mul__(self, other):
#Multiply a point by an integer.
def leftmost_bit( x ):
assert x > 0
result = 1
while result <= x: result = 2 * result
return result // 2
e = other
if self.__order: e = e % self.__order
if e == 0: return INFINITY
if self == INFINITY: return INFINITY
assert e > 0
# From X9.62 D.3.2:
e3 = 3 * e
negative_self = Point( self.__curve, self.__x, -self.__y, self.__order )
i = leftmost_bit( e3 ) // 2
result = self
# print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 )
while i > 1:
result = result.double()
if ( e3 & i ) != 0 and ( e & i ) == 0: result = result + self
if ( e3 & i ) == 0 and ( e & i ) != 0: result = result + negative_self
# print ". . . i = %d, result = %s" % ( i, result )
i = i // 2
return result
"""
def __rmul__(self, other):
"""Multiply a point by an integer."""
return self * other
def __str__(self):
if self == INFINITY:
return "infinity"
return "(%d, %d)" % (self.__x, self.__y)
def inverse(self):
return Point(self.__curve, self.__x, -self.__y % self.__curve.p())
def double(self):
"""Return a new point that is twice the old."""
if self == INFINITY:
return INFINITY
# X9.62 B.3:
p = self.__curve.p()
a = self.__curve.a()
l = ((3 * self.__x * self.__x + a) * inverse_mod(2 * self.__y, p)) % p
x3 = (l * l - 2 * self.__x) % p
y3 = (l * (self.__x - x3) - self.__y) % p
return Point(self.__curve, x3, y3)
def x(self):
return self.__x
def y(self):
return self.__y
def pair(self):
return (self.__x, self.__y)
def curve(self):
return self.__curve
def order(self):
return self.__order
# This one point is the Point At Infinity for all purposes:
INFINITY = Point(None, None, None)
def __main__():
class FailedTest(Exception):
pass
def test_add(c, x1, y1, x2, y2, x3, y3):
"""We expect that on curve c, (x1,y1) + (x2, y2 ) = (x3, y3)."""
p1 = Point(c, x1, y1)
p2 = Point(c, x2, y2)
p3 = p1 + p2
print("%s + %s = %s" % (p1, p2, p3))
if p3.x() != x3 or p3.y() != y3:
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
else:
print(" Good.")
def test_double(c, x1, y1, x3, y3):
"""We expect that on curve c, 2*(x1,y1) = (x3, y3)."""
p1 = Point(c, x1, y1)
p3 = p1.double()
print("%s doubled = %s" % (p1, p3))
if p3.x() != x3 or p3.y() != y3:
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
else:
print(" Good.")
def test_double_infinity(c):
"""We expect that on curve c, 2*INFINITY = INFINITY."""
p1 = INFINITY
p3 = p1.double()
print("%s doubled = %s" % (p1, p3))
if p3.x() != INFINITY.x() or p3.y() != INFINITY.y():
raise FailedTest("Failure: should give (%d,%d)." % (INFINITY.x(), INFINITY.y()))
else:
print(" Good.")
def test_multiply(c, x1, y1, m, x3, y3):
"""We expect that on curve c, m*(x1,y1) = (x3,y3)."""
p1 = Point(c, x1, y1)
p3 = p1 * m
print("%s * %d = %s" % (p1, m, p3))
if p3.x() != x3 or p3.y() != y3:
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
else:
print(" Good.")
# A few tests from X9.62 B.3:
c = CurveFp(23, 1, 1)
test_add(c, 3, 10, 9, 7, 17, 20)
test_double(c, 3, 10, 7, 12)
test_add(c, 3, 10, 3, 10, 7, 12) # (Should just invoke double.)
test_multiply(c, 3, 10, 2, 7, 12)
test_double_infinity(c)
# From X9.62 I.1 (p. 96):
g = Point(c, 13, 7, 7)
check = INFINITY
for i in range(7 + 1):
p = (i % 7) * g
print("%s * %d = %s, expected %s . . ." % (g, i, p, check))
if p == check:
print(" Good.")
else:
raise FailedTest("Bad.")
check = check + g
# NIST Curve P-192:
p = 6277101735386680763835789423207666416083908700390324961279
r = 6277101735386680763835789423176059013767194773182842284081
#s = 0x3045ae6fc8422f64ed579528d38120eae12196d5L
c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65
b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
c192 = CurveFp(p, -3, b)
p192 = Point(c192, Gx, Gy, r)
# Checking against some sample computations presented
# in X9.62:
d = 651056770906015076056810763456358567190100156695615665659
Q = d * p192
if Q.x() != 0x62B12D60690CDCF330BABAB6E69763B471F994DD702D16A5:
raise FailedTest("p192 * d came out wrong.")
else:
print("p192 * d came out right.")
k = 6140507067065001063065065565667405560006161556565665656654
R = k * p192
if R.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
or R.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
raise FailedTest("k * p192 came out wrong.")
else:
print("k * p192 came out right.")
u1 = 2563697409189434185194736134579731015366492496392189760599
u2 = 6266643813348617967186477710235785849136406323338782220568
temp = u1 * p192 + u2 * Q
if temp.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
or temp.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
raise FailedTest("u1 * p192 + u2 * Q came out wrong.")
else:
print("u1 * p192 + u2 * Q came out right.")
if __name__ == "__main__":
__main__()

View File

@@ -1,386 +0,0 @@
# Copyright (c) 2019 Pieter Wuille
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test-only secp256k1 elliptic curve implementation
WARNING: This code is slow, uses bad randomness, does not properly protect
keys, and is trivially vulnerable to side channel attacks. Do not use for
anything but tests."""
import random
def modinv(a, n):
"""Compute the modular inverse of a modulo n
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
"""
t1, t2 = 0, 1
r1, r2 = n, a
while r2 != 0:
q = r1 // r2
t1, t2 = t2, t1 - q * t2
r1, r2 = r2, r1 - q * r2
if r1 > 1:
return None
if t1 < 0:
t1 += n
return t1
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
See http://en.wikipedia.org/wiki/Jacobi_symbol
For our application k is always prime, so this is the same as the Legendre symbol."""
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
n %= k
t = 0
while n != 0:
while n & 1 == 0:
n >>= 1
r = k & 7
t ^= (r == 3 or r == 5)
n, k = k, n
t ^= (n & k & 3 == 3)
n = n % k
if k == 1:
return -1 if t else 1
return 0
def modsqrt(a, p):
"""Compute the square root of a modulo p when p % 4 = 3.
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
Limiting this function to only work for p % 4 = 3 means we don't need to
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
"""
if p % 4 != 3:
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
sqrt = pow(a, (p + 1)//4, p)
if pow(sqrt, 2, p) == a % p:
return sqrt
return None
class EllipticCurve:
def __init__(self, p, a, b):
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
self.p = p
self.a = a % p
self.b = b % p
def affine(self, p1):
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
An affine point is represented as the Jacobian (x, y, 1)"""
x1, y1, z1 = p1
if z1 == 0:
return None
inv = modinv(z1, self.p)
inv_2 = (inv**2) % self.p
inv_3 = (inv_2 * inv) % self.p
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
def negate(self, p1):
"""Negate a Jacobian point tuple p1."""
x1, y1, z1 = p1
return (x1, (self.p - y1) % self.p, z1)
def on_curve(self, p1):
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
x1, y1, z1 = p1
z2 = pow(z1, 2, self.p)
z4 = pow(z2, 2, self.p)
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
def is_x_coord(self, x):
"""Test whether x is a valid X coordinate on the curve."""
x_3 = pow(x, 3, self.p)
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
def lift_x(self, x):
"""Given an X coordinate on the curve, return a corresponding affine point."""
x_3 = pow(x, 3, self.p)
v = x_3 + self.a * x + self.b
y = modsqrt(v, self.p)
if y is None:
return None
return (x, y, 1)
def double(self, p1):
"""Double a Jacobian tuple p1
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
x1, y1, z1 = p1
if z1 == 0:
return (0, 1, 0)
y1_2 = (y1**2) % self.p
y1_4 = (y1_2**2) % self.p
x1_2 = (x1**2) % self.p
s = (4*x1*y1_2) % self.p
m = 3*x1_2
if self.a:
m += self.a * pow(z1, 4, self.p)
m = m % self.p
x2 = (m**2 - 2*s) % self.p
y2 = (m*(s - x2) - 8*y1_4) % self.p
z2 = (2*y1*z1) % self.p
return (x2, y2, z2)
def add_mixed(self, p1, p2):
"""Add a Jacobian tuple p1 and an affine tuple p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
x1, y1, z1 = p1
x2, y2, z2 = p2
assert(z2 == 1)
# Adding to the point at infinity is a no-op
if z1 == 0:
return p2
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
u2 = (x2 * z1_2) % self.p
s2 = (y2 * z1_3) % self.p
if x1 == u2:
if (y1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - x1
r = s2 - y1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (x1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
z3 = (h*z1) % self.p
return (x3, y3, z3)
def add(self, p1, p2):
"""Add two Jacobian tuples p1 and p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
x1, y1, z1 = p1
x2, y2, z2 = p2
# Adding the point at infinity is a no-op
if z1 == 0:
return p2
if z2 == 0:
return p1
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
if z1 == 1:
return self.add_mixed(p2, p1)
if z2 == 1:
return self.add_mixed(p1, p2)
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
z2_2 = (z2**2) % self.p
z2_3 = (z2_2 * z2) % self.p
u1 = (x1 * z2_2) % self.p
u2 = (x2 * z1_2) % self.p
s1 = (y1 * z2_3) % self.p
s2 = (y2 * z1_3) % self.p
if u1 == u2:
if (s1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - u1
r = s2 - s1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (u1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
z3 = (h*z1*z2) % self.p
return (x3, y3, z3)
def mul(self, ps):
"""Compute a (multi) point multiplication
ps is a list of (Jacobian tuple, scalar) pairs.
"""
r = (0, 1, 0)
for i in range(255, -1, -1):
r = self.double(r)
for (p, n) in ps:
if ((n >> i) & 1):
r = self.add(r, p)
return r
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
class ECPubKey():
"""A secp256k1 public key"""
def __init__(self):
"""Construct an uninitialized public key"""
self.valid = False
def set(self, data):
"""Construct a public key from a serialization in compressed or uncompressed format"""
if (len(data) == 65 and data[0] == 0x04):
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
self.valid = SECP256K1.on_curve(p)
if self.valid:
self.p = p
self.compressed = False
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
x = int.from_bytes(data[1:33], 'big')
if SECP256K1.is_x_coord(x):
p = SECP256K1.lift_x(x)
# if the oddness of the y co-ord isn't correct, find the other
# valid y
if (p[1] & 1) != (data[0] & 1):
p = SECP256K1.negate(p)
self.p = p
self.valid = True
self.compressed = True
else:
self.valid = False
else:
self.valid = False
@property
def is_compressed(self):
return self.compressed
@property
def is_valid(self):
return self.valid
def get_bytes(self):
assert(self.valid)
p = SECP256K1.affine(self.p)
if p is None:
return None
if self.compressed:
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
else:
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
def verify_ecdsa(self, sig, msg, low_s=True):
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA verifier algorithm"""
assert(self.valid)
# Extract r and s from the DER formatted signature. Return false for
# any DER encoding errors.
if (sig[1] + 2 != len(sig)):
return False
if (len(sig) < 4):
return False
if (sig[0] != 0x30):
return False
if (sig[2] != 0x02):
return False
rlen = sig[3]
if (len(sig) < 6 + rlen):
return False
if rlen < 1 or rlen > 33:
return False
if sig[4] >= 0x80:
return False
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
return False
r = int.from_bytes(sig[4:4+rlen], 'big')
if (sig[4+rlen] != 0x02):
return False
slen = sig[5+rlen]
if slen < 1 or slen > 33:
return False
if (len(sig) != 6 + rlen + slen):
return False
if sig[6+rlen] >= 0x80:
return False
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
return False
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
# Verify that r and s are within the group order
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
return False
if low_s and s >= SECP256K1_ORDER_HALF:
return False
z = int.from_bytes(msg, 'big')
# Run verifier algorithm on r, s
w = modinv(s, SECP256K1_ORDER)
u1 = z*w % SECP256K1_ORDER
u2 = r*w % SECP256K1_ORDER
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
if R is None or R[0] != r:
return False
return True
class ECKey():
"""A secp256k1 private key"""
def __init__(self):
self.valid = False
def set(self, secret, compressed):
"""Construct a private key object with given 32-byte secret and compressed flag."""
assert(len(secret) == 32)
secret = int.from_bytes(secret, 'big')
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
if self.valid:
self.secret = secret
self.compressed = compressed
def generate(self, compressed=True):
"""Generate a random private key (compressed or uncompressed)."""
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
def get_bytes(self):
"""Retrieve the 32-byte representation of this key."""
assert(self.valid)
return self.secret.to_bytes(32, 'big')
@property
def is_valid(self):
return self.valid
@property
def is_compressed(self):
return self.compressed
def get_pubkey(self):
"""Compute an ECPubKey object for this secret key."""
assert(self.valid)
ret = ECPubKey()
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
ret.p = p
ret.valid = True
ret.compressed = self.compressed
return ret
def sign_ecdsa(self, msg, low_s=True):
"""Construct a DER-encoded ECDSA signature with this key.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA signer algorithm."""
assert(self.valid)
z = int.from_bytes(msg, 'big')
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
k = random.randrange(1, SECP256K1_ORDER)
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
r = R[0] % SECP256K1_ORDER
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
if low_s and s > SECP256K1_ORDER_HALF:
s = SECP256K1_ORDER - s
# Represent in DER format. The byte representations of r and s have
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
# bytes).
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb

View File

@@ -1,393 +0,0 @@
# Copyright (c) 2019 Pieter Wuille
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test-only secp256k1 elliptic curve implementation
WARNING: This code is slow, uses bad randomness, does not properly protect
keys, and is trivially vulnerable to side channel attacks. Do not use for
anything but tests."""
import random
def modinv(a, n):
"""Compute the modular inverse of a modulo n
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
"""
t1, t2 = 0, 1
r1, r2 = n, a
while r2 != 0:
q = r1 // r2
t1, t2 = t2, t1 - q * t2
r1, r2 = r2, r1 - q * r2
if r1 > 1:
return None
if t1 < 0:
t1 += n
return t1
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
See http://en.wikipedia.org/wiki/Jacobi_symbol
For our application k is always prime, so this is the same as the Legendre symbol."""
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
n %= k
t = 0
while n != 0:
while n & 1 == 0:
n >>= 1
r = k & 7
t ^= (r == 3 or r == 5)
n, k = k, n
t ^= (n & k & 3 == 3)
n = n % k
if k == 1:
return -1 if t else 1
return 0
def modsqrt(a, p):
"""Compute the square root of a modulo p when p % 4 = 3.
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
Limiting this function to only work for p % 4 = 3 means we don't need to
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
"""
if p % 4 != 3:
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
sqrt = pow(a, (p + 1)//4, p)
if pow(sqrt, 2, p) == a % p:
return sqrt
return None
class EllipticCurve:
def __init__(self, p, a, b):
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
self.p = p
self.a = a % p
self.b = b % p
def affine(self, p1):
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
An affine point is represented as the Jacobian (x, y, 1)"""
x1, y1, z1 = p1
if z1 == 0:
return None
inv = modinv(z1, self.p)
inv_2 = (inv**2) % self.p
inv_3 = (inv_2 * inv) % self.p
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
def negate(self, p1):
"""Negate a Jacobian point tuple p1."""
x1, y1, z1 = p1
return (x1, (self.p - y1) % self.p, z1)
def on_curve(self, p1):
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
x1, y1, z1 = p1
z2 = pow(z1, 2, self.p)
z4 = pow(z2, 2, self.p)
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
def is_x_coord(self, x):
"""Test whether x is a valid X coordinate on the curve."""
x_3 = pow(x, 3, self.p)
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
def lift_x(self, x):
"""Given an X coordinate on the curve, return a corresponding affine point."""
x_3 = pow(x, 3, self.p)
v = x_3 + self.a * x + self.b
y = modsqrt(v, self.p)
if y is None:
return None
return (x, y, 1)
def double(self, p1):
"""Double a Jacobian tuple p1
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
x1, y1, z1 = p1
if z1 == 0:
return (0, 1, 0)
y1_2 = (y1**2) % self.p
y1_4 = (y1_2**2) % self.p
x1_2 = (x1**2) % self.p
s = (4*x1*y1_2) % self.p
m = 3*x1_2
if self.a:
m += self.a * pow(z1, 4, self.p)
m = m % self.p
x2 = (m**2 - 2*s) % self.p
y2 = (m*(s - x2) - 8*y1_4) % self.p
z2 = (2*y1*z1) % self.p
return (x2, y2, z2)
def add_mixed(self, p1, p2):
"""Add a Jacobian tuple p1 and an affine tuple p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
x1, y1, z1 = p1
x2, y2, z2 = p2
assert(z2 == 1)
# Adding to the point at infinity is a no-op
if z1 == 0:
return p2
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
u2 = (x2 * z1_2) % self.p
s2 = (y2 * z1_3) % self.p
if x1 == u2:
if (y1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - x1
r = s2 - y1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (x1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
z3 = (h*z1) % self.p
return (x3, y3, z3)
def add(self, p1, p2):
"""Add two Jacobian tuples p1 and p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
x1, y1, z1 = p1
x2, y2, z2 = p2
# Adding the point at infinity is a no-op
if z1 == 0:
return p2
if z2 == 0:
return p1
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
if z1 == 1:
return self.add_mixed(p2, p1)
if z2 == 1:
return self.add_mixed(p1, p2)
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
z2_2 = (z2**2) % self.p
z2_3 = (z2_2 * z2) % self.p
u1 = (x1 * z2_2) % self.p
u2 = (x2 * z1_2) % self.p
s1 = (y1 * z2_3) % self.p
s2 = (y2 * z1_3) % self.p
if u1 == u2:
if (s1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - u1
r = s2 - s1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (u1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
z3 = (h*z1*z2) % self.p
return (x3, y3, z3)
def mul(self, ps):
"""Compute a (multi) point multiplication
ps is a list of (Jacobian tuple, scalar) pairs.
"""
r = (0, 1, 0)
for i in range(255, -1, -1):
r = self.double(r)
for (p, n) in ps:
if ((n >> i) & 1):
r = self.add(r, p)
return r
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
class ECPubKey():
"""A secp256k1 public key"""
def __init__(self):
"""Construct an uninitialized public key"""
self.valid = False
def set_int(self, x, y):
p = (x, y, 1)
self.valid = SECP256K1.on_curve(p)
if self.valid:
self.p = p
self.compressed = False
def set(self, data):
"""Construct a public key from a serialization in compressed or uncompressed format"""
if (len(data) == 65 and data[0] == 0x04):
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
self.valid = SECP256K1.on_curve(p)
if self.valid:
self.p = p
self.compressed = False
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
x = int.from_bytes(data[1:33], 'big')
if SECP256K1.is_x_coord(x):
p = SECP256K1.lift_x(x)
# if the oddness of the y co-ord isn't correct, find the other
# valid y
if (p[1] & 1) != (data[0] & 1):
p = SECP256K1.negate(p)
self.p = p
self.valid = True
self.compressed = True
else:
self.valid = False
else:
self.valid = False
@property
def is_compressed(self):
return self.compressed
@property
def is_valid(self):
return self.valid
def get_bytes(self):
assert(self.valid)
p = SECP256K1.affine(self.p)
if p is None:
return None
if self.compressed:
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
else:
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
def verify_ecdsa(self, sig, msg, low_s=True):
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA verifier algorithm"""
assert(self.valid)
# Extract r and s from the DER formatted signature. Return false for
# any DER encoding errors.
if (sig[1] + 2 != len(sig)):
return False
if (len(sig) < 4):
return False
if (sig[0] != 0x30):
return False
if (sig[2] != 0x02):
return False
rlen = sig[3]
if (len(sig) < 6 + rlen):
return False
if rlen < 1 or rlen > 33:
return False
if sig[4] >= 0x80:
return False
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
return False
r = int.from_bytes(sig[4:4+rlen], 'big')
if (sig[4+rlen] != 0x02):
return False
slen = sig[5+rlen]
if slen < 1 or slen > 33:
return False
if (len(sig) != 6 + rlen + slen):
return False
if sig[6+rlen] >= 0x80:
return False
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
return False
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
# Verify that r and s are within the group order
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
return False
if low_s and s >= SECP256K1_ORDER_HALF:
return False
z = int.from_bytes(msg, 'big')
# Run verifier algorithm on r, s
w = modinv(s, SECP256K1_ORDER)
u1 = z*w % SECP256K1_ORDER
u2 = r*w % SECP256K1_ORDER
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
if R is None or R[0] != r:
return False
return True
class ECKey():
"""A secp256k1 private key"""
def __init__(self):
self.valid = False
def set(self, secret, compressed):
"""Construct a private key object with given 32-byte secret and compressed flag."""
assert(len(secret) == 32)
secret = int.from_bytes(secret, 'big')
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
if self.valid:
self.secret = secret
self.compressed = compressed
def generate(self, compressed=True):
"""Generate a random private key (compressed or uncompressed)."""
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
def get_bytes(self):
"""Retrieve the 32-byte representation of this key."""
assert(self.valid)
return self.secret.to_bytes(32, 'big')
@property
def is_valid(self):
return self.valid
@property
def is_compressed(self):
return self.compressed
def get_pubkey(self):
"""Compute an ECPubKey object for this secret key."""
assert(self.valid)
ret = ECPubKey()
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
ret.p = p
ret.valid = True
ret.compressed = self.compressed
return ret
def sign_ecdsa(self, msg, low_s=True):
"""Construct a DER-encoded ECDSA signature with this key.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA signer algorithm."""
assert(self.valid)
z = int.from_bytes(msg, 'big')
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
k = random.randrange(1, SECP256K1_ORDER)
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
r = R[0] % SECP256K1_ORDER
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
if low_s and s > SECP256K1_ORDER_HALF:
s = SECP256K1_ORDER - s
# Represent in DER format. The byte representations of r and s have
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
# bytes).
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb

View File

@@ -640,7 +640,7 @@ class CTransaction:
self.hash = tx.hash
self.wit = copy.deepcopy(tx.wit)
def deserialize(self, f):
def deserialize(self, f, allow_witness: bool = True):
self.nVersion = int.from_bytes(f.read(1), "little")
if self.nVersion == PARTICL_TX_VERSION:
self.nVersion |= int.from_bytes(f.read(1), "little") << 8
@@ -668,7 +668,7 @@ class CTransaction:
# self.nVersion = int.from_bytes(f.read(4), "little")
self.vin = deser_vector(f, CTxIn)
flags = 0
if len(self.vin) == 0:
if len(self.vin) == 0 and allow_witness:
flags = int.from_bytes(f.read(1), "little")
# Not sure why flags can't be zero, but this
# matches the implementation in bitcoind

View File

@@ -166,6 +166,9 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API):
def _message_received_(self, handler, msg):
self.message_received(self.handler_to_client(handler), self, msg)
def _binary_message_received_(self, handler, msg):
self.binary_message_received(self.handler_to_client(handler), self, msg)
def _ping_received_(self, handler, msg):
handler.send_pong(msg)
@@ -309,6 +312,7 @@ class WebSocketHandler(StreamRequestHandler):
opcode = b1 & OPCODE
masked = b2 & MASKED
payload_length = b2 & PAYLOAD_LEN
is_binary: bool = False
if opcode == OPCODE_CLOSE_CONN:
logger.info("Client asked to close connection.")
@@ -322,8 +326,8 @@ class WebSocketHandler(StreamRequestHandler):
logger.warning("Continuation frames are not supported.")
return
elif opcode == OPCODE_BINARY:
logger.warning("Binary frames are not supported.")
return
is_binary = True
opcode_handler = self.server._binary_message_received_
elif opcode == OPCODE_TEXT:
opcode_handler = self.server._message_received_
elif opcode == OPCODE_PING:
@@ -345,7 +349,8 @@ class WebSocketHandler(StreamRequestHandler):
for message_byte in self.read_bytes(payload_length):
message_byte ^= masks[len(message_bytes) % 4]
message_bytes.append(message_byte)
opcode_handler(self, message_bytes.decode('utf8'))
opcode_handler(self, message_bytes if is_binary else message_bytes.decode('utf8'))
def send_message(self, message):
self.send_text(message)
@@ -375,6 +380,35 @@ class WebSocketHandler(StreamRequestHandler):
with self._send_lock:
self.request.send(header + payload)
def send_bytes(self, message, opcode=OPCODE_BINARY):
header = bytearray()
payload = message
payload_length = len(payload)
# Normal payload
if payload_length <= 125:
header.append(FIN | opcode)
header.append(payload_length)
# Extended payload
elif payload_length >= 126 and payload_length <= 65535:
header.append(FIN | opcode)
header.append(PAYLOAD_LEN_EXT16)
header.extend(struct.pack(">H", payload_length))
# Huge extended payload
elif payload_length < 18446744073709551616:
header.append(FIN | opcode)
header.append(PAYLOAD_LEN_EXT64)
header.extend(struct.pack(">Q", payload_length))
else:
raise Exception("Message is too big. Consider breaking it into chunks.")
return
with self._send_lock:
self.request.send(header + payload)
def send_text(self, message, opcode=OPCODE_TEXT):
"""
Important: Fragmented(=continuation) messages are not supported since

View File

@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 28
CURRENT_DB_DATA_VERSION = 6
CURRENT_DB_VERSION = 32
CURRENT_DB_DATA_VERSION = 7
class Concepts(IntEnum):
@@ -185,6 +185,7 @@ class Offer(Table):
amount_negotiable = Column("bool")
rate_negotiable = Column("bool")
auto_accept_type = Column("integer")
message_nets = Column("string")
# Local fields
auto_accept_bids = Column("bool")
@@ -194,6 +195,7 @@ class Offer(Table):
) # Address to spend lock tx to - address from wallet if empty TODO
security_token = Column("blob")
bid_reversed = Column("bool")
smsg_payload_version = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
@@ -219,6 +221,7 @@ class Bid(Table):
bid_addr = Column("string")
pk_bid_addr = Column("blob")
proof_address = Column("string")
proof_signature = Column("blob")
proof_utxos = Column("blob")
# Address to spend lock tx to - address from wallet if empty TODO
withdraw_to_addr = Column("string")
@@ -232,6 +235,7 @@ class Bid(Table):
rate = Column("integer")
pkhash_seller = Column("blob")
message_nets = Column("string")
initiate_txn_redeem = Column("blob")
initiate_txn_refund = Column("blob")
@@ -380,6 +384,8 @@ class SmsgAddress(Table):
use_type = Column("integer")
note = Column("string")
index = Index("smsgaddresses_address_index", "addr")
class Action(Table):
__tablename__ = "actions"
@@ -485,6 +491,14 @@ class XmrSwap(Table):
b_lock_tx_id = Column("blob")
msg_split_info = Column("string")
def getMsgSplitInfo(self):
if self.msg_split_info is None:
return 16000, 17000
msg_split_info = self.msg_split_info.split(":")
return int(msg_split_info[0]), int(msg_split_info[1])
class XmrSplitData(Table):
__tablename__ = "xmr_split_data"
@@ -605,6 +619,8 @@ class BidState(Table):
swap_failed = Column("integer")
swap_ended = Column("integer")
can_accept = Column("integer")
can_expire = Column("integer")
can_timeout = Column("integer")
note = Column("string")
created_at = Column("integer")
@@ -658,13 +674,127 @@ class CoinRates(Table):
last_updated = Column("integer")
def create_db(db_path: str, log) -> None:
con = None
try:
con = sqlite3.connect(db_path)
c = con.cursor()
class CoinVolume(Table):
__tablename__ = "coinvolume"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_id = Column("integer")
volume_24h = Column("string")
price_change_24h = Column("string")
source = Column("string")
last_updated = Column("integer")
class CoinHistory(Table):
__tablename__ = "coinhistory"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_id = Column("integer")
days = Column("integer")
price_data = Column("blob")
source = Column("string")
last_updated = Column("integer")
class MessageNetworks(Table):
__tablename__ = "message_networks"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
name = Column("string")
created_at = Column("integer")
class MessageNetworkLink(Table):
__tablename__ = "message_network_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
network_id = Column("string")
link_type = Column("integer") # MessageNetworkLinkTypes
created_at = Column("integer")
class DirectMessageRoute(Table):
__tablename__ = "direct_message_routes"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
network_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
smsg_addr_local = Column("string")
smsg_addr_remote = Column("string")
# smsg_addr_id_local = Column("integer") # SmsgAddress
# smsg_addr_id_remote = Column("integer") # KnownIdentity
route_data = Column("blob")
created_at = Column("integer")
class DirectMessageRouteLink(Table):
__tablename__ = "direct_message_route_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
direct_message_route_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
created_at = Column("integer")
class NetworkPortal(Table):
__tablename__ = "network_portals"
def set(
self, time_start, time_valid, network_from, network_to, address_from, address_to
):
super().__init__()
self.active_ind = 1
self.time_start = time_start
self.time_valid = time_valid
self.network_from = network_from
self.network_to = network_to
self.address_from = address_from
self.address_to = address_to
self.smsg_difficulty = 0x1EFFFFFF
self.num_refreshes = 0
self.messages_sent = 0
self.responses_seen = 0
self.time_last_used = 0
self.num_issues = 0
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
own_portal = Column("integer")
address_from = Column("string", unique=True)
address_to = Column("string")
network_from = Column("integer")
network_to = Column("integer")
time_start = Column("integer")
time_valid = Column("integer")
smsg_difficulty = Column("integer")
num_refreshes = Column("integer")
messages_sent = Column("integer")
responses_seen = Column("integer")
time_last_used = Column("integer")
num_issues = Column("integer")
created_at = Column("integer")
def extract_schema() -> dict:
g = globals().copy()
tables = {}
for name, obj in g.items():
if not inspect.isclass(obj):
continue
@@ -674,15 +804,13 @@ def create_db(db_path: str, log) -> None:
continue
table_name: str = obj.__tablename__
query: str = f"CREATE TABLE {table_name} ("
table = {}
columns = {}
primary_key = None
constraints = []
indices = []
num_columns: int = 0
for m in inspect.getmembers(obj):
m_name, m_obj = m
if hasattr(m_obj, "__sqlite3_primary_key__"):
primary_key = m_obj
continue
@@ -693,47 +821,110 @@ def create_db(db_path: str, log) -> None:
indices.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_column__"):
if num_columns > 0:
query += ","
col_type: str = m_obj.column_type.upper()
if col_type == "BOOL":
col_type = "INTEGER"
query += f" {m_name} {col_type} "
if m_obj.primary_key:
query += "PRIMARY KEY ASC "
if m_obj.unique:
query += "UNIQUE "
num_columns += 1
columns[m_name] = {
"type": col_type,
"primary_key": m_obj.primary_key,
"unique": m_obj.unique,
}
table["columns"] = columns
if primary_key is not None:
query += f", PRIMARY KEY ({primary_key.column_1}"
table["primary_key"] = {"column_1": primary_key.column_1}
if primary_key.column_2:
query += f", {primary_key.column_2}"
table["primary_key"]["column_2"] = primary_key.column_2
if primary_key.column_3:
query += f", {primary_key.column_3}"
query += ") "
table["primary_key"]["column_3"] = primary_key.column_3
for constraint in constraints:
query += f", UNIQUE ({constraint.column_1}"
if "constraints" not in table:
table["constraints"] = []
table_constraint = {"column_1": constraint.column_1}
if constraint.column_2:
query += f", {constraint.column_2}"
table_constraint["column_2"] = constraint.column_2
if constraint.column_3:
query += f", {constraint.column_3}"
table_constraint["column_3"] = constraint.column_3
table["constraints"].append(table_constraint)
for i in indices:
if "indices" not in table:
table["indices"] = []
table_index = {"index_name": i.name, "column_1": i.column_1}
if i.column_2 is not None:
table_index["column_2"] = i.column_2
if i.column_3 is not None:
table_index["column_3"] = i.column_3
table["indices"].append(table_index)
tables[table_name] = table
return tables
def create_table(c, table_name, table) -> None:
query: str = f"CREATE TABLE {table_name} ("
for i, (colname, column) in enumerate(table["columns"].items()):
col_type = column["type"]
query += ("," if i > 0 else "") + f" {colname} {col_type} "
if column["primary_key"]:
query += "PRIMARY KEY ASC "
if column["unique"]:
query += "UNIQUE "
if "primary_key" in table:
column_1 = table["primary_key"]["column_1"]
column_2 = table["primary_key"].get("column_2", None)
column_3 = table["primary_key"].get("column_3", None)
query += f", PRIMARY KEY ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ") "
constraints = table.get("constraints", [])
for constraint in constraints:
column_1 = constraint["column_1"]
column_2 = constraint.get("column_2", None)
column_3 = constraint.get("column_3", None)
query += f", UNIQUE ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ") "
query += ")"
c.execute(query)
for i in indices:
query: str = f"CREATE INDEX {i.name} ON {table_name} ({i.column_1}"
if i.column_2 is not None:
query += f", {i.column_2}"
if i.column_3 is not None:
query += f", {i.column_3}"
indices = table.get("indices", [])
for index in indices:
index_name = index["index_name"]
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
c.execute(query)
def create_db_(con, log) -> None:
db_schema = extract_schema()
c = con.cursor()
for table_name, table in db_schema.items():
create_table(c, table_name, table)
def create_db(db_path: str, log) -> None:
con = None
try:
con = sqlite3.connect(db_path)
create_db_(con, log)
con.commit()
finally:
if con:
@@ -912,6 +1103,7 @@ class DBMethods:
query += f"{key}=:{key}"
cursor.execute(query, values)
return cursor.lastrowid
def query(
self,
@@ -962,6 +1154,9 @@ class DBMethods:
query += ":" + cv_name
query_data[cv_name] = cv
query += ") "
else:
if constraint_value is None:
query += f" AND {ck} IS NULL "
else:
query += f" AND {ck} = :{ck} "
query_data[ck] = constraint_value

View File

@@ -12,13 +12,17 @@ from .db import (
AutomationStrategy,
BidState,
Concepts,
create_table,
CURRENT_DB_DATA_VERSION,
CURRENT_DB_VERSION,
extract_schema,
)
from .basicswap_util import (
BidStates,
canAcceptBidState,
canExpireBidState,
canTimeoutBidState,
isActiveBidState,
isErrorBidState,
isFailingBidState,
@@ -37,6 +41,8 @@ def addBidState(self, state, now, cursor):
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
can_accept=canAcceptBidState(state),
can_expire=canExpireBidState(state),
can_timeout=canTimeoutBidState(state),
label=strBidState(state),
created_at=now,
),
@@ -49,10 +55,9 @@ def upgradeDatabaseData(self, data_version):
return
self.log.info(
"Upgrading database records from version %d to %d.",
data_version,
CURRENT_DB_DATA_VERSION,
f"Upgrading database records from version {data_version} to {CURRENT_DB_DATA_VERSION}."
)
cursor = self.openDB()
try:
now = int(time.time())
@@ -64,7 +69,7 @@ def upgradeDatabaseData(self, data_version):
label="Accept All",
type_ind=Concepts.OFFER,
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
{"exact_rate_only": True, "max_concurrent_bids": 1}
).encode("utf-8"),
only_known_identities=False,
created_at=now,
@@ -77,7 +82,7 @@ def upgradeDatabaseData(self, data_version):
label="Accept Known",
type_ind=Concepts.OFFER,
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
{"exact_rate_only": True, "max_concurrent_bids": 1}
).encode("utf-8"),
only_known_identities=True,
note="Accept bids from identities with previously successful swaps only",
@@ -104,19 +109,23 @@ def upgradeDatabaseData(self, data_version):
),
cursor,
)
if data_version > 0 and data_version < 6:
if data_version > 0 and data_version < 7:
for state in BidStates:
in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state)
can_accept = canAcceptBidState(state)
can_expire = canExpireBidState(state)
can_timeout = canTimeoutBidState(state)
cursor.execute(
"UPDATE bidstates SET can_accept = :can_accept, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
"UPDATE bidstates SET can_accept = :can_accept, can_expire = :can_expire, can_timeout = :can_timeout, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
{
"in_error": in_error,
"swap_failed": swap_failed,
"swap_ended": swap_ended,
"can_accept": can_accept,
"can_expire": can_expire,
"can_timeout": can_timeout,
"state_id": int(state),
},
)
@@ -138,313 +147,137 @@ def upgradeDatabaseData(self, data_version):
self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKV("db_data_version", self.db_data_version, cursor)
self.commitDB()
self.log.info(
"Upgraded database records to version {}".format(self.db_data_version)
)
self.log.info(f"Upgraded database records to version {self.db_data_version}")
finally:
self.closeDB(cursor, commit=False)
def upgradeDatabase(self, db_version):
if db_version >= CURRENT_DB_VERSION:
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
return
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
)
while True:
# db_version, tablename, oldcolumnname, newcolumnname
rename_columns = [
(13, "actions", "event_id", "action_id"),
(13, "actions", "event_type", "action_type"),
(13, "actions", "event_data", "action_data"),
(
14,
"xmr_swaps",
"coin_a_lock_refund_spend_tx_msg_id",
"coin_a_lock_spend_tx_msg_id",
),
]
expect_schema = extract_schema()
have_tables = {}
try:
cursor = self.openDB()
current_version = db_version
if current_version == 6:
cursor.execute("ALTER TABLE bids ADD COLUMN security_token BLOB")
cursor.execute("ALTER TABLE offers ADD COLUMN security_token BLOB")
db_version += 1
elif current_version == 7:
cursor.execute("ALTER TABLE transactions ADD COLUMN block_hash BLOB")
for rename_column in rename_columns:
dbv, table_name, colname_from, colname_to = rename_column
if db_version < dbv:
cursor.execute(
"ALTER TABLE transactions ADD COLUMN block_height INTEGER"
)
cursor.execute("ALTER TABLE transactions ADD COLUMN block_time INTEGER")
db_version += 1
elif current_version == 8:
cursor.execute(
"""
CREATE TABLE wallets (
record_id INTEGER NOT NULL,
coin_id INTEGER,
wallet_name VARCHAR,
wallet_data VARCHAR,
balance_type INTEGER,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
db_version += 1
elif current_version == 9:
cursor.execute("ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR")
db_version += 1
elif current_version == 10:
cursor.execute(
"ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER"
)
cursor.execute(
"ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER"
)
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR")
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR")
cursor.execute(
"UPDATE smsgaddresses SET active_ind = 1, created_at = 1"
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
)
cursor.execute("ALTER TABLE offers ADD COLUMN addr_to VARCHAR")
cursor.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
db_version += 1
elif current_version == 11:
cursor.execute(
"ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER"
)
cursor.execute(
"ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER"
)
cursor.execute("ALTER TABLE bids ADD COLUMN protocol_version INTEGER")
cursor.execute("ALTER TABLE offers ADD COLUMN protocol_version INTEGER")
cursor.execute("ALTER TABLE transactions ADD COLUMN tx_data BLOB")
db_version += 1
elif current_version == 12:
cursor.execute(
"""
CREATE TABLE knownidentities (
record_id INTEGER NOT NULL,
address VARCHAR,
label VARCHAR,
publickey BLOB,
num_sent_bids_successful INTEGER,
num_recv_bids_successful INTEGER,
num_sent_bids_rejected INTEGER,
num_recv_bids_rejected INTEGER,
num_sent_bids_failed INTEGER,
num_recv_bids_failed INTEGER,
note VARCHAR,
updated_at BIGINT,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE bids ADD COLUMN reject_code INTEGER")
cursor.execute("ALTER TABLE bids ADD COLUMN rate INTEGER")
cursor.execute(
"ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER"
)
cursor.execute("ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER")
db_version += 1
elif current_version == 13:
db_version += 1
cursor.execute(
"""
CREATE TABLE automationstrategies (
record_id INTEGER NOT NULL,
active_ind INTEGER,
label VARCHAR,
type_ind INTEGER,
only_known_identities INTEGER,
num_concurrent INTEGER,
data BLOB,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE automationlinks (
record_id INTEGER NOT NULL,
active_ind INTEGER,
linked_type INTEGER,
linked_id BLOB,
strategy_id INTEGER,
data BLOB,
repeat_limit INTEGER,
repeat_count INTEGER,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE history (
record_id INTEGER NOT NULL,
concept_type INTEGER,
concept_id INTEGER,
changed_data BLOB,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE bidstates (
record_id INTEGER NOT NULL,
active_ind INTEGER,
state_id INTEGER,
label VARCHAR,
in_progress INTEGER,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE wallets ADD COLUMN active_ind INTEGER")
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER"
)
cursor.execute("ALTER TABLE eventqueue RENAME TO actions")
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_id TO action_id"
)
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_type TO action_type"
)
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_data TO action_data"
)
elif current_version == 14:
db_version += 1
cursor.execute(
"ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB"
)
cursor.execute(
"ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id"
)
elif current_version == 15:
db_version += 1
cursor.execute(
"""
CREATE TABLE notifications (
record_id INTEGER NOT NULL,
active_ind INTEGER,
event_type INTEGER,
event_data BLOB,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
elif current_version == 16:
db_version += 1
cursor.execute(
"""
CREATE TABLE prefunded_transactions (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
linked_type INTEGER,
linked_id BLOB,
tx_type INTEGER,
tx_data BLOB,
used_by BLOB,
PRIMARY KEY (record_id))"""
)
elif current_version == 17:
db_version += 1
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER"
)
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER"
)
cursor.execute("ALTER TABLE knownidentities ADD COLUMN data BLOB")
cursor.execute("UPDATE knownidentities SET active_ind = 1")
elif current_version == 18:
db_version += 1
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING")
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING")
elif current_version == 19:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN in_error INTEGER")
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER")
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER")
elif current_version == 20:
db_version += 1
cursor.execute(
"""
CREATE TABLE message_links (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
linked_type INTEGER,
linked_id BLOB,
msg_type INTEGER,
msg_sequence INTEGER,
msg_id BLOB,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
elif current_version == 21:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
elif current_version == 22:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
elif current_version == 23:
db_version += 1
cursor.execute(
"""
CREATE TABLE checkedblocks (
record_id INTEGER NOT NULL,
created_at BIGINT,
coin_type INTEGER,
block_height INTEGER,
block_hash BLOB,
block_time INTEGER,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
elif current_version == 24:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
elif current_version == 25:
db_version += 1
cursor.execute(
"""
CREATE TABLE coinrates (
record_id INTEGER NOT NULL,
currency_from INTEGER,
currency_to INTEGER,
rate VARCHAR,
source VARCHAR,
last_updated INTEGER,
PRIMARY KEY (record_id))"""
)
elif current_version == 26:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN auto_accept_type INTEGER")
elif current_version == 27:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN pk_from BLOB")
cursor.execute("ALTER TABLE bids ADD COLUMN pk_bid_addr BLOB")
if current_version != db_version:
self.db_version = db_version
self.setIntKV("db_version", db_version, cursor)
self.commitDB()
self.log.info("Upgraded database to version {}".format(self.db_version))
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
tables = cursor.execute(query).fetchall()
for table in tables:
table_name = table[0]
if table_name in ("sqlite_sequence",):
continue
have_table = {}
have_columns = {}
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
for column in columns:
cid, name, data_type, notnull, default_value, primary_key = column
have_columns[name] = {"type": data_type, "primary_key": primary_key}
have_table["columns"] = have_columns
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
indices = cursor.fetchall()
for index in indices:
seq, index_name, unique, origin, partial = index
if origin == "pk": # Created by a PRIMARY KEY constraint
continue
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
index_info = cursor.fetchall()
add_index = {"index_name": index_name}
for index_columns in index_info:
seqno, cid, name = index_columns
if origin == "u": # Created by a UNIQUE constraint
have_columns[name]["unique"] = 1
else:
if "column_1" not in add_index:
add_index["column_1"] = name
elif "column_2" not in add_index:
add_index["column_2"] = name
elif "column_3" not in add_index:
add_index["column_3"] = name
else:
raise RuntimeError("Add more index columns.")
if origin == "c":
if "indices" not in table:
have_table["indices"] = []
have_table["indices"].append(add_index)
have_tables[table_name] = have_table
for table_name, table in expect_schema.items():
if table_name not in have_tables:
self.log.info(f"Creating table {table_name}.")
create_table(cursor, table_name, table)
continue
have_table = have_tables[table_name]
have_columns = have_table["columns"]
for colname, column in table["columns"].items():
if colname not in have_columns:
col_type = column["type"]
self.log.info(f"Adding column {colname} to table {table_name}.")
cursor.execute(
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
)
indices = table.get("indices", [])
have_indices = have_table.get("indices", [])
for index in indices:
index_name = index["index_name"]
if not any(
have_idx.get("index_name") == index_name
for have_idx in have_indices
):
self.log.info(f"Adding index {index_name} to table {table_name}.")
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = (
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
)
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
cursor.execute(query)
if CURRENT_DB_VERSION != db_version:
self.db_version = CURRENT_DB_VERSION
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
self.log.info(f"Upgraded database to version {self.db_version}")
self.commitDB()
except Exception as e:
self.log.error("Upgrade failed {}".format(e))
self.log.error(f"Upgrade failed {e}")
self.rollbackDB()
finally:
self.closeDB(cursor, commit=False)
break
if db_version != CURRENT_DB_VERSION:
raise ValueError("Unable to upgrade database.")

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023-2024 The Basicswap Developers
# Copyright (c) 2023-2025 The Basicswap Developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -26,93 +26,46 @@ def remove_expired_data(self, time_offset: int = 0):
)
for offer_row in offer_rows:
num_offers += 1
offer_query_data = {
"type_ind": int(Concepts.OFFER),
"offer_id": offer_row[0],
}
bid_rows = cursor.execute(
"SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id",
{"offer_id": offer_row[0]},
offer_query_data,
)
for bid_row in bid_rows:
num_bids += 1
cursor.execute(
bid_query_data = {"type_ind": int(Concepts.BID), "bid_id": bid_row[0]}
for query_str in [
"DELETE FROM transactions WHERE transactions.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM actions WHERE actions.linked_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM bids WHERE bids.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
]:
cursor.execute(query_str, bid_query_data)
for query_str in [
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
]:
cursor.execute(query_str, offer_query_data)
if num_offers > 0 or num_bids > 0:
self.log.info(

View File

@@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
import secrets
import hashlib
import basicswap.contrib.ed25519_fast as edf
def get_secret():
return 9 + secrets.randbelow(edf.l - 9)
def encodepoint(P):
zi = edf.inv(P[2])
x = (P[0] * zi) % edf.q
y = (P[1] * zi) % edf.q
y += (x & 1) << 255
return y.to_bytes(32, byteorder="little")
def hashToEd25519(bytes_in):
hashed = hashlib.sha256(bytes_in).digest()
for i in range(1000):
h255 = bytearray(hashed)
x_sign = 0 if h255[31] & 0x80 == 0 else 1
h255[31] &= 0x7F # Clear top bit
y = int.from_bytes(h255, byteorder="little")
x = edf.xrecover(y, x_sign)
if x == 0 and y == 1: # Skip infinity point
continue
P = [x, y, 1, (x * y) % edf.q]
# Keep trying until the point is in the correct subgroup
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
return P
hashed = hashlib.sha256(hashed).digest()
raise ValueError("hashToEd25519 failed")

View File

@@ -8,9 +8,6 @@
import json
default_chart_api_key = (
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
)
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"

View File

@@ -14,7 +14,7 @@ import threading
import http.client
import base64
from http.server import BaseHTTPRequestHandler, HTTPServer
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
@@ -25,7 +25,6 @@ from . import __version__
from .util import (
dumpj,
toBool,
LockedCoinError,
format_timestamp,
)
from .chainparams import (
@@ -54,6 +53,7 @@ from .ui.page_automation import (
page_automation_strategy_new,
)
from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api
from .ui.page_bids import page_bids, page_bid
from .ui.page_offers import page_offers, page_offer, page_newoffer
from .ui.page_tor import page_tor, get_tor_established_state
@@ -169,6 +169,7 @@ class HttpHandler(BaseHTTPRequestHandler):
if not session_id:
return False
with self.server.session_lock:
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
@@ -195,6 +196,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return None
form_data = parse.parse_qs(post_string)
form_id = form_data[b"formid"][0].decode("utf-8")
with self.server.form_id_lock:
if self.server.last_form_id.get(name, None) == form_id:
messages.append("Prevented double submit for form {}.".format(form_id))
return None
@@ -216,28 +218,45 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_mode"] = True
if swap_client.debug_ui:
args_dict["debug_ui_mode"] = True
is_authenticated = self.is_authenticated() or not swap_client.settings.get(
"client_auth_hash"
)
if is_authenticated:
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception as e:
except Exception:
args_dict["tor_established"] = False
if swap_client.debug:
swap_client.log.error(f"Error getting Tor state: {str(e)}")
swap_client.log.error(traceback.format_exc())
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception:
args_dict["current_status"] = "stopped"
args_dict["amm_active_count"] = 0
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
else:
args_dict["current_status"] = "unknown"
args_dict["amm_active_count"] = 0
if "messages" in args_dict:
messages_with_ids = []
with self.server.msg_id_lock:
for msg in args_dict["messages"]:
messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
args_dict["messages"] = messages_with_ids
if "err_messages" in args_dict:
err_messages_with_ids = []
with self.server.msg_id_lock:
for msg in args_dict["err_messages"]:
err_messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
@@ -254,13 +273,25 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["current_page"] = "index"
shutdown_token = os.urandom(8).hex()
with self.server.session_lock:
self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
if is_authenticated:
try:
encrypted, locked = swap_client.getLockedState()
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
except Exception as e:
args_dict["encrypted"] = False
args_dict["locked"] = False
if swap_client.debug:
swap_client.log.warning(f"Could not get wallet locked state: {e}")
else:
args_dict["encrypted"] = args_dict.get("encrypted", False)
args_dict["locked"] = args_dict.get("locked", False)
with self.server.msg_id_lock:
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
@@ -352,6 +383,7 @@ class HttpHandler(BaseHTTPRequestHandler):
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
with self.server.session_lock:
self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
@@ -401,6 +433,12 @@ class HttpHandler(BaseHTTPRequestHandler):
extra_headers=extra_headers,
)
def page_shutdown_ping(self, url_split, post_string):
if not self.server.stop_event.is_set():
raise ValueError("Unexpected shutdown ping.")
self.putHeaders(401, "application/json")
return json.dumps({"ack": True}).encode("utf-8")
def page_explorers(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -610,20 +648,49 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 2:
token = url_split[2]
with self.server.session_lock:
expect_token = self.server.session_tokens.get("shutdown", None)
if token != expect_token:
return self.page_info("Unexpected token, still running.")
session_id = self._get_session_cookie()
with self.server.session_lock:
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
swap_client.log.info("Web shutdown stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
swap_client.log.info(f"AMM web shutdown: {msg}")
else:
swap_client.log.warning(f"AMM web shutdown warning: {msg}")
except Exception as e:
swap_client.log.error(f"Error stopping AMM in web shutdown: {e}")
swap_client.stopRunning()
return self.page_info("Shutting down", extra_headers=extra_headers)
def page_donation(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
template = env.get_template("donation.html")
return self.render_template(
template,
{
"summary": summary,
},
)
def page_index(self, url_split):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -654,6 +721,9 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
def handle_http(self, status_code, path, post_string="", is_json=False):
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
swap_client = self.server.swap_client
parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/")
@@ -721,7 +791,11 @@ class HttpHandler(BaseHTTPRequestHandler):
func = js_url_to_function(url_split)
return func(self, url_split, post_string, is_json)
except Exception as ex:
if swap_client.debug is True:
if isinstance(ex, LockedCoinError):
clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked"
swap_client.log.warning(clean_msg)
return js_error(self, clean_msg)
elif swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex))
@@ -779,6 +853,8 @@ class HttpHandler(BaseHTTPRequestHandler):
if page == "login":
return self.page_login(url_split, post_string)
if page == "shutdown_ping":
return self.page_shutdown_ping(url_split, post_string)
if page == "active":
return self.page_active(url_split, post_string)
if page == "wallets":
@@ -813,6 +889,8 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_bids(self, url_split, post_string, available=True)
if page == "watched":
return self.page_watched(url_split, post_string)
if page == "donation":
return self.page_donation(url_split, post_string)
if page == "smsgaddresses":
return page_smsgaddresses(self, url_split, post_string)
if page == "identity":
@@ -825,6 +903,41 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_automation_strategy(self, url_split, post_string)
if page == "newautomationstrategy":
return page_automation_strategy_new(self, url_split, post_string)
if page == "amm":
if len(url_split) > 2 and url_split[2] == "status":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
status_data = amm_status_api(
swap_client, self.path, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(status_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "autostart":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
autostart_data = amm_autostart_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(autostart_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "debug":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
debug_data = amm_debug_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(debug_data).encode("utf-8")
return page_amm(self, url_split, post_string)
if page == "shutdown":
return self.page_shutdown(url_split, post_string)
if page == "changepassword":
@@ -844,13 +957,17 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_error(str(ex))
def do_GET(self):
try:
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
finally:
pass
def do_POST(self):
try:
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
@@ -860,6 +977,8 @@ class HttpHandler(BaseHTTPRequestHandler):
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
finally:
pass
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -872,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
class HttpThread(threading.Thread, HTTPServer):
class HttpThread(threading.Thread, ThreadingHTTPServer):
daemon_threads = True
def __init__(self, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
@@ -888,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
self.env = env
self.msg_id_counter = 0
self.session_lock = threading.Lock()
self.form_id_lock = threading.Lock()
self.msg_id_lock = threading.Lock()
self.timeout = 60
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
if swap_client.debug:
swap_client.log.info("HTTP server initialized with threading support")
def stop(self):
self.stop_event.set()

View File

@@ -53,6 +53,7 @@ class CoinInterface:
self._network = network
self._mx_wallet = threading.Lock()
self._altruistic = True
self._core_version = None # Set in getDaemonVersion()
def interface_type(self) -> int:
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.

View File

@@ -79,6 +79,7 @@ class BCHInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
@@ -817,7 +818,15 @@ class BCHInterface(BTCInterface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
# locktime must be <= chainheight + 2
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if tx.nLockTime != 0:
current_height: int = self.getChainHeight()
if tx.nLockTime > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
)
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)

View File

@@ -16,8 +16,8 @@ import shutil
import sqlite3
import traceback
from io import BytesIO
from typing import Dict, List, Optional
from basicswap.basicswap_util import (
getVoutByAddress,
@@ -25,29 +25,25 @@ from basicswap.basicswap_util import (
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.util import (
b2i,
ensure,
i2b,
b2i,
i2h,
)
from basicswap.util.ecc import (
pointToCPK,
CPKToPoint,
)
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.script import (
SerialiseNumCompact,
decodeScriptNum,
getCompactSizeLen,
SerialiseNumCompact,
getWitnessElementLen,
)
from basicswap.util.address import (
toWIF,
b58encode,
b58decode,
decodeWif,
b58encode,
decodeAddress,
decodeWif,
pubkeyToAddress,
toWIF,
)
from basicswap.util.crypto import (
hash160,
@@ -57,6 +53,7 @@ from coincurve.keys import (
PrivateKey,
PublicKey,
)
from coincurve.types import ffi
from coincurve.ecdsaotves import (
ecdsaotves_enc_sign,
ecdsaotves_enc_verify,
@@ -77,17 +74,19 @@ from basicswap.contrib.test_framework.messages import (
from basicswap.contrib.test_framework.script import (
CScript,
CScriptOp,
OP_IF,
OP_ELSE,
OP_ENDIF,
OP_0,
OP_2,
OP_CHECKSIG,
OP_CHECKMULTISIG,
OP_CHECKSEQUENCEVERIFY,
OP_CHECKSIG,
OP_DROP,
OP_HASH160,
OP_DUP,
OP_ELSE,
OP_ENDIF,
OP_EQUAL,
OP_EQUALVERIFY,
OP_HASH160,
OP_IF,
OP_RETURN,
SIGHASH_ALL,
SegwitV0SignatureHash,
@@ -294,6 +293,9 @@ class BTCInterface(Secp256k1Interface):
self._expect_seedid_hex = None
self._altruistic = coin_settings.get("altruistic", True)
self._use_descriptors = coin_settings.get("use_descriptors", False)
# Use hardened account indices to match existing wallet keys, only applies when use_descriptors is True
self._use_legacy_key_paths = coin_settings.get("use_legacy_key_paths", False)
self._disable_lock_tx_rbf = False
def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@@ -315,6 +317,21 @@ class BTCInterface(Secp256k1Interface):
def checkWallets(self) -> int:
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
self._log.debug(
f"Wallet: {self._rpc_wallet} not active, attempting to load."
)
try:
self.rpc_wallet(
"loadwallet",
[
self._rpc_wallet,
],
)
wallets = self.rpc("listwallets")
except Exception as e:
self._log.debug(f'Error loading wallet "self._rpc_wallet": {e}.')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.warning(f"Changing {self.ticker()} wallet name.")
@@ -347,7 +364,9 @@ class BTCInterface(Secp256k1Interface):
self.rpc_wallet("getwalletinfo" if with_wallet else "getblockchaininfo")
def getDaemonVersion(self):
return self.rpc("getnetworkinfo")["version"]
if self._core_version is None:
self._core_version = self.rpc("getnetworkinfo")["version"]
return self._core_version
def getBlockchainInfo(self):
return self.rpc("getblockchaininfo")
@@ -382,6 +401,13 @@ class BTCInterface(Secp256k1Interface):
last_block_header = prev_block_header
raise ValueError(f"Block header not found at time: {time}")
def getWalletAccountPath(self) -> str:
# Use a bip44 style path, however the seed (derived from the particl mnemonic keychain) can't be turned into a bip39 mnemonic without the matching entropy
purpose: int = 84 # native segwit
coin_type: int = self.chainparams_network()["bip44"]
account: int = 0
return f"{purpose}h/{coin_type}h/{account}h"
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
assert len(key_bytes) == 32
self._have_checked_seed = False
@@ -390,8 +416,15 @@ class BTCInterface(Secp256k1Interface):
ek = ExtKeyPair()
ek.set_seed(key_bytes)
ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
if self._use_legacy_key_paths:
# Match keys from legacy wallets (created from sethdseed)
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
else:
# Use a bip44 path so the seed can be exported as a mnemonic
path: str = self.getWalletAccountPath()
desc_external = descsum_create(f"wpkh({ek_encoded}/{path}/0/*)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/{path}/1/*)")
rv = self.rpc_wallet(
"importdescriptors",
@@ -430,6 +463,50 @@ class BTCInterface(Secp256k1Interface):
"""
raise (e)
def canExportToElectrum(self) -> bool:
# keychains must be unhardened to export into electrum
return self._use_descriptors is True and self._use_legacy_key_paths is False
def getAccountKey(
self,
key_bytes: bytes,
extkey_prefix: Optional[int] = None,
coin_type_overide: Optional[int] = None,
) -> str:
# For electrum, must start with zprv to get P2WPKH, addresses
# extkey_prefix: 0x04b2430c
ek = ExtKeyPair()
ek.set_seed(key_bytes)
path: str = self.getWalletAccountPath()
account_ek = ek.derive_path(path)
return self.encode_secret_extkey(account_ek.encode_v(), extkey_prefix)
def getWalletKeyChains(
self, key_bytes: bytes, extkey_prefix: Optional[int] = None
) -> Dict[str, str]:
ek = ExtKeyPair()
ek.set_seed(key_bytes)
# extkey must contain keydata to derive hardened child keys
if self.canExportToElectrum():
path: str = self.getWalletAccountPath()
external_extkey = ek.derive_path(f"{path}/0")
internal_extkey = ek.derive_path(f"{path}/1")
else:
# Match keychain paths of legacy wallets
external_extkey = ek.derive_path("0h/0h")
internal_extkey = ek.derive_path("0h/1h")
def encode_extkey(extkey):
return self.encode_secret_extkey(extkey.encode_v(), extkey_prefix)
rv = {
"external": encode_extkey(external_extkey),
"internal": encode_extkey(internal_extkey),
}
return rv
def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo")
rv["encrypted"] = "unlocked_until" in rv
@@ -489,10 +566,16 @@ class BTCInterface(Secp256k1Interface):
if descriptor is None:
self._log.debug("Could not find active descriptor.")
return "Not found"
end = descriptor["desc"].find("/")
start = descriptor["desc"].find("]")
if start < 3:
return "Could not parse descriptor"
descriptor = descriptor["desc"][start + 1 :]
end = descriptor.find("/")
if end < 10:
return "Not found"
extkey = descriptor["desc"][5:end]
return "Could not parse descriptor"
extkey = descriptor[:end]
extkey_data = b58decode(extkey)[4:-4]
extkey_data_hash: bytes = hash160(extkey_data)
return extkey_data_hash.hex()
@@ -546,7 +629,7 @@ class BTCInterface(Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate:
self._log.debug(
"Fee rate override used for %s: %f", self.coin_name(), override_feerate
f"Fee rate override used for {self.coin_name()}: {override_feerate}"
)
return override_feerate, "override_feerate"
@@ -596,8 +679,9 @@ class BTCInterface(Secp256k1Interface):
pkh = hash160(pk)
return segwit_addr.encode(bech32_prefix, version, pkh)
def encode_secret_extkey(self, ek_data: bytes) -> str:
def encode_secret_extkey(self, ek_data: bytes, prefix=None) -> str:
assert len(ek_data) == 74
if prefix is None:
prefix = self.chainparams_network()["ext_secret_key_prefix"]
data: bytes = prefix.to_bytes(4, "big") + ek_data
checksum = sha256(sha256(data))
@@ -657,18 +741,12 @@ class BTCInterface(Secp256k1Interface):
wif_prefix = self.chainparams_network()["key_prefix"]
return toWIF(wif_prefix, key_bytes)
def encodePubkey(self, pk: bytes) -> bytes:
return pointToCPK(pk)
def encodeSegwitAddress(self, key_hash: bytes) -> str:
return segwit_addr.encode(self.chainparams_network()["hrp"], 0, key_hash)
def decodeSegwitAddress(self, addr: str) -> bytes:
return bytes(segwit_addr.decode(self.chainparams_network()["hrp"], addr)[1])
def decodePubkey(self, pke):
return CPKToPoint(pke)
def decodeKey(self, k: str) -> bytes:
return decodeWif(k)
@@ -676,10 +754,11 @@ class BTCInterface(Secp256k1Interface):
# p2wpkh
return CScript([OP_0, pkh])
def loadTx(self, tx_bytes: bytes) -> CTransaction:
def loadTx(self, tx_bytes: bytes, allow_witness: bool = True) -> CTransaction:
# Load tx from bytes to internal representation
# Transactions with no inputs require allow_witness set to false to decode correctly
tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes))
tx.deserialize(BytesIO(tx_bytes), allow_witness)
return tx
def createSCLockTx(
@@ -687,27 +766,63 @@ class BTCInterface(Secp256k1Interface):
) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.nLockTime = 0 # TODO: match locktimes by core
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
return tx.serialize()
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate)
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None) -> bytes:
funded_tx = self.fundTx(tx_bytes, feerate)
if self._disable_lock_tx_rbf:
tx = self.loadTx(funded_tx)
for txi in tx.vin:
txi.nSequence = 0xFFFFFFFE
funded_tx = tx.serialize_with_witness()
return funded_tx
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
assert len(Kal) == 33
assert len(Kaf) == 33
# fmt: off
return CScript([
CScriptOp(OP_IF),
2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG),
2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG),
CScriptOp(OP_ELSE),
csv_val, CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
Kaf_enc, CScriptOp(OP_CHECKSIG),
Kaf, CScriptOp(OP_CHECKSIG),
CScriptOp(OP_ENDIF)])
# fmt: on
def isScriptP2PKH(self, script: bytes) -> bool:
if len(script) != 25:
return False
if script[0] != OP_DUP:
return False
if script[1] != OP_HASH160:
return False
if script[2] != 20:
return False
if script[23] != OP_EQUALVERIFY:
return False
if script[24] != OP_CHECKSIG:
return False
return True
def isScriptP2WPKH(self, script: bytes) -> bool:
if len(script) != 22:
return False
if script[0] != OP_0:
return False
if script[1] != 20:
return False
return True
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script):
return [bytes(72), bytes(33)]
raise ValueError("Unknown script type")
def createSCLockRefundTx(
self,
tx_lock_bytes,
@@ -963,7 +1078,15 @@ class BTCInterface(Secp256k1Interface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
# locktime must be <= chainheight + 2
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if tx.nLockTime != 0:
current_height: int = self.getChainHeight()
if tx.nLockTime > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
)
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
@@ -1235,7 +1358,17 @@ class BTCInterface(Secp256k1Interface):
)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
for i in range(10000):
# Grind for low-R value
if i == 0:
nonce = (ffi.NULL, ffi.NULL)
else:
extra_entropy = i.to_bytes(4, "little") + (b"\0" * 28)
nonce = (ffi.NULL, ffi.new("unsigned char [32]", extra_entropy))
sig = eck.sign(sig_hash, hasher=None, custom_nonce=nonce)
if len(sig) < 71:
return sig + bytes((SIGHASH_ALL,))
raise RuntimeError("sign failed.")
def signTxOtVES(
self,
@@ -1301,24 +1434,40 @@ class BTCInterface(Secp256k1Interface):
"feeRate": feerate_str,
}
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
return bytes.fromhex(rv["hex"])
tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes
def lockNonSegwitPrevouts(self) -> None:
# For tests
unspent = self.rpc_wallet("listunspent")
to_lock = []
for u in unspent:
def getNonSegwitOutputs(self):
unspents = self.rpc_wallet("listunspent", [0, 99999999])
nonsegwit_unspents = []
for u in unspents:
if u.get("spendable", False) is False:
continue
if "desc" in u:
desc = u["desc"]
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
nonsegwit_unspents.append(
{
"txid": u["txid"],
"vout": u["vout"],
"amount": u["amount"],
}
)
else:
if not desc.startswith("wpkh"):
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
nonsegwit_unspents.append(
{
"txid": u["txid"],
"vout": u["vout"],
"amount": u["amount"],
}
)
return nonsegwit_unspents
def lockNonSegwitPrevouts(self) -> None:
# For tests
to_lock = self.getNonSegwitOutputs()
if len(to_lock) > 0:
self._log.debug(f"Locking {len(to_lock)} non segwit prevouts")
@@ -1393,6 +1542,9 @@ class BTCInterface(Secp256k1Interface):
def getScriptDest(self, script):
return CScript([OP_0, sha256(script)])
def getP2WSHScriptDest(self, script):
return CScript([OP_0, sha256(script)])
def getScriptScriptSig(self, script: bytes) -> bytes:
return bytes()
@@ -1492,7 +1644,14 @@ class BTCInterface(Secp256k1Interface):
return (weight + wsf - 1) // wsf
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
dest_address = (
self.pubkey_to_segwit_address(Kbs)
@@ -1636,7 +1795,7 @@ class BTCInterface(Secp256k1Interface):
"listunspent",
[
0,
9999999,
99999999,
[
dest_address,
],
@@ -1665,6 +1824,10 @@ class BTCInterface(Secp256k1Interface):
"height": block_height,
}
if "mempoolconflicts" in tx and len(tx["mempoolconflicts"]) > 0:
rv["conflicts"] = tx["mempoolconflicts"]
elif "walletconflicts" in tx and len(tx["walletconflicts"]) > 0:
rv["conflicts"] = tx["walletconflicts"]
except Exception as e:
self._log.debug(
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
@@ -1828,6 +1991,9 @@ class BTCInterface(Secp256k1Interface):
def getBlockWithTxns(self, block_hash: str):
return self.rpc("getblock", [block_hash, 2])
def listUtxos(self):
return self.rpc_wallet("listunspent")
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet("listunspent")
@@ -1864,6 +2030,15 @@ class BTCInterface(Secp256k1Interface):
sum_unspent += self.make_int(o["amount"])
return sum_unspent
def signMessage(self, address: str, message: str) -> str:
return self.rpc_wallet(
"signmessage",
[address, message],
)
def signMessageWithKey(self, key_wif: str, message: str) -> str:
return self.rpc("signmessagewithprivkey", [key_wif, message])
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspent_addr = self.getUnspentsByAddr()
@@ -1881,6 +2056,7 @@ class BTCInterface(Secp256k1Interface):
self._log.debug(f"sign_for_addr {sign_for_addr}")
funds_addr: str = sign_for_addr
if (
self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo
@@ -1889,6 +2065,7 @@ class BTCInterface(Secp256k1Interface):
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
sign_message: str = sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()
if self._use_descriptors:
# https://github.com/bitcoin/bitcoin/issues/10542
# https://github.com/bitcoin/bitcoin/issues/26046
@@ -1905,7 +2082,6 @@ class BTCInterface(Secp256k1Interface):
],
)
hdkeypath = addr_info["hdkeypath"]
sign_for_address_key = None
for descriptor in priv_keys["descriptors"]:
if descriptor["active"] is False or descriptor["internal"] is True:
@@ -1926,22 +2102,10 @@ class BTCInterface(Secp256k1Interface):
sign_for_address_key = self.encodeKey(ek._key)
break
assert sign_for_address_key is not None
signature = self.rpc(
"signmessagewithprivkey",
[
sign_for_address_key,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
)
signature = self.signMessageWithKey(sign_for_address_key, sign_message)
del priv_keys
else:
signature = self.rpc_wallet(
"signmessage",
[
sign_for_addr,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
)
signature = self.signMessage(sign_for_addr, sign_message)
prove_utxos = [] # TODO: Send specific utxos
return (sign_for_addr, signature, prove_utxos)
@@ -2367,6 +2531,59 @@ class BTCInterface(Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool:
return "non-BIP68-final" in err_str or "non-final" in err_str
def combine_non_segwit_prevouts(self):
self._log.info("Combining non-segwit prevouts")
if self._use_segwit is False:
raise RuntimeError("Not configured to use segwit outputs.")
prevouts_to_spend = self.getNonSegwitOutputs()
if len(prevouts_to_spend) < 1:
raise RuntimeError("No non-segwit outputs found.")
total_amount: int = 0
for n, prevout in enumerate(prevouts_to_spend):
total_amount += self.make_int(prevout["amount"])
addr_to: str = self.getNewAddress(
self._use_segwit, "combine_non_segwit_prevouts"
)
txn = self.rpc(
"createrawtransaction",
[prevouts_to_spend, {addr_to: self.format_amount(total_amount)}],
)
fee_rate, rate_src = self.get_fee_rate(self._conf_target)
fee_rate_str: str = self.format_amount(fee_rate, True, 1)
self._log.debug(
f"Using fee rate: {fee_rate_str}, src: {rate_src}, confirms target: {self._conf_target}"
)
options = {
"add_inputs": False,
"subtractFeeFromOutputs": [
0,
],
"feeRate": fee_rate_str,
}
tx_fee_set = self.rpc_wallet("fundrawtransaction", [txn, options])["hex"]
tx_signed = self.rpc_wallet("signrawtransactionwithwallet", [tx_fee_set])["hex"]
tx = self.rpc(
"decoderawtransaction",
[
tx_signed,
],
)
self._log.info(
"Submitting tx to combine non-segwit prevouts: {}".format(
self._log.id(bytes.fromhex(tx["txid"]))
)
)
self.rpc(
"sendrawtransaction",
[
tx_signed,
],
)
return tx["txid"]
def testBTCInterface():
print("TODO: testBTCInterface")

View File

@@ -521,7 +521,7 @@ class CTransaction(object):
self.hash = tx.hash
self.wit = copy.deepcopy(tx.wit)
def deserialize(self, f):
def deserialize(self, f, allow_witness: bool = True):
ver32bit = struct.unpack("<i", f.read(4))[0]
self.nVersion = ver32bit & 0xffff
self.nType = (ver32bit >> 16) & 0xffff

View File

@@ -455,12 +455,12 @@ class CTransaction(object):
self.wit = copy.deepcopy(tx.wit)
self.strDZeel = copy.deepcopy(tx.strDZeel)
def deserialize(self, f):
def deserialize(self, f, allow_witness: bool = True):
self.nVersion = struct.unpack("<i", f.read(4))[0]
self.nTime = struct.unpack("<i", f.read(4))[0]
self.vin = deser_vector(f, CTxIn)
flags = 0
if len(self.vin) == 0:
if len(self.vin) == 0 and allow_witness:
flags = struct.unpack("<B", f.read(1))[0]
# Not sure why flags can't be zero, but this
# matches the implementation in bitcoind

View File

@@ -505,7 +505,7 @@ class CTransaction:
self.sha256 = tx.sha256
self.hash = tx.hash
def deserialize(self, f):
def deserialize(self, f, allow_witness: bool = True):
self.nVersion = struct.unpack("<h", f.read(2))[0]
self.nType = struct.unpack("<h", f.read(2))[0]
self.vin = deser_vector(f, CTxIn)

View File

@@ -13,6 +13,8 @@ import logging
import random
import traceback
from typing import List
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import (
@@ -1085,22 +1087,21 @@ class DCRInterface(Secp256k1Interface):
return self.fundTx(tx_bytes, feerate)
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
assert len(Kal) == 33
assert len(Kaf) == 33
script = bytearray()
script += bytes((OP_IF,))
push_script_data(script, bytes((2,)))
push_script_data(script, Kal_enc)
push_script_data(script, Kaf_enc)
push_script_data(script, Kal)
push_script_data(script, Kaf)
push_script_data(script, bytes((2,)))
script += bytes((OP_CHECKMULTISIG,))
script += bytes((OP_ELSE,))
script += CScriptNum.encode(CScriptNum(csv_val))
script += bytes((OP_CHECKSEQUENCEVERIFY,))
script += bytes((OP_DROP,))
push_script_data(script, Kaf_enc)
push_script_data(script, Kaf)
script += bytes((OP_CHECKSIG,))
script += bytes((OP_ENDIF,))
@@ -1609,11 +1610,11 @@ class DCRInterface(Secp256k1Interface):
script_pk = self.getScriptDest(script)
return findOutput(tx, script_pk)
def getScriptLockTxDummyWitness(self, script: bytes):
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), bytes(72), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), bytes(72), bytes(len(script))]
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)

View File

@@ -89,7 +89,7 @@ class CTransaction:
self.locktime = tx.locktime
self.expiry = tx.expiry
def deserialize(self, data: bytes) -> None:
def deserialize(self, data: bytes, allow_witness: bool = True) -> None:
version = int.from_bytes(data[:4], "little")
self.version = version & 0xFFFF

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -44,6 +44,7 @@ class FIROInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")

View File

@@ -98,6 +98,7 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
)
self.rpc_wallet_watch = self.rpc_wallet
def chainparams(self):
return chainparams[Coins.LTC]

View File

@@ -79,6 +79,7 @@ class NAVInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")

View File

@@ -8,13 +8,13 @@
import hashlib
from enum import IntEnum
from typing import List
from basicswap.contrib.test_framework.messages import (
CTxOutPart,
)
from basicswap.contrib.test_framework.script import (
CScript,
OP_0,
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
@@ -25,7 +25,6 @@ from basicswap.util import (
TemporaryError,
)
from basicswap.util.script import (
getP2WSH,
getCompactSizeLen,
getWitnessElementLen,
)
@@ -136,6 +135,11 @@ class PARTInterface(BTCInterface):
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
return [bytes(72), bytes(33)]
raise ValueError("Unknown script type")
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
@@ -189,6 +193,27 @@ class PARTInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def combine_non_segwit_prevouts(self):
raise RuntimeError("No non-segwit outputs found.")
def signMessage(self, address: str, message: str) -> str:
args = [address, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc_wallet("signmessage", args)
def signMessageWithKey(self, key_wif: str, message: str) -> str:
args = [key_wif, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc("signmessagewithprivkey", args)
class PARTInterfaceBlind(PARTInterface):
@@ -211,6 +236,15 @@ class PARTInterfaceBlind(PARTInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 980
@staticmethod
def compareFeeRates(actual: int, expected: int) -> bool:
# Allow the fee to be up to 10% larger than expected
if actual < expected - 20:
return False
if actual > expected + expected * 0.1:
return False
return True
def coin_name(self) -> str:
return super().coin_name() + " Blind"
@@ -256,7 +290,7 @@ class PARTInterfaceBlind(PARTInterface):
ephemeral_pubkey = self.getPubkey(ephemeral_key)
assert len(ephemeral_pubkey) == 33
nonce = self.getScriptLockTxNonce(vkbv)
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(script))
inputs = []
outputs = [
{
@@ -330,7 +364,7 @@ class PARTInterfaceBlind(PARTInterface):
locked_coin = input_blinded_info["amount"]
tx_lock_id = lock_tx_obj["txid"]
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(refund_script))
inputs = [
{
@@ -480,7 +514,16 @@ class PARTInterfaceBlind(PARTInterface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(lock_txid_hex)))
ensure(lock_tx_obj["version"] == self.txVersion(), "Bad version")
ensure(lock_tx_obj["locktime"] == 0, "Bad nLockTime")
lock_time: int = lock_tx_obj["locktime"]
# locktime must be <= chainheight + 2
# TODO: locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if lock_time != 0:
current_height: int = self.getChainHeight()
if lock_time > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {lock_time}, current height {current_height}"
)
# Find the output of the lock tx to verify
nonce = self.getScriptLockTxNonce(vkbv)
@@ -495,7 +538,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_txo_scriptpk = bytes.fromhex(
lock_tx_obj["vout"][lock_output_n]["scriptPubKey"]["hex"]
)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
script_pk = self.getP2WSHScriptDest(script_out)
ensure(lock_txo_scriptpk == script_pk, "Bad output script")
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, "Bad script leader pubkey")
@@ -572,7 +615,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_txo_scriptpk = bytes.fromhex(
lock_refund_tx_obj["vout"][lock_refund_output_n]["scriptPubKey"]["hex"]
)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
script_pk = self.getP2WSHScriptDest(script_out)
ensure(lock_refund_txo_scriptpk == script_pk, "Bad output script")
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, "Bad script pubkey")
@@ -680,6 +723,7 @@ class PARTInterfaceBlind(PARTInterface):
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_paid = self.make_int(lock_refund_spend_tx_obj["vout"][0]["ct_fee"])
fee_rate_paid = fee_paid * 1000 // vsize
ensure(
self.compareFeeRates(fee_rate_paid, feerate),
@@ -1031,10 +1075,11 @@ class PARTInterfaceBlind(PARTInterface):
self,
kbv,
Kbs,
cb_swap_value,
cb_block_confirmed,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1065,7 +1110,10 @@ class PARTInterfaceBlind(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "blind", "Output is not anon")
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")
@@ -1287,7 +1335,14 @@ class PARTInterfaceAnon(PARTInterface):
return bytes.fromhex(txid)
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
self,
kbv,
Kbs,
cb_swap_value,
cb_block_confirmed,
restore_height,
bid_sender,
check_amount: bool = True,
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1319,7 +1374,10 @@ class PARTInterfaceAnon(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "anon", "Output is not anon")
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")

View File

@@ -33,6 +33,7 @@ class PIVXInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted

View File

@@ -29,7 +29,7 @@ class WOWInterface(XMRInterface):
@staticmethod
def depth_spendable() -> int:
return 3
return 4
# below only needed until wow is rebased to monero v0.18.4.0+
def openWallet(self, filename):

View File

@@ -8,9 +8,9 @@
import logging
import os
import secrets
import time
import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu
import basicswap.util_xmr as xmr_util
from coincurve.ed25519 import (
ed25519_add,
@@ -35,6 +35,9 @@ from basicswap.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface
ed25519_l = 2**252 + 27742317777372353535851937790883648493
class XMRInterface(CoinInterface):
@staticmethod
def curve_type():
@@ -84,7 +87,15 @@ class XMRInterface(CoinInterface):
def is_transient_error(self, ex) -> bool:
str_error: str = str(ex).lower()
if "failed to get earliest fork height" in str_error:
if any(
response in str_error
for response in [
"failed to get earliest fork height",
"failed to get output distribution",
"request-sent",
"idle",
]
):
return True
return super().is_transient_error(ex)
@@ -146,6 +157,8 @@ class XMRInterface(CoinInterface):
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
# walletrpctimeoutlong likely unneeded
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
self._num_chaininfo_retries = coin_settings.get("numchaininforetries", 20)
self._chaininfo_retry_delay = coin_settings.get("chaininforetrydelay", 1)
self.rpc = make_xmr_rpc_func(
coin_settings["rpcport"],
@@ -287,10 +300,13 @@ class XMRInterface(CoinInterface):
self.rpc_wallet("get_languages")
def getDaemonVersion(self):
return self.rpc_wallet("get_version")["version"]
# Returns wallet version
if self._core_version is None:
self._core_version = self.rpc_wallet("get_version")["version"]
return self._core_version
def getBlockchainInfo(self):
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
get_height = self.getChainHeight(full_output=True)
rv = {
"blocks": get_height["height"],
"verificationprogress": 0.0,
@@ -319,8 +335,16 @@ class XMRInterface(CoinInterface):
return rv
def getChainHeight(self):
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
def getChainHeight(self, full_output: bool = False):
for i in range(self._num_chaininfo_retries):
try:
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
return get_height if full_output else get_height["height"]
except Exception as e:
if i < self._num_chaininfo_retries - 1 and self.is_transient_error(e):
time.sleep(self._chaininfo_retry_delay)
continue
raise (e)
def getWalletInfo(self):
with self._mx_wallet:
@@ -378,10 +402,7 @@ class XMRInterface(CoinInterface):
def getNewRandomKey(self) -> bytes:
# Note: Returned bytes are in big endian order
return i2b(edu.get_secret())
def pubkey(self, key: bytes) -> bytes:
return edf.scalarmult_B(key)
return i2b(9 + secrets.randbelow(ed25519_l - 9))
def encodeKey(self, vk: bytes) -> str:
return vk[::-1].hex()
@@ -389,12 +410,6 @@ class XMRInterface(CoinInterface):
def decodeKey(self, k_hex: str) -> bytes:
return bytes.fromhex(k_hex)[::-1]
def encodePubkey(self, pk: bytes) -> str:
return edu.encodepoint(pk)
def decodePubkey(self, pke):
return edf.decodepoint(pke)
def getPubkey(self, privkey):
return ed25519_get_pubkey(privkey)
@@ -405,7 +420,7 @@ class XMRInterface(CoinInterface):
def verifyKey(self, k: int) -> bool:
i = b2i(k)
return i < edf.l and i > 8
return i < ed25519_l and i > 8
def verifyPubkey(self, pubkey_bytes):
# Calls ed25519_decode_check_point() in secp256k1
@@ -455,16 +470,24 @@ class XMRInterface(CoinInterface):
params["priority"] = self._fee_priority
rv = self.rpc_wallet("transfer", params)
self._log.info(
"publishBLockTx %s to address_b58 %s",
"publishBLockTx {} to address_b58 {}".format(
self._log.id(rv["tx_hash"]),
self._log.addr(shared_addr),
)
)
tx_hash = bytes.fromhex(rv["tx_hash"])
return tx_hash
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
@@ -516,7 +539,7 @@ class XMRInterface(CoinInterface):
)
rv = -1
continue
if transfer["amount"] == cb_swap_value:
if transfer["amount"] == cb_swap_value or check_amount is False:
return {
"txid": transfer["tx_hash"],
"amount": transfer["amount"],
@@ -535,16 +558,14 @@ class XMRInterface(CoinInterface):
rv = -1
return rv
def findTxnByHash(self, txid):
def findTxnByHash(self, txid: str):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
try:
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
"height"
]
current_height: int = self.getChainHeight()
self._log.info(
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
)

View File

@@ -42,6 +42,7 @@ from .ui.util import (
)
from .ui.page_offers import postNewOffer
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
from .db import Concepts
def getFormData(post_string: str, is_json: bool):
@@ -122,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(coins), "UTF-8")
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = []
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
balance = "0.0"
if k in wallets:
w = wallets[k]
if "balance" in w and "error" not in w and "no_data" not in w:
raw_balance = w["balance"]
if isinstance(raw_balance, float):
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
elif isinstance(raw_balance, int):
balance = str(raw_balance)
else:
balance = raw_balance
pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
ci = swap_client.ci(k)
pending_amount = 0
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
pending_amount += ci.make_int(w["unconfirmed"])
if "immature" in w and float(w["immature"]) > 0.0:
pending_amount += ci.make_int(w["immature"])
if pending_amount > 0:
pending = ci.format_amount(pending_amount)
coin_entry = {
"id": int(k),
"name": getCoinName(k),
"balance": balance,
"pending": pending,
"ticker": chainparams[k]["ticker"],
}
coins_with_balances.append(coin_entry)
if k == Coins.PART:
variants = [
{
"coin": Coins.PART_ANON,
"balance_field": "anon_balance",
"pending_field": "anon_pending",
},
{
"coin": Coins.PART_BLIND,
"balance_field": "blind_balance",
"pending_field": "blind_unconfirmed",
},
]
for variant_info in variants:
variant_balance = "0.0"
variant_pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if variant_info["balance_field"] in w:
raw_balance = w[variant_info["balance_field"]]
if isinstance(raw_balance, float):
variant_balance = f"{raw_balance:.8f}".rstrip(
"0"
).rstrip(".")
elif isinstance(raw_balance, int):
variant_balance = str(raw_balance)
else:
variant_balance = raw_balance
if (
variant_info["pending_field"] in w
and float(w[variant_info["pending_field"]]) > 0.0
):
variant_pending = str(
w[variant_info["pending_field"]]
)
variant_entry = {
"id": int(variant_info["coin"]),
"name": getCoinName(variant_info["coin"]),
"balance": variant_balance,
"pending": variant_pending,
"ticker": chainparams[Coins.PART]["ticker"],
}
coins_with_balances.append(variant_entry)
elif k == Coins.LTC:
variant_balance = "0.0"
variant_pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if "mweb_balance" in w:
variant_balance = w["mweb_balance"]
pending_amount = 0
if (
"mweb_unconfirmed" in w
and float(w["mweb_unconfirmed"]) > 0.0
):
pending_amount += float(w["mweb_unconfirmed"])
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
pending_amount += float(w["mweb_immature"])
if pending_amount > 0:
variant_pending = f"{pending_amount:.8f}".rstrip(
"0"
).rstrip(".")
variant_entry = {
"id": int(Coins.LTC_MWEB),
"name": getCoinName(Coins.LTC_MWEB),
"balance": variant_balance,
"pending": variant_pending,
"ticker": chainparams[Coins.LTC]["ticker"],
}
coins_with_balances.append(variant_entry)
return bytes(json.dumps(coins_with_balances), "UTF-8")
except Exception as e:
error_data = {"error": str(e)}
return bytes(json.dumps(error_data), "UTF-8")
def js_wallets(self, url_split, post_string, is_json):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -166,12 +306,13 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
)
raise ValueError("Unknown command")
if coin_type == Coins.LTC_MWEB:
coin_type = Coins.LTC
rv = swap_client.getWalletInfo(coin_type)
if not rv:
raise ValueError(f"getWalletInfo failed for coin: {coin_type}")
rv.update(swap_client.getBlockchainInfo(coin_type))
ci = swap_client.ci(coin_type)
checkAddressesOwned(swap_client, ci, rv)
@@ -182,7 +323,19 @@ def js_wallets(self, url_split, post_string, is_json):
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
offer_id = None
if len(url_split) > 3:
if url_split[3] == "new":
@@ -205,6 +358,12 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
if offer_id:
filters["offer_id"] = offer_id
parsed_url = urllib.parse.urlparse(self.path)
query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {}
if "with_extra_info" in query_params:
with_extra_info = toBool(query_params["with_extra_info"][0])
if post_string != "":
post_data = getFormData(post_string, is_json)
filters["coin_from"] = setCoinFilter(post_data, "coin_from")
@@ -257,7 +416,9 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_revoked": True if o.active_ind == 2 else False,
"is_public": o.addr_to == swap_client.network_addr
or o.addr_to.strip() == "",
"message_nets": o.message_nets,
}
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if with_extra_info:
offer_data["amount_negotiable"] = o.amount_negotiable
offer_data["rate_negotiable"] = o.rate_negotiable
@@ -272,6 +433,24 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
offer_data["feerate_from"] = o.from_feerate
offer_data["feerate_to"] = o.to_feerate
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
if o.was_sent:
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id)
if strategy:
offer_data["local_automation_strat_id"] = strategy[0]
swap_client.log.debug(
f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}"
)
else:
offer_data["local_automation_strat_id"] = 0
except Exception as e:
swap_client.log.debug(
f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}"
)
offer_data["local_automation_strat_id"] = 0
rv.append(offer_data)
return bytes(json.dumps(rv), "UTF-8")
@@ -331,7 +510,8 @@ def formatBids(swap_client, bids, filters) -> bytes:
bid_rate: int = 0 if b[10] is None else b[10]
amount_to = None
if ci_to:
amount_to = ci_to.format_amount((b[4] * bid_rate) // ci_from.COIN())
amount_to_int = (b[4] * bid_rate + ci_from.COIN() - 1) // ci_from.COIN()
amount_to = ci_to.format_amount(amount_to_int)
bid_data = {
"bid_id": b[2].hex(),
@@ -513,7 +693,19 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
post_data = getFormData(post_string, is_json)
offer_id, filters = parseBidFilters(post_data)
@@ -609,16 +801,14 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
if amt_from_str is not None:
rate = ci_to.make_int(rate, r=1)
amt_from = inputAmount(amt_from_str, ci_from)
amount_to = ci_to.format_amount(
int((amt_from * rate) // ci_from.COIN()), r=1
)
amount_to_int = (amt_from * rate + ci_from.COIN() - 1) // ci_from.COIN()
amount_to = ci_to.format_amount(amount_to_int)
return bytes(json.dumps({"amount_to": amount_to}), "UTF-8")
if amt_to_str is not None:
rate = ci_from.make_int(1.0 / float(rate), r=1)
amt_to = inputAmount(amt_to_str, ci_to)
amount_from = ci_from.format_amount(
int((amt_to * rate) // ci_to.COIN()), r=1
)
amount_from_int = (amt_to * rate + ci_to.COIN() - 1) // ci_to.COIN()
amount_from = ci_from.format_amount(amount_from_int)
return bytes(json.dumps({"amount_from": amount_from}), "UTF-8")
amt_from: int = inputAmount(get_data_entry(post_data, "amt_from"), ci_from)
@@ -630,7 +820,19 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
def js_index(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
return bytes(json.dumps(swap_client.getSummary()), "UTF-8")
@@ -640,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
if not swap_client.debug:
raise ValueError("Debug mode not active.")
r = random.randint(0, 3)
r = random.randint(0, 4)
if r == 0:
swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()})
swap_client.notify(
NT.OFFER_RECEIVED,
{
"offer_id": random.randbytes(28).hex(),
"coin_from": 2,
"coin_to": 6,
"amount_from": 100000000,
"amount_to": 15500000000000,
"rate": 15500000000000,
},
)
elif r == 1:
swap_client.notify(
NT.BID_RECEIVED,
@@ -650,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
"type": "atomic",
"bid_id": random.randbytes(28).hex(),
"offer_id": random.randbytes(28).hex(),
"coin_from": 2,
"coin_to": 6,
"amount_from": 100000000,
"amount_to": 15500000000000,
"bid_amount": 50000000,
"bid_amount_to": 7750000000000,
"rate": 15500000000000,
},
)
elif r == 2:
@@ -661,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
"type": "ads",
"bid_id": random.randbytes(28).hex(),
"offer_id": random.randbytes(28).hex(),
"coin_from": 1,
"coin_to": 3,
"amount_from": 500000000,
"amount_to": 100000000,
"bid_amount": 250000000,
"bid_amount_to": 50000000,
"rate": 20000000,
},
)
elif r == 4:
swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()})
return bytes(json.dumps({"type": r}), "UTF-8")
def js_checkupdates(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
from basicswap import __version__
if not swap_client.settings.get("check_updates", True):
return bytes(
json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8"
)
import time
now = time.time()
last_manual_check = getattr(swap_client, "_last_manual_update_check", 0)
if not swap_client.debug and (now - last_manual_check) < 3600:
remaining = int(3600 - (now - last_manual_check))
return bytes(
json.dumps(
{
"error": f"Please wait {remaining // 60} minutes before checking again"
}
),
"UTF-8",
)
swap_client._last_manual_update_check = now
swap_client.log.info("Manual update check requested via web interface")
swap_client.checkForUpdates()
if swap_client._update_available:
swap_client.log.info(
f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})"
)
else:
swap_client.log.info(f"Manual check result: Up to date (v{__version__})")
return bytes(
json.dumps(
{
"message": "Update check completed",
"current_version": __version__,
"latest_version": swap_client._latest_version,
"update_available": swap_client._update_available,
}
),
"UTF-8",
)
def js_notifications(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -674,6 +952,32 @@ def js_notifications(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(swap_client.getNotifications()), "UTF-8")
def js_updatestatus(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
from basicswap import __version__
return bytes(
json.dumps(
{
"update_available": swap_client._update_available,
"current_version": __version__,
"latest_version": swap_client._latest_version,
"release_url": (
f"https://github.com/basicswap/basicswap/releases/tag/v{swap_client._latest_version}"
if swap_client._latest_version
else None
),
"release_notes": (
f"New version v{swap_client._latest_version} is available. Click to view details on GitHub."
if swap_client._latest_version
else None
),
}
),
"UTF-8",
)
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -796,7 +1100,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
"label": strat_data.label,
"type_ind": strat_data.type_ind,
"only_known_identities": strat_data.only_known_identities,
"data": json.loads(strat_data.data.decode("utf-8")),
"data": json.loads(strat_data.data.decode("UTF-8")),
"note": "" if strat_data.note is None else strat_data.note,
}
return bytes(json.dumps(rv), "UTF-8")
@@ -851,6 +1155,15 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
post_data = getFormData(post_string, is_json)
coin_in = get_data_entry(post_data, "coin")
extkey_prefix = get_data_entry_or(
post_data, "extkey_prefix", 0x04B2430C
) # default, zprv for P2WPKH in electrum
if isinstance(extkey_prefix, str):
if extkey_prefix.isdigit():
extkey_prefix = int(extkey_prefix)
else:
extkey_prefix = int(extkey_prefix, 16) # Try hex
try:
coin = getCoinIdFromName(coin_in)
except Exception:
@@ -887,7 +1200,6 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
wallet_seed_id = ci.getWalletSeedID()
except Exception as e:
wallet_seed_id = f"Error: {e}"
rv.update(
{
"seed": seed_key.hex(),
@@ -896,6 +1208,10 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
"current_seed_id": wallet_seed_id,
}
)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
rv.update(
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
return bytes(
json.dumps(rv),
@@ -930,7 +1246,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = getFormData(post_string, is_json)
password = get_data_entry(post_data, "password")
password: str = get_data_entry(post_data, "password")
if have_data_entry(post_data, "coin"):
coin = getCoinType(str(get_data_entry(post_data, "coin")))
@@ -998,6 +1314,16 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
try:
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
try:
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
if offer.bid_reversed:
amount_from: int = bid.amount_to
amount_to: int = bid.amount
bid_rate: int = ci_from.make_int(amount_to / amount_from, r=1)
else:
amount_from: int = bid.amount
amount_to: int = bid.amount_to
bid_rate: int = bid.rate
swap_data = {
"bid_id": bid_id.hex(),
"offer_id": offer.offer_id.hex(),
@@ -1006,15 +1332,13 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
"bid_state": strBidState(bid.state),
"tx_state_a": None,
"tx_state_b": None,
"coin_from": swap_client.ci(offer.coin_from).coin_name(),
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
"amount_from": swap_client.ci(offer.coin_from).format_amount(
bid.amount
),
"amount_to": swap_client.ci(offer.coin_to).format_amount(
bid.amount_to
),
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name(),
"amount_from": ci_from.format_amount(amount_from),
"amount_to": ci_to.format_amount(amount_to),
"rate": bid_rate,
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
"was_sent": bid.was_sent,
}
if offer.swap_type == SwapTypes.XMR_SWAP:
@@ -1032,9 +1356,6 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
swap_data["tx_state_a"] = bid.getITxState()
swap_data["tx_state_b"] = bid.getPTxState()
if hasattr(bid, "rate"):
swap_data["rate"] = bid.rate
all_bids.append(swap_data)
except Exception:
@@ -1100,8 +1421,157 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
)
def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
volume_data = swap_client.lookupVolume(
coin_ids, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in volume_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "data": rv}),
"UTF-8",
)
def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 3600))
days: int = int(get_data_entry_or(post_data, "days", 1))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
historical_data = swap_client.lookupHistoricalData(
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in historical_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "days": days, "data": rv}),
"UTF-8",
)
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
filters = {
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
}
if have_data_entry(post_data, "sort_by"):
sort_by = get_data_entry(post_data, "sort_by")
ensure(
sort_by
in [
"created_at",
],
"Invalid sort by",
)
filters["sort_by"] = sort_by
if have_data_entry(post_data, "sort_dir"):
sort_dir = get_data_entry(post_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(post_data, "offset"):
filters["offset"] = int(get_data_entry(post_data, "offset"))
if have_data_entry(post_data, "limit"):
filters["limit"] = int(get_data_entry(post_data, "limit"))
ensure(filters["limit"] > 0, "Invalid limit")
if have_data_entry(post_data, "address_from"):
filters["address_from"] = get_data_entry(post_data, "address_from")
if have_data_entry(post_data, "address_to"):
filters["address_to"] = get_data_entry(post_data, "address_to")
action = get_data_entry_or(post_data, "action", None)
message_routes = swap_client.listMessageRoutes(filters, action)
return bytes(json.dumps(message_routes), "UTF-8")
endpoints = {
"coins": js_coins,
"walletbalances": js_walletbalances,
"wallets": js_wallets,
"offers": js_offers,
"sentoffers": js_sentoffers,
@@ -1114,6 +1584,8 @@ endpoints = {
"rates": js_rates,
"rateslist": js_rates_list,
"generatenotification": js_generatenotification,
"checkupdates": js_checkupdates,
"updatestatus": js_updatestatus,
"notifications": js_notifications,
"identities": js_identities,
"automationstrategies": js_automationstrategies,
@@ -1127,6 +1599,9 @@ endpoints = {
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
"coinvolume": js_coinvolume,
"coinhistory": js_coinhistory,
"messageroutes": js_messageroutes,
}

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -23,6 +24,13 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
from basicswap.util.integer import encode_varint, decode_varint
NPBW_INT = 0
NPBW_BYTES = 2
NPBF_STR = 1
NPBF_BOOL = 2
class NonProtobufClass:
def __init__(self, init_all: bool = True, **kwargs):
for key, value in kwargs.items():
@@ -34,7 +42,7 @@ class NonProtobufClass:
found_field = True
break
if found_field is False:
raise ValueError(f"got an unexpected keyword argument '{key}'")
raise ValueError(f"Got an unexpected keyword argument '{key}'")
if init_all:
self.init_fields()
@@ -117,151 +125,182 @@ class NonProtobufClass:
class OfferMessage(NonProtobufClass):
_map = {
1: ("protocol_version", 0, 0),
2: ("coin_from", 0, 0),
3: ("coin_to", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
6: ("min_bid_amount", 0, 0),
7: ("time_valid", 0, 0),
8: ("lock_type", 0, 0),
9: ("lock_value", 0, 0),
10: ("swap_type", 0, 0),
11: ("proof_address", 2, 1),
12: ("proof_signature", 2, 1),
13: ("pkhash_seller", 2, 0),
14: ("secret_hash", 2, 0),
15: ("fee_rate_from", 0, 0),
16: ("fee_rate_to", 0, 0),
17: ("amount_negotiable", 0, 2),
18: ("rate_negotiable", 0, 2),
19: ("proof_utxos", 2, 0),
20: ("auto_accept_type", 0, 0),
1: ("protocol_version", NPBW_INT, 0),
2: ("coin_from", NPBW_INT, 0),
3: ("coin_to", NPBW_INT, 0),
4: ("amount_from", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("min_bid_amount", NPBW_INT, 0),
7: ("time_valid", NPBW_INT, 0),
8: ("lock_type", NPBW_INT, 0),
9: ("lock_value", NPBW_INT, 0),
10: ("swap_type", NPBW_INT, 0),
11: ("proof_address", NPBW_BYTES, NPBF_STR),
12: ("proof_signature", NPBW_BYTES, NPBF_STR),
13: ("pkhash_seller", NPBW_BYTES, 0),
14: ("secret_hash", NPBW_BYTES, 0),
15: ("fee_rate_from", NPBW_INT, 0),
16: ("fee_rate_to", NPBW_INT, 0),
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
19: ("proof_utxos", NPBW_BYTES, 0),
20: ("auto_accept_type", NPBW_INT, 0),
21: ("message_nets", NPBW_BYTES, NPBF_STR),
}
class BidMessage(NonProtobufClass):
_map = {
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkhash_buyer", 2, 0),
7: ("proof_address", 2, 1),
8: ("proof_signature", 2, 1),
9: ("proof_utxos", 2, 0),
10: ("pkhash_buyer_to", 2, 0),
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("pkhash_buyer", NPBW_BYTES, 0),
7: ("proof_address", NPBW_BYTES, NPBF_STR),
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
9: ("proof_utxos", NPBW_BYTES, 0),
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
11: ("message_nets", NPBW_BYTES, NPBF_STR),
}
class BidAcceptMessage(NonProtobufClass):
# Step 3, seller -> buyer
_map = {
1: ("bid_msg_id", 2, 0),
2: ("initiate_txid", 2, 0),
3: ("contract_script", 2, 0),
4: ("pkhash_seller", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("initiate_txid", NPBW_BYTES, 0),
3: ("contract_script", NPBW_BYTES, 0),
4: ("pkhash_seller", NPBW_BYTES, 0),
}
class OfferRevokeMessage(NonProtobufClass):
_map = {
1: ("offer_msg_id", 2, 0),
2: ("signature", 2, 0),
1: ("offer_msg_id", NPBW_BYTES, 0),
2: ("signature", NPBW_BYTES, 0),
}
class BidRejectMessage(NonProtobufClass):
_map = {
1: ("bid_msg_id", 2, 0),
2: ("reject_code", 0, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("reject_code", NPBW_INT, 0),
}
class XmrBidMessage(NonProtobufClass):
# MSG1L, F -> L
_map = {
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkaf", 2, 0),
7: ("kbvf", 2, 0),
8: ("kbsf_dleag", 2, 0),
9: ("dest_af", 2, 0),
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("pkaf", NPBW_BYTES, 0),
7: ("kbvf", NPBW_BYTES, 0),
8: ("kbsf_dleag", NPBW_BYTES, 0),
9: ("dest_af", NPBW_BYTES, 0),
10: ("message_nets", NPBW_BYTES, NPBF_STR),
}
class XmrSplitMessage(NonProtobufClass):
_map = {
1: ("msg_id", 2, 0),
2: ("msg_type", 0, 0),
3: ("sequence", 0, 0),
4: ("dleag", 2, 0),
1: ("msg_id", NPBW_BYTES, 0),
2: ("msg_type", NPBW_INT, 0),
3: ("sequence", NPBW_INT, 0),
4: ("dleag", NPBW_BYTES, 0),
}
class XmrBidAcceptMessage(NonProtobufClass):
_map = {
1: ("bid_msg_id", 2, 0),
2: ("pkal", 2, 0),
3: ("kbvl", 2, 0),
4: ("kbsl_dleag", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("pkal", NPBW_BYTES, 0),
3: ("kbvl", NPBW_BYTES, 0),
4: ("kbsl_dleag", NPBW_BYTES, 0),
# MSG2F
5: ("a_lock_tx", 2, 0),
6: ("a_lock_tx_script", 2, 0),
7: ("a_lock_refund_tx", 2, 0),
8: ("a_lock_refund_tx_script", 2, 0),
9: ("a_lock_refund_spend_tx", 2, 0),
10: ("al_lock_refund_tx_sig", 2, 0),
5: ("a_lock_tx", NPBW_BYTES, 0),
6: ("a_lock_tx_script", NPBW_BYTES, 0),
7: ("a_lock_refund_tx", NPBW_BYTES, 0),
8: ("a_lock_refund_tx_script", NPBW_BYTES, 0),
9: ("a_lock_refund_spend_tx", NPBW_BYTES, 0),
10: ("al_lock_refund_tx_sig", NPBW_BYTES, 0),
}
class XmrBidLockTxSigsMessage(NonProtobufClass):
# MSG3L
_map = {
1: ("bid_msg_id", 2, 0),
2: ("af_lock_refund_spend_tx_esig", 2, 0),
3: ("af_lock_refund_tx_sig", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("af_lock_refund_spend_tx_esig", NPBW_BYTES, 0),
3: ("af_lock_refund_tx_sig", NPBW_BYTES, 0),
}
class XmrBidLockSpendTxMessage(NonProtobufClass):
# MSG4F
_map = {
1: ("bid_msg_id", 2, 0),
2: ("a_lock_spend_tx", 2, 0),
3: ("kal_sig", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("a_lock_spend_tx", NPBW_BYTES, 0),
3: ("kal_sig", NPBW_BYTES, 0),
}
class XmrBidLockReleaseMessage(NonProtobufClass):
# MSG5F
_map = {
1: ("bid_msg_id", 2, 0),
2: ("al_lock_spend_tx_esig", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("al_lock_spend_tx_esig", NPBW_BYTES, 0),
}
class ADSBidIntentMessage(NonProtobufClass):
# L -> F Sent from bidder, construct a reverse bid
_map = {
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount_from", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("message_nets", NPBW_BYTES, NPBF_STR),
}
class ADSBidIntentAcceptMessage(NonProtobufClass):
# F -> L Sent from offerer, construct a reverse bid
_map = {
1: ("bid_msg_id", 2, 0),
2: ("pkaf", 2, 0),
3: ("kbvf", 2, 0),
4: ("kbsf_dleag", 2, 0),
5: ("dest_af", 2, 0),
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("pkaf", NPBW_BYTES, 0),
3: ("kbvf", NPBW_BYTES, 0),
4: ("kbsf_dleag", NPBW_BYTES, 0),
5: ("dest_af", NPBW_BYTES, 0),
}
class ConnectReqMessage(NonProtobufClass):
_map = {
1: ("network_type", NPBW_INT, 0),
2: ("network_data", NPBW_BYTES, 0),
3: ("request_type", NPBW_INT, 0),
4: ("request_data", NPBW_BYTES, 0),
}
class MessagePortalOffer(NonProtobufClass):
_map = {
1: ("network_type_from", NPBW_INT, 0),
2: ("network_type_to", NPBW_INT, 0),
3: ("portal_address_from", NPBW_BYTES, 0),
4: ("portal_address_to", NPBW_BYTES, 0),
5: ("time_valid", NPBW_INT, 0),
6: ("smsg_difficulty", NPBW_INT, 0),
}
class MessagePortalSend(NonProtobufClass):
_map = {
1: ("forward_address", NPBW_BYTES, 0), # pubkey, 33 bytes
2: ("message_bytes", NPBW_BYTES, 0),
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
import base64
import json
import threading
import traceback
import websocket
@@ -22,12 +23,9 @@ from basicswap.chainparams import (
Coins,
)
from basicswap.util.address import (
b58decode,
decodeWif,
)
from basicswap.basicswap_util import (
BidStates,
)
from basicswap.basicswap_util import AddressTypes
def encode_base64(data: bytes) -> str:
@@ -52,6 +50,20 @@ class WebSocketThread(threading.Thread):
self.recv_queue = Queue()
self.cmd_recv_queue = Queue()
self.delayed_events_queue = Queue()
self.ignore_events: bool = False
self.num_messages_received: int = 0
def disable_debug_mode(self):
self.ignore_events = False
for i in range(100):
try:
message = self.delayed_events_queue.get(block=False)
except Empty:
break
self.recv_queue.put(message)
def on_message(self, ws, message):
if self.logger:
@@ -62,6 +74,7 @@ class WebSocketThread(threading.Thread):
if message.startswith('{"corrId"'):
self.cmd_recv_queue.put(message)
else:
self.num_messages_received += 1
self.recv_queue.put(message)
def queue_get(self):
@@ -106,6 +119,20 @@ class WebSocketThread(threading.Thread):
self.ws.send(cmd)
return self.corrId
def wait_for_command_response(self, cmd_id, num_tries: int = 200):
cmd_id = str(cmd_id)
for i in range(num_tries):
message = self.cmd_queue_get()
if message is not None:
data = json.loads(message)
if "corrId" in data:
if data["corrId"] == cmd_id:
return data
self.delay_event.wait(0.5)
raise ValueError(
f"wait_for_command_response timed-out waiting for ID: {cmd_id}"
)
def run(self):
self.ws = websocket.WebSocketApp(
self.url,
@@ -126,16 +153,15 @@ class WebSocketThread(threading.Thread):
def waitForResponse(ws_thread, sent_id, delay_event):
sent_id = str(sent_id)
for i in range(100):
for i in range(200):
message = ws_thread.cmd_queue_get()
if message is not None:
data = json.loads(message)
# print(f"json: {json.dumps(data, indent=4)}")
if "corrId" in data:
if data["corrId"] == sent_id:
return data
delay_event.wait(0.5)
raise ValueError(f"waitForResponse timed-out waiting for id: {sent_id}")
raise ValueError(f"waitForResponse timed-out waiting for ID: {sent_id}")
def waitForConnected(ws_thread, delay_event):
@@ -146,81 +172,102 @@ def waitForConnected(ws_thread, delay_event):
raise ValueError("waitForConnected timed-out.")
def getPrivkeyForAddress(self, addr) -> bytes:
def encryptMsg(
self,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp=None,
deterministic=False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("encryptMsg")
ci_part = self.ci(Coins.PART)
try:
return ci_part.decodeKey(
self.callrpc(
"smsgdumpprivkey",
[
addr,
],
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
smsg_msg: bytes = smsgEncrypt(
privkey_from,
pubkey_to,
payload,
timestamp,
deterministic,
msg_valid,
difficulty_target=difficulty_target,
)
)
except Exception as e: # noqa: F841
pass
try:
return ci_part.decodeKey(
ci_part.rpc_wallet(
"dumpprivkey",
[
addr,
],
)
)
except Exception as e: # noqa: F841
pass
raise ValueError("key not found")
return smsg_msg
def sendSimplexMsg(
self, network, addr_from: str, addr_to: str, payload: bytes, msg_valid: int, cursor
self,
network,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp: int = None,
deterministic: bool = False,
to_user_name: str = None,
return_msg: bool = False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("sendSimplexMsg")
try:
rv = self.callrpc(
"smsggetpubkey",
[
smsg_msg: bytes = encryptMsg(
self,
addr_from,
addr_to,
],
payload,
msg_valid,
cursor,
timestamp,
deterministic,
difficulty_target,
)
pubkey_to: bytes = b58decode(rv["publickey"])
except Exception as e: # noqa: F841
use_cursor = self.openDB(cursor)
try:
query: str = "SELECT pk_from FROM offers WHERE addr_from = :addr_to LIMIT 1"
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
query: str = (
"SELECT pk_bid_addr FROM bids WHERE bid_addr = :addr_to LIMIT 1"
)
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
raise ValueError(f"Could not get public key for address {addr_to}")
finally:
if cursor is None:
self.closeDB(use_cursor, commit=False)
privkey_from = getPrivkeyForAddress(self, addr_from)
payload += bytes((0,)) # Include null byte to match smsg
smsg_msg: bytes = smsgEncrypt(privkey_from, pubkey_to, payload)
smsg_id = smsgGetID(smsg_msg)
ws_thread = network["ws_thread"]
sent_id = ws_thread.send_command("#bsx " + encode_base64(smsg_msg))
if to_user_name is not None:
to = "@" + to_user_name + " "
else:
to = "#bsx "
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if response["resp"]["type"] != "newChatItems":
if getResponseData(response, "type") != "newChatItems":
json_str = json.dumps(response, indent=4)
self.log.debug(f"Response {json_str}")
raise ValueError("Send failed")
if to_user_name is not None:
self.num_direct_simplex_messages_sent += 1
else:
self.num_group_simplex_messages_sent += 1
if return_msg:
return smsg_id, smsg_msg
return smsg_id
def forwardSimplexMsg(self, network, smsg_msg, to_user_name: str = None):
smsg_id = smsgGetID(smsg_msg)
ws_thread = network["ws_thread"]
if to_user_name is not None:
to = "@" + to_user_name + " "
else:
to = "#bsx "
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if getResponseData(response, "type") != "newChatItems":
json_str = json.dumps(response, indent=4)
self.log.debug(f"Response {json_str}")
raise ValueError("Send failed")
if to_user_name is not None:
self.num_direct_simplex_messages_sent += 1
else:
self.num_group_simplex_messages_sent += 1
return smsg_id
@@ -233,7 +280,7 @@ def decryptSimplexMsg(self, msg_data):
try:
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
bytes.fromhex(decrypted["pubkey_from"])
)
decrypted["to"] = self.network_addr
decrypted["msg_net"] = "simplex"
@@ -243,10 +290,14 @@ def decryptSimplexMsg(self, msg_data):
# Try with all active bid/offer addresses
query: str = """SELECT DISTINCT address FROM (
SELECT bid_addr AS address FROM bids WHERE active_ind = 1
AND (in_progress = 1 OR (state > :bid_received AND state < :bid_completed) OR (state IN (:bid_received, :bid_sent) AND expire_at > :now))
SELECT b.bid_addr AS address FROM bids b
JOIN bidstates s ON b.state = s.state_id
WHERE b.active_ind = 1
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
UNION
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
UNION
SELECT addr AS address FROM smsgaddresses WHERE active_ind = 1 AND use_type = :local_portal
)"""
now: int = self.getTime()
@@ -254,75 +305,147 @@ def decryptSimplexMsg(self, msg_data):
try:
cursor = self.openDB()
addr_rows = cursor.execute(
query,
{
"bid_received": int(BidStates.BID_RECEIVED),
"bid_completed": int(BidStates.SWAP_COMPLETED),
"bid_sent": int(BidStates.BID_SENT),
"now": now,
},
query, {"now": now, "local_portal": AddressTypes.PORTAL_LOCAL}
).fetchall()
finally:
self.closeDB(cursor, commit=False)
decrypted = None
for row in addr_rows:
addr = row[0]
try:
vk_addr = getPrivkeyForAddress(self, addr)
vk_addr = self.getPrivkeyForAddress(cursor, addr)
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
bytes.fromhex(decrypted["pubkey_from"])
)
decrypted["to"] = addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
finally:
self.closeDB(cursor, commit=False)
return decrypted
def parseSimplexMsg(self, chat_item):
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
dir_type = item_status["type"]
if dir_type not in ("sndRcvd", "rcvNew"):
return None
snd_progress = item_status.get("sndProgress", None)
if snd_progress and snd_progress != "complete":
item_id = chat_item["chatItem"]["meta"]["itemId"]
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
return None
conn_id = None
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
chat_type: str = chat_item["chatInfo"]["type"]
if chat_type == "group":
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
self.num_group_simplex_messages_received += 1
elif chat_type == "direct":
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
self.num_direct_simplex_messages_received += 1
else:
return None
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
try:
msg_data: bytes = decode_base64(msg_content)
decrypted_msg = decryptSimplexMsg(self, msg_data)
if decrypted_msg is None:
return None
decrypted_msg["chat_type"] = chat_type
decrypted_msg["chat_name"] = chat_name
decrypted_msg["conn_id"] = conn_id
decrypted_msg["msg_dir"] = msg_dir
return decrypted_msg
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
pass
return None
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
if ws_thread.ignore_events:
if msg_type not in ("contactConnected", "contactDeletedByContact"):
return False
ws_thread.delayed_events_queue.put(json.dumps(data))
return True
if msg_type == "contactConnected":
self.processContactConnected(data)
elif msg_type == "contactDeletedByContact":
self.processContactDisconnected(data)
else:
return False
return True
def readSimplexMsgs(self, network):
ws_thread = network["ws_thread"]
for i in range(100):
message = ws_thread.queue_get()
if message is None:
break
if self.delay_event.is_set():
break
data = json.loads(message)
# self.log.debug(f"message 1: {json.dumps(data, indent=4)}")
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
try:
if data["resp"]["type"] in ("chatItemsStatusesUpdated", "newChatItems"):
for chat_item in data["resp"]["chatItems"]:
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
if item_status["type"] in ("sndRcvd", "rcvNew"):
snd_progress = item_status.get("sndProgress", None)
if snd_progress:
if snd_progress != "complete":
item_id = chat_item["chatItem"]["meta"]["itemId"]
self.log.debug(
f"simplex chat item {item_id} {snd_progress}"
)
continue
try:
msg_data: bytes = decode_base64(
chat_item["chatItem"]["content"]["msgContent"]["text"]
)
decrypted_msg = decryptSimplexMsg(self, msg_data)
msg_type: str = getResponseData(data, "type")
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
for chat_item in getResponseData(data, "chatItems"):
decrypted_msg = parseSimplexMsg(self, chat_item)
if decrypted_msg is None:
continue
self.processMsg(decrypted_msg)
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
elif msg_type == "chatError":
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
pass
elif processEvent(self, ws_thread, msg_type, data):
pass
else:
self.log.debug(f"simplex: Unknown msg_type: {msg_type}")
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
except Exception as e:
self.log.debug(f"readSimplexMsgs error: {e}")
if self.debug:
self.log.error(traceback.format_exc())
self.delay_event.wait(0.05)
def getResponseData(data, tag=None):
for pretag in ("Right", "Left"):
if pretag in data["resp"]:
if tag:
return data["resp"][pretag][tag]
return data["resp"][pretag]
if tag:
return data["resp"][tag]
return data["resp"]
def getNewSimplexLink(data):
response_data = getResponseData(data)
if "connLinkContact" in response_data:
return response_data["connLinkContact"]["connFullLink"]
return response_data["connReqContact"]
def getJoinedSimplexLink(data):
response_data = getResponseData(data)
if "connLinkInvitation" in response_data:
return response_data["connLinkInvitation"]["connFullLink"]
return response_data["connReqInvitation"]
def initialiseSimplexNetwork(self, network_config) -> None:
self.log.debug("initialiseSimplexNetwork")
@@ -337,14 +460,57 @@ def initialiseSimplexNetwork(self, network_config) -> None:
sent_id = ws_thread.send_command("/groups")
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if len(response["resp"]["groups"]) < 1:
if len(getResponseData(response, "groups")) < 1:
sent_id = ws_thread.send_command("/c " + network_config["group_link"])
response = waitForResponse(ws_thread, sent_id, self.delay_event)
assert "groupLinkId" in response["resp"]["connection"]
assert "groupLinkId" in getResponseData(response, "connection")
network = {
add_network = {
"type": "simplex",
"ws_thread": ws_thread,
}
if "bridged" in network_config:
add_network["bridged"] = network_config["bridged"]
self.active_networks.append(network)
self.active_networks.append(add_network)
def closeSimplexChat(self, net_i, connId) -> bool:
try:
cmd_id = net_i.send_command("/chats")
response = net_i.wait_for_command_response(cmd_id, num_tries=500)
remote_name = None
for chat in getResponseData(response, "chats"):
if (
"chatInfo" not in chat
or "type" not in chat["chatInfo"]
or chat["chatInfo"]["type"] != "direct"
):
continue
try:
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
break
except Exception as e:
self.log.debug(f"Error parsing chat: {e}")
if remote_name is None:
self.log.warning(
f"Unable to find remote name for simplex direct chat, ID: {connId}"
)
return False
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
cmd_id = net_i.send_command(f"/delete @{remote_name}")
cmd_response = net_i.wait_for_command_response(cmd_id)
if getResponseData(cmd_response, "type") != "contactDeleted":
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
self.log.debug(
"cmd_response: {}".format(json.dumps(cmd_response, indent=4))
)
return False
except Exception as e:
self.log.warning(f"Error deleting simplex chat, ID: {connId} - {e}")
return False
return True

View File

@@ -7,13 +7,50 @@
import os
import select
import sqlite3
import subprocess
import time
from basicswap.bin.run import Daemon
from basicswap.util.daemon import Daemon
def serverExistsInDatabase(simplex_db_path: str, server_address: str, logger) -> bool:
try:
# Extract hostname from SMP URL format: smp://fingerprint@hostname
if server_address.startswith("smp://") and "@" in server_address:
host = server_address.split("@")[-1]
elif ":" in server_address:
host = server_address.split(":", 1)[0]
else:
host = server_address
with sqlite3.connect(simplex_db_path) as con:
c = con.cursor()
# Check for any server entry with this hostname
query = (
"SELECT COUNT(*) FROM protocol_servers WHERE host LIKE ? OR host = ?"
)
host_pattern = f"%{host}%"
count = c.execute(query, (host_pattern, host)).fetchone()[0]
if count > 0:
logger.debug(
f"Server {host} already exists in database ({count} entries)"
)
return True
else:
logger.debug(f"Server {host} not found in database")
return False
except Exception as e:
logger.error(f"Database check failed: {e}")
return False
def initSimplexClient(args, logger, delay_event):
# Need to set initial profile through CLI
# TODO: Must be a better way?
logger.info("Initialising Simplex client")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
@@ -29,7 +66,7 @@ def initSimplexClient(args, logger, delay_event):
def readOutput():
buf = os.read(pipe_r, 1024).decode("utf-8")
response = None
# logging.debug(f"simplex-chat output: {buf}")
# logger.debug(f"simplex-chat output: {buf}")
if "display name:" in buf:
logger.debug("Setting display name")
response = b"user\n"
@@ -45,7 +82,7 @@ def initSimplexClient(args, logger, delay_event):
max_wait_seconds: int = 60
while p.poll() is None:
if time.time() > start_time + max_wait_seconds:
raise ValueError("Timed out")
raise RuntimeError("Timed out")
if os.name == "nt":
readOutput()
delay_event.wait(0.1)
@@ -70,22 +107,35 @@ def startSimplexClient(
websocket_port: int,
logger,
delay_event,
socks_proxy=None,
log_level: str = "debug",
) -> Daemon:
logger.info("Starting Simplex client")
if not os.path.exists(data_path):
os.makedirs(data_path)
db_path = os.path.join(data_path, "simplex_client_data")
simplex_data_prefix = os.path.join(data_path, "simplex_client_data")
simplex_db_path = simplex_data_prefix + "_chat.db"
args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)]
args = [bin_path, "-d", db_path, "-s", server_address, "-p", str(websocket_port)]
if socks_proxy:
args += ["--socks-proxy", socks_proxy]
if not os.path.exists(db_path):
# Need to set initial profile through CLI
# TODO: Must be a better way?
init_args = args + ["-e", "/help"] # Run command ro exit client
if not os.path.exists(simplex_db_path):
# Database doesn't exist - safe to add server during initialization
logger.info("Database not found, initializing Simplex client")
init_args = args + ["-e", "/help"] # Run command to exit client
init_args += ["-s", server_address]
initSimplexClient(init_args, logger, delay_event)
else:
# Database exists - only add server if it's not already there
if not serverExistsInDatabase(simplex_db_path, server_address, logger):
logger.debug(f"Adding server to Simplex CLI args: {server_address}")
args += ["-s", server_address]
else:
logger.debug("Server already exists, not adding to CLI args")
args += ["-l", "debug"]
args += ["-l", log_level]
opened_files = []
stdout_dest = open(
@@ -104,4 +154,5 @@ def startSimplexClient(
cwd=data_path,
),
opened_files,
"simplex-chat",
)

View File

@@ -9,8 +9,8 @@ from basicswap.util.address import b58decode
def getMsgPubkey(self, msg) -> bytes:
if "pk_from" in msg:
return bytes.fromhex(msg["pk_from"])
if "pubkey_from" in msg:
return bytes.fromhex(msg["pubkey_from"])
rv = self.callrpc(
"smsggetpubkey",
[

View File

@@ -0,0 +1,54 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC
Comment: SimpleX Chat <chat@simplex.chat>
xsFNBGRDvZkBEACsxFENFWj5hMS1dCPCOXIJTNnWClVarltfUOESy5q0Ar84WJaj
hmAcc8j1Qw7uiLxVq/j+tMxcZOy79jnmhWpV5KrYA6H/E3I5NNlZOyT23rvah9mg
KtxfMHnhz/jJSwSXifYN2mmAYetQ1TQBSdLZayC7aW6BFhUaaQsaFABGli5abRUW
KArmnSfVEHI0f7TthLerPZ0hCoK06ZOPxEKCWt5CSqrC3J2d+8Cyb6j2jxkkB3GN
JXr9kI4JebivqrFNwvGw15xEDbSXIZf9I/+B/t9EA4Ebs+qrbLFRH5Drha50RIhu
LNYCkVnpKbrO6Y90KkJibm4ZtdUeNTFXjfXxT81Gi5lDmsvIyIMkFC78ePK68knM
dnESnIzEEwDtniV+ZvY0L9t/Ig1tGYggqPGVTVp9672bHKTGdiL3eXEzwv0FROD2
0HaZORXj2UZkAJTQO2ia7aS3hWdJL/iVBf4yIYARr+6NjPxv/sUMCaeuPYXTqCOB
Ykl6Lv3SPoSkEyPfVJY+12STtHH1ZofxJKYwo6Xe7EvmCiC9DK0KKVbeakZZ6wfd
5LO/tArDkqT2YjT3DUsfGqxQOoQvGCmk9yUuCm0s0vLwTHdJhSVgn9dxrEuK4FYL
IM3tGENAPAcK3e1VEbncgBMRikxvECKIz+YZyQVtoYzX2HDlT4D2HrbgXQARAQAB
zSBTaW1wbGVYIENoYXQgPGNoYXRAc2ltcGxleC5jaGF0PsLBlAQTAQgAPhYhBPtE
r4GkW94ycxl5fIUQfjV9Shf8BQJkQ72ZAhsDBQkHhh9CBQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJEIUQfjV9Shf8GekP/jpZYGJrna7467Qe82KV+qtwu+p2cRIy
IsoOmCje2p0D9DmmmDQH1IdxlJhvHZ8uEu21QwDK03r5y4iaXhz9bx4CDSDB5JPp
fMIDfOdc1V1GDT8Q2f/sYd5DX9kwpW6LdWOQZf6hwRDAeWDa+BQVhwo3E0WsPvRK
o5fqrbJzfWj8pz+JMlT8RGGt0ZxEyUjnD9C6XfqGckLdubBycs9CipPKV+3X4cY/
ix0zM2Nb3oSJ27VWMIFxi7lnBGtyUY69bE248Xhj0nJ79twPwzvk94+3e5tLQvyt
NIZcWEZEu+eYthyKcGDo/aA6lIvt1Bqp8eeFMogRxs5GJI4L/wQGwIDckemtLb45
gUdjpufEfPEfxuYWuuHuQ8W7Yvd2/ndiRkir4k+r8ypXx8yeCgocxnuUm1+4s+Wv
h64Op+M+l56cTjVCaEn25kv/T+4ll/RBplzKdNe4ClcH4NXppwXFkAHXq/j3RX++
64gRzIEC33TGheTo3btowUW+0/6iOi7Jy1RDsNvigzWwpm0p+pje54+d7hTxDmLR
bHxOZ9QiauO/HnlqNw/MezZLYL18hyEsghD3ns6QIHcUsHf17u/tRfLgN11x9tiE
ADqORsNgQ8FIRGdJcxGIt8lUlSe5vKPArsjpiomoA9CeAqepU27haIesl2QGe/jI
5OuS7CsRVDlOzsFNBGRDvZkBEADGYf7E+bzYgORnlSY3TZgS5UvkMIGswlw6GW7j
Vx6hAsMbiCwoKCVdzl/J4BImbJIJg2Pxvn/k7tYS2Jqb1q/EcpBmOZU9BRiTw49A
TiK8UfeH9aIPNFwuiatmA29dGxPH2RgSCwa3f4l2RsnQl301UdNlXj6mmWngD6mj
ae5/COUgH6CbKptfLp0Xw0WpPfKV1GK9+/X8Hv7W6RDA6xoWFlgzTyuy96rMmXJ1
3E7P/50ebIOundVzCni10dZyn7+W13cJGOyzxQnbR6PEMVHsgi4uZB/Gt6PxF0dC
s56IUi05hr9uH++p7ps2G8iIwvqXDu8VOvwN9hvt1fpxnRC2+Zv0lHwpDrnSBvjY
8er2tJxXlybXwEpk1nzctmDDWrgbBgQugOxTu4rkqIvAGwq7U98aLUb3vEqlyWSp
YDufsiLbGYC5owCli36yDzjfm48W0DwaOA5Ne5yVCih1f4ocF3RXVU6o1TEW1pfL
DEDZOXDT9sj0qef9NW0Nz+x/EiCT2k2Bkwt0ETf4TralsJ7smCcbhqfJbu1NG22g
oLNXZcZgTUxmOWmU+nlrFk8Hk7EK2KDeMKSgiX6jrAGpwbphrYYBZ3NLpvJ311l2
d56ZgmUt8gb1O5tLNiD2ySCvWKnpG0A5WoKZ2329nlnX2R30otYdpP1vcAEvA3GU
7fw5lQARAQABwsF8BBgBCAAmFiEE+0SvgaRb3jJzGXl8hRB+NX1KF/wFAmRDvZkC
GwwFCQeGH0IACgkQhRB+NX1KF/wNbw//bi4RcxEOVJpT37pyx6wSlq6urHopuZA5
duy0fGYxRXt4w/WR0UMH9i7iSU8J2E/UKgE7OMZg3oJqVt7g70zQDiT8ez+ep9d0
YvPAqgRnT1VDmAyMO8FOTPQPIrPMsQTnmtmxf9qrdoxW8HVqiyK+7mCGqd9ldcer
XGplALTugRWABY7iYyRyfpDSid+xMKV7KLHabv/0WdcT41HpZuUt0gmH0sMDDiJt
XrWW01LDqEZTdfaZ1xXPPp7oXUYGY6U7cH5CdLS6D38tPKR9x0ttgM83/SOx/hOO
XApcA+g113eMOyh4udowGYEkpxT26V3u8cLzCBOPDNSFx/H8ggFbfMsCWNBYV2Nx
EmAmciHvPMNLR7Hjfvn018/Q+lo1J6snoEhT9zFwpL15Lwurkqy5Z4n1D9BUyZ7m
hS/Wg7LDpaEeJCkSkOvQEPKz8YsnMpsbPc44ZZf0yuTUsWwJkZCVEqN8qByKXRdI
28zGBBJr5/rjaSJJ7+VGbh/FGUzaEkLONybzKcxazwjSASBNZXmasgStngOGWGpM
GKDnIuXs/Z7vljkKF2YoNT9bvGr7yoY74PCKrMkWdVSA1cQBj+cJ4OOojVvOGJaR
Gdpp/2r7me5UKImmUw2dhHf0KdM1iYwjzztCO72hi5Fw7vFlNS7QoadmYDzAgWkk
0oXYKNS+x2w=
=68E9
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -215,10 +215,10 @@ class XmrSwapInterface(ProtocolInterface):
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
assert len(Kal) == 33
assert len(Kaf) == 33
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)

View File

@@ -6,8 +6,10 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import logging
import traceback
import urllib
import http.client
from xmlrpc.client import (
Fault,
Transport,
@@ -15,6 +17,35 @@ from xmlrpc.client import (
)
from .util import jsonDecimal
_use_rpc_pooling = False
_rpc_pool_settings = {}
def enable_rpc_pooling(settings):
global _use_rpc_pooling, _rpc_pool_settings
_use_rpc_pooling = settings.get("enabled", False)
_rpc_pool_settings = settings
class TimeoutTransport(Transport):
def __init__(self, timeout=10, *args, **kwargs):
self.timeout = timeout
super().__init__(*args, **kwargs)
def make_connection(self, host):
conn = http.client.HTTPConnection(host, timeout=self.timeout)
return conn
class TimeoutSafeTransport(SafeTransport):
def __init__(self, timeout=10, *args, **kwargs):
self.timeout = timeout
super().__init__(*args, **kwargs)
def make_connection(self, host):
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
return conn
class Jsonrpc:
# __getattr__ complicates extending ServerProxy
@@ -29,22 +60,40 @@ class Jsonrpc:
use_builtin_types=False,
*,
context=None,
timeout=10,
):
# establish a "logical" server connection
# get the url
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
self.__auth = None
if "@" in parsed.netloc:
auth_part, host_port = parsed.netloc.rsplit("@", 1)
self.__host = host_port
if ":" in auth_part:
import base64
auth_bytes = auth_part.encode("utf-8")
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
self.__auth = f"Basic {auth_b64}"
else:
self.__host = parsed.netloc
if not self.__host:
raise ValueError(f"Invalid or empty hostname in URI: {uri}")
self.__handler = parsed.path
if not self.__handler:
self.__handler = "/RPC2"
if transport is None:
handler = SafeTransport if parsed.scheme == "https" else Transport
handler = (
TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
)
extra_kwargs = {}
transport = handler(
timeout=timeout,
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs,
@@ -62,6 +111,7 @@ class Jsonrpc:
self.__transport.close()
def json_request(self, method, params):
connection = None
try:
connection = self.__transport.make_connection(self.__host)
headers = self.__transport._extra_headers[:]
@@ -71,6 +121,10 @@ class Jsonrpc:
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", "jsonrpc"))
if self.__auth:
headers.append(("Authorization", self.__auth))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(
connection,
@@ -79,18 +133,29 @@ class Jsonrpc:
self.__request_id += 1
resp = connection.getresponse()
return resp.read()
result = resp.read()
connection.close()
return result
except Fault:
raise
except Exception:
# All unexpected errors leave connection in
# a strange state, so we clear it.
self.__transport.close()
raise
finally:
if connection is not None:
try:
connection.close()
except Exception:
pass
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
if _use_rpc_pooling:
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
@@ -101,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
x.close()
r = json.loads(v.decode("utf-8"))
except Exception as ex:
traceback.print_exc()
raise ValueError(f"RPC server error: {ex}, method: {method}")
if "error" in r and r["error"] is not None:
@@ -110,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
return r["result"]
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
from .rpc_pool import get_rpc_pool
import http.client
import socket
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet)
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
pool = get_rpc_pool(url, max_connections)
max_retries = 2
for attempt in range(max_retries):
conn = pool.get_connection()
try:
v = conn.json_request(method, params)
r = json.loads(v.decode("utf-8"))
if "error" in r and r["error"] is not None:
pool.discard_connection(conn)
raise ValueError("RPC error " + str(r["error"]))
pool.return_connection(conn)
return r["result"]
except (
http.client.RemoteDisconnected,
http.client.IncompleteRead,
http.client.BadStatusLine,
ConnectionError,
ConnectionResetError,
ConnectionAbortedError,
BrokenPipeError,
TimeoutError,
socket.timeout,
socket.error,
OSError,
) as ex:
pool.discard_connection(conn)
if attempt < max_retries - 1:
continue
logging.warning(
f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
)
raise ValueError(f"RPC server error: {ex}, method: {method}")
except ValueError:
raise
except Exception as ex:
pool.discard_connection(conn)
logging.error(f"Unexpected RPC error: {ex}, method: {method}")
raise ValueError(f"RPC server error: {ex}, method: {method}")
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
@@ -142,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
def escape_rpcauth(auth_str: str) -> str:
username, password = auth_str.split(":", 1)
username = urllib.parse.quote(username, safe="")
password = urllib.parse.quote(password, safe="")
return f"{username}:{password}"

131
basicswap/rpc_pool.py Normal file
View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import queue
import threading
import time
from basicswap.rpc import Jsonrpc
class RPCConnectionPool:
def __init__(
self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300
):
self.url = url
self.max_connections = max_connections
self.timeout = timeout
self.logger = logger
self.max_idle_time = max_idle_time
self._pool = queue.Queue(maxsize=max_connections)
self._lock = threading.Lock()
self._created_connections = 0
self._connection_timestamps = {}
def get_connection(self):
try:
conn_data = self._pool.get(block=False)
conn, timestamp = (
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
)
if time.time() - timestamp > self.max_idle_time:
if self.logger:
self.logger.debug(
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
)
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
return self._create_new_connection()
return conn
except queue.Empty:
return self._create_new_connection()
def _create_new_connection(self):
with self._lock:
if self._created_connections < self.max_connections:
self._created_connections += 1
return Jsonrpc(self.url)
try:
conn_data = self._pool.get(block=True, timeout=self.timeout)
conn, timestamp = (
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
)
if time.time() - timestamp > self.max_idle_time:
if self.logger:
self.logger.debug(
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
)
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
return Jsonrpc(self.url)
return conn
except queue.Empty:
if self.logger:
self.logger.warning(
f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
)
return Jsonrpc(self.url)
def return_connection(self, conn):
try:
self._pool.put((conn, time.time()), block=False)
except queue.Full:
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
def discard_connection(self, conn):
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
def close_all(self):
while not self._pool.empty():
try:
conn_data = self._pool.get(block=False)
conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
conn.close()
except queue.Empty:
break
with self._lock:
self._created_connections = 0
self._connection_timestamps.clear()
_rpc_pools = {}
_pool_lock = threading.Lock()
_pool_logger = None
def set_pool_logger(logger):
global _pool_logger
_pool_logger = logger
def get_rpc_pool(url, max_connections=5):
with _pool_lock:
if url not in _rpc_pools:
_rpc_pools[url] = RPCConnectionPool(
url, max_connections, logger=_pool_logger
)
return _rpc_pools[url]
def close_all_pools():
with _pool_lock:
for pool in _rpc_pools.values():
pool.close_all()
_rpc_pools.clear()

View File

@@ -14,6 +14,62 @@
z-index: 9999;
}
/* Toast Notification Animations */
.toast-slide-in {
animation: slideInRight 0.3s ease-out;
}
.toast-slide-out {
animation: slideOutRight 0.3s ease-in forwards;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* Toast Container Styles */
#ul_updates {
list-style: none;
padding: 0;
margin: 0;
max-width: 400px;
}
#ul_updates li {
margin-bottom: 0.5rem;
}
/* Toast Hover Effects */
#ul_updates .bg-white:hover {
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-1px);
transition: all 0.2s ease-in-out;
}
.dark #ul_updates .dark\:bg-gray-800:hover {
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
transition: all 0.2s ease-in-out;
}
/* Table Styles */
.padded_row td {
padding-top: 1.5em;
@@ -365,3 +421,147 @@ select.disabled-select-enabled {
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
/* Multi-select dropdown styles */
.multi-select-dropdown::-webkit-scrollbar {
width: 12px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: none !important;
border-color: inherit !important;
}
.multi-select-dropdown label:focus-within {
outline: none !important;
box-shadow: none !important;
}
#coin_to_button:focus,
#coin_from_button:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.coin-badge {
background: #3b82f6;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 2px;
}
.coin-badge .remove {
cursor: pointer;
font-weight: bold;
opacity: 0.7;
margin-left: 4px;
}
.coin-badge .remove:hover {
opacity: 1;
}
.multi-select-dropdown {
max-height: 300px;
overflow-y: auto;
z-index: 9999 !important;
position: fixed !important;
min-width: 200px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.multi-select-dropdown::-webkit-scrollbar {
width: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dropdown-container {
position: relative;
z-index: 1;
}
.dropdown-container.open {
z-index: 9999;
}
.filter-button-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.multi-select-dropdown input[type="checkbox"] {
outline: none !important;
box-shadow: none !important;
border: 1px solid #d1d5db;
border-radius: 3px;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.dark .multi-select-dropdown input[type="checkbox"] {
border-color: #6b7280;
background-color: #374151;
}
.dark .multi-select-dropdown input[type="checkbox"]:focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
}
.dark .multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown label {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@@ -4,34 +4,34 @@ const ApiManager = (function() {
isInitialized: false
};
const config = {
function getConfig() {
return window.config || window.ConfigManager || {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
}
coingecko: { requestsPerMinute: 50, minInterval: 1200 }
}
};
}
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: 1200,
cryptocompare: 2000
},
requestQueue: {},
retryDelays: [5000, 15000, 30000],
getMinInterval: function(apiName) {
const config = getConfig();
return config.rateLimits?.[apiName]?.minInterval || 1200;
},
getRetryDelays: function() {
const config = getConfig();
return config.retryDelays || [5000, 15000, 30000];
},
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return (now - lastRequest) >= this.minRequestInterval[apiName];
return (now - lastRequest) >= this.getMinInterval(apiName);
},
updateLastRequestTime: function(apiName) {
@@ -41,7 +41,7 @@ const ApiManager = (function() {
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
@@ -55,29 +55,30 @@ const ApiManager = (function() {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
this.updateLastRequestTime(apiName);
return await requestFn();
} catch (error) {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
const retryDelays = this.getRetryDelays();
if (error.message.includes('429') && retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
apiName,
retryCount,
error: error.message
});
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -118,19 +119,7 @@ const ApiManager = (function() {
}
if (options.config) {
Object.assign(config, options.config);
}
if (config.rateLimits) {
Object.keys(config.rateLimits).forEach(api => {
if (config.rateLimits[api].minInterval) {
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
}
});
}
if (config.retryDelays) {
rateLimiter.retryDelays = [...config.retryDelays];
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
}
if (window.CleanupManager) {
@@ -143,6 +132,31 @@ const ApiManager = (function() {
},
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
if (window.ErrorHandler) {
return window.ErrorHandler.safeExecuteAsync(async () => {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}, `ApiManager.makeRequest(${url})`, null);
}
try {
const options = {
method: method,
@@ -150,7 +164,7 @@ const ApiManager = (function() {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(config.requestTimeout)
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
@@ -233,11 +247,8 @@ const ApiManager = (function() {
.join(',') :
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
//console.log('Fetching coin prices for:', coins);
const response = await this.fetchCoinPrices(coins);
//console.log('Full API response:', response);
if (!response || typeof response !== 'object') {
throw new Error('Invalid response type');
}
@@ -260,28 +271,36 @@ const ApiManager = (function() {
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
const coins = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
.join(',') :
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
const response = await this.makePostRequest(url, {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
coins: coinSymbols.join(','),
source: 'coingecko.com',
ttl: 300
});
const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol) {
volumeData[coinId] = {
total_volume: data.usd_24h_vol,
price_change_percentage_24h: data.usd_24h_change || 0
};
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
const volumeData = {};
Object.entries(response.data).forEach(([coinSymbol, data]) => {
const coinKey = coinSymbol.toLowerCase();
volumeData[coinKey] = {
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
price_change_percentage_24h: data.price_change_24h || 0
};
});
return volumeData;
@@ -292,79 +311,48 @@ const ApiManager = (function() {
});
},
fetchCryptoCompareData: function(coin) {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
const headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
};
return await this.makePostRequest(url, headers);
} catch (error) {
console.error(`CryptoCompare request failed for ${coin}:`, error);
throw error;
}
});
},
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
const results = {};
const fetchPromises = coinSymbols.map(async coin => {
if (coin === 'WOW') {
return this.rateLimiter.queueRequest('coingecko', async () => {
const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
try {
const response = await this.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
}
} catch (error) {
console.error(`Error fetching CoinGecko data for WOW:`, error);
throw error;
}
});
} else {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
let url;
let days;
if (resolution === 'day') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
days = 1;
} else if (resolution === 'year') {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
days = 365;
} else {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
days = 180;
}
const response = await this.makePostRequest(url);
if (response.Response === "Error") {
console.error(`API Error for ${coin}:`, response.Message);
throw new Error(response.Message);
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
coins: coinSymbols.join(','),
days: days,
source: 'coingecko.com',
ttl: 3600
});
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
return response.data;
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
console.error('Error fetching historical data:', error);
throw error;
}
});
}
});
await Promise.all(fetchPromises);
return results;
},
dispose: function() {
// Clear any pending requests or resources
rateLimiter.requestQueue = {};
rateLimiter.lastRequestTime = {};
state.isInitialized = false;
@@ -375,17 +363,6 @@ const ApiManager = (function() {
return publicAPI;
})();
function getCoinBackendId(coinName) {
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'zcoin',
'zcoin': 'zcoin',
'bitcoincash': 'bitcoin-cash'
};
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
}
window.Api = ApiManager;
window.ApiManager = ApiManager;
@@ -396,5 +373,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');

View File

@@ -0,0 +1,244 @@
const BalanceUpdatesManager = (function() {
'use strict';
const config = {
balanceUpdateDelay: 2000,
swapEventDelay: 5000,
periodicRefreshInterval: 120000,
walletPeriodicRefreshInterval: 60000,
};
const state = {
handlers: new Map(),
timeouts: new Map(),
intervals: new Map(),
initialized: false
};
async function fetchBalanceData() {
if (window.ApiManager) {
const data = await window.ApiManager.makeRequest('/json/walletbalances', 'GET');
if (data && data.error) {
throw new Error(data.error);
}
if (!Array.isArray(data)) {
throw new Error('Invalid response format');
}
return data;
}
return fetch('/json/walletbalances', {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(balanceData => {
if (balanceData.error) {
throw new Error(balanceData.error);
}
if (!Array.isArray(balanceData)) {
throw new Error('Invalid response format');
}
return balanceData;
});
}
function clearTimeoutByKey(key) {
if (state.timeouts.has(key)) {
const timeoutId = state.timeouts.get(key);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
state.timeouts.delete(key);
}
}
function setTimeoutByKey(key, callback, delay) {
clearTimeoutByKey(key);
const timeoutId = window.CleanupManager
? window.CleanupManager.setTimeout(callback, delay)
: setTimeout(callback, delay);
state.timeouts.set(key, timeoutId);
}
function clearIntervalByKey(key) {
if (state.intervals.has(key)) {
const intervalId = state.intervals.get(key);
if (window.CleanupManager) {
window.CleanupManager.clearInterval(intervalId);
} else {
clearInterval(intervalId);
}
state.intervals.delete(key);
}
}
function setIntervalByKey(key, callback, interval) {
clearIntervalByKey(key);
const intervalId = window.CleanupManager
? window.CleanupManager.setInterval(callback, interval)
: setInterval(callback, interval);
state.intervals.set(key, intervalId);
}
function handleBalanceUpdate(contextKey, updateCallback, errorContext) {
clearTimeoutByKey(`${contextKey}_balance_update`);
setTimeoutByKey(`${contextKey}_balance_update`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error updating ${errorContext} balances via WebSocket:`, error);
});
}, config.balanceUpdateDelay);
}
function handleSwapEvent(contextKey, updateCallback, errorContext) {
clearTimeoutByKey(`${contextKey}_swap_event`);
setTimeoutByKey(`${contextKey}_swap_event`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error updating ${errorContext} balances via swap event:`, error);
});
}, config.swapEventDelay);
}
function setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext) {
const handlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'coin_balance_updated') {
handleBalanceUpdate(contextKey, balanceUpdateCallback, errorContext);
}
if (swapEventCallback) {
const swapEvents = ['new_bid', 'bid_accepted', 'swap_completed'];
if (swapEvents.includes(data.event)) {
handleSwapEvent(contextKey, swapEventCallback, errorContext);
}
}
}
});
state.handlers.set(contextKey, handlerId);
return handlerId;
}
function setupPeriodicRefresh(contextKey, updateCallback, errorContext, interval) {
const refreshInterval = interval || config.periodicRefreshInterval;
setIntervalByKey(`${contextKey}_periodic`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error in periodic ${errorContext} balance refresh:`, error);
});
}, refreshInterval);
}
function cleanup(contextKey) {
if (state.handlers.has(contextKey)) {
const handlerId = state.handlers.get(contextKey);
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
window.WebSocketManager.removeMessageHandler('message', handlerId);
}
state.handlers.delete(contextKey);
}
clearTimeoutByKey(`${contextKey}_balance_update`);
clearTimeoutByKey(`${contextKey}_swap_event`);
clearIntervalByKey(`${contextKey}_periodic`);
}
function cleanupAll() {
state.handlers.forEach((handlerId) => {
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
window.WebSocketManager.removeMessageHandler('message', handlerId);
}
});
state.handlers.clear();
state.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
state.timeouts.clear();
state.intervals.forEach(intervalId => clearInterval(intervalId));
state.intervals.clear();
state.initialized = false;
}
return {
initialize: function() {
if (state.initialized) {
return this;
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('balanceUpdatesManager', this, (mgr) => mgr.dispose());
}
window.addEventListener('beforeunload', cleanupAll);
state.initialized = true;
console.log('BalanceUpdatesManager initialized');
return this;
},
setup: function(options) {
const {
contextKey,
balanceUpdateCallback,
swapEventCallback,
errorContext,
enablePeriodicRefresh = false,
periodicInterval
} = options;
if (!contextKey || !balanceUpdateCallback || !errorContext) {
throw new Error('Missing required options: contextKey, balanceUpdateCallback, errorContext');
}
setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext);
if (enablePeriodicRefresh) {
setupPeriodicRefresh(contextKey, balanceUpdateCallback, errorContext, periodicInterval);
}
return this;
},
fetchBalanceData: fetchBalanceData,
cleanup: cleanup,
dispose: cleanupAll,
isInitialized: function() {
return state.initialized;
}
};
})();
if (typeof window !== 'undefined') {
window.BalanceUpdatesManager = BalanceUpdatesManager;
}

View File

@@ -1,9 +1,19 @@
const CacheManager = (function() {
const defaults = window.config?.cacheConfig?.storage || {
function getDefaults() {
if (window.config?.cacheConfig?.storage) {
return window.config.cacheConfig.storage;
}
if (window.ConfigManager?.cacheConfig?.storage) {
return window.ConfigManager.cacheConfig.storage;
}
return {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200,
defaultTTL: 5 * 60 * 1000
};
}
const defaults = getDefaults();
const PRICES_CACHE_KEY = 'crypto_prices_unified';
@@ -45,8 +55,12 @@ const CacheManager = (function() {
const cacheAPI = {
getTTL: function(resourceType) {
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
window.ConfigManager?.cacheConfig?.ttlSettings || {};
const defaultTTL = window.config?.cacheConfig?.defaultTTL ||
window.ConfigManager?.cacheConfig?.defaultTTL ||
defaults.defaultTTL;
return ttlConfig[resourceType] || defaultTTL;
},
set: function(key, value, resourceTypeOrCustomTtl = null) {
@@ -73,13 +87,18 @@ const CacheManager = (function() {
expiresAt: Date.now() + ttl
};
let serializedItem;
const serializedItem = window.ErrorHandler
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
: (() => {
try {
serializedItem = JSON.stringify(item);
return JSON.stringify(item);
} catch (e) {
console.error('Failed to serialize cache item:', e);
return false;
return null;
}
})();
if (!serializedItem) return false;
const itemSize = new Blob([serializedItem]).size;
if (itemSize > defaults.maxSizeBytes) {
@@ -118,7 +137,7 @@ const CacheManager = (function() {
const keysToDelete = Array.from(memoryCache.keys())
.filter(k => isCacheKey(k))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.2)); // Remove oldest 20%
.slice(0, Math.floor(memoryCache.size * 0.2));
keysToDelete.forEach(k => memoryCache.delete(k));
}
@@ -285,7 +304,7 @@ const CacheManager = (function() {
const keysToDelete = Array.from(memoryCache.keys())
.filter(key => isCacheKey(key))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.3)); // Remove oldest 30% during aggressive cleanup
.slice(0, Math.floor(memoryCache.size * 0.3));
keysToDelete.forEach(key => memoryCache.delete(key));
}
@@ -328,7 +347,6 @@ const CacheManager = (function() {
.filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key));
console.log("Cache cleared successfully");
return true;
},
@@ -531,6 +549,4 @@ const CacheManager = (function() {
window.CacheManager = CacheManager;
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
console.log('CacheManager initialized');

View File

@@ -1,12 +1,12 @@
const CleanupManager = (function() {
const state = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
resources: new Map(),
debug: false
debug: false,
memoryOptimizationInterval: null
};
function log(message, ...args) {
@@ -232,6 +232,229 @@ const CleanupManager = (function() {
};
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
if (state.memoryOptimizationInterval) {
this.clearInterval(state.memoryOptimizationInterval);
}
this.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
log('Tab hidden - running memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
} else if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
});
state.memoryOptimizationInterval = this.setInterval(() => {
if (document.hidden) {
log('Periodic memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
}
}, memoryCheckInterval);
log('Memory optimization setup complete');
return state.memoryOptimizationInterval;
},
optimizeMemory: function(options = {}) {
log('Running memory optimization');
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
}
this.cleanupOrphanedResources();
if (window.gc) {
try {
window.gc();
log('Forced garbage collection');
} catch (e) {
}
}
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
timestamp: Date.now(),
maxDataSize: options.maxDataSize || 1000
}
}));
log('Memory optimization complete');
},
cleanupOrphanedResources: function() {
let removedListeners = 0;
const validListeners = [];
for (let i = 0; i < state.eventListeners.length; i++) {
const listener = state.eventListeners[i];
if (!listener.element) {
removedListeners++;
continue;
}
try {
const isDetached = !(listener.element instanceof Node) ||
!document.body.contains(listener.element) ||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
(listener.element.style && listener.element.style.display === 'none');
if (isDetached) {
try {
if (listener.element instanceof Node) {
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
}
removedListeners++;
} catch (e) {
}
} else {
validListeners.push(listener);
}
} catch (e) {
log(`Error checking listener (removing): ${e.message}`);
removedListeners++;
}
}
if (removedListeners > 0) {
state.eventListeners = validListeners;
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
}
let removedResources = 0;
const resourcesForRemoval = [];
state.resources.forEach((info, id) => {
const resource = info.resource;
try {
if (resource instanceof Element && !document.body.contains(resource)) {
resourcesForRemoval.push(id);
}
if (resource && resource.element) {
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
resourcesForRemoval.push(id);
}
}
} catch (e) {
log(`Error checking resource ${id}: ${e.message}`);
}
});
resourcesForRemoval.forEach(id => {
this.unregisterResource(id);
removedResources++;
});
if (removedResources > 0) {
log(`Removed ${removedResources} orphaned resources`);
}
if (window.TooltipManager) {
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
try {
window.TooltipManager.cleanupOrphanedTooltips();
} catch (e) {
if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (err) {
log(`Error cleaning up tooltips: ${err.message}`);
}
}
}
} else if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (e) {
log(`Error cleaning up tooltips: ${e.message}`);
}
}
}
try {
this.cleanupTooltipDOM();
} catch (e) {
log(`Error in cleanupTooltipDOM: ${e.message}`);
}
},
cleanupTooltipDOM: function() {
let removedElements = 0;
try {
const tooltipSelectors = [
'[role="tooltip"]',
'[id^="tooltip-"]',
'.tippy-box',
'[data-tippy-root]'
];
tooltipSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
try {
if (!(element instanceof Element)) return;
const isDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden') ||
element.style.display === 'none' ||
element.style.visibility === 'hidden';
if (isDetached) {
try {
element.remove();
removedElements++;
} catch (e) {
}
}
} catch (err) {
}
});
} catch (err) {
log(`Error querying for ${selector}: ${err.message}`);
}
});
} catch (e) {
log(`Error in tooltip DOM cleanup: ${e.message}`);
}
if (removedElements > 0) {
log(`Removed ${removedElements} detached tooltip elements`);
}
},
setDebugMode: function(enabled) {
state.debug = Boolean(enabled);
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
@@ -247,6 +470,17 @@ const CleanupManager = (function() {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
this.addListener(window, 'beforeunload', () => {
this.clearAll();
});
}
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
this.setupMemoryOptimization(options.memoryOptions || {});
}
log('CleanupManager initialized');
return this;
}
@@ -255,16 +489,20 @@ const CleanupManager = (function() {
return publicAPI;
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = CleanupManager;
}
window.CleanupManager = CleanupManager;
if (typeof window !== 'undefined') {
window.CleanupManager = CleanupManager;
}
document.addEventListener('DOMContentLoaded', function() {
if (!window.cleanupManagerInitialized) {
CleanupManager.initialize();
window.cleanupManagerInitialized = true;
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
CleanupManager.initialize({ debug: false });
} else {
document.addEventListener('DOMContentLoaded', () => {
CleanupManager.initialize({ debug: false });
}, { once: true });
}
});
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
console.log('CleanupManager initialized');
}

View File

@@ -47,7 +47,7 @@ const CoinManager = (function() {
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin-Cash.png'
icon: 'Bitcoin%20Cash.png'
},
{
symbol: 'PIVX',
@@ -178,19 +178,7 @@ const CoinManager = (function() {
function getCoinByAnyIdentifier(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
const coin = coinAliasesMap[normalizedId];
if (coin) return coin;
if (normalizedId.includes('bitcoin') && normalizedId.includes('cash') ||
normalizedId === 'bch') {
return symbolToInfo['bch'];
}
if (normalizedId === 'zcoin' || normalizedId.includes('firo')) {
return symbolToInfo['firo'];
}
if (normalizedId.includes('particl')) {
return symbolToInfo['part'];
}
return null;
return coinAliasesMap[normalizedId] || null;
}
return {
@@ -203,6 +191,19 @@ const CoinManager = (function() {
return coin ? coin.symbol : null;
},
getDisplayName: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl Anon';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl Blind';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin MWEB';
}
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.displayName : null;
},
@@ -222,6 +223,31 @@ const CoinManager = (function() {
const coin = getCoinByAnyIdentifier(coinIdentifier);
if (!coin) return coinIdentifier.toLowerCase();
return coin.coingeckoId;
},
getCoinIcon: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl.png';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl.png';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin.png';
}
const coin = getCoinByAnyIdentifier(identifier);
if (coin && coin.icon) {
return coin.icon;
}
const capitalizedName = identifier.toString().split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('%20');
return `${capitalizedName}.png`;
}
};
})();

View File

@@ -0,0 +1,191 @@
const CoinUtils = (function() {
function buildAliasesFromCoinManager() {
const aliases = {};
const symbolMap = {};
if (window.CoinManager) {
const coins = window.CoinManager.getAllCoins();
coins.forEach(coin => {
const canonical = coin.name.toLowerCase();
aliases[canonical] = coin.aliases || [coin.name.toLowerCase()];
symbolMap[canonical] = coin.symbol;
});
}
return { aliases, symbolMap };
}
let COIN_ALIASES = {};
let CANONICAL_TO_SYMBOL = {};
function initializeAliases() {
const { aliases, symbolMap } = buildAliasesFromCoinManager();
COIN_ALIASES = aliases;
CANONICAL_TO_SYMBOL = symbolMap;
}
if (window.CoinManager) {
initializeAliases();
} else {
document.addEventListener('DOMContentLoaded', () => {
if (window.CoinManager) {
initializeAliases();
}
});
}
function getCanonicalName(coin) {
if (!coin) return null;
const lower = coin.toString().toLowerCase().trim();
for (const [canonical, aliases] of Object.entries(COIN_ALIASES)) {
if (aliases.includes(lower)) {
return canonical;
}
}
return lower;
}
return {
normalizeCoinName: function(coin, priceData = null) {
const canonical = getCanonicalName(coin);
if (!canonical) return null;
if (priceData) {
if (canonical === 'bitcoin-cash') {
if (priceData['bitcoin-cash']) return 'bitcoin-cash';
if (priceData['bch']) return 'bch';
if (priceData['bitcoincash']) return 'bitcoincash';
return 'bitcoin-cash';
}
if (canonical === 'particl') {
if (priceData['part']) return 'part';
if (priceData['particl']) return 'particl';
return 'part';
}
}
return canonical;
},
isSameCoin: function(coin1, coin2) {
if (!coin1 || !coin2) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(coin1, coin2);
}
const canonical1 = getCanonicalName(coin1);
const canonical2 = getCanonicalName(coin2);
if (canonical1 === canonical2) return true;
const lower1 = coin1.toString().toLowerCase().trim();
const lower2 = coin2.toString().toLowerCase().trim();
const particlVariants = ['particl', 'particl anon', 'particl blind', 'part', 'part_anon', 'part_blind'];
if (particlVariants.includes(lower1) && particlVariants.includes(lower2)) {
return true;
}
if (lower1.includes(' ') || lower2.includes(' ')) {
const word1 = lower1.split(' ')[0];
const word2 = lower2.split(' ')[0];
if (word1 === word2 && word1.length > 4) {
return true;
}
}
return false;
},
getCoinSymbol: function(identifier) {
if (!identifier) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
if (coin) return coin.symbol;
}
const canonical = getCanonicalName(identifier);
if (canonical && CANONICAL_TO_SYMBOL[canonical]) {
return CANONICAL_TO_SYMBOL[canonical];
}
return identifier.toString().toUpperCase();
},
getDisplayName: function(identifier) {
if (!identifier) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
if (coin) return coin.displayName || coin.name;
}
const symbol = this.getCoinSymbol(identifier);
return symbol || identifier;
},
getCoinImage: function(coinName) {
if (!coinName) return null;
const canonical = getCanonicalName(coinName);
const symbol = this.getCoinSymbol(canonical);
if (!symbol) return null;
const imagePath = `/static/images/coins/${symbol.toLowerCase()}.png`;
return imagePath;
},
getPriceKey: function(coin, priceData = null) {
return this.normalizeCoinName(coin, priceData);
},
getCoingeckoId: function(coinName) {
if (!coinName) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
if (coin && coin.coingeckoId) {
return coin.coingeckoId;
}
}
const canonical = getCanonicalName(coinName);
return canonical;
},
formatCoinAmount: function(amount, decimals = 8) {
if (amount === null || amount === undefined) return '0';
const numAmount = parseFloat(amount);
if (isNaN(numAmount)) return '0';
return numAmount.toFixed(decimals).replace(/\.?0+$/, '');
},
getAllAliases: function(coin) {
const canonical = getCanonicalName(coin);
return COIN_ALIASES[canonical] || [canonical];
},
isValidCoin: function(coin) {
if (!coin) return false;
const canonical = getCanonicalName(coin);
return canonical !== null && COIN_ALIASES.hasOwnProperty(canonical);
},
refreshAliases: function() {
initializeAliases();
return Object.keys(COIN_ALIASES).length;
}
};
})();
if (typeof window !== 'undefined') {
window.CoinUtils = CoinUtils;
}
console.log('CoinUtils module loaded');

View File

@@ -35,38 +35,22 @@ const ConfigManager = (function() {
},
itemsPerPage: 50,
apiEndpoints: {
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
coinGecko: 'https://api.coingecko.com/api/v3',
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
},
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
}
},
retryDelays: [5000, 15000, 30000],
get coins() {
return window.CoinManager ? window.CoinManager.getAllCoins() : [
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
];
if (window.CoinManager) {
return window.CoinManager.getAllCoins();
}
console.warn('[ConfigManager] CoinManager not available, returning empty array');
return [];
},
chartConfig: {
colors: {
@@ -108,12 +92,10 @@ const ConfigManager = (function() {
if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys();
return {
cryptoCompare: apiKeys.cryptoCompare || '',
coinGecko: apiKeys.coinGecko || ''
};
}
return {
cryptoCompare: '',
coinGecko: ''
};
},
@@ -122,40 +104,20 @@ const ConfigManager = (function() {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'firo',
'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash'
};
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName;
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coinName);
}
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
},
coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
if (window.CoinUtils) {
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
@@ -214,7 +176,7 @@ const ConfigManager = (function() {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {

View File

@@ -0,0 +1,207 @@
(function() {
'use strict';
const originalGetElementById = document.getElementById.bind(document);
const DOMCache = {
cache: {},
get: function(id, forceRefresh = false) {
if (!id) {
console.warn('DOMCache: No ID provided');
return null;
}
if (!forceRefresh && this.cache[id]) {
if (document.body.contains(this.cache[id])) {
return this.cache[id];
} else {
delete this.cache[id];
}
}
const element = originalGetElementById(id);
if (element) {
this.cache[id] = element;
}
return element;
},
getMultiple: function(ids) {
const elements = {};
ids.forEach(id => {
elements[id] = this.get(id);
});
return elements;
},
setValue: function(id, value) {
const element = this.get(id);
if (element) {
element.value = value;
return true;
}
console.warn(`DOMCache: Element not found: ${id}`);
return false;
},
getValue: function(id, defaultValue = '') {
const element = this.get(id);
return element ? element.value : defaultValue;
},
setText: function(id, text) {
const element = this.get(id);
if (element) {
element.textContent = text;
return true;
}
console.warn(`DOMCache: Element not found: ${id}`);
return false;
},
getText: function(id, defaultValue = '') {
const element = this.get(id);
return element ? element.textContent : defaultValue;
},
addClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.add(className);
return true;
}
return false;
},
removeClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.remove(className);
return true;
}
return false;
},
toggleClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.toggle(className);
return true;
}
return false;
},
show: function(id) {
const element = this.get(id);
if (element) {
element.style.display = '';
return true;
}
return false;
},
hide: function(id) {
const element = this.get(id);
if (element) {
element.style.display = 'none';
return true;
}
return false;
},
exists: function(id) {
return this.get(id) !== null;
},
clear: function(id) {
if (id) {
delete this.cache[id];
} else {
this.cache = {};
}
},
size: function() {
return Object.keys(this.cache).length;
},
validate: function() {
const ids = Object.keys(this.cache);
let removed = 0;
ids.forEach(id => {
const element = this.cache[id];
if (!document.body.contains(element)) {
delete this.cache[id];
removed++;
}
});
return removed;
},
createScope: function(elementIds) {
const scope = {};
elementIds.forEach(id => {
Object.defineProperty(scope, id, {
get: () => this.get(id),
enumerable: true
});
});
return scope;
},
batch: function(operations) {
Object.keys(operations).forEach(id => {
const ops = operations[id];
const element = this.get(id);
if (!element) {
console.warn(`DOMCache: Element not found in batch operation: ${id}`);
return;
}
if (ops.value !== undefined) element.value = ops.value;
if (ops.text !== undefined) element.textContent = ops.text;
if (ops.html !== undefined) element.innerHTML = ops.html;
if (ops.class) element.classList.add(ops.class);
if (ops.removeClass) element.classList.remove(ops.removeClass);
if (ops.hide) element.style.display = 'none';
if (ops.show) element.style.display = '';
if (ops.disabled !== undefined) element.disabled = ops.disabled;
});
}
};
window.DOMCache = DOMCache;
if (!window.$) {
window.$ = function(id) {
return DOMCache.get(id);
};
}
document.getElementById = function(id) {
return DOMCache.get(id);
};
document.getElementByIdOriginal = originalGetElementById;
if (window.CleanupManager) {
const validationInterval = CleanupManager.setInterval(() => {
DOMCache.validate();
}, 30000);
CleanupManager.registerResource('domCacheValidation', validationInterval, () => {
clearInterval(validationInterval);
});
}
})();

View File

@@ -0,0 +1,215 @@
const ErrorHandler = (function() {
const config = {
logErrors: true,
throwErrors: false,
errorCallbacks: []
};
function formatError(error, context) {
const timestamp = new Date().toISOString();
const contextStr = context ? ` [${context}]` : '';
if (error instanceof Error) {
return `${timestamp}${contextStr} ${error.name}: ${error.message}`;
}
return `${timestamp}${contextStr} ${String(error)}`;
}
function notifyCallbacks(error, context) {
config.errorCallbacks.forEach(callback => {
try {
callback(error, context);
} catch (e) {
console.error('[ErrorHandler] Error in callback:', e);
}
});
}
return {
configure: function(options = {}) {
Object.assign(config, options);
return this;
},
addCallback: function(callback) {
if (typeof callback === 'function') {
config.errorCallbacks.push(callback);
}
return this;
},
removeCallback: function(callback) {
const index = config.errorCallbacks.indexOf(callback);
if (index > -1) {
config.errorCallbacks.splice(index, 1);
}
return this;
},
safeExecute: function(fn, context = null, fallbackValue = null) {
try {
return fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
},
safeExecuteAsync: async function(fn, context = null, fallbackValue = null) {
try {
return await fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
},
wrap: function(fn, context = null, fallbackValue = null) {
return (...args) => {
try {
return fn(...args);
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
};
},
wrapAsync: function(fn, context = null, fallbackValue = null) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
};
},
handleError: function(error, context = null, fallbackValue = null) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
},
try: function(fn, catchFn = null, finallyFn = null) {
try {
return fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, 'ErrorHandler.try'));
}
notifyCallbacks(error, 'ErrorHandler.try');
if (catchFn) {
return catchFn(error);
}
if (config.throwErrors) {
throw error;
}
return null;
} finally {
if (finallyFn) {
finallyFn();
}
}
},
tryAsync: async function(fn, catchFn = null, finallyFn = null) {
try {
return await fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, 'ErrorHandler.tryAsync'));
}
notifyCallbacks(error, 'ErrorHandler.tryAsync');
if (catchFn) {
return await catchFn(error);
}
if (config.throwErrors) {
throw error;
}
return null;
} finally {
if (finallyFn) {
await finallyFn();
}
}
},
createBoundary: function(context) {
return {
execute: (fn, fallbackValue = null) => {
return ErrorHandler.safeExecute(fn, context, fallbackValue);
},
executeAsync: (fn, fallbackValue = null) => {
return ErrorHandler.safeExecuteAsync(fn, context, fallbackValue);
},
wrap: (fn, fallbackValue = null) => {
return ErrorHandler.wrap(fn, context, fallbackValue);
},
wrapAsync: (fn, fallbackValue = null) => {
return ErrorHandler.wrapAsync(fn, context, fallbackValue);
}
};
}
};
})();
if (typeof window !== 'undefined') {
window.ErrorHandler = ErrorHandler;
}
console.log('ErrorHandler module loaded');

View File

@@ -0,0 +1,342 @@
(function() {
'use strict';
const EventHandlers = {
confirmPopup: function(action = 'proceed', coinName = '') {
const message = action === 'Accept'
? 'Are you sure you want to accept this bid?'
: coinName
? `Are you sure you want to ${action} ${coinName}?`
: 'Are you sure you want to proceed?';
return confirm(message);
},
confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
},
confirmWithdrawal: function() {
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
return window.WalletPage.confirmWithdrawal();
}
return confirm('Are you sure you want to withdraw? Please verify the address and amount.');
},
confirmUTXOResize: function() {
return confirm('Are you sure you want to create a UTXO? This will split your balance.');
},
confirmRemoveExpired: function() {
return confirm('Are you sure you want to remove all expired offers and bids?');
},
fillDonationAddress: function(address, coinType) {
let addressInput = null;
addressInput = window.DOMCache
? window.DOMCache.get('address_to')
: document.getElementById('address_to');
if (!addressInput) {
addressInput = document.querySelector('input[name^="to_"]');
}
if (!addressInput) {
addressInput = document.querySelector('input[placeholder*="Address"]');
}
if (addressInput) {
addressInput.value = address;
console.log(`Filled donation address for ${coinType}: ${address}`);
} else {
console.error('EventHandlers: Address input not found');
}
},
setAmmAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: AMM amount input not found:', inputId);
return;
}
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
if (balance > 0) {
const calculatedAmount = balance * percent;
amountInput.value = calculatedAmount.toFixed(8);
} else {
console.warn('EventHandlers: No balance found for AMM amount calculation');
}
},
setOfferAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: Offer amount input not found:', inputId);
return;
}
const coinFromSelect = document.getElementById('coin_from');
if (!coinFromSelect) {
console.error('EventHandlers: coin_from select not found');
return;
}
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption || selectedOption.value === '-1') {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Please select a coin first');
} else {
alert('Please select a coin first');
}
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('EventHandlers: Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
} else {
alert('Invalid balance for selected coin');
}
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
},
resetForm: function() {
const form = document.querySelector('form[name="offer_form"]') || document.querySelector('form');
if (form) {
form.reset();
}
},
hideConfirmModal: function() {
if (window.DOMCache) {
window.DOMCache.hide('confirmModal');
} else {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.style.display = 'none';
}
}
},
lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates();
} else {
console.error('EventHandlers: lookup_rates function not found');
}
},
checkForUpdatesNow: function() {
if (window.checkForUpdatesNow && typeof window.checkForUpdatesNow === 'function') {
window.checkForUpdatesNow();
} else {
console.error('EventHandlers: checkForUpdatesNow function not found');
}
},
testUpdateNotification: function() {
if (window.testUpdateNotification && typeof window.testUpdateNotification === 'function') {
window.testUpdateNotification();
} else {
console.error('EventHandlers: testUpdateNotification function not found');
}
},
toggleNotificationDropdown: function(event) {
if (window.toggleNotificationDropdown && typeof window.toggleNotificationDropdown === 'function') {
window.toggleNotificationDropdown(event);
} else {
console.error('EventHandlers: toggleNotificationDropdown function not found');
}
},
closeMessage: function(messageId) {
if (window.DOMCache) {
window.DOMCache.hide(messageId);
} else {
const messageElement = document.getElementById(messageId);
if (messageElement) {
messageElement.style.display = 'none';
}
}
},
initialize: function() {
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm]');
if (target) {
const action = target.getAttribute('data-confirm-action') || 'proceed';
const coinName = target.getAttribute('data-confirm-coin') || '';
if (!this.confirmPopup(action, coinName)) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-reseed]');
if (target) {
if (!this.confirmReseed()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]');
if (target) {
if (!this.confirmUTXOResize()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-remove-expired]');
if (target) {
if (!this.confirmRemoveExpired()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-fill-donation]');
if (target) {
e.preventDefault();
const address = target.getAttribute('data-address');
const coinType = target.getAttribute('data-coin-type');
this.fillDonationAddress(address, coinType);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-amm-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-amm-amount'));
const inputId = target.getAttribute('data-input-id');
this.setAmmAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-offer-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-offer-amount'));
const inputId = target.getAttribute('data-input-id');
this.setOfferAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]');
if (target) {
e.preventDefault();
this.resetForm();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-hide-modal]');
if (target) {
e.preventDefault();
this.hideConfirmModal();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-lookup-rates]');
if (target) {
e.preventDefault();
this.lookup_rates();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-check-updates]');
if (target) {
e.preventDefault();
this.checkForUpdatesNow();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-test-notification]');
if (target) {
e.preventDefault();
const type = target.getAttribute('data-test-notification');
if (type === 'update') {
this.testUpdateNotification();
} else {
window.NotificationManager && window.NotificationManager.testToasts();
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-close-message]');
if (target) {
e.preventDefault();
const messageId = target.getAttribute('data-close-message');
this.closeMessage(messageId);
}
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
EventHandlers.initialize();
});
} else {
EventHandlers.initialize();
}
window.EventHandlers = EventHandlers;
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
})();

View File

@@ -0,0 +1,225 @@
(function() {
'use strict';
const FormValidator = {
checkPasswordStrength: function(password) {
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password)
};
let score = 0;
if (requirements.length) score += 25;
if (requirements.uppercase) score += 25;
if (requirements.lowercase) score += 25;
if (requirements.number) score += 25;
return {
score: score,
requirements: requirements,
isStrong: score >= 60
};
},
updatePasswordStrengthUI: function(password, elements) {
const result = this.checkPasswordStrength(password);
const { score, requirements } = result;
if (!elements.bar || !elements.text) {
console.warn('FormValidator: Missing strength UI elements');
return result.isStrong;
}
elements.bar.style.width = `${score}%`;
if (score === 0) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
elements.text.textContent = 'Enter password';
elements.text.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
} else if (score < 40) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
elements.text.textContent = 'Weak';
elements.text.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (score < 70) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
elements.text.textContent = 'Fair';
elements.text.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
} else if (score < 90) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
elements.text.textContent = 'Good';
elements.text.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
elements.text.textContent = 'Strong';
elements.text.className = 'text-sm font-medium text-green-600 dark:text-green-400';
}
if (elements.requirements) {
this.updateRequirement(elements.requirements.length, requirements.length);
this.updateRequirement(elements.requirements.uppercase, requirements.uppercase);
this.updateRequirement(elements.requirements.lowercase, requirements.lowercase);
this.updateRequirement(elements.requirements.number, requirements.number);
}
return result.isStrong;
},
updateRequirement: function(element, met) {
if (!element) return;
if (met) {
element.className = 'flex items-center text-green-600 dark:text-green-400';
} else {
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
}
},
checkPasswordMatch: function(password1, password2, elements) {
if (!elements) {
return password1 === password2;
}
const { container, success, error } = elements;
if (password2.length === 0) {
if (container) container.classList.add('hidden');
return false;
}
if (container) container.classList.remove('hidden');
if (password1 === password2) {
if (success) success.classList.remove('hidden');
if (error) error.classList.add('hidden');
return true;
} else {
if (success) success.classList.add('hidden');
if (error) error.classList.remove('hidden');
return false;
}
},
validateEmail: function(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
},
validateRequired: function(value) {
return value && value.trim().length > 0;
},
validateMinLength: function(value, minLength) {
return value && value.length >= minLength;
},
validateMaxLength: function(value, maxLength) {
return value && value.length <= maxLength;
},
validateNumeric: function(value) {
return !isNaN(value) && !isNaN(parseFloat(value));
},
validateRange: function(value, min, max) {
const num = parseFloat(value);
return !isNaN(num) && num >= min && num <= max;
},
showError: function(element, message) {
if (!element) return;
element.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
element.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
let errorElement = element.parentElement.querySelector('.validation-error');
if (!errorElement) {
errorElement = document.createElement('p');
errorElement.className = 'validation-error text-red-600 dark:text-red-400 text-sm mt-1';
element.parentElement.appendChild(errorElement);
}
errorElement.textContent = message;
errorElement.classList.remove('hidden');
},
clearError: function(element) {
if (!element) return;
element.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
element.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
const errorElement = element.parentElement.querySelector('.validation-error');
if (errorElement) {
errorElement.classList.add('hidden');
}
},
validateForm: function(form, rules) {
if (!form || !rules) return false;
let isValid = true;
Object.keys(rules).forEach(fieldName => {
const field = form.querySelector(`[name="${fieldName}"]`);
if (!field) return;
const fieldRules = rules[fieldName];
let fieldValid = true;
let errorMessage = '';
if (fieldRules.required && !this.validateRequired(field.value)) {
fieldValid = false;
errorMessage = fieldRules.requiredMessage || 'This field is required';
}
if (fieldValid && fieldRules.minLength && !this.validateMinLength(field.value, fieldRules.minLength)) {
fieldValid = false;
errorMessage = fieldRules.minLengthMessage || `Minimum ${fieldRules.minLength} characters required`;
}
if (fieldValid && fieldRules.maxLength && !this.validateMaxLength(field.value, fieldRules.maxLength)) {
fieldValid = false;
errorMessage = fieldRules.maxLengthMessage || `Maximum ${fieldRules.maxLength} characters allowed`;
}
if (fieldValid && fieldRules.email && !this.validateEmail(field.value)) {
fieldValid = false;
errorMessage = fieldRules.emailMessage || 'Invalid email format';
}
if (fieldValid && fieldRules.numeric && !this.validateNumeric(field.value)) {
fieldValid = false;
errorMessage = fieldRules.numericMessage || 'Must be a number';
}
if (fieldValid && fieldRules.range && !this.validateRange(field.value, fieldRules.range.min, fieldRules.range.max)) {
fieldValid = false;
errorMessage = fieldRules.rangeMessage || `Must be between ${fieldRules.range.min} and ${fieldRules.range.max}`;
}
if (fieldValid && fieldRules.custom) {
const customResult = fieldRules.custom(field.value, form);
if (!customResult.valid) {
fieldValid = false;
errorMessage = customResult.message || 'Invalid value';
}
}
if (fieldValid) {
this.clearError(field);
} else {
this.showError(field, errorMessage);
isValid = false;
}
});
return isValid;
}
};
window.FormValidator = FormValidator;
})();

View File

@@ -23,10 +23,24 @@ const IdentityManager = (function() {
return null;
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
log(`Cache hit for ${address}`);
return cachedData;
const cached = state.cache.get(address);
const now = Date.now();
if (cached && (now - cached.timestamp) < state.config.cacheTimeout) {
log(`Cache hit (fresh) for ${address}`);
return cached.data;
}
if (cached && (now - cached.timestamp) < state.config.cacheTimeout * 2) {
log(`Cache hit (stale) for ${address}, refreshing in background`);
const staleData = cached.data;
if (!state.pendingRequests.has(address)) {
this.refreshIdentityInBackground(address);
}
return staleData;
}
if (state.pendingRequests.has(address)) {
@@ -47,6 +61,20 @@ const IdentityManager = (function() {
}
},
refreshIdentityInBackground: function(address) {
const request = fetchWithRetry(address);
state.pendingRequests.set(address, request);
request.then(data => {
this.setCachedIdentity(address, data);
log(`Background refresh completed for ${address}`);
}).catch(error => {
log(`Background refresh failed for ${address}:`, error);
}).finally(() => {
state.pendingRequests.delete(address);
});
},
getCachedIdentity: function(address) {
const cached = state.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
@@ -155,6 +183,11 @@ const IdentityManager = (function() {
async function fetchWithRetry(address, attempt = 1) {
try {
let data;
if (window.ApiManager) {
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
} else {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
@@ -163,7 +196,10 @@ const IdentityManager = (function() {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
data = await response.json();
}
return data;
} catch (error) {
if (attempt >= state.config.maxRetries) {
console.error(`[IdentityManager] Error:`, error.message);
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
return null;
}
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
const delay = state.config.retryDelay * attempt;
await new Promise(resolve => {
CleanupManager.setTimeout(resolve, delay);
});
return fetchWithRetry(address, attempt + 1);
}
}
@@ -188,5 +227,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
console.log('IdentityManager initialized');

View File

@@ -1,15 +1,47 @@
const MemoryManager = (function() {
const state = {
isMonitoringEnabled: false,
monitorInterval: null,
cleanupInterval: null
const config = {
tooltipCleanupInterval: 300000,
diagnosticsInterval: 600000,
elementVerificationInterval: 300000,
maxTooltipsThreshold: 100,
maxTooltips: 300,
cleanupThreshold: 1.5,
minTimeBetweenCleanups: 180000,
memoryGrowthThresholdMB: 100,
debug: false,
protectedWebSockets: ['wsPort', 'ws_port'],
interactiveSelectors: [
'tr:hover',
'[data-tippy-root]:hover',
'.tooltip:hover',
'[data-tooltip-trigger-id]:hover',
'[data-tooltip-target]:hover'
],
protectedContainers: [
'#sent-tbody',
'#received-tbody',
'#offers-body'
]
};
const config = {
monitorInterval: 30000,
cleanupInterval: 60000,
debug: false
const state = {
pendingAnimationFrames: new Set(),
pendingTimeouts: new Set(),
cleanupInterval: null,
diagnosticsInterval: null,
elementVerificationInterval: null,
mutationObserver: null,
lastCleanupTime: Date.now(),
startTime: Date.now(),
isCleanupRunning: false,
metrics: {
tooltipsRemoved: 0,
cleanupRuns: 0,
lastMemoryUsage: null,
lastCleanupDetails: {},
history: []
},
originalTooltipFunctions: {}
};
function log(message, ...args) {
@@ -18,202 +50,533 @@ const MemoryManager = (function() {
}
}
const publicAPI = {
enableMonitoring: function(interval = config.monitorInterval) {
if (state.monitorInterval) {
clearInterval(state.monitorInterval);
function preserveTooltipFunctions() {
if (window.TooltipManager && !state.originalTooltipFunctions.destroy) {
state.originalTooltipFunctions = {
destroy: window.TooltipManager.destroy,
cleanup: window.TooltipManager.cleanup,
create: window.TooltipManager.create
};
}
}
state.isMonitoringEnabled = true;
config.monitorInterval = interval;
function isInProtectedContainer(element) {
if (!element) return false;
this.logMemoryUsage();
state.monitorInterval = setInterval(() => {
this.logMemoryUsage();
}, interval);
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
for (const selector of config.protectedContainers) {
if (element.closest && element.closest(selector)) {
return true;
},
disableMonitoring: function() {
if (state.monitorInterval) {
clearInterval(state.monitorInterval);
state.monitorInterval = null;
}
}
state.isMonitoringEnabled = false;
console.log('Memory monitoring disabled');
return true;
},
return false;
}
logMemoryUsage: function() {
const timestamp = new Date().toLocaleTimeString();
console.log(`=== Memory Monitor [${timestamp}] ===`);
function shouldSkipCleanup() {
if (state.isCleanupRunning) return true;
const selector = config.interactiveSelectors.join(', ');
const hoveredElements = document.querySelectorAll(selector);
return hoveredElements.length > 0;
}
function performCleanup(force = false) {
if (shouldSkipCleanup() && !force) {
return false;
}
if (state.isCleanupRunning) {
return false;
}
const now = Date.now();
if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) {
return false;
}
try {
state.isCleanupRunning = true;
state.lastCleanupTime = now;
state.metrics.cleanupRuns++;
const startTime = performance.now();
const startMemory = checkMemoryUsage();
state.pendingAnimationFrames.forEach(id => {
cancelAnimationFrame(id);
});
state.pendingAnimationFrames.clear();
state.pendingTimeouts.forEach(id => {
clearTimeout(id);
});
state.pendingTimeouts.clear();
const tooltipsResult = removeOrphanedTooltips();
state.metrics.tooltipsRemoved += tooltipsResult;
const disconnectedResult = checkForDisconnectedElements();
tryRunGarbageCollection(false);
const endTime = performance.now();
const endMemory = checkMemoryUsage();
const runStats = {
timestamp: new Date().toISOString(),
duration: endTime - startTime,
tooltipsRemoved: tooltipsResult,
disconnectedRemoved: disconnectedResult,
memoryBefore: startMemory ? startMemory.usedMB : null,
memoryAfter: endMemory ? endMemory.usedMB : null,
memorySaved: startMemory && endMemory ?
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
};
state.metrics.history.unshift(runStats);
if (state.metrics.history.length > 10) {
state.metrics.history.pop();
}
state.metrics.lastCleanupDetails = runStats;
if (config.debug) {
log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`);
}
return true;
} catch (error) {
console.error("Error during cleanup:", error);
return false;
} finally {
state.isCleanupRunning = false;
}
}
function removeOrphanedTooltips() {
try {
const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)');
let removed = 0;
tippyRoots.forEach(root => {
const tooltipId = root.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
if (!trigger || !document.body.contains(trigger)) {
if (root.parentNode) {
root.parentNode.removeChild(root);
removed++;
}
}
});
return removed;
} catch (error) {
console.error("Error removing orphaned tooltips:", error);
return 0;
}
}
function checkForDisconnectedElements() {
try {
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)');
const disconnectedElements = new Set();
tooltipTriggers.forEach(el => {
if (!document.body.contains(el)) {
const tooltipId = el.getAttribute('data-tooltip-trigger-id');
disconnectedElements.add(tooltipId);
}
});
const tooltipRoots = document.querySelectorAll('[data-for-tooltip-id]');
let removed = 0;
disconnectedElements.forEach(id => {
for (const root of tooltipRoots) {
if (root.getAttribute('data-for-tooltip-id') === id && root.parentNode) {
root.parentNode.removeChild(root);
removed++;
break;
}
}
});
return disconnectedElements.size;
} catch (error) {
console.error("Error checking for disconnected elements:", error);
return 0;
}
}
function tryRunGarbageCollection(aggressive = false) {
setTimeout(() => {
const cache = {};
for (let i = 0; i < 100; i++) {
cache[`key${i}`] = {};
}
for (const key in cache) {
delete cache[key];
}
}, 100);
return true;
}
function checkMemoryUsage() {
const result = {
usedJSHeapSize: 0,
totalJSHeapSize: 0,
jsHeapSizeLimit: 0,
percentUsed: "0",
usedMB: "0",
totalMB: "0",
limitMB: "0"
};
if (window.performance && window.performance.memory) {
console.log('Memory usage:', {
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
result.usedJSHeapSize = window.performance.memory.usedJSHeapSize;
result.totalJSHeapSize = window.performance.memory.totalJSHeapSize;
result.jsHeapSizeLimit = window.performance.memory.jsHeapSizeLimit;
result.percentUsed = (result.usedJSHeapSize / result.jsHeapSizeLimit * 100).toFixed(2);
result.usedMB = (result.usedJSHeapSize / (1024 * 1024)).toFixed(2);
result.totalMB = (result.totalJSHeapSize / (1024 * 1024)).toFixed(2);
result.limitMB = (result.jsHeapSizeLimit / (1024 * 1024)).toFixed(2);
} else {
result.usedMB = "Unknown";
result.totalMB = "Unknown";
result.limitMB = "Unknown";
result.percentUsed = "Unknown";
}
state.metrics.lastMemoryUsage = result;
return result;
}
function handleVisibilityChange() {
if (document.hidden) {
removeOrphanedTooltips();
checkForDisconnectedElements();
}
}
function setupMutationObserver() {
if (state.mutationObserver) {
state.mutationObserver.disconnect();
state.mutationObserver = null;
}
let processingScheduled = false;
let lastProcessTime = 0;
const MIN_PROCESS_INTERVAL = 10000;
const processMutations = (mutations) => {
const now = Date.now();
if (now - lastProcessTime < MIN_PROCESS_INTERVAL || processingScheduled) {
return;
}
processingScheduled = true;
setTimeout(() => {
processingScheduled = false;
lastProcessTime = Date.now();
if (state.isCleanupRunning) {
return;
}
const tooltipSelectors = ['[data-tippy-root]', '[data-tooltip-trigger-id]', '.tooltip'];
let tooltipCount = 0;
tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length;
if (tooltipCount > config.maxTooltipsThreshold &&
(Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) {
removeOrphanedTooltips();
checkForDisconnectedElements();
state.lastCleanupTime = Date.now();
}
}, 5000);
};
state.mutationObserver = new MutationObserver(processMutations);
state.mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
return state.mutationObserver;
}
if (navigator.deviceMemory) {
console.log('Device memory:', navigator.deviceMemory, 'GB');
function enhanceTooltipManager() {
if (!window.TooltipManager || window.TooltipManager._memoryManagerEnhanced) return false;
preserveTooltipFunctions();
const originalDestroy = window.TooltipManager.destroy;
const originalCleanup = window.TooltipManager.cleanup;
window.TooltipManager.destroy = function(element) {
if (!element) return;
try {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (isInProtectedContainer(element)) {
if (originalDestroy) {
return originalDestroy.call(window.TooltipManager, element);
}
return;
}
const nodeCount = document.querySelectorAll('*').length;
console.log('DOM node count:', nodeCount);
if (window.CleanupManager) {
const counts = CleanupManager.getResourceCounts();
console.log('Managed resources:', counts);
if (tooltipId) {
if (originalDestroy) {
originalDestroy.call(window.TooltipManager, element);
}
if (window.TooltipManager) {
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
if (tooltipRoot && tooltipRoot.parentNode) {
tooltipRoot.parentNode.removeChild(tooltipRoot);
}
if (window.CacheManager && window.CacheManager.getStats) {
const cacheStats = CacheManager.getStats();
console.log('Cache stats:', cacheStats);
element.removeAttribute('data-tooltip-trigger-id');
element.removeAttribute('aria-describedby');
if (element._tippy) {
try {
element._tippy.destroy();
element._tippy = null;
} catch (e) {}
}
}
} catch (error) {
console.error('Error in enhanced tooltip destroy:', error);
if (originalDestroy) {
originalDestroy.call(window.TooltipManager, element);
}
}
};
window.TooltipManager.cleanup = function() {
try {
if (originalCleanup) {
originalCleanup.call(window.TooltipManager);
}
if (window.IdentityManager && window.IdentityManager.getStats) {
const identityStats = window.IdentityManager.getStats();
console.log('Identity cache stats:', identityStats);
removeOrphanedTooltips();
} catch (error) {
console.error('Error in enhanced tooltip cleanup:', error);
if (originalCleanup) {
originalCleanup.call(window.TooltipManager);
}
console.log('==============================');
},
enableAutoCleanup: function(interval = config.cleanupInterval) {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
}
};
config.cleanupInterval = interval;
window.TooltipManager._memoryManagerEnhanced = true;
window.TooltipManager._originalDestroy = originalDestroy;
window.TooltipManager._originalCleanup = originalCleanup;
this.forceCleanup();
state.cleanupInterval = setInterval(() => {
this.forceCleanup();
}, interval);
log('Auto-cleanup enabled every', interval/1000, 'seconds');
return true;
},
}
disableAutoCleanup: function() {
function initializeScheduledCleanups() {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
state.cleanupInterval = null;
}
console.log('Memory auto-cleanup disabled');
if (state.diagnosticsInterval) {
clearInterval(state.diagnosticsInterval);
state.diagnosticsInterval = null;
}
if (state.elementVerificationInterval) {
clearInterval(state.elementVerificationInterval);
state.elementVerificationInterval = null;
}
state.cleanupInterval = setInterval(() => {
removeOrphanedTooltips();
checkForDisconnectedElements();
}, config.tooltipCleanupInterval);
state.diagnosticsInterval = setInterval(() => {
checkMemoryUsage();
}, config.diagnosticsInterval);
state.elementVerificationInterval = setInterval(() => {
checkForDisconnectedElements();
}, config.elementVerificationInterval);
document.removeEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('visibilitychange', handleVisibilityChange);
setupMutationObserver();
return true;
},
forceCleanup: function() {
if (config.debug) {
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
}
if (window.CacheManager && CacheManager.cleanup) {
CacheManager.cleanup(true);
function initialize(options = {}) {
preserveTooltipFunctions();
if (options) {
Object.assign(config, options);
}
if (window.TooltipManager && TooltipManager.cleanup) {
window.TooltipManager.cleanup();
enhanceTooltipManager();
if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) {
window.WebSocketManager.cleanupOrphanedSockets = function() {
return 0;
};
}
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
if (window.TooltipManager && TooltipManager.destroy) {
window.TooltipManager.destroy(element);
const manager = window.ApiManager || window.Api;
if (manager && !manager.abortPendingRequests) {
manager.abortPendingRequests = function() {
return 0;
};
}
initializeScheduledCleanups();
setTimeout(() => {
removeOrphanedTooltips();
checkForDisconnectedElements();
}, 5000);
return this;
}
function dispose() {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
state.cleanupInterval = null;
}
if (state.diagnosticsInterval) {
clearInterval(state.diagnosticsInterval);
state.diagnosticsInterval = null;
}
if (state.elementVerificationInterval) {
clearInterval(state.elementVerificationInterval);
state.elementVerificationInterval = null;
}
if (state.mutationObserver) {
state.mutationObserver.disconnect();
state.mutationObserver = null;
}
document.removeEventListener('visibilitychange', handleVisibilityChange);
return true;
}
function displayStats() {
const stats = getDetailedStats();
console.group('Memory Manager Stats');
console.log('Memory Usage:', stats.memory ?
`${stats.memory.usedMB}MB / ${stats.memory.limitMB}MB (${stats.memory.percentUsed}%)` :
'Not available');
console.log('Total Cleanups:', stats.metrics.cleanupRuns);
console.log('Total Tooltips Removed:', stats.metrics.tooltipsRemoved);
console.log('Current Tooltips:', stats.tooltips.total);
console.log('Last Cleanup:', stats.metrics.lastCleanupDetails);
console.log('Cleanup History:', stats.metrics.history);
console.groupEnd();
return stats;
}
function getDetailedStats() {
const allTooltipElements = document.querySelectorAll('[data-tippy-root], [data-tooltip-trigger-id], .tooltip');
const tooltips = {
roots: document.querySelectorAll('[data-tippy-root]').length,
triggers: document.querySelectorAll('[data-tooltip-trigger-id]').length,
tooltipElements: document.querySelectorAll('.tooltip').length,
total: allTooltipElements.length,
protectedContainers: {}
};
config.protectedContainers.forEach(selector => {
const container = document.querySelector(selector);
if (container) {
tooltips.protectedContainers[selector] = {
tooltips: container.querySelectorAll('.tooltip').length,
triggers: container.querySelectorAll('[data-tooltip-trigger-id]').length,
roots: document.querySelectorAll(`[data-tippy-root][data-for-tooltip-id]`).length
};
}
});
if (window.chartModule && chartModule.cleanup) {
chartModule.cleanup();
return {
memory: checkMemoryUsage(),
metrics: { ...state.metrics },
tooltips,
config: { ...config }
};
}
if (window.gc) {
window.gc();
} else {
const arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
arr[i] = new Array(10000).join('x');
}
}
if (config.debug) {
console.log('Memory cleanup completed');
}
return true;
return {
initialize,
cleanup: performCleanup,
forceCleanup: function() {
return performCleanup(true);
},
fullCleanup: function() {
return performCleanup(true);
},
getStats: getDetailedStats,
displayStats,
setDebugMode: function(enabled) {
config.debug = Boolean(enabled);
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
return config.debug;
},
getStatus: function() {
return {
monitoring: {
enabled: Boolean(state.monitorInterval),
interval: config.monitorInterval
addProtectedContainer: function(selector) {
if (!config.protectedContainers.includes(selector)) {
config.protectedContainers.push(selector);
}
return config.protectedContainers;
},
autoCleanup: {
enabled: Boolean(state.cleanupInterval),
interval: config.cleanupInterval
removeProtectedContainer: function(selector) {
const index = config.protectedContainers.indexOf(selector);
if (index !== -1) {
config.protectedContainers.splice(index, 1);
}
return config.protectedContainers;
},
debug: config.debug
dispose
};
},
initialize: function(options = {}) {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (options.enableMonitoring) {
this.enableMonitoring(options.monitorInterval || config.monitorInterval);
}
if (options.enableAutoCleanup) {
this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
}
log('MemoryManager initialized');
return this;
},
dispose: function() {
this.disableMonitoring();
this.disableAutoCleanup();
log('MemoryManager disposed');
}
};
return publicAPI;
})();
window.MemoryManager = MemoryManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.memoryManagerInitialized) {
MemoryManager.initialize();
window.memoryManagerInitialized = true;
}
const isDevMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
MemoryManager.initialize({
debug: isDevMode
});
console.log('Memory Manager initialized');
});
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
console.log('MemoryManager initialized');
window.MemoryManager = MemoryManager;

View File

@@ -108,7 +108,7 @@ const NetworkManager = (function() {
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
state.reconnectTimer = setTimeout(() => {
state.reconnectTimer = CleanupManager.setTimeout(() => {
state.reconnectTimer = null;
this.attemptReconnect();
}, delay);
@@ -167,7 +167,20 @@ const NetworkManager = (function() {
});
},
testBackendConnection: function() {
testBackendConnection: async function() {
if (window.ApiManager) {
try {
await window.ApiManager.makeRequest(config.connectionTestEndpoint, 'HEAD', {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
return true;
} catch (error) {
log('Backend connection test failed:', error.message);
return false;
}
}
return fetch(config.connectionTestEndpoint, {
method: 'HEAD',
headers: {
@@ -275,6 +288,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
console.log('NetworkManager initialized');

File diff suppressed because it is too large Load Diff

View File

@@ -42,8 +42,9 @@ const PriceManager = (function() {
});
}
setTimeout(() => this.getPrices(), 1500);
CleanupManager.setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
console.log('PriceManager initialized');
return this;
},
@@ -59,7 +60,6 @@ const PriceManager = (function() {
return fetchPromise;
}
console.log('PriceManager: Fetching latest prices.');
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
@@ -89,8 +89,6 @@ const PriceManager = (function() {
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
if (!coinSymbols.length) {
throw new Error('No valid coins configured');
}
@@ -132,15 +130,15 @@ const PriceManager = (function() {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
if (coin) {
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
} else if (window.CoinUtils) {
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
normalizedCoinId = coinId.toLowerCase();
}
} else if (window.CoinUtils) {
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
if (coinId.toLowerCase() === 'zcoin') {
normalizedCoinId = 'firo';
normalizedCoinId = coinId.toLowerCase();
}
processedData[normalizedCoinId] = {
@@ -166,14 +164,14 @@ const PriceManager = (function() {
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
console.log('Using cached price data');
return cachedData.value;
}
try {
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
if (existingCache) {
console.log('Using localStorage cached price data');
return JSON.parse(existingCache).value;
}
} catch (e) {
@@ -229,5 +227,3 @@ document.addEventListener('DOMContentLoaded', function() {
window.priceManagerInitialized = true;
}
});
console.log('PriceManager initialized');

View File

@@ -0,0 +1,79 @@
(function() {
'use strict';
const QRCodeManager = {
defaultOptions: {
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
},
initialize: function() {
const qrElements = document.querySelectorAll('[data-qrcode]');
qrElements.forEach(element => {
this.generateQRCode(element);
});
},
generateQRCode: function(element) {
const address = element.getAttribute('data-address');
const width = parseInt(element.getAttribute('data-width')) || this.defaultOptions.width;
const height = parseInt(element.getAttribute('data-height')) || this.defaultOptions.height;
if (!address) {
console.error('QRCodeManager: No address provided for element', element);
return;
}
element.innerHTML = '';
try {
new QRCode(element, {
text: address,
width: width,
height: height,
colorDark: this.defaultOptions.colorDark,
colorLight: this.defaultOptions.colorLight,
correctLevel: this.defaultOptions.correctLevel
});
} catch (error) {
console.error('QRCodeManager: Failed to generate QR code', error);
}
},
generateById: function(elementId, address, options = {}) {
const element = window.DOMCache
? window.DOMCache.get(elementId)
: document.getElementById(elementId);
if (!element) {
console.error('QRCodeManager: Element not found:', elementId);
return;
}
element.setAttribute('data-address', address);
if (options.width) element.setAttribute('data-width', options.width);
if (options.height) element.setAttribute('data-height', options.height);
this.generateQRCode(element);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
QRCodeManager.initialize();
});
} else {
QRCodeManager.initialize();
}
window.QRCodeManager = QRCodeManager;
})();

View File

@@ -4,7 +4,8 @@ const SummaryManager = (function() {
summaryEndpoint: '/json',
retryDelay: 5000,
maxRetries: 3,
requestTimeout: 15000
requestTimeout: 15000,
debug: false
};
let refreshTimer = null;
@@ -60,12 +61,15 @@ const SummaryManager = (function() {
updateElement('network-offers-counter', data.num_network_offers);
updateElement('offers-counter', data.num_sent_active_offers);
updateElement('offers-counter-mobile', data.num_sent_active_offers);
updateElement('sent-bids-counter', data.num_sent_active_bids);
updateElement('recv-bids-counter', data.num_recv_active_bids);
updateElement('bid-requests-counter', data.num_available_bids);
updateElement('swaps-counter', data.num_swapping);
updateElement('watched-outputs-counter', data.num_watched_outputs);
updateTooltips(data);
const shutdownButtons = document.querySelectorAll('.shutdown-button');
shutdownButtons.forEach(button => {
button.setAttribute('data-active-swaps', data.num_swapping);
@@ -81,6 +85,100 @@ const SummaryManager = (function() {
});
}
function updateTooltips(data) {
debugLog(`updateTooltips called with data:`, data);
const yourOffersTooltip = document.getElementById('tooltip-your-offers');
debugLog('Looking for tooltip-your-offers element:', yourOffersTooltip);
if (yourOffersTooltip) {
const newContent = `
<p><b>Total offers:</b> ${data.num_sent_offers || 0}</p>
<p><b>Active offers:</b> ${data.num_sent_active_offers || 0}</p>
`;
const totalParagraph = yourOffersTooltip.querySelector('p:first-child');
const activeParagraph = yourOffersTooltip.querySelector('p:last-child');
debugLog('Found paragraphs:', { totalParagraph, activeParagraph });
if (totalParagraph && activeParagraph) {
totalParagraph.innerHTML = `<b>Total offers:</b> ${data.num_sent_offers || 0}`;
activeParagraph.innerHTML = `<b>Active offers:</b> ${data.num_sent_active_offers || 0}`;
debugLog(`Updated Your Offers tooltip paragraphs: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
} else {
yourOffersTooltip.innerHTML = newContent;
debugLog(`Replaced Your Offers tooltip content: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
}
refreshTooltipInstances('tooltip-your-offers', newContent);
} else {
debugLog('Your Offers tooltip element not found - checking all tooltip elements');
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
}
const bidsTooltip = document.getElementById('tooltip-bids');
if (bidsTooltip) {
const newBidsContent = `
<p><b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)</p>
<p><b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)</p>
`;
const sentParagraph = bidsTooltip.querySelector('p:first-child');
const recvParagraph = bidsTooltip.querySelector('p:last-child');
if (sentParagraph && recvParagraph) {
sentParagraph.innerHTML = `<b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)`;
recvParagraph.innerHTML = `<b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)`;
debugLog(`Updated Bids tooltip: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
} else {
bidsTooltip.innerHTML = newBidsContent;
debugLog(`Replaced Bids tooltip content: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
}
refreshTooltipInstances('tooltip-bids', newBidsContent);
} else {
debugLog('Bids tooltip element not found');
}
}
function refreshTooltipInstances(tooltipId, newContent) {
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
triggers.forEach(trigger => {
if (trigger._tippy) {
trigger._tippy.setContent(newContent);
debugLog(`Updated Tippy instance content for ${tooltipId}`);
} else {
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
window.TooltipManager.create(trigger, newContent, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
debugLog(`Created new Tippy instance for ${tooltipId}`);
}
}
});
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
window.TooltipManager.refreshTooltip(tooltipId, newContent);
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
}
}
function debugLog(message) {
if (config.debug && console && console.log) {
console.log(`[SummaryManager] ${message}`);
}
}
function cacheSummaryData(data) {
if (!data) return;
@@ -107,8 +205,16 @@ const SummaryManager = (function() {
}
function fetchSummaryDataWithTimeout() {
if (window.ApiManager) {
return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
return fetch(config.summaryEndpoint, {
signal: controller.signal,
@@ -119,7 +225,11 @@ const SummaryManager = (function() {
}
})
.then(response => {
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
@@ -128,7 +238,11 @@ const SummaryManager = (function() {
return response.json();
})
.catch(error => {
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
throw error;
});
}
@@ -163,9 +277,12 @@ const SummaryManager = (function() {
}
if (data.event) {
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
@@ -174,7 +291,7 @@ const SummaryManager = (function() {
};
webSocket.onclose = () => {
setTimeout(setupWebSocket, 5000);
CleanupManager.setTimeout(setupWebSocket, 5000);
};
}
@@ -202,7 +319,7 @@ const SummaryManager = (function() {
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
refreshTimer = CleanupManager.setInterval(() => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
@@ -236,9 +353,12 @@ const SummaryManager = (function() {
wsManager.addMessageHandler('message', (data) => {
if (data.event) {
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
this.fetchSummaryData()
.then(() => {})
.catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
@@ -282,7 +402,7 @@ const SummaryManager = (function() {
}
return new Promise(resolve => {
setTimeout(() => {
CleanupManager.setTimeout(() => {
resolve(this.fetchSummaryData());
}, config.retryDelay);
});
@@ -303,6 +423,14 @@ const SummaryManager = (function() {
});
},
updateTooltips: function(data) {
updateTooltips(data || lastSuccessfulData);
},
updateUI: function(data) {
updateUIFromData(data || lastSuccessfulData);
},
startRefreshTimer: function() {
startRefreshTimer();
},
@@ -334,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
(function() {
'use strict';
const WalletAmountManager = {
coinConfigs: {
1: {
types: ['plain', 'blind', 'anon'],
hasSubfee: true,
hasSweepAll: false
},
3: {
types: ['plain', 'mweb'],
hasSubfee: true,
hasSweepAll: false
},
6: {
types: ['default'],
hasSubfee: false,
hasSweepAll: true
},
9: {
types: ['default'],
hasSubfee: false,
hasSweepAll: true
}
},
safeParseFloat: function(value) {
const numValue = Number(value);
if (!isNaN(numValue) && numValue > 0) {
return numValue;
}
console.warn('WalletAmountManager: Invalid balance value:', value);
return 0;
},
getBalance: function(coinId, balances, selectedType) {
const cid = parseInt(coinId);
if (cid === 1) {
switch(selectedType) {
case 'plain':
return this.safeParseFloat(balances.main || balances.balance);
case 'blind':
return this.safeParseFloat(balances.blind);
case 'anon':
return this.safeParseFloat(balances.anon);
default:
return this.safeParseFloat(balances.main || balances.balance);
}
}
if (cid === 3) {
switch(selectedType) {
case 'plain':
return this.safeParseFloat(balances.main || balances.balance);
case 'mweb':
return this.safeParseFloat(balances.mweb);
default:
return this.safeParseFloat(balances.main || balances.balance);
}
}
return this.safeParseFloat(balances.main || balances.balance);
},
calculateAmount: function(balance, percent, coinId) {
const cid = parseInt(coinId);
if (percent === 1) {
return balance;
}
if (cid === 1) {
return Math.max(0, Math.floor(balance * percent * 100000000) / 100000000);
}
const calculatedAmount = balance * percent;
if (calculatedAmount < 0.00000001) {
console.warn('WalletAmountManager: Calculated amount too small, setting to zero');
return 0;
}
return calculatedAmount;
},
setAmount: function(percent, balances, coinId) {
const amountInput = window.DOMCache
? window.DOMCache.get('amount')
: document.getElementById('amount');
const typeSelect = window.DOMCache
? window.DOMCache.get('withdraw_type')
: document.getElementById('withdraw_type');
if (!amountInput) {
console.error('WalletAmountManager: Amount input not found');
return;
}
const cid = parseInt(coinId);
const selectedType = typeSelect ? typeSelect.value : 'plain';
const balance = this.getBalance(cid, balances, selectedType);
const calculatedAmount = this.calculateAmount(balance, percent, cid);
const specialCids = [6, 9];
if (specialCids.includes(cid) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
const sweepAllCheckbox = window.DOMCache
? window.DOMCache.get('sweepall')
: document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = true;
}
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
amountInput.placeholder = '';
amountInput.disabled = false;
const sweepAllCheckbox = window.DOMCache
? window.DOMCache.get('sweepall')
: document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = false;
}
}
const subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
},
initialize: function() {
const amountButtons = document.querySelectorAll('[data-set-amount]');
amountButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const percent = parseFloat(button.getAttribute('data-set-amount'));
const balancesJson = button.getAttribute('data-balances');
const coinId = button.getAttribute('data-coin-id');
if (!balancesJson || !coinId) {
console.error('WalletAmountManager: Missing data attributes on button', button);
return;
}
try {
const balances = JSON.parse(balancesJson);
this.setAmount(percent, balances, coinId);
} catch (error) {
console.error('WalletAmountManager: Failed to parse balances', error);
}
});
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
WalletAmountManager.initialize();
});
} else {
WalletAmountManager.initialize();
}
window.WalletAmountManager = WalletAmountManager;
window.setAmount = function(percent, balance, coinId, balance2, balance3) {
const balances = {
main: balance || balance,
balance: balance,
blind: balance2,
anon: balance3,
mweb: balance2
};
WalletAmountManager.setAmount(percent, balances, coinId);
};
})();

View File

@@ -11,8 +11,7 @@ const WalletManager = (function() {
defaultTTL: 300,
priceSource: {
primary: 'coingecko.com',
fallback: 'cryptocompare.com',
enabledSources: ['coingecko.com', 'cryptocompare.com']
enabledSources: ['coingecko.com']
}
};
@@ -95,6 +94,15 @@ const WalletManager = (function() {
const fetchCoinsString = coinsToFetch.join(',');
let mainData;
if (window.ApiManager) {
mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
});
} else {
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
@@ -109,7 +117,8 @@ const WalletManager = (function() {
throw new Error(`HTTP error: ${mainResponse.status}`);
}
const mainData = await mainResponse.json();
mainData = await mainResponse.json();
}
if (mainData && mainData.rates) {
document.querySelectorAll('.coinname-value').forEach(el => {
@@ -154,7 +163,7 @@ const WalletManager = (function() {
if (attempt < config.maxRetries - 1) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
}
}
}
@@ -180,8 +189,29 @@ const WalletManager = (function() {
if (coinSymbol) {
if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
let isBlind = false;
let isAnon = false;
const flexContainer = el.closest('.flex');
if (flexContainer) {
const h4Element = flexContainer.querySelector('h4');
if (h4Element) {
isBlind = h4Element.textContent?.includes('Blind');
isAnon = h4Element.textContent?.includes('Anon');
}
}
if (!isBlind && !isAnon) {
const parentRow = el.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell) {
isBlind = labelCell.textContent?.includes('Blind');
isAnon = labelCell.textContent?.includes('Anon');
}
}
}
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
@@ -248,8 +278,29 @@ const WalletManager = (function() {
const usdValue = (amount * price).toFixed(2);
if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
let isBlind = false;
let isAnon = false;
const flexContainer = el.closest('.flex');
if (flexContainer) {
const h4Element = flexContainer.querySelector('h4');
if (h4Element) {
isBlind = h4Element.textContent?.includes('Blind');
isAnon = h4Element.textContent?.includes('Anon');
}
}
if (!isBlind && !isAnon) {
const parentRow = el.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell) {
isBlind = labelCell.textContent?.includes('Blind');
isAnon = labelCell.textContent?.includes('Anon');
}
}
}
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
@@ -386,7 +437,7 @@ const WalletManager = (function() {
clearTimeout(state.toggleDebounceTimer);
}
state.toggleDebounceTimer = window.setTimeout(async () => {
state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
state.toggleInProgress = false;
if (newVisibility) {
await updatePrices(true);
@@ -497,7 +548,6 @@ const WalletManager = (function() {
}
}
// Public API
const publicAPI = {
initialize: async function(options) {
if (state.initialized) {
@@ -537,7 +587,7 @@ const WalletManager = (function() {
clearInterval(state.priceUpdateInterval);
}
state.priceUpdateInterval = setInterval(() => {
state.priceUpdateInterval = CleanupManager.setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
updatePrices(false);
}
@@ -619,5 +669,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
console.log('WalletManager initialized');

View File

@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
}
function determineWebSocketPort() {
let wsPort;
if (window.ConfigManager && window.ConfigManager.wsPort) {
return window.ConfigManager.wsPort.toString();
}
if (window.config && window.config.wsPort) {
wsPort = window.config.wsPort;
return wsPort;
return window.config.wsPort.toString();
}
if (window.ws_port) {
wsPort = window.ws_port.toString();
return wsPort;
return window.ws_port.toString();
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
return wsPort;
return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
}
wsPort = '11700';
return wsPort;
return '11700';
}
const publicAPI = {
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
}
if (state.reconnectTimeout) {
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
state.connectTimeout = setTimeout(() => {
const timeoutFn = () => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
}, 5000);
};
state.connectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(timeoutFn, 5000)
: setTimeout(timeoutFn, 5000);
return true;
} catch (error) {
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
cleanup: function() {
log('Cleaning up WebSocket resources');
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
stopHealthCheck();
if (state.reconnectTimeout) {
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
if (ws) {
ws.onopen = null;
ws.onmessage = null;
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
state.lastHealthCheck = Date.now();
window.ws = ws;
@@ -298,37 +315,42 @@ const WebSocketManager = (function() {
function handlePageHidden() {
log('Page hidden');
state.isPageHidden = true;
stopHealthCheck();
if (ws && ws.readyState === WebSocket.OPEN) {
state.isIntentionallyClosed = true;
ws.close(1000, 'Page hidden');
}
}
function handlePageVisible() {
log('Page visible');
state.isPageHidden = false;
state.isIntentionallyClosed = false;
setTimeout(() => {
const resumeFn = () => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
startHealthCheck();
}, 0);
};
if (window.CleanupManager) {
window.CleanupManager.setTimeout(resumeFn, 0);
} else {
setTimeout(resumeFn, 0);
}
}
function startHealthCheck() {
stopHealthCheck();
state.healthCheckInterval = setInterval(() => {
const healthCheckFn = () => {
performHealthCheck();
}, 30000);
};
state.healthCheckInterval = window.CleanupManager
? window.CleanupManager.setInterval(healthCheckFn, 30000)
: setInterval(healthCheckFn, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
if (window.CleanupManager) {
window.CleanupManager.clearInterval(state.healthCheckInterval);
} else {
clearInterval(state.healthCheckInterval);
}
state.healthCheckInterval = null;
}
}
@@ -356,7 +378,11 @@ const WebSocketManager = (function() {
function handleReconnect() {
if (state.reconnectTimeout) {
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -369,23 +395,31 @@ const WebSocketManager = (function() {
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
state.reconnectTimeout = setTimeout(() => {
const reconnectFn = () => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
}, delay);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(reconnectFn, delay)
: setTimeout(reconnectFn, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
state.reconnectTimeout = setTimeout(() => {
const resetFn = () => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
}, 60000);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(resetFn, 60000)
: setTimeout(resetFn, 60000);
}
}
@@ -442,5 +476,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');

View File

@@ -0,0 +1,294 @@
(function() {
'use strict';
const AMMConfigTabs = {
init: function() {
const jsonTab = document.getElementById('json-tab');
const settingsTab = document.getElementById('settings-tab');
const overviewTab = document.getElementById('overview-tab');
const jsonContent = document.getElementById('json-content');
const settingsContent = document.getElementById('settings-content');
const overviewContent = document.getElementById('overview-content');
if (!jsonTab || !settingsTab || !overviewTab || !jsonContent || !settingsContent || !overviewContent) {
return;
}
const activeConfigTab = localStorage.getItem('amm_active_config_tab');
const switchConfigTab = (tabId) => {
jsonContent.classList.add('hidden');
jsonContent.classList.remove('block');
settingsContent.classList.add('hidden');
settingsContent.classList.remove('block');
overviewContent.classList.add('hidden');
overviewContent.classList.remove('block');
jsonTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
settingsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
overviewTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
if (tabId === 'json-tab') {
jsonContent.classList.remove('hidden');
jsonContent.classList.add('block');
jsonTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'json-tab');
} else if (tabId === 'settings-tab') {
settingsContent.classList.remove('hidden');
settingsContent.classList.add('block');
settingsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'settings-tab');
this.loadSettingsFromJson();
} else if (tabId === 'overview-tab') {
overviewContent.classList.remove('hidden');
overviewContent.classList.add('block');
overviewTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'overview-tab');
}
};
jsonTab.addEventListener('click', () => switchConfigTab('json-tab'));
settingsTab.addEventListener('click', () => switchConfigTab('settings-tab'));
overviewTab.addEventListener('click', () => switchConfigTab('overview-tab'));
const returnToTab = localStorage.getItem('amm_return_to_tab');
if (returnToTab && (returnToTab === 'json-tab' || returnToTab === 'settings-tab' || returnToTab === 'overview-tab')) {
localStorage.removeItem('amm_return_to_tab');
switchConfigTab(returnToTab);
} else if (activeConfigTab === 'settings-tab') {
switchConfigTab('settings-tab');
} else if (activeConfigTab === 'overview-tab') {
switchConfigTab('overview-tab');
} else {
switchConfigTab('json-tab');
}
const globalSettingsForm = document.getElementById('global-settings-form');
if (globalSettingsForm) {
globalSettingsForm.addEventListener('submit', () => {
this.updateJsonFromSettings();
});
}
this.setupCollapsibles();
this.setupConfigForm();
this.setupCreateDefaultButton();
this.handleCreateDefaultRefresh();
},
loadSettingsFromJson: function() {
const configTextarea = document.querySelector('textarea[name="config_content"]');
if (!configTextarea) return;
try {
const configText = configTextarea.value.trim();
if (!configText) return;
const config = JSON.parse(configText);
document.getElementById('min_seconds_between_offers').value = config.min_seconds_between_offers || 15;
document.getElementById('max_seconds_between_offers').value = config.max_seconds_between_offers || 60;
document.getElementById('main_loop_delay').value = config.main_loop_delay || 60;
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
const pruneStateDelayEl = document.getElementById('prune_state_delay');
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
if (minSecondsBetweenBidsEl) minSecondsBetweenBidsEl.value = config.min_seconds_between_bids || 15;
if (maxSecondsBetweenBidsEl) maxSecondsBetweenBidsEl.value = config.max_seconds_between_bids || 60;
if (pruneStateDelayEl) pruneStateDelayEl.value = config.prune_state_delay || 120;
if (pruneStateAfterSecondsEl) pruneStateAfterSecondsEl.value = config.prune_state_after_seconds || 604800;
document.getElementById('auth').value = config.auth || '';
} catch (error) {
console.error('Error loading settings from JSON:', error);
}
},
updateJsonFromSettings: function() {
const configTextarea = document.querySelector('textarea[name="config_content"]');
if (!configTextarea) return;
try {
const configText = configTextarea.value.trim();
let config = {};
if (configText) {
config = JSON.parse(configText);
}
config.min_seconds_between_offers = parseInt(document.getElementById('min_seconds_between_offers').value) || 15;
config.max_seconds_between_offers = parseInt(document.getElementById('max_seconds_between_offers').value) || 60;
config.main_loop_delay = parseInt(document.getElementById('main_loop_delay').value) || 60;
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
const pruneStateDelayEl = document.getElementById('prune_state_delay');
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
if (minSecondsBetweenBidsEl) config.min_seconds_between_bids = parseInt(minSecondsBetweenBidsEl.value) || 15;
if (maxSecondsBetweenBidsEl) config.max_seconds_between_bids = parseInt(maxSecondsBetweenBidsEl.value) || 60;
if (pruneStateDelayEl) config.prune_state_delay = parseInt(pruneStateDelayEl.value) || 120;
if (pruneStateAfterSecondsEl) config.prune_state_after_seconds = parseInt(pruneStateAfterSecondsEl.value) || 604800;
config.auth = document.getElementById('auth').value || '';
configTextarea.value = JSON.stringify(config, null, 2);
localStorage.setItem('amm_return_to_tab', 'settings-tab');
} catch (error) {
console.error('Error updating JSON from settings:', error);
alert('Error updating configuration: ' + error.message);
}
},
setupCollapsibles: function() {
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
if (collapsibleHeaders.length === 0) return;
let collapsibleStates = {};
try {
const storedStates = localStorage.getItem('amm_collapsible_states');
if (storedStates) {
collapsibleStates = JSON.parse(storedStates);
}
} catch (e) {
console.error('Error parsing stored collapsible states:', e);
collapsibleStates = {};
}
const toggleCollapsible = (header) => {
const targetId = header.getAttribute('data-target');
const content = document.getElementById(targetId);
const arrow = header.querySelector('svg');
if (content) {
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
arrow.classList.add('rotate-180');
collapsibleStates[targetId] = 'open';
} else {
content.classList.add('hidden');
arrow.classList.remove('rotate-180');
collapsibleStates[targetId] = 'closed';
}
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
}
};
collapsibleHeaders.forEach(header => {
const targetId = header.getAttribute('data-target');
const content = document.getElementById(targetId);
const arrow = header.querySelector('svg');
if (content) {
if (collapsibleStates[targetId] === 'open') {
content.classList.remove('hidden');
arrow.classList.add('rotate-180');
} else {
content.classList.add('hidden');
arrow.classList.remove('rotate-180');
collapsibleStates[targetId] = 'closed';
}
}
header.addEventListener('click', () => toggleCollapsible(header));
});
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
},
setupConfigForm: function() {
const configForm = document.querySelector('form[method="post"]');
const saveConfigBtn = document.getElementById('save_config_btn');
if (configForm && saveConfigBtn) {
configForm.addEventListener('submit', (e) => {
if (e.submitter && e.submitter.name === 'save_config') {
localStorage.setItem('amm_update_tables', 'true');
}
});
if (localStorage.getItem('amm_update_tables') === 'true') {
localStorage.removeItem('amm_update_tables');
CleanupManager.setTimeout(() => {
if (window.ammTablesManager && window.ammTablesManager.updateTables) {
window.ammTablesManager.updateTables();
}
}, 500);
}
}
},
setupCreateDefaultButton: function() {
const createDefaultBtn = document.getElementById('create_default_btn');
const configForm = document.querySelector('form[method="post"]');
if (createDefaultBtn && configForm) {
createDefaultBtn.addEventListener('click', (e) => {
e.preventDefault();
const title = 'Create Default Configuration';
const message = 'This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?';
if (window.showConfirmModal) {
window.showConfirmModal(title, message, () => {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'create_default';
hiddenInput.value = 'true';
configForm.appendChild(hiddenInput);
localStorage.setItem('amm_create_default_refresh', 'true');
configForm.submit();
});
} else {
if (confirm('This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?')) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'create_default';
hiddenInput.value = 'true';
configForm.appendChild(hiddenInput);
localStorage.setItem('amm_create_default_refresh', 'true');
configForm.submit();
}
}
});
}
},
handleCreateDefaultRefresh: function() {
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh');
CleanupManager.setTimeout(() => {
window.location.href = window.location.pathname + window.location.search;
}, 500);
}
},
cleanup: function() {
}
};
document.addEventListener('DOMContentLoaded', function() {
AMMConfigTabs.init();
if (window.CleanupManager) {
CleanupManager.registerResource('ammConfigTabs', AMMConfigTabs, (tabs) => {
if (tabs.cleanup) tabs.cleanup();
});
}
});
window.AMMConfigTabs = AMMConfigTabs;
})();

View File

@@ -0,0 +1,255 @@
const AmmCounterManager = (function() {
const config = {
refreshInterval: 10000,
ammStatusEndpoint: '/amm/status',
retryDelay: 5000,
maxRetries: 3,
debug: false
};
let refreshTimer = null;
let fetchRetryCount = 0;
let lastAmmStatus = null;
function isDebugEnabled() {
return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug;
}
function debugLog(message, data) {
}
function updateAmmCounter(count, status) {
const ammCounter = document.getElementById('amm-counter');
const ammCounterMobile = document.getElementById('amm-counter-mobile');
debugLog(`Updating AMM counter: count=${count}, status=${status}`);
if (ammCounter) {
ammCounter.textContent = count;
ammCounter.classList.remove('bg-blue-500', 'bg-gray-400');
ammCounter.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
if (ammCounterMobile) {
ammCounterMobile.textContent = count;
ammCounterMobile.classList.remove('bg-blue-500', 'bg-gray-400');
ammCounterMobile.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
updateAmmTooltips(count, status);
}
function updateAmmTooltips(count, status) {
debugLog(`updateAmmTooltips called with count=${count}, status=${status}`);
const subheaderTooltip = document.getElementById('tooltip-amm-subheader');
debugLog('Looking for tooltip-amm-subheader element:', subheaderTooltip);
if (subheaderTooltip) {
const statusText = status === 'running' ? 'Active' : 'Inactive';
const newContent = `
<p><b>Status:</b> ${statusText}</p>
<p><b>Currently active offers/bids:</b> ${count}</p>
`;
const statusParagraph = subheaderTooltip.querySelector('p:first-child');
const countParagraph = subheaderTooltip.querySelector('p:last-child');
if (statusParagraph && countParagraph) {
statusParagraph.innerHTML = `<b>Status:</b> ${statusText}`;
countParagraph.innerHTML = `<b>Currently active offers/bids:</b> ${count}`;
debugLog(`Updated AMM subheader tooltip paragraphs: status=${statusText}, count=${count}`);
} else {
subheaderTooltip.innerHTML = newContent;
debugLog(`Replaced AMM subheader tooltip content: status=${statusText}, count=${count}`);
}
refreshTooltipInstances('tooltip-amm-subheader', newContent);
} else {
debugLog('AMM subheader tooltip element not found - checking all tooltip elements');
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
}
}
function refreshTooltipInstances(tooltipId, newContent) {
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
triggers.forEach(trigger => {
if (trigger._tippy) {
trigger._tippy.setContent(newContent);
debugLog(`Updated Tippy instance content for ${tooltipId}`);
} else {
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
window.TooltipManager.create(trigger, newContent, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
debugLog(`Created new Tippy instance for ${tooltipId}`);
}
}
});
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
window.TooltipManager.refreshTooltip(tooltipId, newContent);
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
}
}
function fetchAmmStatus() {
debugLog('Fetching AMM status...');
let url = config.ammStatusEndpoint;
if (isDebugEnabled()) {
url += '?debug=true';
}
return fetch(url, {
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
lastAmmStatus = data;
debugLog('AMM status data received:', data);
updateAmmCounter(data.amm_active_count, data.status);
fetchRetryCount = 0;
return data;
})
.catch(error => {
if (isDebugEnabled()) {
console.error('[AmmCounter] AMM status fetch error:', error);
}
if (fetchRetryCount < config.maxRetries) {
fetchRetryCount++;
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
return new Promise(resolve => {
CleanupManager.setTimeout(() => {
resolve(fetchAmmStatus());
}, config.retryDelay);
});
} else {
fetchRetryCount = 0;
throw error;
}
});
}
function startRefreshTimer() {
stopRefreshTimer();
debugLog('Starting AMM status refresh timer');
fetchAmmStatus()
.then(() => {})
.catch(() => {});
refreshTimer = CleanupManager.setInterval(() => {
fetchAmmStatus()
.then(() => {})
.catch(() => {});
}, config.refreshInterval);
}
function stopRefreshTimer() {
if (refreshTimer) {
debugLog('Stopping AMM status refresh timer');
clearInterval(refreshTimer);
refreshTimer = null;
}
}
function setupWebSocketHandler() {
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
debugLog('Setting up WebSocket handler for AMM status updates');
window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
debugLog('WebSocket event received, refreshing AMM status');
fetchAmmStatus()
.then(() => {})
.catch(() => {});
}
});
}
}
function setupDebugListener() {
const debugCheckbox = document.getElementById('amm_debug');
if (debugCheckbox) {
debugLog('Found AMM debug checkbox, setting up listener');
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked ? 'true' : 'false');
debugCheckbox.addEventListener('change', function() {
localStorage.setItem('amm_debug_enabled', this.checked ? 'true' : 'false');
debugLog(`Debug mode ${this.checked ? 'enabled' : 'disabled'}`);
});
}
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
setupWebSocketHandler();
setupDebugListener();
startRefreshTimer();
debugLog('AMM Counter Manager initialized');
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammCounterManager', this, (mgr) => mgr.dispose());
}
return this;
},
fetchAmmStatus: fetchAmmStatus,
updateCounter: updateAmmCounter,
updateTooltips: updateAmmTooltips,
startRefreshTimer: startRefreshTimer,
stopRefreshTimer: stopRefreshTimer,
dispose: function() {
debugLog('Disposing AMM Counter Manager');
stopRefreshTimer();
}
};
return publicAPI;
})();
document.addEventListener('DOMContentLoaded', function() {
if (!window.ammCounterManagerInitialized) {
window.AmmCounterManager = AmmCounterManager.initialize();
window.ammCounterManagerInitialized = true;
if (window.CleanupManager) {
CleanupManager.registerResource('ammCounter', window.AmmCounterManager, (mgr) => {
if (mgr && mgr.dispose) mgr.dispose();
});
}
}
});

View File

@@ -0,0 +1,573 @@
(function() {
'use strict';
const AMMPage = {
init: function() {
this.loadDebugSetting();
this.setupAutostartCheckbox();
this.setupStartupValidation();
this.setupDebugCheckbox();
this.setupModals();
this.setupClearStateButton();
this.setupWebSocketBalanceUpdates();
this.setupCleanup();
},
saveDebugSetting: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked);
}
},
loadDebugSetting: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
const savedSetting = localStorage.getItem('amm_debug_enabled');
if (savedSetting !== null) {
debugCheckbox.checked = savedSetting === 'true';
}
}
},
setupDebugCheckbox: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
debugCheckbox.addEventListener('change', this.saveDebugSetting.bind(this));
}
},
saveAutostartSetting: function(checked) {
const bodyData = `autostart=${checked ? 'true' : 'false'}`;
fetch('/amm/autostart', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: bodyData
})
.then(response => response.json())
.then(data => {
if (data.success) {
localStorage.setItem('amm_autostart_enabled', checked);
if (data.autostart !== checked) {
console.warn('WARNING: API returned different autostart value than expected!', {
sent: checked,
received: data.autostart
});
}
} else {
console.error('Failed to save autostart setting:', data.error);
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.checked = !checked;
}
}
})
.catch(error => {
console.error('Error saving autostart setting:', error);
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.checked = !checked;
}
});
},
setupAutostartCheckbox: function() {
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.addEventListener('change', () => {
this.saveAutostartSetting(autostartCheckbox.checked);
});
}
},
showErrorModal: function(title, message) {
document.getElementById('errorTitle').textContent = title || 'Error';
document.getElementById('errorMessage').textContent = message || 'An error occurred';
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.remove('hidden');
}
},
hideErrorModal: function() {
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.add('hidden');
}
},
showConfirmModal: function(title, message, callback) {
document.getElementById('confirmTitle').textContent = title || 'Confirm Action';
document.getElementById('confirmMessage').textContent = message || 'Are you sure?';
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
window.confirmCallback = callback;
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
window.confirmCallback = null;
},
setupModals: function() {
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
}
const errorModal = document.getElementById('errorModal');
if (errorModal) {
errorModal.addEventListener('click', (e) => {
if (e.target === errorModal) {
this.hideErrorModal();
}
});
}
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (window.confirmCallback && typeof window.confirmCallback === 'function') {
window.confirmCallback();
}
this.hideConfirmModal();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', this.hideConfirmModal.bind(this));
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal) {
this.hideConfirmModal();
}
});
}
},
setupStartupValidation: function() {
const controlForm = document.querySelector('form[method="post"]');
if (!controlForm) return;
const startButton = controlForm.querySelector('input[name="start"]');
if (!startButton) return;
startButton.addEventListener('click', (e) => {
e.preventDefault();
this.performStartupValidation();
});
},
performStartupValidation: function() {
const feedbackDiv = document.getElementById('startup-feedback');
const titleEl = document.getElementById('startup-title');
const messageEl = document.getElementById('startup-message');
const progressBar = document.getElementById('startup-progress-bar');
feedbackDiv.classList.remove('hidden');
const steps = [
{ message: 'Checking configuration...', progress: 20 },
{ message: 'Validating offers and bids...', progress: 40 },
{ message: 'Checking wallet balances...', progress: 60 },
{ message: 'Verifying API connection...', progress: 80 },
{ message: 'Starting AMM process...', progress: 100 }
];
let currentStep = 0;
const runNextStep = () => {
if (currentStep >= steps.length) {
this.submitStartForm();
return;
}
const step = steps[currentStep];
messageEl.textContent = step.message;
progressBar.style.width = step.progress + '%';
CleanupManager.setTimeout(() => {
this.validateStep(currentStep).then(result => {
if (result.success) {
currentStep++;
runNextStep();
} else {
this.showStartupError(result.error);
}
}).catch(error => {
this.showStartupError('Validation failed: ' + error.message);
});
}, 500);
};
runNextStep();
},
validateStep: async function(stepIndex) {
try {
switch (stepIndex) {
case 0:
return await this.validateConfiguration();
case 1:
return await this.validateOffersAndBids();
case 2:
return await this.validateWalletBalances();
case 3:
return await this.validateApiConnection();
case 4:
return { success: true };
default:
return { success: true };
}
} catch (error) {
return { success: false, error: error.message };
}
},
validateConfiguration: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) {
return { success: false, error: 'No configuration found. Please save a configuration first.' };
}
if (!configData.min_seconds_between_offers || !configData.max_seconds_between_offers) {
return { success: false, error: 'Missing timing configuration. Please check your settings.' };
}
return { success: true };
},
validateOffersAndBids: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) {
return { success: false, error: 'Configuration not available for validation.' };
}
const offers = configData.offers || [];
const bids = configData.bids || [];
const enabledOffers = offers.filter(o => o.enabled);
const enabledBids = bids.filter(b => b.enabled);
if (enabledOffers.length === 0 && enabledBids.length === 0) {
return { success: false, error: 'No enabled offers or bids found. Please enable at least one offer or bid before starting.' };
}
for (const offer of enabledOffers) {
if (!offer.amount_step) {
return { success: false, error: `Offer "${offer.name}" is missing required Amount Step (privacy feature).` };
}
const amountStep = parseFloat(offer.amount_step);
const amount = parseFloat(offer.amount);
if (amountStep <= 0 || amountStep < 0.001) {
return { success: false, error: `Offer "${offer.name}" has invalid Amount Step. Must be >= 0.001.` };
}
if (amountStep > amount) {
return { success: false, error: `Offer "${offer.name}" Amount Step (${amountStep}) cannot be greater than offer amount (${amount}).` };
}
}
return { success: true };
},
validateWalletBalances: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) return { success: true };
const offers = configData.offers || [];
const enabledOffers = offers.filter(o => o.enabled);
for (const offer of enabledOffers) {
if (!offer.min_coin_from_amt || parseFloat(offer.min_coin_from_amt) <= 0) {
return { success: false, error: `Offer "${offer.name}" needs a minimum coin amount to protect your wallet balance.` };
}
}
return { success: true };
},
validateApiConnection: async function() {
return { success: true };
},
showStartupError: function(errorMessage) {
const feedbackDiv = document.getElementById('startup-feedback');
feedbackDiv.classList.add('hidden');
if (window.showErrorModal) {
window.showErrorModal('AMM Startup Failed', errorMessage);
} else {
alert('AMM Startup Failed: ' + errorMessage);
}
},
submitStartForm: function() {
const feedbackDiv = document.getElementById('startup-feedback');
const titleEl = document.getElementById('startup-title');
const messageEl = document.getElementById('startup-message');
titleEl.textContent = 'Starting AMM...';
messageEl.textContent = 'AMM process is starting. Please wait...';
const controlForm = document.querySelector('form[method="post"]');
if (controlForm) {
const formData = new FormData(controlForm);
formData.append('start', 'Start');
fetch(window.location.pathname, {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
throw new Error('Failed to start AMM');
}
}).catch(error => {
this.showStartupError('Failed to start AMM: ' + error.message);
});
}
},
setupClearStateButton: function() {
const clearStateBtn = document.getElementById('clearStateBtn');
if (clearStateBtn) {
clearStateBtn.addEventListener('click', () => {
this.showConfirmModal(
'Clear AMM State',
'This will clear the AMM state file. All running offers/bids will be lost. Are you sure?',
() => {
const form = clearStateBtn.closest('form');
if (form) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'prune_state';
hiddenInput.value = 'true';
form.appendChild(hiddenInput);
form.submit();
}
}
);
});
}
},
setAmmAmount: function(percent, fieldId) {
const amountInput = document.getElementById(fieldId);
let coinSelect;
let modalType = null;
if (fieldId.includes('add-amm')) {
const addModal = document.getElementById('add-amm-modal');
modalType = addModal ? addModal.getAttribute('data-amm-type') : null;
} else if (fieldId.includes('edit-amm')) {
const editModal = document.getElementById('edit-amm-modal');
modalType = editModal ? editModal.getAttribute('data-amm-type') : null;
}
if (fieldId.includes('add-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'add-amm-coin-to' : 'add-amm-coin-from');
} else if (fieldId.includes('edit-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'edit-amm-coin-to' : 'edit-amm-coin-from');
}
if (!amountInput || !coinSelect) {
console.error('Required elements not found');
return;
}
const selectedOption = coinSelect.options[coinSelect.selectedIndex];
if (!selectedOption) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Please select a coin first');
} else {
alert('Please select a coin first');
}
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
} else {
alert('Invalid balance for selected coin');
}
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
const event = new Event('input', { bubbles: true });
amountInput.dispatchEvent(event);
},
updateAmmModalBalances: function(balanceData) {
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let modalType = null;
if (addModalVisible) {
modalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
modalType = editModal.getAttribute('data-amm-type');
}
if (modalType === 'offer') {
this.updateOfferDropdownBalances(balanceData);
} else if (modalType === 'bid') {
this.updateBidDropdownBalances(balanceData);
}
},
setupWebSocketBalanceUpdates: function() {
window.BalanceUpdatesManager.setup({
contextKey: 'amm',
balanceUpdateCallback: this.updateAmmModalBalances.bind(this),
swapEventCallback: this.updateAmmModalBalances.bind(this),
errorContext: 'AMM',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
},
updateAmmDropdownBalances: function(balanceData) {
const balanceMap = {};
const pendingMap = {};
balanceData.forEach(coin => {
balanceMap[coin.name] = coin.balance;
pendingMap[coin.name] = coin.pending || '0.0';
});
const dropdownIds = ['add-amm-coin-from', 'edit-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select) {
return;
}
Array.from(select.options).forEach(option => {
const coinName = option.value;
const balance = balanceMap[coinName] || '0.0';
const pending = pendingMap[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
});
});
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let currentModalType = null;
if (addModalVisible) {
currentModalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
currentModalType = editModal.getAttribute('data-amm-type');
}
if (currentModalType && window.ammTablesManager) {
if (currentModalType === 'offer' && typeof window.ammTablesManager.refreshOfferDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshOfferDropdownBalanceDisplay();
} else if (currentModalType === 'bid' && typeof window.ammTablesManager.refreshBidDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshBidDropdownBalanceDisplay();
}
}
},
updateOfferDropdownBalances: function(balanceData) {
this.updateAmmDropdownBalances(balanceData);
},
updateBidDropdownBalances: function(balanceData) {
this.updateAmmDropdownBalances(balanceData);
},
cleanupAmmBalanceUpdates: function() {
window.BalanceUpdatesManager.cleanup('amm');
if (window.ammDropdowns) {
window.ammDropdowns.forEach(dropdown => {
if (dropdown.parentNode) {
dropdown.parentNode.removeChild(dropdown);
}
});
window.ammDropdowns = [];
}
},
setupCleanup: function() {
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammBalanceUpdates', null, this.cleanupAmmBalanceUpdates.bind(this));
}
const beforeUnloadHandler = this.cleanupAmmBalanceUpdates.bind(this);
window.addEventListener('beforeunload', beforeUnloadHandler);
if (window.CleanupManager) {
CleanupManager.registerResource('ammBeforeUnload', beforeUnloadHandler, () => {
window.removeEventListener('beforeunload', beforeUnloadHandler);
});
}
},
cleanup: function() {
const debugCheckbox = document.getElementById('amm_debug');
const autostartCheckbox = document.getElementById('amm_autostart');
const errorOkBtn = document.getElementById('errorOk');
const confirmYesBtn = document.getElementById('confirmYes');
const confirmNoBtn = document.getElementById('confirmNo');
const startButton = document.getElementById('startAMM');
const clearStateBtn = document.getElementById('clearAmmState');
this.cleanupAmmBalanceUpdates();
}
};
document.addEventListener('DOMContentLoaded', function() {
AMMPage.init();
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.initialize();
}
});
window.AMMPage = AMMPage;
window.showErrorModal = AMMPage.showErrorModal.bind(AMMPage);
window.hideErrorModal = AMMPage.hideErrorModal.bind(AMMPage);
window.showConfirmModal = AMMPage.showConfirmModal.bind(AMMPage);
window.hideConfirmModal = AMMPage.hideConfirmModal.bind(AMMPage);
window.setAmmAmount = AMMPage.setAmmAmount.bind(AMMPage);
})();

File diff suppressed because it is too large Load Diff

View File

@@ -53,9 +53,9 @@ const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
if (timeLeft <= 300) return '#9CA3AF';
if (timeLeft <= 1800) return '#3B82F6';
return '#10B981';
};
const createTimeTooltip = (bid) => {
@@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => {
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
if (refreshIcon) {
// Add CSS transition for smoother animation
refreshIcon.style.transition = 'transform 0.3s ease';
refreshIcon.classList.toggle('animate-spin', isLoading);
}
@@ -352,7 +352,7 @@ const createBidTableRow = async (bid) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_from)}"
alt="${bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -361,7 +361,7 @@ const createBidTableRow = async (bid) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_to)}"
alt="${bid.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -631,7 +631,7 @@ if (elements.refreshBidsButton) {
updateLoadingState(true);
await new Promise(resolve => setTimeout(resolve, 500));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 500));
try {
await updateBidsTable({ resetPage: true, refreshData: true });

View File

@@ -4,17 +4,18 @@ const BidExporter = {
return 'No data to export';
}
const isSent = type === 'sent';
const isAllTab = type === 'all';
const headers = [
'Date/Time',
'Bid ID',
'Offer ID',
'From Address',
isSent ? 'You Send Amount' : 'You Receive Amount',
isSent ? 'You Send Coin' : 'You Receive Coin',
isSent ? 'You Receive Amount' : 'You Send Amount',
isSent ? 'You Receive Coin' : 'You Send Coin',
...(isAllTab ? ['Type'] : []),
'You Send Amount',
'You Send Coin',
'You Receive Amount',
'You Receive Coin',
'Status',
'Created At',
'Expires At'
@@ -23,11 +24,13 @@ const BidExporter = {
let csvContent = headers.join(',') + '\n';
bids.forEach(bid => {
const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent');
const row = [
`"${formatTime(bid.created_at)}"`,
`"${bid.bid_id}"`,
`"${bid.offer_id}"`,
`"${bid.addr_from}"`,
...(isAllTab ? [`"${bid.source}"`] : []),
isSent ? bid.amount_from : bid.amount_to,
`"${isSent ? bid.coin_from : bid.coin_to}"`,
isSent ? bid.amount_to : bid.amount_from,
@@ -63,7 +66,7 @@ const BidExporter = {
link.click();
document.body.removeChild(link);
setTimeout(() => {
CleanupManager.setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
@@ -101,8 +104,17 @@ const BidExporter = {
};
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
CleanupManager.setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
const exportAllButton = document.getElementById('exportAllBids');
if (exportAllButton) {
EventManager.add(exportAllButton, 'click', (e) => {
e.preventDefault();
state.currentTab = 'all';
BidExporter.exportCurrentView();
});
}
const exportSentButton = document.getElementById('exportSentBids');
if (exportSentButton) {
EventManager.add(exportSentButton, 'click', (e) => {
@@ -128,9 +140,14 @@ const originalCleanup = window.cleanup || function(){};
window.cleanup = function() {
originalCleanup();
const exportAllButton = document.getElementById('exportAllBids');
const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportAllButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportAllButton, 'click');
}
if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click');
}

View File

@@ -0,0 +1,216 @@
(function() {
'use strict';
const originalOnload = window.onload;
window.onload = function() {
if (typeof originalOnload === 'function') {
originalOnload();
}
CleanupManager.setTimeout(function() {
initBidsTabNavigation();
handleInitialNavigation();
}, 100);
};
document.addEventListener('DOMContentLoaded', function() {
initBidsTabNavigation();
if (window.CleanupManager) {
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
window.removeEventListener('hashchange', handleHashChange);
});
}
});
window.addEventListener('hashchange', handleHashChange);
window.bidsTabNavigationInitialized = false;
function initBidsTabNavigation() {
if (window.bidsTabNavigationInitialized) {
return;
}
document.querySelectorAll('.bids-tab-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const targetTabId = this.getAttribute('data-tab-target');
if (targetTabId) {
if (window.location.pathname === '/bids') {
navigateToTabDirectly(targetTabId);
} else {
localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', ''));
window.location.href = '/bids';
}
}
});
});
window.bidsTabNavigationInitialized = true;
}
function handleInitialNavigation() {
if (window.location.pathname !== '/bids') {
return;
}
const tabToActivate = localStorage.getItem('bidsTabToActivate');
if (tabToActivate) {
localStorage.removeItem('bidsTabToActivate');
activateTabWithRetry('#' + tabToActivate);
} else if (window.location.hash) {
activateTabWithRetry(window.location.hash);
} else {
activateTabWithRetry('#all');
}
}
function handleHashChange() {
if (window.location.pathname !== '/bids') {
return;
}
const hash = window.location.hash;
if (hash) {
activateTabWithRetry(hash);
} else {
activateTabWithRetry('#all');
}
}
function activateTabWithRetry(tabId, retryCount = 0) {
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
activateTabWithRetry('#all');
return;
}
const tabButtonId = normalizedTabId === '#all' ? 'all-tab' :
(normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab');
const tabButton = document.getElementById(tabButtonId);
if (!tabButton) {
if (retryCount < 5) {
CleanupManager.setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100);
}
return;
}
tabButton.click();
if (window.Tabs) {
const tabsEl = document.querySelector('[data-tabs-toggle="#bidstab"]');
if (tabsEl) {
const allTabs = Array.from(tabsEl.querySelectorAll('[role="tab"]'));
const targetTab = allTabs.find(tab => tab.getAttribute('data-tabs-target') === normalizedTabId);
if (targetTab) {
allTabs.forEach(tab => {
tab.setAttribute('aria-selected', tab === targetTab ? 'true' : 'false');
if (tab === targetTab) {
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
} else {
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
}
});
const allContent = document.getElementById('all');
const sentContent = document.getElementById('sent');
const receivedContent = document.getElementById('received');
if (allContent && sentContent && receivedContent) {
allContent.classList.toggle('hidden', normalizedTabId !== '#all');
sentContent.classList.toggle('hidden', normalizedTabId !== '#sent');
receivedContent.classList.toggle('hidden', normalizedTabId !== '#received');
}
}
}
}
const allPanel = document.getElementById('all');
const sentPanel = document.getElementById('sent');
const receivedPanel = document.getElementById('received');
if (allPanel && sentPanel && receivedPanel) {
allPanel.classList.toggle('hidden', normalizedTabId !== '#all');
sentPanel.classList.toggle('hidden', normalizedTabId !== '#sent');
receivedPanel.classList.toggle('hidden', normalizedTabId !== '#received');
}
const newHash = normalizedTabId.replace('#', '');
if (window.location.hash !== '#' + newHash) {
history.replaceState(null, null, '#' + newHash);
}
triggerDataLoad(normalizedTabId);
}
function triggerDataLoad(tabId) {
CleanupManager.setTimeout(() => {
if (window.state) {
window.state.currentTab = tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received');
if (typeof window.updateBidsTable === 'function') {
window.updateBidsTable();
}
}
const event = new CustomEvent('tabactivated', {
detail: {
tabId: tabId,
type: tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received')
}
});
document.dispatchEvent(event);
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
CleanupManager.setTimeout(() => {
window.TooltipManager.cleanup();
if (typeof window.initializeTooltips === 'function') {
window.initializeTooltips();
}
}, 200);
}
}, 100);
}
function navigateToTabDirectly(tabId) {
const oldScrollPosition = window.scrollY;
activateTabWithRetry(tabId);
CleanupManager.setTimeout(function() {
window.scrollTo(0, oldScrollPosition);
}, 0);
}
window.navigateToBidsTab = function(tabId) {
if (window.location.pathname === '/bids') {
navigateToTabDirectly('#' + tabId);
} else {
localStorage.setItem('bidsTabToActivate', tabId);
window.location.href = '/bids';
}
};
})();

View File

@@ -16,6 +16,30 @@ const DOM = {
queryAll: (selector) => document.querySelectorAll(selector)
};
const ErrorModal = {
show: function(title, message) {
const errorTitle = document.getElementById('errorTitle');
const errorMessage = document.getElementById('errorMessage');
const modal = document.getElementById('errorModal');
if (errorTitle) errorTitle.textContent = title || 'Error';
if (errorMessage) errorMessage.textContent = message || 'An error occurred';
if (modal) modal.classList.remove('hidden');
},
hide: function() {
const modal = document.getElementById('errorModal');
if (modal) modal.classList.add('hidden');
},
init: function() {
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hide.bind(this));
}
}
};
const Storage = {
get: (key) => {
try {
@@ -329,8 +353,10 @@ const SwapTypeManager = {
} else {
swapTypeElement.disabled = false;
swapTypeElement.classList.remove('select-disabled');
if (['xmr_swap', 'seller_first'].includes(swapTypeElement.value) == false) {
swapTypeElement.value = 'xmr_swap';
}
}
let swapTypeHidden = DOM.get('swap_type_hidden');
if (makeHidden) {
@@ -353,10 +379,6 @@ const SwapTypeManager = {
}
};
function set_swap_type_enabled(coinFrom, coinTo, swapTypeElement) {
SwapTypeManager.setSwapTypeEnabled(coinFrom, coinTo, swapTypeElement);
}
const UIEnhancer = {
handleErrorHighlighting: () => {
const errMsgs = document.querySelectorAll('p.error_msg');
@@ -445,7 +467,40 @@ const UIEnhancer = {
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
const parts = name.split(' - Balance: ');
const coinName = parts[0];
const balanceInfo = parts[1] || '';
selectNameElement.innerHTML = '';
selectNameElement.style.display = 'flex';
selectNameElement.style.flexDirection = 'column';
selectNameElement.style.alignItems = 'flex-start';
selectNameElement.style.lineHeight = '1.2';
const coinNameDiv = document.createElement('div');
coinNameDiv.textContent = coinName;
coinNameDiv.style.fontWeight = 'normal';
coinNameDiv.style.color = 'inherit';
const balanceDiv = document.createElement('div');
balanceDiv.textContent = `Balance: ${balanceInfo}`;
balanceDiv.style.fontSize = '0.75rem';
balanceDiv.style.color = '#6b7280';
balanceDiv.style.marginTop = '1px';
selectNameElement.appendChild(coinNameDiv);
selectNameElement.appendChild(balanceDiv);
} else {
selectNameElement.textContent = name;
selectNameElement.style.display = 'block';
selectNameElement.style.flexDirection = '';
selectNameElement.style.alignItems = '';
}
}
updateSelectCache(select);
@@ -539,6 +594,8 @@ function initializeApp() {
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
ErrorModal.init();
}
if (document.readyState === 'loading') {
@@ -546,3 +603,6 @@ if (document.readyState === 'loading') {
} else {
initializeApp();
}
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);

View File

@@ -0,0 +1,364 @@
(function() {
'use strict';
const OfferPage = {
xhr_rates: null,
xhr_bid_params: null,
init: function() {
this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest();
this.setupXHRHandlers();
this.setupEventListeners();
this.handleBidsPageAddress();
},
setupXHRHandlers: function() {
this.xhr_rates.onload = () => {
if (this.xhr_rates.status == 200) {
const obj = JSON.parse(this.xhr_rates.response);
const inner_html = '<h4 class="bold">Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
const ratesDisplay = document.getElementById('rates_display');
if (ratesDisplay) {
ratesDisplay.innerHTML = inner_html;
}
}
};
this.xhr_bid_params.onload = () => {
if (this.xhr_bid_params.status == 200) {
const obj = JSON.parse(this.xhr_bid_params.response);
const bidAmountSendInput = document.getElementById('bid_amount_send');
if (bidAmountSendInput) {
bidAmountSendInput.value = obj['amount_to'];
}
this.updateModalValues();
}
};
},
setupEventListeners: function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
if (sendBidBtn) {
sendBidBtn.onclick = this.showConfirmModal.bind(this);
}
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
if (modalCancelBtn) {
modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
}
const mainCancelBtn = document.querySelector('button[name="cancel"]');
if (mainCancelBtn) {
mainCancelBtn.onclick = this.handleCancelClick.bind(this);
}
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
}
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
}
},
lookup_rates: function() {
const coin_from = document.getElementById('coin_from')?.value;
const coin_to = document.getElementById('coin_to')?.value;
if (!coin_from || !coin_to || coin_from === '-1' || coin_to === '-1') {
alert('Coins from and to must be set first.');
return;
}
const ratesDisplay = document.getElementById('rates_display');
if (ratesDisplay) {
ratesDisplay.innerHTML = '<h4>Rates</h4><p>Updating...</p>';
}
this.xhr_rates.open('POST', '/json/rates');
this.xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`);
},
resetForm: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
const bidRateInput = document.getElementById('bid_rate');
const validMinsInput = document.querySelector('input[name="validmins"]');
const amtVar = document.getElementById('amt_var')?.value === 'True';
if (bidAmountSendInput) {
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
}
if (bidAmountInput) {
bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max');
}
if (bidRateInput && !bidRateInput.disabled) {
const defaultRate = document.getElementById('offer_rate')?.value || '';
bidRateInput.value = defaultRate;
}
if (validMinsInput) {
validMinsInput.value = "60";
}
if (!amtVar) {
this.updateBidParams('rate');
}
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
const inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.classList.remove('border-red-500', 'focus:border-red-500');
});
},
roundUpToDecimals: function(value, decimals) {
const factor = Math.pow(10, decimals);
return Math.ceil(value * factor) / factor;
},
updateBidParams: function(value_changed) {
const coin_from = document.getElementById('coin_from')?.value;
const coin_to = document.getElementById('coin_to')?.value;
const coin_from_exp = parseInt(document.getElementById('coin_from_exp')?.value || '8');
const coin_to_exp = parseInt(document.getElementById('coin_to_exp')?.value || '8');
const amt_var = document.getElementById('amt_var')?.value;
const rate_var = document.getElementById('rate_var')?.value;
const bidAmountInput = document.getElementById('bid_amount');
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate');
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
const rate = rate_var === 'True' && bidRateInput ?
parseFloat(bidRateInput.value) || 0 :
parseFloat(offerRateInput?.value || '0');
if (!rate) return;
if (value_changed === 'rate') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'sending') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'receiving') {
if (bidAmountInput && bidAmountSendInput) {
const receiveAmount = parseFloat(bidAmountInput.value) || 0;
const sendAmount = this.roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp);
bidAmountSendInput.value = sendAmount;
}
}
this.validateAmountsAfterChange();
this.xhr_bid_params.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues();
},
validateAmountsAfterChange: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountSendInput) {
const maxSend = parseFloat(bidAmountSendInput.getAttribute('max'));
this.validateMaxAmount(bidAmountSendInput, maxSend);
}
if (bidAmountInput) {
const maxReceive = parseFloat(bidAmountInput.getAttribute('max'));
this.validateMaxAmount(bidAmountInput, maxReceive);
}
},
validateMaxAmount: function(input, maxAmount) {
if (!input) return;
const value = parseFloat(input.value) || 0;
if (value > maxAmount) {
input.value = maxAmount;
}
},
showErrorModal: function(title, message) {
document.getElementById('errorTitle').textContent = title || 'Error';
document.getElementById('errorMessage').textContent = message || 'An error occurred';
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.remove('hidden');
}
},
hideErrorModal: function() {
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.add('hidden');
}
},
showConfirmModal: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
const validMinsInput = document.querySelector('input[name="validmins"]');
const addrFromSelect = document.querySelector('select[name="addr_from"]');
let sendAmount = 0;
let receiveAmount = 0;
if (bidAmountSendInput && bidAmountSendInput.value) {
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
}
if (bidAmountInput && bidAmountInput.value) {
receiveAmount = parseFloat(bidAmountInput.value) || 0;
}
if (sendAmount <= 0 || receiveAmount <= 0) {
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false;
}
const coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || '';
const tlaFrom = document.getElementById('tla_from')?.value || '';
const tlaTo = document.getElementById('tla_to')?.value || '';
const validMins = validMinsInput ? validMinsInput.value : '60';
const addrFrom = addrFromSelect ? addrFromSelect.value : '';
const modalAmtReceive = document.getElementById('modal-amt-receive');
const modalReceiveCurrency = document.getElementById('modal-receive-currency');
const modalAmtSend = document.getElementById('modal-amt-send');
const modalSendCurrency = document.getElementById('modal-send-currency');
const modalAddrFrom = document.getElementById('modal-addr-from');
const modalValidMins = document.getElementById('modal-valid-mins');
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
return false;
},
updateModalValues: function() {
},
handleBidsPageAddress: function() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
if (!selectElement) return;
const loadInitialAddress = () => {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
};
const selectFirstAddress = () => {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
this.saveAddress(firstOption.value, firstOption.text);
}
}
};
selectElement.addEventListener('change', (event) => {
this.saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
},
saveAddress: function(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData));
},
confirmPopup: function() {
return confirm("Are you sure?");
},
handleCancelClick: function(event) {
if (event) event.preventDefault();
const pathParts = window.location.pathname.split('/');
const offerId = pathParts[pathParts.indexOf('offer') + 1];
window.location.href = `/offer/${offerId}`;
},
cleanup: function() {
}
};
document.addEventListener('DOMContentLoaded', function() {
OfferPage.init();
if (window.CleanupManager) {
CleanupManager.registerResource('offerPage', OfferPage, (page) => {
if (page.cleanup) page.cleanup();
});
}
});
window.OfferPage = OfferPage;
window.lookup_rates = OfferPage.lookup_rates.bind(OfferPage);
window.resetForm = OfferPage.resetForm.bind(OfferPage);
window.updateBidParams = OfferPage.updateBidParams.bind(OfferPage);
window.validateMaxAmount = OfferPage.validateMaxAmount.bind(OfferPage);
window.showConfirmModal = OfferPage.showConfirmModal.bind(OfferPage);
window.hideConfirmModal = OfferPage.hideConfirmModal.bind(OfferPage);
window.showErrorModal = OfferPage.showErrorModal.bind(OfferPage);
window.hideErrorModal = OfferPage.hideErrorModal.bind(OfferPage);
window.confirmPopup = OfferPage.confirmPopup.bind(OfferPage);
window.handleBidsPageAddress = OfferPage.handleBidsPageAddress.bind(OfferPage);
})();

View File

@@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig;
const coins = window.config.coins;
const apiKeys = window.config.getAPIKeys();
const utils = {
formatNumber: (number, decimals = 2) => {
if (typeof number !== 'number' || isNaN(number)) {
return '0';
}
try {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
} catch (e) {
return '0';
}
},
formatDate: (timestamp, resolution) => {
const date = new Date(timestamp);
const options = {
day: { hour: '2-digit', minute: '2-digit', hour12: true },
week: { month: 'short', day: 'numeric' },
month: { year: 'numeric', month: 'short', day: 'numeric' }
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
debounce: (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
};
class AppError extends Error {
constructor(message, type = 'AppError') {
super(message);
this.name = type;
}
}
const logger = {
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
@@ -64,7 +24,6 @@ const api = {
}
const volumeData = await Api.fetchVolumeData({
cryptoCompare: apiKeys.cryptoCompare,
coinGecko: apiKeys.coinGecko
});
@@ -94,44 +53,36 @@ const api = {
}
},
fetchCryptoCompareDataXHR: (coin) => {
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
}
return Api.fetchCryptoCompareData(coin, {
cryptoCompare: apiKeys.cryptoCompare
});
} catch (error) {
logger.error(`CryptoCompare request failed for ${coin}:`, error);
NetworkManager.handleNetworkError(error);
const cachedData = CacheManager.get(`coinData_${coin}`);
if (cachedData) {
logger.info(`Using cached data for ${coin}`);
return cachedData.value;
}
return { error: error.message };
}
},
fetchCoinGeckoDataXHR: async () => {
try {
const priceData = await window.PriceManager.getPrices();
const transformedData = {};
const btcPriceUSD = priceData.bitcoin?.usd || 0;
if (btcPriceUSD > 0) {
window.btcPriceUSD = btcPriceUSD;
}
window.config.coins.forEach(coin => {
const symbol = coin.symbol.toLowerCase();
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
if (coinData && coinData.usd) {
let priceBtc;
if (symbol === 'btc') {
priceBtc = 1;
} else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
priceBtc = coinData.usd / window.btcPriceUSD;
} else {
priceBtc = coinData.btc || 0;
}
transformedData[symbol] = {
current_price: coinData.usd,
price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0),
displayName: coin.displayName || coin.symbol
price_btc: priceBtc,
displayName: coin.displayName || coin.symbol,
total_volume: coinData.total_volume,
price_change_percentage_24h: coinData.price_change_percentage_24h
};
}
});
@@ -157,10 +108,7 @@ const api = {
const historicalData = await Api.fetchHistoricalData(
coinSymbols,
window.config.currentResolution,
{
cryptoCompare: window.config.getAPIKeys().cryptoCompare
}
window.config.currentResolution
);
Object.keys(historicalData).forEach(coin => {
@@ -194,8 +142,7 @@ const api = {
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: window.config.rateLimits.coingecko.minInterval,
cryptocompare: window.config.rateLimits.cryptocompare.minInterval
coingecko: window.config.rateLimits.coingecko.minInterval
},
requestQueue: {},
retryDelays: window.config.retryDelays,
@@ -227,7 +174,7 @@ const rateLimiter = {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
@@ -237,7 +184,7 @@ const rateLimiter = {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -245,7 +192,7 @@ const rateLimiter = {
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -281,20 +228,17 @@ const ui = {
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
if (priceUsdElement) {
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
}
if (volumeDiv && volumeElement) {
if (isError || volume24h === null || volume24h === undefined) {
volumeElement.textContent = 'N/A';
} else {
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
}
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
}
if (btcPriceDiv && priceBtcElement) {
if (coin === 'BTC') {
btcPriceDiv.style.display = 'none';
@@ -303,34 +247,46 @@ const ui = {
btcPriceDiv.style.display = 'flex';
}
}
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
};
try {
if (data.error) {
throw new Error(data.error);
}
if (!data || !data.current_price) {
throw new Error(`Invalid data structure for ${coin}`);
}
priceUSD = data.current_price;
priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
priceChange1d = data.price_change_percentage_24h || 0;
volume24h = data.total_volume || 0;
if (coin === 'BTC') {
priceBTC = 1;
} else {
if (data.price_btc !== undefined && data.price_btc !== null) {
priceBTC = data.price_btc;
}
else if (window.btcPriceUSD && window.btcPriceUSD > 0) {
priceBTC = priceUSD / window.btcPriceUSD;
}
else if (app && app.btcPriceUSD && app.btcPriceUSD > 0) {
priceBTC = priceUSD / app.btcPriceUSD;
}
else {
priceBTC = 0;
}
}
priceChange1d = data.price_change_percentage_24h || 0;
volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null;
if (isNaN(priceUSD) || isNaN(priceBTC)) {
throw new Error(`Invalid numeric values in data for ${coin}`);
}
updateUI(false);
} catch (error) {
logger.error(`Failed to display data for ${coin}:`, error.message);
updateUI(true);
}
},
},
showLoader: () => {
const loader = document.getElementById('loader');
@@ -474,7 +430,7 @@ const ui = {
chartContainer.classList.add('blurred');
if (duration > 0) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
ui.hideErrorMessage();
}, duration);
}
@@ -554,7 +510,7 @@ const chartModule = {
this.chartRefs.set(element, chart);
},
destroyChart: function() {
destroyChart: function() {
if (chartModule.chart) {
try {
const chartInstance = chartModule.chart;
@@ -568,12 +524,17 @@ const chartModule = {
if (canvas) {
chartModule.chartRefs.delete(canvas);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
} catch (e) {
console.error('Error destroying chart:', e);
}
}
},
},
initChart: function() {
this.destroyChart();
@@ -900,8 +861,11 @@ const chartModule = {
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
data = allData[coinSymbol];
if (!data || Object.keys(data).length === 0) {
throw new Error(`No data returned for ${coinSymbol}`);
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
console.warn(`No price data available for ${coinSymbol}`);
chartModule.hideChartLoader();
chartModule.showNoDataMessage(coinSymbol);
return;
}
CacheManager.set(cacheKey, data, 'chart');
@@ -931,6 +895,8 @@ const chartModule = {
chartModule.initChart();
}
chartModule.hideNoDataMessage();
const chartData = chartModule.prepareChartData(coinSymbol, data);
if (chartData.length > 0 && chartModule.chart) {
chartModule.chart.data.datasets[0].data = chartData;
@@ -985,6 +951,41 @@ const chartModule = {
chart.classList.remove('hidden');
},
showNoDataMessage: function(coinSymbol) {
const chartCanvas = document.getElementById('coin-chart');
if (!chartCanvas) {
return;
}
if (this.chart) {
this.chart.data.datasets[0].data = [];
this.chart.update('none');
}
let messageDiv = document.getElementById('chart-no-data-message');
if (!messageDiv) {
messageDiv = document.createElement('div');
messageDiv.id = 'chart-no-data-message';
messageDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #888; font-size: 14px; z-index: 10;';
chartCanvas.parentElement.style.position = 'relative';
chartCanvas.parentElement.appendChild(messageDiv);
}
messageDiv.innerHTML = `
<div style="padding: 20px; background: rgba(0,0,0,0.05); border-radius: 8px;">
<div style="font-size: 16px; margin-bottom: 8px;">No Price Data Available</div>
</div>
`;
messageDiv.classList.remove('hidden');
},
hideNoDataMessage: function() {
const messageDiv = document.getElementById('chart-no-data-message');
if (messageDiv) {
messageDiv.classList.add('hidden');
}
},
cleanup: function() {
if (this.pendingAnimationFrame) {
cancelAnimationFrame(this.pendingAnimationFrame);
@@ -1057,7 +1058,7 @@ const app = {
nextRefreshTime: null,
lastRefreshedTime: null,
isRefreshing: false,
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'true',
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') === 'true',
updateNextRefreshTimeRAF: null,
refreshTexts: {
@@ -1107,15 +1108,32 @@ const app = {
if (chartModule.chart) {
window.config.currentResolution = 'day';
await chartModule.updateChart('BTC');
app.updateResolutionButtons('BTC');
let defaultCoin = null;
if (window.config.coins && window.config.coins.length > 0) {
for (const coin of window.config.coins) {
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
if (container) {
defaultCoin = coin.symbol;
break;
}
}
}
if (!defaultCoin) {
defaultCoin = 'BTC';
}
await chartModule.updateChart(defaultCoin);
app.updateResolutionButtons(defaultCoin);
const chartTitle = document.getElementById('chart-title');
if (chartTitle) {
chartTitle.textContent = 'Price Chart (BTC)';
chartTitle.textContent = `Price Chart (${defaultCoin})`;
}
ui.setActiveContainer(`${defaultCoin.toLowerCase()}-container`);
}
ui.setActiveContainer('btc-container');
app.setupEventListeners();
app.initAutoRefresh();
@@ -1153,11 +1171,11 @@ const app = {
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
}
@@ -1185,11 +1203,7 @@ const app = {
} else {
try {
ui.showCoinLoader(coin.symbol);
if (coin.usesCoinGecko) {
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
} else {
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
}
if (data.error) {
throw new Error(data.error);
}
@@ -1336,7 +1350,7 @@ const app = {
}
const timeUntilRefresh = nextRefreshTime - now;
app.nextRefreshTime = nextRefreshTime;
app.autoRefreshInterval = setTimeout(() => {
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
if (NetworkManager.isOnline()) {
app.refreshAllData();
} else {
@@ -1348,7 +1362,6 @@ const app = {
},
refreshAllData: async function() {
console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...');
@@ -1369,7 +1382,7 @@ refreshAllData: async function() {
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
let remainingTime = seconds;
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
@@ -1382,7 +1395,6 @@ refreshAllData: async function() {
return;
}
console.log('Starting refresh of all data...');
app.isRefreshing = true;
app.updateNextRefreshTime();
ui.showLoader();
@@ -1397,7 +1409,7 @@ refreshAllData: async function() {
console.warn('BTC price update failed, continuing with cached or default value');
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
@@ -1422,11 +1434,11 @@ refreshAllData: async function() {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
} else {
try {
@@ -1449,15 +1461,13 @@ refreshAllData: async function() {
const cacheKey = `coinData_${coin.symbol}`;
CacheManager.set(cacheKey, coinData, 'prices');
console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
} catch (coinError) {
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
failedCoins.push(coin.symbol);
}
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
if (chartModule.currentCoin) {
try {
@@ -1479,7 +1489,7 @@ refreshAllData: async function() {
let countdown = 5;
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
@@ -1489,7 +1499,6 @@ refreshAllData: async function() {
}
}, 1000);
}
console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
} catch (error) {
console.error('Critical error during refresh:', error);
@@ -1498,7 +1507,7 @@ refreshAllData: async function() {
let countdown = 10;
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
@@ -1520,7 +1529,6 @@ refreshAllData: async function() {
app.scheduleNextRefresh();
}
console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
}
},
@@ -1544,7 +1552,7 @@ refreshAllData: async function() {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.add('animate-spin');
setTimeout(() => {
CleanupManager.setTimeout(() => {
svg.classList.remove('animate-spin');
}, 2000);
}
@@ -1746,7 +1754,14 @@ document.addEventListener('DOMContentLoaded', () => {
app.init();
if (window.MemoryManager) {
if (typeof MemoryManager.enableAutoCleanup === 'function') {
MemoryManager.enableAutoCleanup();
} else {
MemoryManager.initialize({
autoCleanup: true,
debug: false
});
}
}
CleanupManager.setInterval(() => {

View File

@@ -0,0 +1,332 @@
(function() {
'use strict';
const SettingsPage = {
confirmCallback: null,
triggerElement: null,
init: function() {
this.setupTabs();
this.setupCoinHeaders();
this.setupConfirmModal();
this.setupNotificationSettings();
},
setupTabs: function() {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
const switchTab = (targetTab) => {
tabButtons.forEach(btn => {
if (btn.dataset.tab === targetTab) {
btn.className = 'tab-button border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
} else {
btn.className = 'tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
}
});
tabContents.forEach(content => {
if (content.id === targetTab) {
content.classList.remove('hidden');
} else {
content.classList.add('hidden');
}
});
};
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
switchTab(btn.dataset.tab);
});
});
},
setupCoinHeaders: function() {
const coinHeaders = document.querySelectorAll('.coin-header');
coinHeaders.forEach(header => {
header.addEventListener('click', function() {
const coinName = this.dataset.coin;
const details = document.getElementById(`details-${coinName}`);
const arrow = this.querySelector('.toggle-arrow');
if (details.classList.contains('hidden')) {
details.classList.remove('hidden');
arrow.style.transform = 'rotate(180deg)';
} else {
details.classList.add('hidden');
arrow.style.transform = 'rotate(0deg)';
}
});
});
},
setupConfirmModal: function() {
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (typeof this.confirmCallback === 'function') {
this.confirmCallback();
}
this.hideConfirmDialog();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', () => {
this.hideConfirmDialog();
});
}
},
showConfirmDialog: function(title, message, callback) {
this.confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
},
hideConfirmDialog: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
this.confirmCallback = null;
return false;
},
confirmDisableCoin: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Disable Coin",
"Are you sure you want to disable this coin?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
setupNotificationSettings: function() {
const notificationsTab = document.getElementById('notifications-tab');
if (notificationsTab) {
notificationsTab.addEventListener('click', () => {
CleanupManager.setTimeout(() => this.syncNotificationSettings(), 100);
});
}
document.addEventListener('change', (e) => {
if (e.target.closest('#notifications')) {
this.syncNotificationSettings();
}
});
this.syncNotificationSettings();
},
syncNotificationSettings: function() {
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
const backendSettings = {
showNewOffers: document.getElementById('notifications_new_offers')?.checked || false,
showNewBids: document.getElementById('notifications_new_bids')?.checked || false,
showBidAccepted: document.getElementById('notifications_bid_accepted')?.checked || false,
showBalanceChanges: document.getElementById('notifications_balance_changes')?.checked || false,
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions')?.checked || false,
showSwapCompleted: document.getElementById('notifications_swap_completed')?.checked || false,
showUpdateNotifications: document.getElementById('check_updates')?.checked || false,
notificationDuration: parseInt(document.getElementById('notifications_duration')?.value || '5') * 1000
};
window.NotificationManager.updateSettings(backendSettings);
}
},
testUpdateNotification: function() {
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v0.14.6 • Click to view release (Test/Dummy)',
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
}
);
}
},
testLiveUpdateCheck: function(event) {
const button = event?.target || event?.currentTarget || document.querySelector('[onclick*="testLiveUpdateCheck"]');
if (!button) return;
const originalText = button.textContent;
button.textContent = 'Checking...';
button.disabled = true;
fetch('/json/checkupdates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (window.NotificationManager) {
const currentVer = data.current_version || 'Unknown';
const latestVer = data.latest_version || currentVer;
if (data.update_available) {
window.NotificationManager.createToast(
`Live Update Available: v${latestVer}`,
'update_available',
{
latest_version: latestVer,
current_version: currentVer,
subtitle: `Current: v${currentVer} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
releaseNotes: 'This is a real update check from GitHub API.'
}
);
} else {
window.NotificationManager.createToast(
'No Updates Available',
'success',
{
subtitle: `Current version v${currentVer} is up to date`
}
);
}
}
})
.catch(error => {
console.error('Update check failed:', error);
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: 'Could not check for updates. See console for details.'
}
);
}
})
.finally(() => {
if (button) {
button.textContent = originalText;
button.disabled = false;
}
});
},
checkForUpdatesNow: function(event) {
const button = event?.target || event?.currentTarget || document.querySelector('[data-check-updates]');
if (!button) return;
const originalText = button.textContent;
button.textContent = 'Checking...';
button.disabled = true;
fetch('/json/checkupdates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: data.error
}
);
}
return;
}
if (window.NotificationManager) {
const currentVer = data.current_version || 'Unknown';
const latestVer = data.latest_version || currentVer;
if (data.update_available) {
window.NotificationManager.createToast(
`Update Available: v${latestVer}`,
'update_available',
{
latest_version: latestVer,
current_version: currentVer,
subtitle: `Current: v${currentVer} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
}
);
} else {
window.NotificationManager.createToast(
'You\'re Up to Date!',
'success',
{
subtitle: `Current version v${currentVer} is the latest`
}
);
}
}
})
.catch(error => {
console.error('Update check failed:', error);
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: 'Network error. Please try again later.'
}
);
}
})
.finally(() => {
if (button) {
button.textContent = originalText;
button.disabled = false;
}
});
}
};
SettingsPage.cleanup = function() {
};
document.addEventListener('DOMContentLoaded', function() {
SettingsPage.init();
if (window.CleanupManager) {
CleanupManager.registerResource('settingsPage', SettingsPage, (page) => {
if (page.cleanup) page.cleanup();
});
}
});
window.SettingsPage = SettingsPage;
window.syncNotificationSettings = SettingsPage.syncNotificationSettings.bind(SettingsPage);
window.testUpdateNotification = SettingsPage.testUpdateNotification.bind(SettingsPage);
window.testLiveUpdateCheck = SettingsPage.testLiveUpdateCheck.bind(SettingsPage);
window.checkForUpdatesNow = SettingsPage.checkForUpdatesNow.bind(SettingsPage);
window.showConfirmDialog = SettingsPage.showConfirmDialog.bind(SettingsPage);
window.hideConfirmDialog = SettingsPage.hideConfirmDialog.bind(SettingsPage);
window.confirmDisableCoin = SettingsPage.confirmDisableCoin.bind(SettingsPage);
})();

View File

@@ -127,9 +127,9 @@ const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
if (timeLeft <= 300) return '#9CA3AF';
if (timeLeft <= 1800) return '#3B82F6';
return '#10B981';
};
const updateConnectionStatus = (status) => {
@@ -293,12 +293,33 @@ const createSwapTableRow = async (swap) => {
const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from;
const toSymbol = window.CoinManager.getSymbol(swap.coin_to) || swap.coin_to;
const fromSymbol = window.CoinManager.getDisplayName(swap.coin_from) || swap.coin_from;
const toSymbol = window.CoinManager.getDisplayName(swap.coin_to) || swap.coin_to;
const timeColor = getTimeStrokeColor(swap.expire_at);
const fromAmount = parseFloat(swap.amount_from) || 0;
const toAmount = parseFloat(swap.amount_to) || 0;
let send_column = "";
let recv_column = "";
if (swap.was_sent) {
send_column = `
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
`
recv_column = `
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
`
} else {
send_column = `
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
`
recv_column = `
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
`
}
return `
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${swap.bid_id}">
<td class="relative w-0 p-0 m-0">
@@ -356,8 +377,7 @@ const createSwapTableRow = async (swap) => {
<div class="py-3 px-4 text-left">
<div class="items-center monospace">
<div class="pr-2">
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
${send_column}
</div>
</div>
</div>
@@ -369,7 +389,7 @@ const createSwapTableRow = async (swap) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_from)}"
alt="${swap.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -378,7 +398,7 @@ const createSwapTableRow = async (swap) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_to)}"
alt="${swap.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -390,8 +410,7 @@ const createSwapTableRow = async (swap) => {
<td class="py-0">
<div class="py-3 px-4 text-right">
<div class="items-center monospace">
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
${recv_column}
</div>
</div>
</td>
@@ -501,8 +520,6 @@ const createSwapTableRow = async (swap) => {
async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
//console.log('Updating swaps table:', { resetPage, refreshData });
if (state.refreshPromise) {
await state.refreshPromise;
return;
@@ -528,19 +545,17 @@ async function updateSwapsTable(options = {}) {
}
const data = await response.json();
//console.log('Received swap data:', data);
state.swapsData = Array.isArray(data)
? data.filter(swap => {
const isActive = isActiveSwap(swap);
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
return isActive;
})
: [];
//console.log('Filtered active swaps:', state.swapsData);
} catch (error) {
//console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
@@ -566,8 +581,6 @@ async function updateSwapsTable(options = {}) {
const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
//console.log('Current page swaps:', currentPageSwaps);
if (elements.swapsBody) {
if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
@@ -588,7 +601,7 @@ async function updateSwapsTable(options = {}) {
});
}
} else {
//console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
@@ -660,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
WebSocketManager.initialize();
setupEventListeners();
await updateSwapsTable({ resetPage: true, refreshData: true });
const autoRefreshInterval = setInterval(async () => {
const autoRefreshInterval = CleanupManager.setInterval(async () => {
await updateSwapsTable({ resetPage: false, refreshData: true });
}, 10000); // 30 seconds
}, 10000);
CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => {
clearInterval(autoRefreshInterval);
});
});

View File

@@ -0,0 +1,372 @@
(function() {
'use strict';
const WalletPage = {
confirmCallback: null,
triggerElement: null,
currentCoinId: '',
activeTooltip: null,
init: function() {
this.setupAddressCopy();
this.setupConfirmModal();
this.setupWithdrawalConfirmation();
this.setupTransactionDisplay();
this.setupWebSocketUpdates();
},
setupAddressCopy: function() {
const copyableElements = [
'main_deposit_address',
'monero_main_address',
'monero_sub_address',
'stealth_address'
];
copyableElements.forEach(id => {
const element = document.getElementById(id);
if (!element) return;
element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors');
if (!element.querySelector('.copy-icon')) {
const copyIcon = document.createElement('span');
copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300';
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>`;
element.style.position = 'relative';
element.style.paddingRight = '2.5rem';
element.appendChild(copyIcon);
}
element.addEventListener('click', (e) => {
const textToCopy = element.innerText.trim();
this.copyToClipboard(textToCopy);
element.classList.add('bg-blue-50', 'dark:bg-blue-900');
this.showCopyFeedback(element);
CleanupManager.setTimeout(() => {
element.classList.remove('bg-blue-50', 'dark:bg-blue-900');
}, 1000);
});
});
},
copyToClipboard: function(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
console.log('Address copied to clipboard');
}).catch(err => {
console.error('Failed to copy address:', err);
this.fallbackCopyToClipboard(text);
});
} else {
this.fallbackCopyToClipboard(text);
}
},
fallbackCopyToClipboard: function(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
console.log('Address copied to clipboard (fallback)');
} catch (err) {
console.error('Fallback: Failed to copy address', err);
}
document.body.removeChild(textArea);
},
showCopyFeedback: function(element) {
if (this.activeTooltip && this.activeTooltip.parentNode) {
this.activeTooltip.parentNode.removeChild(this.activeTooltip);
}
const popup = document.createElement('div');
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
popup.innerText = 'Copied!';
document.body.appendChild(popup);
this.activeTooltip = popup;
this.updateTooltipPosition(popup, element);
const scrollHandler = () => {
if (popup.parentNode) {
requestAnimationFrame(() => {
this.updateTooltipPosition(popup, element);
});
}
};
window.addEventListener('scroll', scrollHandler, { passive: true });
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.2s ease-in-out';
CleanupManager.setTimeout(() => {
popup.style.opacity = '1';
}, 10);
CleanupManager.setTimeout(() => {
window.removeEventListener('scroll', scrollHandler);
popup.style.opacity = '0';
CleanupManager.setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
if (this.activeTooltip === popup) {
this.activeTooltip = null;
}
}, 200);
}, 1500);
},
updateTooltipPosition: function(tooltip, element) {
const rect = element.getBoundingClientRect();
let top = rect.top - tooltip.offsetHeight - 8;
const left = rect.left + rect.width / 2;
if (top < 10) {
top = rect.bottom + 8;
}
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
tooltip.style.transform = 'translateX(-50%)';
},
setupWithdrawalConfirmation: function() {
const withdrawalClickHandler = (e) => {
const target = e.target.closest('[data-confirm-withdrawal]');
if (target) {
e.preventDefault();
this.triggerElement = target;
this.confirmWithdrawal().catch(() => {
});
}
};
document.addEventListener('click', withdrawalClickHandler);
if (window.CleanupManager) {
CleanupManager.registerResource('walletWithdrawalClick', withdrawalClickHandler, () => {
document.removeEventListener('click', withdrawalClickHandler);
});
}
},
setupConfirmModal: function() {
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (this.confirmCallback && typeof this.confirmCallback === 'function') {
this.confirmCallback();
}
this.hideConfirmDialog();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', () => {
this.hideConfirmDialog();
});
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal) {
this.hideConfirmDialog();
}
});
}
},
showConfirmDialog: function(title, message, callback) {
return new Promise((resolve, reject) => {
this.confirmCallback = () => {
if (callback) callback();
resolve();
};
this.confirmReject = reject;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
});
},
hideConfirmDialog: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
if (this.confirmReject) {
this.confirmReject();
}
this.confirmCallback = null;
this.confirmReject = null;
return false;
},
confirmReseed: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Reseed Wallet",
"Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmWithdrawal: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Withdrawal",
"Are you sure you want to proceed with this withdrawal?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmCreateUTXO: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Create UTXO",
"Are you sure you want to create this UTXO?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmUTXOResize: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm UTXO Resize",
"Are you sure you want to resize UTXOs?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
setupTransactionDisplay: function() {
},
setupWebSocketUpdates: function() {
if (window.BalanceUpdatesManager) {
const coinId = this.getCoinIdFromPage();
if (coinId) {
this.currentCoinId = coinId;
window.BalanceUpdatesManager.setup({
contextKey: 'wallet_' + coinId,
balanceUpdateCallback: this.handleBalanceUpdate.bind(this),
swapEventCallback: this.handleSwapEvent.bind(this),
errorContext: 'Wallet',
enablePeriodicRefresh: true,
periodicInterval: 60000
});
}
}
},
getCoinIdFromPage: function() {
const pathParts = window.location.pathname.split('/');
const walletIndex = pathParts.indexOf('wallet');
if (walletIndex !== -1 && pathParts[walletIndex + 1]) {
return pathParts[walletIndex + 1];
}
return null;
},
handleBalanceUpdate: function(balanceData) {
console.log('Balance updated:', balanceData);
},
handleSwapEvent: function(eventData) {
console.log('Swap event:', eventData);
}
};
document.addEventListener('DOMContentLoaded', function() {
WalletPage.init();
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.initialize();
}
});
window.WalletPage = WalletPage;
window.setupAddressCopy = WalletPage.setupAddressCopy.bind(WalletPage);
window.showConfirmDialog = WalletPage.showConfirmDialog.bind(WalletPage);
window.hideConfirmDialog = WalletPage.hideConfirmDialog.bind(WalletPage);
window.confirmReseed = WalletPage.confirmReseed.bind(WalletPage);
window.confirmWithdrawal = WalletPage.confirmWithdrawal.bind(WalletPage);
window.confirmCreateUTXO = WalletPage.confirmCreateUTXO.bind(WalletPage);
window.confirmUTXOResize = WalletPage.confirmUTXOResize.bind(WalletPage);
window.copyToClipboard = WalletPage.copyToClipboard.bind(WalletPage);
window.showCopyFeedback = WalletPage.showCopyFeedback.bind(WalletPage);
})();

View File

@@ -0,0 +1,344 @@
(function() {
'use strict';
const WalletsPage = {
init: function() {
this.setupWebSocketUpdates();
},
setupWebSocketUpdates: function() {
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
window.WebSocketManager.initialize();
}
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.setup({
contextKey: 'wallets',
balanceUpdateCallback: this.updateWalletBalances.bind(this),
swapEventCallback: this.updateWalletBalances.bind(this),
errorContext: 'Wallets',
enablePeriodicRefresh: true,
periodicInterval: 60000
});
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'price_updated' || data.event === 'prices_updated') {
clearTimeout(window.walletsPriceUpdateTimeout);
window.walletsPriceUpdateTimeout = CleanupManager.setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 500);
}
}
});
window.walletsPriceHandlerId = priceHandlerId;
}
}
},
updateWalletBalances: function(balanceData) {
if (balanceData) {
balanceData.forEach(coin => {
this.updateWalletDisplay(coin);
});
CleanupManager.setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 250);
} else {
window.BalanceUpdatesManager.fetchBalanceData()
.then(data => this.updateWalletBalances(data))
.catch(error => {
console.error('Error updating wallet balances:', error);
});
}
},
updateWalletDisplay: function(coinData) {
if (coinData.name === 'Particl') {
this.updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
} else if (coinData.name === 'Particl Anon') {
this.updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
this.removePendingBalance('Particl', 'Anon Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
}
} else if (coinData.name === 'Particl Blind') {
this.updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
this.removePendingBalance('Particl', 'Blind Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData);
} else {
this.removePendingDisplay(coinData.name);
}
}
}
},
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) {
if (isPending) {
const cleanBalance = balance.toString().replace(/^\+/, '');
element.textContent = `+${cleanBalance} ${ticker}`;
} else {
element.textContent = `${balance} ${ticker}`;
}
}
}
}
});
},
updatePendingDisplay: function(coinData) {
const walletContainer = this.findWalletContainer(coinData.name);
if (!walletContainer) return;
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
let staticPendingElement = null;
let staticUsdElement = null;
existingPendingElements.forEach(element => {
const labelElement = element.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
staticPendingElement = element;
} else if (labelText.includes('Pending USD value:')) {
staticUsdElement = element;
}
}
});
if (staticPendingElement && staticUsdElement) {
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
if (pendingSpan) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
}
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = staticUsdElement.querySelector('.usd-value');
if (usdDiv) {
usdDiv.textContent = initialUSD;
}
return;
}
let pendingContainer = walletContainer.querySelector('.pending-container');
if (!pendingContainer) {
const balanceContainer = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
if (!balanceContainer) return;
pendingContainer = document.createElement('div');
pendingContainer.className = 'pending-container';
balanceContainer.parentNode.insertBefore(pendingContainer, balanceContainer.nextSibling);
}
pendingContainer.innerHTML = '';
const pendingDiv = document.createElement('div');
pendingDiv.className = 'flex mb-2 justify-between items-center';
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending:</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
`;
pendingContainer.appendChild(pendingDiv);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = document.createElement('div');
usdDiv.className = 'flex mb-2 justify-between items-center';
usdDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
pendingContainer.appendChild(usdDiv);
},
removePendingDisplay: function(coinName) {
const walletContainer = this.findWalletContainer(coinName);
if (!walletContainer) return;
const pendingContainer = walletContainer.querySelector('.pending-container');
if (pendingContainer) {
pendingContainer.remove();
}
},
findWalletContainer: function(coinName) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-white, .dark\\:bg-gray-500');
}
}
return null;
},
removePendingBalance: function(coinName, balanceType) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel.includes('Pending:') || currentLabel.includes('Unconfirmed:')) {
const nextElement = parentDiv.nextElementSibling;
if (nextElement && nextElement.querySelector('h4')?.textContent.includes('USD value:')) {
nextElement.remove();
}
parentDiv.remove();
}
}
}
});
},
updatePendingBalance: function(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let targetElement = null;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
if (parentElement) {
const labelElement = parentElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes(balanceType)) {
targetElement = parentElement;
}
}
}
});
if (!targetElement) return;
let insertAfterElement = targetElement;
let nextElement = targetElement.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
insertAfterElement = nextElement;
break;
}
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
break;
}
}
nextElement = nextElement.nextElementSibling;
}
let pendingElement = insertAfterElement.nextElementSibling;
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
pendingElement = pendingElement.nextElementSibling;
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
pendingElement = null;
break;
}
}
if (!pendingElement) {
const newPendingDiv = document.createElement('div');
newPendingDiv.className = 'flex mb-2 justify-between items-center';
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
newPendingDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel}</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinName}">+${cleanPending} ${ticker}</span>
`;
insertAfterElement.parentNode.insertBefore(newPendingDiv, insertAfterElement.nextSibling);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinName.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = document.createElement('div');
usdDiv.className = 'flex mb-2 justify-between items-center';
usdDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel.replace(':', '')} USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
newPendingDiv.parentNode.insertBefore(usdDiv, newPendingDiv.nextSibling);
} else {
const pendingSpan = pendingElement.querySelector('.coinname-value');
if (pendingSpan) {
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
}
}
}
};
document.addEventListener('DOMContentLoaded', function() {
WalletsPage.init();
});
window.WalletsPage = WalletsPage;
})();

View File

@@ -89,7 +89,7 @@
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
});
window.addEventListener('scroll', this._handleScroll, true);
window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true });
window.addEventListener('resize', this._handleResize);
}
@@ -170,7 +170,7 @@
destroy() {
document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('scroll', this._handleScroll, { passive: true, capture: true });
window.removeEventListener('resize', this._handleResize);
const index = dropdownInstances.indexOf(this);

View File

@@ -1,21 +1,13 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/404">404</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': '404', 'url': '/404'}
]) }}
</div>
</div>
</section>

View File

@@ -1,21 +1,8 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
{% from 'macros.html' import page_header %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Swaps in Progress</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Monitor your currently active swap transactions.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('Swaps in Progress', 'Monitor your currently active swap transactions.') }}
{% include 'inc_messages.html' %}
@@ -113,6 +100,6 @@
</div>
</section>
<script src="/static/js/swaps_in_progress.js"></script>
<script src="/static/js/pages/swaps-page.js"></script>
{% include 'footer.html' %}

1396
basicswap/templates/amm.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,14 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %}
{% from 'style.html' import white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -1,24 +1,14 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">ID:<!-- todo ID here {{ strategy_id }} --></a>
</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'},
{'text': 'ID:', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -1,24 +1,14 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">New</a>
</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'},
{'text': 'New', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -1,25 +1,16 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ bid_id }}">BID ID: {{ bid_id }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Bids', 'url': '#'},
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
]) }}
</div>
</div>
</section>
@@ -76,7 +67,7 @@
</th>
</tr>
</thead>
{% if data.was_sent == 'True' %}
{% if data.was_sent %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Swap</td>
<td class="py-3 px-6">
@@ -103,7 +94,7 @@
<td class="py-3 px-6 bold">Bid Rate</td>
<td class="py-3 px-6">{{ data.bid_rate }}</td>
</tr>
{% if data.was_sent == 'True' %}
{% if data.was_sent %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Send</td>
<td class="py-3 px-6">
@@ -183,12 +174,8 @@
<td class="py-3 px-6">{{ data.expired_at }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Sent</td>
<td class="py-3 px-6">{{ data.was_sent }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Received</td>
<td class="py-3 px-6">{{ data.was_received }}</td>
<td class="py-3 px-6 bold">Bid Type</td>
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Initiate Tx</td>
@@ -536,16 +523,16 @@
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
</div>
{% endif %}
{% endif %}
{% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
{% endif %}
</div>
@@ -560,10 +547,39 @@
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6" id="confirmTitle">Confirm Action</h2>
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
{% if data.was_sent %}
{{ data.amt_to }} {{ data.ticker_to }}
{% else %}
{{ data.amt_from }} {{ data.ticker_from }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
{% if data.was_sent %}
{{ data.amt_from }} {{ data.ticker_from }}
{% else %}
{{ data.amt_to }} {{ data.ticker_to }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
</div>
</div>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<div class="flex justify-center gap-4">
<button type="button" id="confirmYes"
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">
@@ -598,10 +614,22 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback) {
function showConfirmDialog(title, message, callback, showBidDetails = false) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const bidDetailsSection = document.getElementById('bidDetailsSection');
const confirmMessage = document.getElementById('confirmMessage');
if (showBidDetails && bidDetailsSection) {
bidDetailsSection.classList.remove('hidden');
confirmMessage.classList.add('hidden');
} else {
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
confirmMessage.classList.remove('hidden');
}
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
@@ -621,7 +649,14 @@ document.addEventListener('DOMContentLoaded', function() {
window.confirmPopup = function(action = 'Abandon') {
triggerElement = document.activeElement;
const title = `Confirm ${action} Bid`;
const message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
const showBidDetails = action.toLowerCase() === 'accept';
let message;
if (showBidDetails) {
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
} else {
message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
}
return showConfirmDialog(title, message, function() {
if (triggerElement) {
@@ -633,7 +668,7 @@ document.addEventListener('DOMContentLoaded', function() {
form.appendChild(hiddenInput);
form.submit();
}
});
}, showBidDetails);
};
const overrideButtonConfirm = function(button, action) {

View File

@@ -1,25 +1,16 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ bid_id }}">BID ID: {{ bid_id }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Bids', 'url': '#'},
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
]) }}
</div>
</div>
</section>
@@ -76,7 +67,7 @@
</th>
</tr>
</thead>
{% if data.was_sent == 'True' %}
{% if data.was_sent %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Swap</td>
<td class="py-3 px-6">
@@ -103,7 +94,7 @@
<td class="py-3 px-6 bold">Bid Rate</td>
<td class="py-3 px-6">{{ data.bid_rate }}</td>
</tr>
{% if data.was_sent == 'True' %}
{% if data.was_sent %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Send</td>
<td class="py-3 px-6">
@@ -188,12 +179,8 @@
<td class="py-3 px-6">{{ data.expired_at }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Sent</td>
<td class="py-3 px-6">{{ data.was_sent }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Received</td>
<td class="py-3 px-6">{{ data.was_received }}</td>
<td class="py-3 px-6 bold">Bid Type</td>
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}{% if data.reverse_bid %} (Transposed){% endif %}</td>
</tr>
{% if data.coin_a_lock_refund_tx_est_final != 'None' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -812,16 +799,16 @@
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
</div>
{% endif %}
{% endif %}
{% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
{% endif %}
</div>
@@ -833,6 +820,56 @@
</div>
</div>
</section>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6" id="confirmTitle">Confirm Action</h2>
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
{% if data.was_sent %}
{{ data.amt_to }} {{ data.ticker_to }}
{% else %}
{{ data.amt_from }} {{ data.ticker_from }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
{% if data.was_sent %}
{{ data.amt_from }} {{ data.ticker_from }}
{% else %}
{{ data.amt_to }} {{ data.ticker_to }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
</div>
</div>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<div class="flex justify-center gap-4">
<button type="button" id="confirmYes"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Confirm
</button>
<button type="button" id="confirmNo"
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
</div>
</div>
@@ -840,9 +877,93 @@
</div>
</form>
<script>
function confirmPopup(name) {
return confirm(name + " Bid - Are you sure?");
document.addEventListener('DOMContentLoaded', function() {
let confirmCallback = null;
let triggerElement = null;
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback, showBidDetails = false) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const bidDetailsSection = document.getElementById('bidDetailsSection');
const confirmMessage = document.getElementById('confirmMessage');
if (showBidDetails && bidDetailsSection) {
bidDetailsSection.classList.remove('hidden');
confirmMessage.classList.add('hidden');
} else {
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
confirmMessage.classList.remove('hidden');
}
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
function hideConfirmDialog() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
confirmCallback = null;
return false;
}
window.confirmPopup = function(action = 'Abandon') {
triggerElement = document.activeElement;
const title = `Confirm ${action} Bid`;
const showBidDetails = action.toLowerCase() === 'accept';
let message;
if (showBidDetails) {
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
} else {
message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
}
return showConfirmDialog(title, message, function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}, showBidDetails);
};
const overrideButtonConfirm = function(button, action) {
if (button) {
button.removeAttribute('onclick');
button.addEventListener('click', function(e) {
e.preventDefault();
triggerElement = this;
return confirmPopup(action);
});
}
};
const abandonBidBtn = document.querySelector('button[name="abandon_bid"]');
overrideButtonConfirm(abandonBidBtn, 'Abandon');
const acceptBidBtn = document.querySelector('button[name="accept_bid"]');
overrideButtonConfirm(acceptBidBtn, 'Accept');
});
</script>
</div>
{% include 'footer.html' %}

View File

@@ -1,22 +1,8 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
{% from 'macros.html' import page_header %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('All Bids / Sent Bids / Received Bids', 'View, and manage bids.') }}
{% include 'inc_messages.html' %}
@@ -28,7 +14,12 @@
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="all-tab" data-tabs-target="#all" type="button" role="tab" aria-controls="all" aria-selected="true">
All Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count + received_bids_count }})</span>
</button>
</li>
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="false">
Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span>
</button>
</li>
@@ -167,7 +158,106 @@
</section>
<div id="bidstab">
<div class="rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<!-- All Bids Tab -->
<div class="rounded-lg lg:px-6" id="all" role="tabpanel" aria-labelledby="all-tab">
<div id="all-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto overflow-auto lg:overflow-hidden">
<table class="w-full lg:min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0 hidden lg:block">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody id="all-tbody">
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot-all" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text-all" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400">
All Bids: <span id="allBidsCount">0</span>
</p>
{% if debug_ui_mode == true %}
<button id="refreshAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshAllText">Refresh</span>
</button>
{% endif %}
<button id="exportAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>Export CSV</span>
</button>
</div>
<div id="pagination-controls-all" class="flex items-center space-x-2" style="display: none;">
<button id="prevPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageAll">1</span></p>
<button id="nextPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sent Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div id="sent-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
@@ -208,7 +298,7 @@
</th>
</tr>
</thead>
<tbody>
<tbody id="sent-tbody">
</tbody>
</table>
</div>
@@ -264,6 +354,7 @@
</div>
</div>
<!-- Received Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab">
<div id="received-content">
<div class="xl:container mx-auto lg:px-0">
@@ -278,7 +369,7 @@
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0">
<th class="p-0 hidden lg:block">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
@@ -305,7 +396,7 @@
</th>
</tr>
</thead>
<tbody>
<tbody id="received-tbody">
</tbody>
</table>
</div>
@@ -362,7 +453,8 @@
</div>
</div>
<script src="/static/js/bids_sentreceived.js"></script>
<script src="/static/js/bids_sentreceived_export.js"></script>
<script src="/static/js/pages/bids-tab-navigation.js"></script>
<script src="/static/js/pages/bids-page.js"></script>
<script src="/static/js/pages/bids-export.js"></script>
{% include 'footer.html' %}

View File

@@ -1,21 +1,8 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
{% from 'macros.html' import page_header %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Bid Requests</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Review and accept bids from other users.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('Bid Requests', 'Review and accept bids from other users.') }}
{% include 'inc_messages.html' %}
@@ -54,7 +41,7 @@
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Get</span>
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
@@ -113,6 +100,6 @@
</div>
</section>
<script src="/static/js/bids_available.js"></script>
<script src="/static/js/pages/bids-available-page.js"></script>
{% include 'footer.html' %}

View File

@@ -1,29 +1,19 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<!-- Breadcrumb -->
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/changepassword">Change Password</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Change Password', 'url': '/changepassword'}
]) }}
</div>
</div>
</section>
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
@@ -39,7 +29,50 @@
</div>
</div>
</section>
{% include 'inc_messages.html' %}
{% set disabled_coins = [] %}
{% for c in chains_formatted %}
{% if c.connection_type == "none" %}
{% set _ = disabled_coins.append(c.display_name) %}
{% endif %}
{% endfor %}
{% if disabled_coins|length > 0 %}
<section class="py-4 px-6" role="alert">
<div class="lg:container mx-auto">
<div class="p-6 text-red-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-red-500 error_msg"><span class="bold">WARNING:</span></li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">Password Change Blocked - Disabled Coins Detected</li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">
<strong>Changing your password now will break your installation!</strong>
</li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">
The following coins are currently disabled and will NOT have their passwords updated:
</li>
{% for coin_name in disabled_coins %}
<li class="font-medium text-sm text-red-500 error_msg ml-4">• {{ coin_name }}</li>
{% endfor %}
<li class="font-medium text-sm text-red-500 error_msg mb-2 mt-2">
<strong>What this means:</strong> When you re-enable these coins later, they will still have the old password while your other coins have the new password, causing authentication failures.
</li>
<li class="font-medium text-sm text-red-500 error_msg">
<strong>Solution:</strong> Please <a href="/settings" class="underline font-medium">enable all coins</a> before changing your password, or wait until all coins are enabled.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="border-coolGray-100">
@@ -48,138 +81,429 @@
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Password</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold py-3 px-6"></span>
</div>
</th>
</tr>
</thead>
<form method="post" autocomplete="off">
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Old Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-old" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-old" id="input-old-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
</svg>
<form method="post" autocomplete="off" id="change-password-form" {% if disabled_coins|length > 0 %}class="form-disabled"{% endif %}>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-6">
<div>
<label for="oldpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{% if encrypted %}Current Password{% else %}Current Password (leave empty for first-time setup){% endif %}
</label>
</div>
<input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="oldpassword" id="input-old">
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">New Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-new" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-new" id="input-new-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
<div class="relative">
<input
type="password"
id="oldpassword"
name="oldpassword"
class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="{% if encrypted %}Enter your current password{% else %}Leave empty for first-time setup{% endif %}"
{% if disabled_coins|length > 0 %}disabled{% endif %}
{% if encrypted %}required{% endif %}
/>
<button
type="button"
id="toggle-old-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-old" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</label>
</div>
<input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="newpassword" id="input-new">
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Confirm Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-conf" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-conf" id="input-confirm-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
<svg id="eye-closed-old" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
</div>
<div>
<label for="newpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
New Password
</label>
<div class="relative">
<input
type="password"
id="newpassword"
name="newpassword"
class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Enter your new password"
{% if disabled_coins|length > 0 %}disabled{% endif %}
required
/>
<button
type="button"
id="toggle-new-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-new" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg id="eye-closed-new" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="confirmpassword" id="input-confirm">
<div id="caps-warning-new" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
Caps Lock is on
</div>
</td>
</tr>
</table>
<div class="mt-3">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Password Strength:</span>
<span id="strength-text" class="text-sm font-medium text-gray-500 dark:text-gray-400">Enter password</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div id="strength-bar" class="h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500" style="width: 0%"></div>
</div>
</div>
</div>
<div>
<label for="confirmpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Confirm New Password
</label>
<div class="relative">
<input
type="password"
id="confirmpassword"
name="confirmpassword"
class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Confirm your new password"
{% if disabled_coins|length > 0 %}disabled{% endif %}
required
/>
<button
type="button"
id="toggle-confirm-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-confirm" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg id="eye-closed-confirm" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<div id="password-match" class="mt-2 text-sm hidden">
<div id="match-success" class="text-green-600 dark:text-green-400 flex items-center hidden">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Passwords match
</div>
<div id="match-error" class="text-red-600 dark:text-red-400 flex items-center hidden">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L10 10.586l1.293-1.293a1 1 0 001.414 1.414L10 13.414l-1.293-1.293a1 1 0 00-1.414-1.414z" clip-rule="evenodd"></path>
</svg>
Passwords do not match
</div>
</div>
</div>
</div>
<div class="space-y-6">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Password Suggestions</h3>
<div class="space-y-3">
<div id="req-length" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
At least 8 characters
</div>
<div id="req-uppercase" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Uppercase letter (A-Z)
</div>
<div id="req-lowercase" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Lowercase letter (a-z)
</div>
<div id="req-number" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Number (0-9)
</div>
</div>
</div>
</div>
</div>
<div class="mt-8 flex justify-end">
<button
type="submit"
id="submit-btn"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-8 rounded-lg transition-colors focus:outline-none disabled:cursor-not-allowed"
{% if disabled_coins|length > 0 %}disabled{% endif %}
>
<span id="submit-text">{% if disabled_coins|length > 0 %}Disabled - Enable All Coins First{% else %}Change Password{% endif %}</span>
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden ">
<div class="pb-6 ">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
<div class="px-6">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5 ml-2">
<button type="submit" name="unlock" value="Unlock" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Apply</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</section>
{% include 'footer.html' %}
<script>
function togglePassword(event) {
let input_name = 'input-new';
if (event.target.id == 'toggle-old') {
input_name = 'input-old';
} else
if (event.target.id == 'toggle-conf') {
input_name = 'input-confirm';
}
const password = document.getElementById(input_name),
passwordLabel = document.getElementById(input_name + '-label');
if (password.type === 'password') {
password.type = 'text';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>';
} else {
password.type = 'password';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>';
}
password.focus();
}
<script>
document.addEventListener('DOMContentLoaded', function() {
const oldPasswordInput = document.getElementById('oldpassword');
const newPasswordInput = document.getElementById('newpassword');
const confirmPasswordInput = document.getElementById('confirmpassword');
const form = document.getElementById('change-password-form');
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
const toggles = ["toggle-old", "toggle-new", "toggle-conf"]
toggles.forEach(function (toggle_id, index) {
document.getElementById(toggle_id).addEventListener('change', togglePassword, false);
});
</script>
</body>
</html>
setupPasswordToggle('old');
setupPasswordToggle('new');
setupPasswordToggle('confirm');
function setupPasswordToggle(type) {
const toggleBtn = document.getElementById(`toggle-${type}-password`);
const passwordInput = document.getElementById(`${type}password`);
const eyeOpen = document.getElementById(`eye-open-${type}`);
const eyeClosed = document.getElementById(`eye-closed-${type}`);
if (toggleBtn && passwordInput) {
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const isPassword = passwordInput.type === 'password';
const cursorPosition = passwordInput.selectionStart;
const inputValue = passwordInput.value;
passwordInput.type = isPassword ? 'text' : 'password';
passwordInput.value = inputValue;
setTimeout(() => {
passwordInput.setSelectionRange(cursorPosition, cursorPosition);
}, 0);
if (isPassword) {
eyeOpen.classList.add('hidden');
eyeClosed.classList.remove('hidden');
} else {
eyeOpen.classList.remove('hidden');
eyeClosed.classList.add('hidden');
}
});
toggleBtn.addEventListener('mousedown', function(e) {
e.preventDefault();
});
}
}
if (newPasswordInput) {
const capsWarning = document.getElementById('caps-warning-new');
newPasswordInput.addEventListener('keydown', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (capsLockOn && capsWarning) {
capsWarning.classList.remove('hidden');
} else if (capsWarning) {
capsWarning.classList.add('hidden');
}
});
newPasswordInput.addEventListener('keyup', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (!capsLockOn && capsWarning) {
capsWarning.classList.add('hidden');
}
});
}
function calculatePasswordStrength(password) {
let score = 0;
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /\d/.test(password)
};
if (requirements.length) score += 25;
if (requirements.uppercase) score += 25;
if (requirements.lowercase) score += 25;
if (requirements.number) score += 25;
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 10;
if (password.length >= 12) score += 10;
return { score: Math.min(score, 100), requirements };
}
function updatePasswordStrength(password) {
const { score, requirements } = calculatePasswordStrength(password);
const strengthBar = document.getElementById('strength-bar');
const strengthText = document.getElementById('strength-text');
if (strengthBar) {
strengthBar.style.width = `${score}%`;
if (score === 0) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
strengthText.textContent = 'Enter password';
strengthText.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
} else if (score < 40) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
strengthText.textContent = 'Weak';
strengthText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (score < 70) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
strengthText.textContent = 'Fair';
strengthText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
} else if (score < 90) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
strengthText.textContent = 'Good';
strengthText.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
strengthText.textContent = 'Strong';
strengthText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
}
}
updateRequirement('length', requirements.length);
updateRequirement('uppercase', requirements.uppercase);
updateRequirement('lowercase', requirements.lowercase);
updateRequirement('number', requirements.number);
return score >= 60;
}
function updateRequirement(type, met) {
const element = document.getElementById(`req-${type}`);
if (element) {
if (met) {
element.className = 'flex items-center text-green-600 dark:text-green-400';
} else {
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
}
}
}
function checkPasswordMatch() {
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
const matchContainer = document.getElementById('password-match');
const matchSuccess = document.getElementById('match-success');
const matchError = document.getElementById('match-error');
if (confirmPassword.length === 0) {
matchContainer.classList.add('hidden');
return false;
}
matchContainer.classList.remove('hidden');
if (newPassword === confirmPassword) {
matchSuccess.classList.remove('hidden');
matchError.classList.add('hidden');
return true;
} else {
matchSuccess.classList.add('hidden');
matchError.classList.remove('hidden');
return false;
}
}
if (newPasswordInput) {
newPasswordInput.addEventListener('input', function() {
updatePasswordStrength(this.value);
if (confirmPasswordInput.value) {
checkPasswordMatch();
}
});
}
if (confirmPasswordInput) {
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
}
if (form) {
form.addEventListener('submit', function(e) {
if (form.classList.contains('form-disabled')) {
e.preventDefault();
alert('Cannot change password while coins are disabled. Please enable all coins first.');
return;
}
const newPassword = newPasswordInput.value;
const isStrongEnough = updatePasswordStrength(newPassword);
const passwordsMatch = checkPasswordMatch();
if (!isStrongEnough) {
e.preventDefault();
alert('Please choose a stronger password.');
return;
}
if (!passwordsMatch) {
e.preventDefault();
alert('Passwords do not match.');
return;
}
if (submitBtn && submitText && submitSpinner) {
submitBtn.disabled = true;
submitText.textContent = 'Changing Password...';
submitSpinner.classList.remove('hidden');
}
});
}
});
</script>
<style>
.form-disabled {
opacity: 0.6;
pointer-events: none;
}
.form-disabled input[disabled] {
background-color: #f3f4f6 !important;
color: #9ca3af !important;
cursor: not-allowed !important;
}
.form-disabled button[disabled] {
background-color: #9ca3af !important;
cursor: not-allowed !important;
}
.dark .form-disabled input[disabled] {
background-color: #374151 !important;
color: #6b7280 !important;
}
.dark .form-disabled button[disabled] {
background-color: #6b7280 !important;
}
</style>
{% include 'footer.html' %}

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