134 Commits

Author SHA1 Message Date
tecnovert
cd98a8eb64 guix: update packed version 2025-11-15 18:26:41 +02:00
tecnovert
0bc9d3a5db build: raise version to 0.15.1 2025-11-15 17:59:47 +02:00
tecnovert
cd3c8e7c26 Merge pull request #402 from nahuhh/icon_padding
images: uniform padding on coin icons
2025-11-15 15:44:52 +00:00
tecnovert
ff9bbbfe06 Merge pull request #401 from nahuhh/icon_fix
images: change monero icon (white m)
2025-11-15 15:44:46 +00:00
nahuhh
47aab3140f images: uniform padding on coin icons 2025-11-14 05:52:50 +00:00
nahuhh
5512c9fd7f images: change monero icon (white m) 2025-11-14 04:38:25 +00:00
tecnovert
d0614fd7a3 Merge pull request #390 from nahuhh/xmr_interactive
xmr: --non-interactive doesnt have any functionality on wallet
2025-11-11 08:27:51 +00:00
tecnovert
84223fabb0 Merge pull request #398 from basicswap/dependabot/pip/dev/black-25.11.0
build(deps-dev): bump black from 25.9.0 to 25.11.0
2025-11-11 08:27:34 +00:00
tecnovert
4938a203f2 Merge pull request #394 from tecnovert/event_text
doc: clarify LOCK_TX_A_REFUND event descriptions
2025-11-11 08:27:17 +00:00
tecnovert
2a641567ba Merge pull request #393 from justanwar/firo_v0141500_hardfork
firo: v0.14.15.0 hardfork 2025-11-19 mandatory
2025-11-11 08:27:04 +00:00
tecnovert
fbeece4fc9 Merge pull request #389 from nahuhh/ignore-docs
ci: don't run on documentation updates
2025-11-11 08:26:27 +00:00
tecnovert
6906e8ac1b Merge pull request #388 from nahuhh/mac_zmq
docs: add zeromq to mac dependencies
2025-11-11 08:26:08 +00:00
dependabot[bot]
1a86d371c3 build(deps-dev): bump black from 25.9.0 to 25.11.0
Bumps [black](https://github.com/psf/black) from 25.9.0 to 25.11.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.9.0...25.11.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 07:19:44 +00:00
tecnovert
e6c1c86dff doc: clarify LOCK_TX_A_REFUND event descriptions
"Lock tx A refund tx seen in chain" is confusing as that tx must still be spent for the refund to occur.
2025-10-28 23:40:36 +02:00
justanwar
ee8ab69d57 firo: v0.14.15.0 hardfork 2025-11-19 mandatory 2025-10-25 22:52:53 +08:00
nahuhh
2279ed84dc xmr: --non-interactive doesnt have any functionality on wallet 2025-10-23 15:56:56 +00:00
nahuhh
497793ae8c ci: don't run on documentation updates 2025-10-22 15:27:18 +00:00
gerlofvanek
052c722d76 docs: add zeromq to mac dependencies 2025-10-22 15:14:20 +00:00
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
172 changed files with 13206 additions and 6804 deletions

3
.djlintrc Normal file
View File

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

View File

@@ -1,6 +1,16 @@
name: ci
on: [push, pull_request]
on:
push:
paths-ignore:
- 'doc/**'
- '**/README.md'
- '**/LICENSE'
pull_request:
paths-ignore:
- 'doc/**'
- '**/README.md'
- '**/LICENSE'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

2
.gitignore vendored
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.6"
__version__ = "0.15.1"

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
@@ -73,6 +74,31 @@ class BaseApp(DBMethods):
self.default_socket_getaddrinfo = socket.getaddrinfo
self._force_db_upgrade = False
self._enabled_log_categories = set()
for category in self.settings.get("enabled_log_categories", []):
category = category.lower()
if category == "net":
self._enabled_log_categories.add(LC.NET)
else:
self.log.warning(
f'Unknown entry "{category}" in "enabled_log_categories"'
)
if len(self._enabled_log_categories) > 0:
self.log.info(
"Enabled logging categories: {}".format(
",".join(sorted([c.name for c in self._enabled_log_categories]))
)
)
super().__init__(
data_dir=data_dir,
settings=settings,
chain=chain,
log_name=log_name,
**kwargs,
)
def __del__(self):
if self.fp:
self.fp.close()
@@ -236,11 +262,16 @@ class BaseApp(DBMethods):
request = urllib.request.Request(url, headers=headers)
return opener.open(request, timeout=timeout).read()
def logException(self, message) -> None:
def logException(self, message: str) -> None:
self.log.error(message)
if self.debug:
self.log.error(traceback.format_exc())
def logD(self, log_category: int, message: str) -> None:
if log_category not in self._enabled_log_categories:
return
self.log.debug("(" + LC(log_category).name + ") " + message)
def torControl(self, query):
try:
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,11 @@ class MessageNetworks(IntEnum):
SIMPLEX = auto()
class MessageNetworkLinkTypes(IntEnum):
RECEIVED_ON = auto()
SENT_ON = auto()
class MessageTypes(IntEnum):
OFFER = auto()
BID = auto()
@@ -59,6 +64,8 @@ class MessageTypes(IntEnum):
ADS_BID_ACCEPT_FL = auto()
CONNECT_REQ = auto()
PORTAL_OFFER = auto()
PORTAL_SEND = auto()
class AddressTypes(IntEnum):
@@ -66,6 +73,8 @@ class AddressTypes(IntEnum):
BID = auto()
RECV_OFFER = auto()
SEND_OFFER = auto()
PORTAL_LOCAL = auto()
PORTAL = auto()
class SwapTypes(IntEnum):
@@ -201,6 +210,8 @@ class EventLogTypes(IntEnum):
LOCK_TX_B_IN_MEMPOOL = auto()
BCH_MERCY_TX_PUBLISHED = auto()
BCH_MERCY_TX_FOUND = auto()
LOCK_TX_A_IN_MEMPOOL = auto()
LOCK_TX_A_CONFLICTS = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -234,6 +245,8 @@ class NotificationTypes(IntEnum):
OFFER_RECEIVED = auto()
BID_RECEIVED = auto()
BID_ACCEPTED = auto()
SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto()
class ConnectionRequestTypes(IntEnum):
@@ -395,15 +408,14 @@ def strTxType(tx_type):
def strAddressType(addr_type):
if addr_type == AddressTypes.OFFER:
return "Offer"
if addr_type == AddressTypes.BID:
return "Bid"
if addr_type == AddressTypes.RECV_OFFER:
return "Offer recv"
if addr_type == AddressTypes.SEND_OFFER:
return "Offer send"
return "Unknown"
return {
AddressTypes.OFFER: "Offer",
AddressTypes.BID: "Bid",
AddressTypes.RECV_OFFER: "Offer recv",
AddressTypes.SEND_OFFER: "Offer send",
AddressTypes.PORTAL_LOCAL: "Portal (local)",
AddressTypes.PORTAL: "Portal",
}.get(addr_type, "Unknown")
def getLockName(lock_type):
@@ -426,6 +438,10 @@ def describeEventEntry(event_type, event_msg):
return "Lock tx B published"
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
return "Failed to publish lock tx B spend: " + event_msg
if event_type == EventLogTypes.LOCK_TX_A_IN_MEMPOOL:
return "Lock tx A seen in mempool"
if event_type == EventLogTypes.LOCK_TX_A_CONFLICTS:
return "Lock tx A conflicting txn/s"
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
return "Lock tx A seen in chain"
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
@@ -443,7 +459,7 @@ def describeEventEntry(event_type, event_msg):
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return "Detected invalid lock Tx B"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return "Lock tx A refund tx published"
return "Lock tx A pre-refund tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
return "Lock tx A refund spend tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
@@ -455,7 +471,7 @@ def describeEventEntry(event_type, event_msg):
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
return "Lock tx B spend tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
return "Lock tx A refund tx seen in chain"
return "Lock tx A pre-refund tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
return "Lock tx A refund spend tx seen in chain"
if event_type == EventLogTypes.SYSTEM_WARNING:
@@ -595,6 +611,26 @@ def canAcceptBidState(state):
)
def canExpireBidState(state):
return state in (
BidStates.BID_SENT,
BidStates.BID_RECEIVING,
BidStates.BID_RECEIVED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
BidStates.BID_REQUEST_SENT,
)
def canTimeoutBidState(state):
return state in (
BidStates.BID_ACCEPTED,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
)
def isActiveBidState(state):
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
return True

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
@@ -48,7 +50,8 @@ from basicswap.bin.run import (
)
# Coin clients
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
PARTICL_REPO = os.getenv("PARTICL_REPO", "tecnovert")
PARTICL_VERSION = os.getenv("PARTICL_VERSION", "27.2.2.0")
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
@@ -64,10 +67,10 @@ DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.0")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.3")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = (
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version
"df28b670cb3a174d7763dd6d22fb4ef20597d0ac" # Lock hashes.txt to monero version
)
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -79,10 +82,10 @@ WOW_SITE_COMMIT = (
PIVX_VERSION = os.getenv("PIVX_VERSION", "5.6.1")
PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "22.0.0")
DASH_VERSION = os.getenv("DASH_VERSION", "22.1.3")
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.14.1")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.15.0")
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
@@ -158,10 +161,10 @@ expected_key_ids = {
}
GUIX_SSL_CERT_DIR = None
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", "false"))
OVERRIDE_DISABLED_COINS = toBool(os.getenv("OVERRIDE_DISABLED_COINS", False))
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false"))
SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", False))
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
if USE_PLATFORM == "Darwin":
@@ -189,11 +192,11 @@ if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", "false"))
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", "false"))
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", "false"))
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", False))
BSX_TEST_MODE = toBool(os.getenv("BSX_TEST_MODE", False))
BSX_UPDATE_UNMANAGED = toBool(
os.getenv("BSX_UPDATE_UNMANAGED", "true")
os.getenv("BSX_UPDATE_UNMANAGED", True)
) # Disable updating unmanaged coin cores.
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
@@ -318,10 +321,8 @@ def setTorrcVars():
)
TEST_TOR_PROXY = toBool(
os.getenv("TEST_TOR_PROXY", "true")
) # Expects a known exit node
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", "false"))
TEST_TOR_PROXY = toBool(os.getenv("TEST_TOR_PROXY", True)) # Expects a known exit node
TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", False))
BITCOIN_FASTSYNC_URL = os.getenv(
"BITCOIN_FASTSYNC_URL",
@@ -338,6 +339,8 @@ BITCOIN_FASTSYNC_SIG_URL = os.getenv(
# Encrypt new wallets with this password, must match the Particl wallet password when adding coins
WALLET_ENCRYPTION_PWD = os.getenv("WALLET_ENCRYPTION_PWD", "")
CHECK_FOR_BSX_UPDATES = toBool(os.getenv("CHECK_FOR_BSX_UPDATES", True))
use_tor_proxy: bool = False
with_coins_changed: bool = False
@@ -356,21 +359,6 @@ monero_wallet_rpc_proxy_config = [
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
wownerod_proxy_config = [
f"proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}",
"proxy-allow-dns-leaks=0",
"no-igd=1", # Disable UPnP port mapping
"hide-my-port=1", # Don't share the p2p port
"p2p-bind-ip=127.0.0.1", # Don't broadcast ip
"in-peers=0", # Changes "error" in log to "incoming connections disabled"
"out-peers=24",
f"tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16", # Outgoing tx relay to onion
]
wownero_wallet_rpc_proxy_config = [
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout()
default_socket_getaddrinfo = socket.getaddrinfo
@@ -416,6 +404,12 @@ def getDescriptorWalletOption(coin_params):
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
def getLegacyKeyPathOption(coin_params):
ticker: str = coin_params["ticker"]
default_option: bool = False
return toBool(os.getenv(ticker + "_USE_LEGACY_KEY_PATHS", default_option))
def getKnownVersion(coin_name: str) -> str:
version, version_tag, _ = known_coins[coin_name]
return version + version_tag
@@ -894,7 +888,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
downloadFile(assert_sig_url, assert_sig_path)
else:
major_version = int(version.split(".")[0])
use_guix: bool = coin in ("dash",) or major_version >= 22
arch_name = BIN_ARCH
if os_name == "osx" and use_guix:
@@ -915,21 +908,21 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
coin, version + version_tag, arch_name, filename_extra, FILE_EXT
)
if coin == "particl":
release_url = "https://github.com/particl/particl-core/releases/download/v{}/{}".format(
version + version_tag, release_filename
release_url = (
"https://github.com/{}/particl-core/releases/download/v{}/{}".format(
PARTICL_REPO, version + version_tag, release_filename
)
)
assert_filename = "{}-{}-{}-build.assert".format(coin, os_name, version)
if use_guix:
assert_url = f"https://raw.githubusercontent.com/particl/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
assert_url = f"https://raw.githubusercontent.com/{PARTICL_REPO}/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS"
else:
assert_url = (
"https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s"
% (
version + version_tag,
os_dir_name,
signing_key_name,
assert_filename,
)
assert_url = "https://raw.githubusercontent.com/{}/gitian.sigs/master/{}-{}/{}/{}".format(
PARTICL_REPO,
version + version_tag,
os_dir_name,
signing_key_name,
assert_filename,
)
elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
@@ -1223,16 +1216,12 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if coin == "monero":
if XMR_RPC_USER != "":
fp.write(f"rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n")
if tor_control_password is not None:
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
if coin == "wownero":
if WOW_RPC_USER != "":
fp.write(f"rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n")
if tor_control_password is not None:
for opt_line in wownerod_proxy_config:
fp.write(opt_line + "\n")
if tor_control_password is not None:
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
wallets_dir = core_settings.get("walletsdir", data_dir)
if not os.path.exists(wallets_dir):
@@ -1378,10 +1367,25 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
salt = generate_salt(16)
if coin == "particl":
fp.write("deprecatedrpc=create_bdb\n")
fp.write("debugexclude=libevent\n")
if chain == "mainnet":
fp.write("rpcdoccheck=0\n")
fp.write(
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
)
fp.write(
"zmqpubhashwtx=tcp://{}:{}\n".format(
COINS_RPCBIND_IP, settings["zmqport"]
)
)
zmqsecret = extra_opts.get("zmqsecret", None)
if zmqsecret:
try:
_ = base64.b64decode(zmqsecret)
except Exception as e: # noqa: F841
raise ValueError("zmqsecret must be base64 encoded")
fp.write(f"serverkeyzmq={zmqsecret}\n")
fp.write("spentindex=1\n")
fp.write("txindex=1\n")
fp.write("staking=0\n")
@@ -1562,27 +1566,18 @@ def modify_tor_config(
# Disable tor first
for line in fp_in:
skip_line: bool = False
if coin == "monero":
if coin in ("wownero", "monero"):
for opt_line in monerod_proxy_config:
setting: str = opt_line[0 : opt_line.find("=") + 1]
if line.startswith(setting):
skip_line = True
break
if coin == "wownero":
for opt_line in wownerod_proxy_config:
setting: str = opt_line[0 : opt_line.find("=") + 1]
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
if coin == "monero":
if coin in ("wownero", "monero"):
for opt_line in monerod_proxy_config:
fp.write(opt_line + "\n")
if coin == "wownero":
for opt_line in wownerod_proxy_config:
fp.write(opt_line + "\n")
with open(wallet_conf_path, "w") as fp:
with open(wallet_conf_path + ".last") as fp_in:
@@ -1773,7 +1768,7 @@ def finalise_daemon(d):
fp.close()
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
def test_particl_encryption(data_dir, settings, chain, use_tor_proxy, extra_opts):
swap_client = None
daemons = []
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
@@ -1810,12 +1805,14 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
"Must set WALLET_ENCRYPTION_PWD to add coin when Particl wallet is encrypted"
)
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
extra_opts["particl_daemon"] = daemons[-1]
finally:
if swap_client:
swap_client.finalise()
del swap_client
for d in daemons:
finalise_daemon(d)
if "particl_daemon" not in extra_opts:
for d in daemons:
finalise_daemon(d)
def encrypt_wallet(swap_client, coin_type) -> None:
@@ -1909,15 +1906,20 @@ def initialise_wallets(
]
extra_config = {"stdout_to_file": True, "coin_name": coin_name}
daemons.append(
startDaemon(
coin_settings["datadir"],
coin_settings["bindir"],
filename,
daemon_args + coin_args,
extra_config=extra_config,
if c == Coins.PART and "particl_daemon" in extra_opts:
daemons.append(extra_opts["particl_daemon"])
del extra_opts["particl_daemon"]
else:
daemons.append(
startDaemon(
coin_settings["datadir"],
coin_settings["bindir"],
filename,
daemon_args + coin_args,
extra_config=extra_config,
)
)
)
swap_client.setDaemonPID(c, daemons[-1].handle.pid)
swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c)
@@ -2357,10 +2359,10 @@ def main():
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1].strip('"'))
data_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
continue
if name == "bindir":
bin_dir = os.path.expanduser(s[1].strip('"'))
bin_dir = os.path.abspath(os.path.expanduser(s[1].strip('"')))
continue
if name == "portoffset":
port_offset = int(s[1])
@@ -2417,7 +2419,9 @@ def main():
extra_opts["walletrestoretime"] = int(s[1])
continue
if name == "keysdirpath":
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
extra_opts["keysdirpath"] = os.path.abspath(
os.path.expanduser(s[1].strip('"'))
)
continue
if name == "trustremotenode":
extra_opts["trust_remote_node"] = toBool(s[1])
@@ -2815,6 +2819,8 @@ def main():
coin_settings["watch_wallet_name"] = getWalletName(
coin_params, "bsx_watch", prefix_override=f"{ticker}_WATCH"
)
if getLegacyKeyPathOption(coin_params) is True:
coin_settings["use_legacy_key_paths"] = True
if PART_RPC_USER != "":
chainclients["particl"]["rpcuser"] = PART_RPC_USER
@@ -2967,41 +2973,51 @@ def main():
save_config(config_path, settings)
logger.info("Done.")
return 0
exitWithError("{} is already in the settings file".format(add_coin))
exitWithError(f"{add_coin} is already in the settings file")
if tor_control_password is None and settings.get("use_tor", False):
extra_opts["tor_control_password"] = settings.get(
"tor_control_password", None
)
if particl_wallet_mnemonic != "none":
# Ensure Particl wallet is unencrypted or correct password is supplied
test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
settings["chainclients"][add_coin] = chainclients[add_coin]
if not no_cores:
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
if not (prepare_bin_only or upgrade_cores):
prepareDataDir(
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
)
try:
if particl_wallet_mnemonic != "none":
initialise_wallets(
None,
{
add_coin,
},
data_dir,
settings,
chain,
use_tor_proxy,
extra_opts=extra_opts,
# Ensure Particl wallet is unencrypted or correct password is supplied
# Keep daemon running to use in initialise_wallets
test_particl_encryption(
data_dir, settings, chain, use_tor_proxy, extra_opts
)
save_config(config_path, settings)
settings["chainclients"][add_coin] = chainclients[add_coin]
if not no_cores:
prepareCore(
add_coin, known_coins[add_coin], settings, data_dir, extra_opts
)
if not (prepare_bin_only or upgrade_cores):
prepareDataDir(
add_coin, settings, chain, particl_wallet_mnemonic, extra_opts
)
if particl_wallet_mnemonic != "none":
initialise_wallets(
None,
{
add_coin,
},
data_dir,
settings,
chain,
use_tor_proxy,
extra_opts=extra_opts,
)
save_config(config_path, settings)
finally:
if "particl_daemon" in extra_opts:
finalise_daemon(extra_opts["particl_daemon"])
del extra_opts["particl_daemon"]
logger.info(f"Done. Coin {add_coin} successfully added.")
return 0
@@ -3226,6 +3242,10 @@ def main():
for c in with_coins:
withchainclients[c] = chainclients[c]
zmq_server_pubkey, zmq_server_key = zmq.curve_keypair()
zmq_client_pubkey, zmq_client_key = zmq.curve_keypair()
extra_opts["zmqsecret"] = base64.b64encode(zmq_server_key).decode("utf-8")
settings = {
"debug": True,
"zmqhost": f"tcp://{PART_RPC_HOST}",
@@ -3241,6 +3261,12 @@ def main():
"check_watched_seconds": 60,
"check_expired_seconds": 60,
"wallet_update_timeout": 10, # Seconds to wait for wallet page update
"zmq_client_key": base64.b64encode(zmq_client_key).decode("utf-8"),
"zmq_client_pubkey": base64.b64encode(zmq_client_pubkey).decode("utf-8"),
"zmq_server_pubkey": base64.b64encode(zmq_server_pubkey).decode("utf-8"),
"enabled_log_categories": [
"net",
],
}
wshost: str = extra_opts.get("wshost", htmlhost)
@@ -3248,6 +3274,11 @@ def main():
settings["wshost"] = wshost
settings["wsport"] = UI_WS_PORT + port_offset
if "CHECK_FOR_BSX_UPDATES" in os.environ:
settings["check_updates"] = CHECK_FOR_BSX_UPDATES
elif BSX_TEST_MODE is True:
settings["check_updates"] = False
if use_tor_proxy:
tor_control_password = generate_salt(24)
addTorSettings(settings, tor_control_password)

View File

@@ -19,8 +19,6 @@ import basicswap.config as cfg
from basicswap import __version__
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.contrib.websocket_server import WebsocketServer
from basicswap.http_server import HttpThread
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
@@ -56,6 +54,42 @@ def signal_handler(sig, frame):
swap_client.stopRunning()
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
try:
datadir = part_settings.get("datadir")
if not datadir:
return
config_path = os.path.join(datadir, "particl.conf")
if not os.path.exists(config_path):
return
with open(config_path, "r") as f:
config_content = f.read()
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
zmq_port = swap_settings.get("zmqport", 14792)
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
if "zmqpubhashwtx=" not in config_content:
with open(config_path, "a") as f:
f.write(f"{expected_line}\n")
elif expected_line not in config_content:
lines = config_content.split("\n")
updated_lines = []
for line in lines:
if line.startswith("zmqpubhashwtx="):
updated_lines.append(expected_line)
else:
updated_lines.append(line)
with open(config_path, "w") as f:
f.write("\n".join(updated_lines))
except Exception as e:
logger.debug(f"Error checking PART ZMQ config: {e}")
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
@@ -187,7 +221,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_path, "--non-interactive"]
args = [daemon_path]
needs_rewrite: bool = False
config_to_remove = [
@@ -249,25 +283,6 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
)
def ws_new_client(client, server):
if swap_client:
swap_client.log.debug(f'ws_new_client {client["id"]}')
def ws_client_left(client, server):
if client is None:
return
if swap_client:
swap_client.log.debug(f'ws_client_left {client["id"]}')
def ws_message_received(client, server, message):
if len(message) > 200:
message = message[:200] + ".."
if swap_client:
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get(
"core_binname", chainparams[coin_id].get("core_binname", default_name)
@@ -336,7 +351,6 @@ def runClient(
global swap_client, logger
daemons = []
pids = []
threads = []
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
pids_path = os.path.join(data_dir, ".pids")
@@ -379,7 +393,7 @@ def runClient(
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type = network.get("type", "unknown")
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
@@ -548,6 +562,9 @@ def runClient(
continue # /decred
if v["manage_daemon"] is True:
if c == "particl" and swap_client._zmq_queue_enabled:
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
@@ -584,39 +601,6 @@ def runClient(
mainLoop(daemons, update=False)
else:
swap_client.start()
if "htmlhost" in settings:
swap_client.log.info(
"Starting http server at http://%s:%d."
% (settings["htmlhost"], settings["htmlport"])
)
allow_cors = (
settings["allowcors"]
if "allowcors" in settings
else cfg.DEFAULT_ALLOW_CORS
)
thread_http = HttpThread(
settings["htmlhost"],
settings["htmlport"],
allow_cors,
swap_client,
)
threads.append(thread_http)
thread_http.start()
if "wshost" in settings:
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
swap_client.log.info(f"Starting ws server at {ws_url}.")
swap_client.ws_server = WebsocketServer(
host=settings["wshost"], port=settings["wsport"]
)
swap_client.ws_server.client_port = settings.get(
"wsclientport", settings["wsport"]
)
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
logger.info("Exit with Ctrl + c.")
mainLoop(daemons)
@@ -632,13 +616,6 @@ def runClient(
traceback.print_exc()
swap_client.finalise()
swap_client.log.info("Stopping HTTP threads.")
for t in threads:
try:
t.stop()
t.join()
except Exception as e: # noqa: F841
traceback.print_exc()
closed_pids = []
for d in daemons:
@@ -758,7 +735,7 @@ def main():
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1])
data_dir = os.path.abspath(os.path.expanduser(s[1]))
continue
if name == "logprefix":
log_prefix = s[1]

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 = 29
CURRENT_DB_DATA_VERSION = 6
CURRENT_DB_VERSION = 32
CURRENT_DB_DATA_VERSION = 7
class Concepts(IntEnum):
@@ -185,6 +185,7 @@ class Offer(Table):
amount_negotiable = Column("bool")
rate_negotiable = Column("bool")
auto_accept_type = Column("integer")
message_nets = Column("string")
# Local fields
auto_accept_bids = Column("bool")
@@ -194,6 +195,7 @@ class Offer(Table):
) # Address to spend lock tx to - address from wallet if empty TODO
security_token = Column("blob")
bid_reversed = Column("bool")
smsg_payload_version = Column("integer")
state = Column("integer")
states = Column("blob") # Packed states and times
@@ -233,6 +235,7 @@ class Bid(Table):
rate = Column("integer")
pkhash_seller = Column("blob")
message_nets = Column("string")
initiate_txn_redeem = Column("blob")
initiate_txn_refund = Column("blob")
@@ -381,6 +384,8 @@ class SmsgAddress(Table):
use_type = Column("integer")
note = Column("string")
index = Index("smsgaddresses_address_index", "addr")
class Action(Table):
__tablename__ = "actions"
@@ -614,6 +619,8 @@ class BidState(Table):
swap_failed = Column("integer")
swap_ended = Column("integer")
can_accept = Column("integer")
can_expire = Column("integer")
can_timeout = Column("integer")
note = Column("string")
created_at = Column("integer")
@@ -667,6 +674,28 @@ class CoinRates(Table):
last_updated = Column("integer")
class CoinVolume(Table):
__tablename__ = "coinvolume"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_id = Column("integer")
volume_24h = Column("string")
price_change_24h = Column("string")
source = Column("string")
last_updated = Column("integer")
class CoinHistory(Table):
__tablename__ = "coinhistory"
record_id = Column("integer", primary_key=True, autoincrement=True)
coin_id = Column("integer")
days = Column("integer")
price_data = Column("blob")
source = Column("string")
last_updated = Column("integer")
class MessageNetworks(Table):
__tablename__ = "message_networks"
@@ -676,6 +705,20 @@ class MessageNetworks(Table):
created_at = Column("integer")
class MessageNetworkLink(Table):
__tablename__ = "message_network_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
network_id = Column("string")
link_type = Column("integer") # MessageNetworkLinkTypes
created_at = Column("integer")
class DirectMessageRoute(Table):
__tablename__ = "direct_message_routes"
@@ -694,6 +737,7 @@ class DirectMessageRoute(Table):
class DirectMessageRouteLink(Table):
__tablename__ = "direct_message_route_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
direct_message_route_id = Column("integer")
@@ -702,6 +746,52 @@ class DirectMessageRouteLink(Table):
created_at = Column("integer")
class NetworkPortal(Table):
__tablename__ = "network_portals"
def set(
self, time_start, time_valid, network_from, network_to, address_from, address_to
):
super().__init__()
self.active_ind = 1
self.time_start = time_start
self.time_valid = time_valid
self.network_from = network_from
self.network_to = network_to
self.address_from = address_from
self.address_to = address_to
self.smsg_difficulty = 0x1EFFFFFF
self.num_refreshes = 0
self.messages_sent = 0
self.responses_seen = 0
self.time_last_used = 0
self.num_issues = 0
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
own_portal = Column("integer")
address_from = Column("string", unique=True)
address_to = Column("string")
network_from = Column("integer")
network_to = Column("integer")
time_start = Column("integer")
time_valid = Column("integer")
smsg_difficulty = Column("integer")
num_refreshes = Column("integer")
messages_sent = Column("integer")
responses_seen = Column("integer")
time_last_used = Column("integer")
num_issues = Column("integer")
created_at = Column("integer")
def extract_schema() -> dict:
g = globals().copy()
tables = {}

View File

@@ -21,6 +21,8 @@ from .db import (
from .basicswap_util import (
BidStates,
canAcceptBidState,
canExpireBidState,
canTimeoutBidState,
isActiveBidState,
isErrorBidState,
isFailingBidState,
@@ -39,6 +41,8 @@ def addBidState(self, state, now, cursor):
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
can_accept=canAcceptBidState(state),
can_expire=canExpireBidState(state),
can_timeout=canTimeoutBidState(state),
label=strBidState(state),
created_at=now,
),
@@ -65,7 +69,7 @@ def upgradeDatabaseData(self, data_version):
label="Accept All",
type_ind=Concepts.OFFER,
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
{"exact_rate_only": True, "max_concurrent_bids": 1}
).encode("utf-8"),
only_known_identities=False,
created_at=now,
@@ -78,7 +82,7 @@ def upgradeDatabaseData(self, data_version):
label="Accept Known",
type_ind=Concepts.OFFER,
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
{"exact_rate_only": True, "max_concurrent_bids": 1}
).encode("utf-8"),
only_known_identities=True,
note="Accept bids from identities with previously successful swaps only",
@@ -105,19 +109,23 @@ def upgradeDatabaseData(self, data_version):
),
cursor,
)
if data_version > 0 and data_version < 6:
if data_version > 0 and data_version < 7:
for state in BidStates:
in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state)
can_accept = canAcceptBidState(state)
can_expire = canExpireBidState(state)
can_timeout = canTimeoutBidState(state)
cursor.execute(
"UPDATE bidstates SET can_accept = :can_accept, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
"UPDATE bidstates SET can_accept = :can_accept, can_expire = :can_expire, can_timeout = :can_timeout, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
{
"in_error": in_error,
"swap_failed": swap_failed,
"swap_ended": swap_ended,
"can_accept": can_accept,
"can_expire": can_expire,
"can_timeout": can_timeout,
"state_id": int(state),
},
)

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,97 +26,46 @@ def remove_expired_data(self, time_offset: int = 0):
)
for offer_row in offer_rows:
num_offers += 1
offer_query_data = {
"type_ind": int(Concepts.OFFER),
"offer_id": offer_row[0],
}
bid_rows = cursor.execute(
"SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id",
{"offer_id": offer_row[0]},
offer_query_data,
)
for bid_row in bid_rows:
num_bids += 1
cursor.execute(
bid_query_data = {"type_ind": int(Concepts.BID), "bid_id": bid_row[0]}
for query_str in [
"DELETE FROM transactions WHERE transactions.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id",
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM actions WHERE actions.linked_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM bids WHERE bids.bid_id = :bid_id",
{"bid_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
]:
cursor.execute(query_str, bid_query_data)
for query_str in [
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
{"offer_id": offer_row[0]},
)
cursor.execute(
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
)
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
]:
cursor.execute(query_str, offer_query_data)
if num_offers > 0 or num_bids > 0:
self.log.info(

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
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
if not session_id:
return False
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
with self.server.session_lock:
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
return False
def log_error(self, format, *args):
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
return None
form_data = parse.parse_qs(post_string)
form_id = form_data[b"formid"][0].decode("utf-8")
if self.server.last_form_id.get(name, None) == form_id:
messages.append("Prevented double submit for form {}.".format(form_id))
return None
self.server.last_form_id[name] = form_id
with self.server.form_id_lock:
if self.server.last_form_id.get(name, None) == form_id:
messages.append("Prevented double submit for form {}.".format(form_id))
return None
self.server.last_form_id[name] = form_id
return form_data
def render_template(
@@ -216,43 +218,48 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_mode"] = True
if swap_client.debug_ui:
args_dict["debug_ui_mode"] = True
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
is_authenticated = self.is_authenticated() or not swap_client.settings.get(
"client_auth_hash"
)
if is_authenticated:
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception:
args_dict["tor_established"] = False
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception as e:
args_dict["tor_established"] = False
if swap_client.debug:
swap_client.log.error(f"Error getting Tor state: {str(e)}")
swap_client.log.error(traceback.format_exc())
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception:
args_dict["current_status"] = "stopped"
args_dict["amm_active_count"] = 0
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception as e:
args_dict["current_status"] = "stopped"
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
else:
args_dict["current_status"] = "unknown"
args_dict["amm_active_count"] = 0
if swap_client.debug:
swap_client.log.error(f"Error getting AMM state: {str(e)}")
swap_client.log.error(traceback.format_exc())
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
if "messages" in args_dict:
messages_with_ids = []
for msg in args_dict["messages"]:
messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
with self.server.msg_id_lock:
for msg in args_dict["messages"]:
messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
args_dict["messages"] = messages_with_ids
if "err_messages" in args_dict:
err_messages_with_ids = []
for msg in args_dict["err_messages"]:
err_messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
with self.server.msg_id_lock:
for msg in args_dict["err_messages"]:
err_messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids
if self.path:
@@ -266,15 +273,27 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["current_page"] = "index"
shutdown_token = os.urandom(8).hex()
self.server.session_tokens["shutdown"] = shutdown_token
with self.server.session_lock:
self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
encrypted, locked = swap_client.getLockedState()
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
if is_authenticated:
try:
encrypted, locked = swap_client.getLockedState()
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
except Exception as e:
args_dict["encrypted"] = False
args_dict["locked"] = False
if swap_client.debug:
swap_client.log.warning(f"Could not get wallet locked state: {e}")
else:
args_dict["encrypted"] = args_dict.get("encrypted", False)
args_dict["locked"] = args_dict.get("locked", False)
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
with self.server.msg_id_lock:
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
args_dict["version"] = version
@@ -364,7 +383,8 @@ class HttpHandler(BaseHTTPRequestHandler):
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
self.server.active_sessions[session_id] = {"expires": expires}
with self.server.session_lock:
self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
if is_json_request:
@@ -628,13 +648,15 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 2:
token = url_split[2]
expect_token = self.server.session_tokens.get("shutdown", None)
with self.server.session_lock:
expect_token = self.server.session_tokens.get("shutdown", None)
if token != expect_token:
return self.page_info("Unexpected token, still running.")
session_id = self._get_session_cookie()
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
with self.server.session_lock:
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
@@ -935,22 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_error(str(ex))
def do_GET(self):
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
finally:
pass
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
finally:
pass
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -963,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
class HttpThread(threading.Thread, HTTPServer):
class HttpThread(threading.Thread, ThreadingHTTPServer):
daemon_threads = True
def __init__(self, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
@@ -979,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
self.env = env
self.msg_id_counter = 0
self.session_lock = threading.Lock()
self.form_id_lock = threading.Lock()
self.msg_id_lock = threading.Lock()
self.timeout = 60
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
if swap_client.debug:
swap_client.log.info("HTTP server initialized with threading support")
def stop(self):
self.stop_event.set()

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

@@ -818,7 +818,15 @@ class BCHInterface(BTCInterface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
# locktime must be <= chainheight + 2
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if tx.nLockTime != 0:
current_height: int = self.getChainHeight()
if tx.nLockTime > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
)
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)

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)
@@ -362,7 +364,9 @@ class BTCInterface(Secp256k1Interface):
self.rpc_wallet("getwalletinfo" if with_wallet else "getblockchaininfo")
def getDaemonVersion(self):
return self.rpc("getnetworkinfo")["version"]
if self._core_version is None:
self._core_version = self.rpc("getnetworkinfo")["version"]
return self._core_version
def getBlockchainInfo(self):
return self.rpc("getblockchaininfo")
@@ -397,6 +401,13 @@ class BTCInterface(Secp256k1Interface):
last_block_header = prev_block_header
raise ValueError(f"Block header not found at time: {time}")
def getWalletAccountPath(self) -> str:
# Use a bip44 style path, however the seed (derived from the particl mnemonic keychain) can't be turned into a bip39 mnemonic without the matching entropy
purpose: int = 84 # native segwit
coin_type: int = self.chainparams_network()["bip44"]
account: int = 0
return f"{purpose}h/{coin_type}h/{account}h"
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
assert len(key_bytes) == 32
self._have_checked_seed = False
@@ -405,8 +416,15 @@ class BTCInterface(Secp256k1Interface):
ek = ExtKeyPair()
ek.set_seed(key_bytes)
ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
if self._use_legacy_key_paths:
# Match keys from legacy wallets (created from sethdseed)
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
else:
# Use a bip44 path so the seed can be exported as a mnemonic
path: str = self.getWalletAccountPath()
desc_external = descsum_create(f"wpkh({ek_encoded}/{path}/0/*)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/{path}/1/*)")
rv = self.rpc_wallet(
"importdescriptors",
@@ -445,6 +463,50 @@ class BTCInterface(Secp256k1Interface):
"""
raise (e)
def canExportToElectrum(self) -> bool:
# keychains must be unhardened to export into electrum
return self._use_descriptors is True and self._use_legacy_key_paths is False
def getAccountKey(
self,
key_bytes: bytes,
extkey_prefix: Optional[int] = None,
coin_type_overide: Optional[int] = None,
) -> str:
# For electrum, must start with zprv to get P2WPKH, addresses
# extkey_prefix: 0x04b2430c
ek = ExtKeyPair()
ek.set_seed(key_bytes)
path: str = self.getWalletAccountPath()
account_ek = ek.derive_path(path)
return self.encode_secret_extkey(account_ek.encode_v(), extkey_prefix)
def getWalletKeyChains(
self, key_bytes: bytes, extkey_prefix: Optional[int] = None
) -> Dict[str, str]:
ek = ExtKeyPair()
ek.set_seed(key_bytes)
# extkey must contain keydata to derive hardened child keys
if self.canExportToElectrum():
path: str = self.getWalletAccountPath()
external_extkey = ek.derive_path(f"{path}/0")
internal_extkey = ek.derive_path(f"{path}/1")
else:
# Match keychain paths of legacy wallets
external_extkey = ek.derive_path("0h/0h")
internal_extkey = ek.derive_path("0h/1h")
def encode_extkey(extkey):
return self.encode_secret_extkey(extkey.encode_v(), extkey_prefix)
rv = {
"external": encode_extkey(external_extkey),
"internal": encode_extkey(internal_extkey),
}
return rv
def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo")
rv["encrypted"] = "unlocked_until" in rv
@@ -504,10 +566,16 @@ class BTCInterface(Secp256k1Interface):
if descriptor is None:
self._log.debug("Could not find active descriptor.")
return "Not found"
end = descriptor["desc"].find("/")
start = descriptor["desc"].find("]")
if start < 3:
return "Could not parse descriptor"
descriptor = descriptor["desc"][start + 1 :]
end = descriptor.find("/")
if end < 10:
return "Not found"
extkey = descriptor["desc"][5:end]
return "Could not parse descriptor"
extkey = descriptor[:end]
extkey_data = b58decode(extkey)[4:-4]
extkey_data_hash: bytes = hash160(extkey_data)
return extkey_data_hash.hex()
@@ -611,9 +679,10 @@ class BTCInterface(Secp256k1Interface):
pkh = hash160(pk)
return segwit_addr.encode(bech32_prefix, version, pkh)
def encode_secret_extkey(self, ek_data: bytes) -> str:
def encode_secret_extkey(self, ek_data: bytes, prefix=None) -> str:
assert len(ek_data) == 74
prefix = self.chainparams_network()["ext_secret_key_prefix"]
if prefix is None:
prefix = self.chainparams_network()["ext_secret_key_prefix"]
data: bytes = prefix.to_bytes(4, "big") + ek_data
checksum = sha256(sha256(data))
return b58encode(data + checksum[0:4])
@@ -672,18 +741,12 @@ class BTCInterface(Secp256k1Interface):
wif_prefix = self.chainparams_network()["key_prefix"]
return toWIF(wif_prefix, key_bytes)
def encodePubkey(self, pk: bytes) -> bytes:
return pointToCPK(pk)
def encodeSegwitAddress(self, key_hash: bytes) -> str:
return segwit_addr.encode(self.chainparams_network()["hrp"], 0, key_hash)
def decodeSegwitAddress(self, addr: str) -> bytes:
return bytes(segwit_addr.decode(self.chainparams_network()["hrp"], addr)[1])
def decodePubkey(self, pke):
return CPKToPoint(pke)
def decodeKey(self, k: str) -> bytes:
return decodeWif(k)
@@ -691,10 +754,11 @@ class BTCInterface(Secp256k1Interface):
# p2wpkh
return CScript([OP_0, pkh])
def loadTx(self, tx_bytes: bytes) -> CTransaction:
def loadTx(self, tx_bytes: bytes, allow_witness: bool = True) -> CTransaction:
# Load tx from bytes to internal representation
# Transactions with no inputs require allow_witness set to false to decode correctly
tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes))
tx.deserialize(BytesIO(tx_bytes), allow_witness)
return tx
def createSCLockTx(
@@ -702,27 +766,63 @@ class BTCInterface(Secp256k1Interface):
) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.nLockTime = 0 # TODO: match locktimes by core
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
return tx.serialize()
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate)
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None) -> bytes:
funded_tx = self.fundTx(tx_bytes, feerate)
if self._disable_lock_tx_rbf:
tx = self.loadTx(funded_tx)
for txi in tx.vin:
txi.nSequence = 0xFFFFFFFE
funded_tx = tx.serialize_with_witness()
return funded_tx
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
assert len(Kal) == 33
assert len(Kaf) == 33
# fmt: off
return CScript([
CScriptOp(OP_IF),
2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG),
2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG),
CScriptOp(OP_ELSE),
csv_val, CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
Kaf_enc, CScriptOp(OP_CHECKSIG),
Kaf, CScriptOp(OP_CHECKSIG),
CScriptOp(OP_ENDIF)])
# fmt: on
def isScriptP2PKH(self, script: bytes) -> bool:
if len(script) != 25:
return False
if script[0] != OP_DUP:
return False
if script[1] != OP_HASH160:
return False
if script[2] != 20:
return False
if script[23] != OP_EQUALVERIFY:
return False
if script[24] != OP_CHECKSIG:
return False
return True
def isScriptP2WPKH(self, script: bytes) -> bool:
if len(script) != 22:
return False
if script[0] != OP_0:
return False
if script[1] != 20:
return False
return True
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script):
return [bytes(72), bytes(33)]
raise ValueError("Unknown script type")
def createSCLockRefundTx(
self,
tx_lock_bytes,
@@ -978,7 +1078,15 @@ class BTCInterface(Secp256k1Interface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
# locktime must be <= chainheight + 2
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if tx.nLockTime != 0:
current_height: int = self.getChainHeight()
if tx.nLockTime > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
)
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
@@ -1250,7 +1358,17 @@ class BTCInterface(Secp256k1Interface):
)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
for i in range(10000):
# Grind for low-R value
if i == 0:
nonce = (ffi.NULL, ffi.NULL)
else:
extra_entropy = i.to_bytes(4, "little") + (b"\0" * 28)
nonce = (ffi.NULL, ffi.new("unsigned char [32]", extra_entropy))
sig = eck.sign(sig_hash, hasher=None, custom_nonce=nonce)
if len(sig) < 71:
return sig + bytes((SIGHASH_ALL,))
raise RuntimeError("sign failed.")
def signTxOtVES(
self,
@@ -1316,7 +1434,8 @@ class BTCInterface(Secp256k1Interface):
"feeRate": feerate_str,
}
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
return bytes.fromhex(rv["hex"])
tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes
def getNonSegwitOutputs(self):
unspents = self.rpc_wallet("listunspent", [0, 99999999])
@@ -1705,6 +1824,10 @@ class BTCInterface(Secp256k1Interface):
"height": block_height,
}
if "mempoolconflicts" in tx and len(tx["mempoolconflicts"]) > 0:
rv["conflicts"] = tx["mempoolconflicts"]
elif "walletconflicts" in tx and len(tx["walletconflicts"]) > 0:
rv["conflicts"] = tx["walletconflicts"]
except Exception as e:
self._log.debug(
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
@@ -1868,6 +1991,9 @@ class BTCInterface(Secp256k1Interface):
def getBlockWithTxns(self, block_hash: str):
return self.rpc("getblock", [block_hash, 2])
def listUtxos(self):
return self.rpc_wallet("listunspent")
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet("listunspent")
@@ -1904,6 +2030,15 @@ class BTCInterface(Secp256k1Interface):
sum_unspent += self.make_int(o["amount"])
return sum_unspent
def signMessage(self, address: str, message: str) -> str:
return self.rpc_wallet(
"signmessage",
[address, message],
)
def signMessageWithKey(self, key_wif: str, message: str) -> str:
return self.rpc("signmessagewithprivkey", [key_wif, message])
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspent_addr = self.getUnspentsByAddr()
@@ -1921,6 +2056,7 @@ class BTCInterface(Secp256k1Interface):
self._log.debug(f"sign_for_addr {sign_for_addr}")
funds_addr: str = sign_for_addr
if (
self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo
@@ -1929,6 +2065,7 @@ class BTCInterface(Secp256k1Interface):
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
sign_message: str = sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()
if self._use_descriptors:
# https://github.com/bitcoin/bitcoin/issues/10542
# https://github.com/bitcoin/bitcoin/issues/26046
@@ -1945,7 +2082,6 @@ class BTCInterface(Secp256k1Interface):
],
)
hdkeypath = addr_info["hdkeypath"]
sign_for_address_key = None
for descriptor in priv_keys["descriptors"]:
if descriptor["active"] is False or descriptor["internal"] is True:
@@ -1966,22 +2102,10 @@ class BTCInterface(Secp256k1Interface):
sign_for_address_key = self.encodeKey(ek._key)
break
assert sign_for_address_key is not None
signature = self.rpc(
"signmessagewithprivkey",
[
sign_for_address_key,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
)
signature = self.signMessageWithKey(sign_for_address_key, sign_message)
del priv_keys
else:
signature = self.rpc_wallet(
"signmessage",
[
sign_for_addr,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
)
signature = self.signMessage(sign_for_addr, sign_message)
prove_utxos = [] # TODO: Send specific utxos
return (sign_for_addr, signature, prove_utxos)

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

@@ -8,6 +8,7 @@
import hashlib
from enum import IntEnum
from typing import List
from basicswap.contrib.test_framework.messages import (
CTxOutPart,
@@ -134,6 +135,11 @@ class PARTInterface(BTCInterface):
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
return [bytes(72), bytes(33)]
raise ValueError("Unknown script type")
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
@@ -190,6 +196,24 @@ class PARTInterface(BTCInterface):
def combine_non_segwit_prevouts(self):
raise RuntimeError("No non-segwit outputs found.")
def signMessage(self, address: str, message: str) -> str:
args = [address, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc_wallet("signmessage", args)
def signMessageWithKey(self, key_wif: str, message: str) -> str:
args = [key_wif, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc("signmessagewithprivkey", args)
class PARTInterfaceBlind(PARTInterface):
@@ -490,7 +514,16 @@ class PARTInterfaceBlind(PARTInterface):
self._log.info("Verifying lock tx: {}.".format(self._log.id(lock_txid_hex)))
ensure(lock_tx_obj["version"] == self.txVersion(), "Bad version")
ensure(lock_tx_obj["locktime"] == 0, "Bad nLockTime")
lock_time: int = lock_tx_obj["locktime"]
# locktime must be <= chainheight + 2
# TODO: locktime is set to 0 to keep compaitibility with older nodes.
# Set locktime to current chainheight in createSCLockTx.
if lock_time != 0:
current_height: int = self.getChainHeight()
if lock_time > current_height + 2:
raise ValueError(
f"{self.coin_name()} - Bad nLockTime {lock_time}, current height {current_height}"
)
# Find the output of the lock tx to verify
nonce = self.getScriptLockTxNonce(vkbv)

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
@@ -543,16 +558,14 @@ class XMRInterface(CoinInterface):
rv = -1
return rv
def findTxnByHash(self, txid):
def findTxnByHash(self, txid: str):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
try:
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
"height"
]
current_height: int = self.getChainHeight()
self._log.info(
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
)

View File

@@ -123,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(coins), "UTF-8")
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = []
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
balance = "0.0"
if k in wallets:
w = wallets[k]
if "balance" in w and "error" not in w and "no_data" not in w:
raw_balance = w["balance"]
if isinstance(raw_balance, float):
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
elif isinstance(raw_balance, int):
balance = str(raw_balance)
else:
balance = raw_balance
pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
ci = swap_client.ci(k)
pending_amount = 0
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
pending_amount += ci.make_int(w["unconfirmed"])
if "immature" in w and float(w["immature"]) > 0.0:
pending_amount += ci.make_int(w["immature"])
if pending_amount > 0:
pending = ci.format_amount(pending_amount)
coin_entry = {
"id": int(k),
"name": getCoinName(k),
"balance": balance,
"pending": pending,
"ticker": chainparams[k]["ticker"],
}
coins_with_balances.append(coin_entry)
if k == Coins.PART:
variants = [
{
"coin": Coins.PART_ANON,
"balance_field": "anon_balance",
"pending_field": "anon_pending",
},
{
"coin": Coins.PART_BLIND,
"balance_field": "blind_balance",
"pending_field": "blind_unconfirmed",
},
]
for variant_info in variants:
variant_balance = "0.0"
variant_pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if variant_info["balance_field"] in w:
raw_balance = w[variant_info["balance_field"]]
if isinstance(raw_balance, float):
variant_balance = f"{raw_balance:.8f}".rstrip(
"0"
).rstrip(".")
elif isinstance(raw_balance, int):
variant_balance = str(raw_balance)
else:
variant_balance = raw_balance
if (
variant_info["pending_field"] in w
and float(w[variant_info["pending_field"]]) > 0.0
):
variant_pending = str(
w[variant_info["pending_field"]]
)
variant_entry = {
"id": int(variant_info["coin"]),
"name": getCoinName(variant_info["coin"]),
"balance": variant_balance,
"pending": variant_pending,
"ticker": chainparams[Coins.PART]["ticker"],
}
coins_with_balances.append(variant_entry)
elif k == Coins.LTC:
variant_balance = "0.0"
variant_pending = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if "mweb_balance" in w:
variant_balance = w["mweb_balance"]
pending_amount = 0
if (
"mweb_unconfirmed" in w
and float(w["mweb_unconfirmed"]) > 0.0
):
pending_amount += float(w["mweb_unconfirmed"])
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
pending_amount += float(w["mweb_immature"])
if pending_amount > 0:
variant_pending = f"{pending_amount:.8f}".rstrip(
"0"
).rstrip(".")
variant_entry = {
"id": int(Coins.LTC_MWEB),
"name": getCoinName(Coins.LTC_MWEB),
"balance": variant_balance,
"pending": variant_pending,
"ticker": chainparams[Coins.LTC]["ticker"],
}
coins_with_balances.append(variant_entry)
return bytes(json.dumps(coins_with_balances), "UTF-8")
except Exception as e:
error_data = {"error": str(e)}
return bytes(json.dumps(error_data), "UTF-8")
def js_wallets(self, url_split, post_string, is_json):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -277,6 +416,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_revoked": True if o.active_ind == 2 else False,
"is_public": o.addr_to == swap_client.network_addr
or o.addr_to.strip() == "",
"message_nets": o.message_nets,
}
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if with_extra_info:
@@ -294,7 +434,6 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
offer_data["feerate_to"] = o.to_feerate
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if o.was_sent:
try:
@@ -371,7 +510,8 @@ def formatBids(swap_client, bids, filters) -> bytes:
bid_rate: int = 0 if b[10] is None else b[10]
amount_to = None
if ci_to:
amount_to = ci_to.format_amount((b[4] * bid_rate) // ci_from.COIN())
amount_to_int = (b[4] * bid_rate + ci_from.COIN() - 1) // ci_from.COIN()
amount_to = ci_to.format_amount(amount_to_int)
bid_data = {
"bid_id": b[2].hex(),
@@ -661,16 +801,14 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
if amt_from_str is not None:
rate = ci_to.make_int(rate, r=1)
amt_from = inputAmount(amt_from_str, ci_from)
amount_to = ci_to.format_amount(
int((amt_from * rate) // ci_from.COIN()), r=1
)
amount_to_int = (amt_from * rate + ci_from.COIN() - 1) // ci_from.COIN()
amount_to = ci_to.format_amount(amount_to_int)
return bytes(json.dumps({"amount_to": amount_to}), "UTF-8")
if amt_to_str is not None:
rate = ci_from.make_int(1.0 / float(rate), r=1)
amt_to = inputAmount(amt_to_str, ci_to)
amount_from = ci_from.format_amount(
int((amt_to * rate) // ci_to.COIN()), r=1
)
amount_from_int = (amt_to * rate + ci_to.COIN() - 1) // ci_to.COIN()
amount_from = ci_from.format_amount(amount_from_int)
return bytes(json.dumps({"amount_from": amount_from}), "UTF-8")
amt_from: int = inputAmount(get_data_entry(post_data, "amt_from"), ci_from)
@@ -704,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
if not swap_client.debug:
raise ValueError("Debug mode not active.")
r = random.randint(0, 3)
r = random.randint(0, 4)
if r == 0:
swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()})
swap_client.notify(
NT.OFFER_RECEIVED,
{
"offer_id": random.randbytes(28).hex(),
"coin_from": 2,
"coin_to": 6,
"amount_from": 100000000,
"amount_to": 15500000000000,
"rate": 15500000000000,
},
)
elif r == 1:
swap_client.notify(
NT.BID_RECEIVED,
@@ -714,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
"type": "atomic",
"bid_id": random.randbytes(28).hex(),
"offer_id": random.randbytes(28).hex(),
"coin_from": 2,
"coin_to": 6,
"amount_from": 100000000,
"amount_to": 15500000000000,
"bid_amount": 50000000,
"bid_amount_to": 7750000000000,
"rate": 15500000000000,
},
)
elif r == 2:
@@ -725,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
"type": "ads",
"bid_id": random.randbytes(28).hex(),
"offer_id": random.randbytes(28).hex(),
"coin_from": 1,
"coin_to": 3,
"amount_from": 500000000,
"amount_to": 100000000,
"bid_amount": 250000000,
"bid_amount_to": 50000000,
"rate": 20000000,
},
)
elif r == 4:
swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()})
return bytes(json.dumps({"type": r}), "UTF-8")
def js_checkupdates(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
from basicswap import __version__
if not swap_client.settings.get("check_updates", True):
return bytes(
json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8"
)
import time
now = time.time()
last_manual_check = getattr(swap_client, "_last_manual_update_check", 0)
if not swap_client.debug and (now - last_manual_check) < 3600:
remaining = int(3600 - (now - last_manual_check))
return bytes(
json.dumps(
{
"error": f"Please wait {remaining // 60} minutes before checking again"
}
),
"UTF-8",
)
swap_client._last_manual_update_check = now
swap_client.log.info("Manual update check requested via web interface")
swap_client.checkForUpdates()
if swap_client._update_available:
swap_client.log.info(
f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})"
)
else:
swap_client.log.info(f"Manual check result: Up to date (v{__version__})")
return bytes(
json.dumps(
{
"message": "Update check completed",
"current_version": __version__,
"latest_version": swap_client._latest_version,
"update_available": swap_client._update_available,
}
),
"UTF-8",
)
def js_notifications(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -738,6 +952,32 @@ def js_notifications(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(swap_client.getNotifications()), "UTF-8")
def js_updatestatus(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
from basicswap import __version__
return bytes(
json.dumps(
{
"update_available": swap_client._update_available,
"current_version": __version__,
"latest_version": swap_client._latest_version,
"release_url": (
f"https://github.com/basicswap/basicswap/releases/tag/v{swap_client._latest_version}"
if swap_client._latest_version
else None
),
"release_notes": (
f"New version v{swap_client._latest_version} is available. Click to view details on GitHub."
if swap_client._latest_version
else None
),
}
),
"UTF-8",
)
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -915,6 +1155,15 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
post_data = getFormData(post_string, is_json)
coin_in = get_data_entry(post_data, "coin")
extkey_prefix = get_data_entry_or(
post_data, "extkey_prefix", 0x04B2430C
) # default, zprv for P2WPKH in electrum
if isinstance(extkey_prefix, str):
if extkey_prefix.isdigit():
extkey_prefix = int(extkey_prefix)
else:
extkey_prefix = int(extkey_prefix, 16) # Try hex
try:
coin = getCoinIdFromName(coin_in)
except Exception:
@@ -951,7 +1200,6 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
wallet_seed_id = ci.getWalletSeedID()
except Exception as e:
wallet_seed_id = f"Error: {e}"
rv.update(
{
"seed": seed_key.hex(),
@@ -960,6 +1208,10 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
"current_seed_id": wallet_seed_id,
}
)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
rv.update(
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
return bytes(
json.dumps(rv),
@@ -1169,6 +1421,111 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
)
def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
volume_data = swap_client.lookupVolume(
coin_ids, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in volume_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "data": rv}),
"UTF-8",
)
def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 3600))
days: int = int(get_data_entry_or(post_data, "days", 1))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
historical_data = swap_client.lookupHistoricalData(
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in historical_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "days": days, "data": rv}),
"UTF-8",
)
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
@@ -1214,6 +1571,7 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
endpoints = {
"coins": js_coins,
"walletbalances": js_walletbalances,
"wallets": js_wallets,
"offers": js_offers,
"sentoffers": js_sentoffers,
@@ -1226,6 +1584,8 @@ endpoints = {
"rates": js_rates,
"rateslist": js_rates_list,
"generatenotification": js_generatenotification,
"checkupdates": js_checkupdates,
"updatestatus": js_updatestatus,
"notifications": js_notifications,
"identities": js_identities,
"automationstrategies": js_automationstrategies,
@@ -1239,6 +1599,8 @@ endpoints = {
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
"coinvolume": js_coinvolume,
"coinhistory": js_coinhistory,
"messageroutes": js_messageroutes,
}

View File

@@ -144,7 +144,8 @@ class OfferMessage(NonProtobufClass):
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
19: ("proof_utxos", NPBW_BYTES, 0),
20: ("auto_accept_type", 0, 0),
20: ("auto_accept_type", NPBW_INT, 0),
21: ("message_nets", NPBW_BYTES, NPBF_STR),
}
@@ -160,6 +161,7 @@ class BidMessage(NonProtobufClass):
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
9: ("proof_utxos", NPBW_BYTES, 0),
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
11: ("message_nets", NPBW_BYTES, NPBF_STR),
}
@@ -199,6 +201,7 @@ class XmrBidMessage(NonProtobufClass):
7: ("kbvf", NPBW_BYTES, 0),
8: ("kbsf_dleag", NPBW_BYTES, 0),
9: ("dest_af", NPBW_BYTES, 0),
10: ("message_nets", NPBW_BYTES, NPBF_STR),
}
@@ -261,6 +264,7 @@ class ADSBidIntentMessage(NonProtobufClass):
3: ("time_valid", NPBW_INT, 0),
4: ("amount_from", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("message_nets", NPBW_BYTES, NPBF_STR),
}
@@ -282,3 +286,21 @@ class ConnectReqMessage(NonProtobufClass):
3: ("request_type", NPBW_INT, 0),
4: ("request_data", NPBW_BYTES, 0),
}
class MessagePortalOffer(NonProtobufClass):
_map = {
1: ("network_type_from", NPBW_INT, 0),
2: ("network_type_to", NPBW_INT, 0),
3: ("portal_address_from", NPBW_BYTES, 0),
4: ("portal_address_to", NPBW_BYTES, 0),
5: ("time_valid", NPBW_INT, 0),
6: ("smsg_difficulty", NPBW_INT, 0),
}
class MessagePortalSend(NonProtobufClass):
_map = {
1: ("forward_address", NPBW_BYTES, 0), # pubkey, 33 bytes
2: ("message_bytes", NPBW_BYTES, 0),
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,9 @@ from basicswap.chainparams import (
Coins,
)
from basicswap.util.address import (
b58decode,
decodeWif,
)
from basicswap.basicswap_util import AddressTypes
def encode_base64(data: bytes) -> str:
@@ -172,34 +172,6 @@ def waitForConnected(ws_thread, delay_event):
raise ValueError("waitForConnected timed-out.")
def getPrivkeyForAddress(self, addr) -> bytes:
ci_part = self.ci(Coins.PART)
try:
return ci_part.decodeKey(
self.callrpc(
"smsgdumpprivkey",
[
addr,
],
)
)
except Exception as e: # noqa: F841
pass
try:
return ci_part.decodeKey(
ci_part.rpc_wallet(
"dumpprivkey",
[
addr,
],
)
)
except Exception as e: # noqa: F841
pass
raise ValueError("key not found")
def encryptMsg(
self,
addr_from: str,
@@ -209,42 +181,21 @@ def encryptMsg(
cursor,
timestamp=None,
deterministic=False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("encryptMsg")
try:
rv = self.callrpc(
"smsggetpubkey",
[
addr_to,
],
)
pubkey_to: bytes = b58decode(rv["publickey"])
except Exception as e: # noqa: F841
use_cursor = self.openDB(cursor)
try:
query: str = "SELECT pk_from FROM offers WHERE addr_from = :addr_to LIMIT 1"
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
query: str = (
"SELECT pk_bid_addr FROM bids WHERE bid_addr = :addr_to LIMIT 1"
)
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
raise ValueError(f"Could not get public key for address {addr_to}")
finally:
if cursor is None:
self.closeDB(use_cursor, commit=False)
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
privkey_from = getPrivkeyForAddress(self, addr_from)
payload += bytes((0,)) # Include null byte to match smsg
smsg_msg: bytes = smsgEncrypt(
privkey_from, pubkey_to, payload, timestamp, deterministic
privkey_from,
pubkey_to,
payload,
timestamp,
deterministic,
msg_valid,
difficulty_target=difficulty_target,
)
return smsg_msg
@@ -261,11 +212,21 @@ def sendSimplexMsg(
timestamp: int = None,
deterministic: bool = False,
to_user_name: str = None,
return_msg: bool = False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("sendSimplexMsg")
smsg_msg: bytes = encryptMsg(
self, addr_from, addr_to, payload, msg_valid, cursor, timestamp, deterministic
self,
addr_from,
addr_to,
payload,
msg_valid,
cursor,
timestamp,
deterministic,
difficulty_target,
)
smsg_id = smsgGetID(smsg_msg)
@@ -280,6 +241,33 @@ def sendSimplexMsg(
json_str = json.dumps(response, indent=4)
self.log.debug(f"Response {json_str}")
raise ValueError("Send failed")
if to_user_name is not None:
self.num_direct_simplex_messages_sent += 1
else:
self.num_group_simplex_messages_sent += 1
if return_msg:
return smsg_id, smsg_msg
return smsg_id
def forwardSimplexMsg(self, network, smsg_msg, to_user_name: str = None):
smsg_id = smsgGetID(smsg_msg)
ws_thread = network["ws_thread"]
if to_user_name is not None:
to = "@" + to_user_name + " "
else:
to = "#bsx "
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if getResponseData(response, "type") != "newChatItems":
json_str = json.dumps(response, indent=4)
self.log.debug(f"Response {json_str}")
raise ValueError("Send failed")
if to_user_name is not None:
self.num_direct_simplex_messages_sent += 1
else:
self.num_group_simplex_messages_sent += 1
return smsg_id
@@ -292,7 +280,7 @@ def decryptSimplexMsg(self, msg_data):
try:
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
bytes.fromhex(decrypted["pubkey_from"])
)
decrypted["to"] = self.network_addr
decrypted["msg_net"] = "simplex"
@@ -308,31 +296,34 @@ def decryptSimplexMsg(self, msg_data):
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
UNION
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
UNION
SELECT addr AS address FROM smsgaddresses WHERE active_ind = 1 AND use_type = :local_portal
)"""
now: int = self.getTime()
try:
cursor = self.openDB()
addr_rows = cursor.execute(query, {"now": now}).fetchall()
addr_rows = cursor.execute(
query, {"now": now, "local_portal": AddressTypes.PORTAL_LOCAL}
).fetchall()
decrypted = None
for row in addr_rows:
addr = row[0]
try:
vk_addr = self.getPrivkeyForAddress(cursor, addr)
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pubkey_from"])
)
decrypted["to"] = addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
finally:
self.closeDB(cursor, commit=False)
decrypted = None
for row in addr_rows:
addr = row[0]
try:
vk_addr = getPrivkeyForAddress(self, addr)
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
)
decrypted["to"] = addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
return decrypted
@@ -375,7 +366,6 @@ def parseSimplexMsg(self, chat_item):
return decrypted_msg
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
self.log.debug(f"decryptSimplexMsg error: {e}")
pass
return None
@@ -421,7 +411,7 @@ def readSimplexMsgs(self, network):
elif processEvent(self, ws_thread, msg_type, data):
pass
else:
self.log.debug(f"Unknown msg_type: {msg_type}")
self.log.debug(f"simplex: Unknown msg_type: {msg_type}")
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
except Exception as e:
self.log.debug(f"readSimplexMsgs error: {e}")
@@ -432,10 +422,11 @@ def readSimplexMsgs(self, network):
def getResponseData(data, tag=None):
if "Right" in data["resp"]:
if tag:
return data["resp"]["Right"][tag]
return data["resp"]["Right"]
for pretag in ("Right", "Left"):
if pretag in data["resp"]:
if tag:
return data["resp"][pretag][tag]
return data["resp"][pretag]
if tag:
return data["resp"][tag]
return data["resp"]
@@ -474,12 +465,14 @@ def initialiseSimplexNetwork(self, network_config) -> None:
response = waitForResponse(ws_thread, sent_id, self.delay_event)
assert "groupLinkId" in getResponseData(response, "connection")
network = {
add_network = {
"type": "simplex",
"ws_thread": ws_thread,
}
if "bridged" in network_config:
add_network["bridged"] = network_config["bridged"]
self.active_networks.append(network)
self.active_networks.append(add_network)
def closeSimplexChat(self, net_i, connId) -> bool:

View File

@@ -14,7 +14,43 @@ import time
from basicswap.util.daemon import Daemon
def serverExistsInDatabase(simplex_db_path: str, server_address: str, logger) -> bool:
try:
# Extract hostname from SMP URL format: smp://fingerprint@hostname
if server_address.startswith("smp://") and "@" in server_address:
host = server_address.split("@")[-1]
elif ":" in server_address:
host = server_address.split(":", 1)[0]
else:
host = server_address
with sqlite3.connect(simplex_db_path) as con:
c = con.cursor()
# Check for any server entry with this hostname
query = (
"SELECT COUNT(*) FROM protocol_servers WHERE host LIKE ? OR host = ?"
)
host_pattern = f"%{host}%"
count = c.execute(query, (host_pattern, host)).fetchone()[0]
if count > 0:
logger.debug(
f"Server {host} already exists in database ({count} entries)"
)
return True
else:
logger.debug(f"Server {host} not found in database")
return False
except Exception as e:
logger.error(f"Database check failed: {e}")
return False
def initSimplexClient(args, logger, delay_event):
# Need to set initial profile through CLI
# TODO: Must be a better way?
logger.info("Initialising Simplex client")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
@@ -86,28 +122,18 @@ def startSimplexClient(
args += ["--socks-proxy", socks_proxy]
if not os.path.exists(simplex_db_path):
# Need to set initial profile through CLI
# TODO: Must be a better way?
# Database doesn't exist - safe to add server during initialization
logger.info("Database not found, initializing Simplex client")
init_args = args + ["-e", "/help"] # Run command to exit client
init_args += ["-s", server_address]
initSimplexClient(init_args, logger, delay_event)
else:
# Workaround to avoid error:
# SQLite3 returned ErrorConstraint while attempting to perform step: UNIQUE constraint failed: protocol_servers.user_id, protocol_servers.host, protocol_servers.port
# TODO: Remove?
with sqlite3.connect(simplex_db_path) as con:
c = con.cursor()
if ":" in server_address:
host, port = server_address.split(":")
else:
host = server_address
port = ""
query: str = (
"SELECT COUNT(*) FROM protocol_servers WHERE host = :host and port = :port"
)
q = c.execute(query, {"host": host, "port": port}).fetchone()
if q[0] < 1:
args += ["-s", server_address]
# Database exists - only add server if it's not already there
if not serverExistsInDatabase(simplex_db_path, server_address, logger):
logger.debug(f"Adding server to Simplex CLI args: {server_address}")
args += ["-s", server_address]
else:
logger.debug("Server already exists, not adding to CLI args")
args += ["-l", log_level]

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

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

131
basicswap/rpc_pool.py Normal file
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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

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

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

View File

@@ -233,7 +233,7 @@ const CleanupManager = (function() {
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
@@ -288,11 +288,11 @@ const CleanupManager = (function() {
}
}
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
timestamp: Date.now(),
maxDataSize: options.maxDataSize || 1000
}
}
}));
log('Memory optimization complete');
@@ -311,11 +311,11 @@ const CleanupManager = (function() {
try {
const isDetached = !(listener.element instanceof Node) ||
!document.body.contains(listener.element) ||
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) {
@@ -362,12 +362,12 @@ const CleanupManager = (function() {
log(`Error checking resource ${id}: ${e.message}`);
}
});
resourcesForRemoval.forEach(id => {
this.unregisterResource(id);
removedResources++;
});
if (removedResources > 0) {
log(`Removed ${removedResources} orphaned resources`);
}
@@ -408,22 +408,22 @@ const CleanupManager = (function() {
try {
const tooltipSelectors = [
'[role="tooltip"]',
'[id^="tooltip-"]',
'.tippy-box',
'[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 ||
const isDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden') ||
element.style.display === 'none' ||

View File

@@ -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 {

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,55 +104,20 @@ const ConfigManager = (function() {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'firo',
'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash'
};
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName;
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coinName);
}
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
},
coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
if (window.CoinUtils) {
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
@@ -229,7 +176,7 @@ const ConfigManager = (function() {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {

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,15 +183,23 @@ const IdentityManager = (function() {
async function fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
let data;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
if (window.ApiManager) {
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
} else {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data = await response.json();
}
return await response.json();
return data;
} catch (error) {
if (attempt >= state.config.maxRetries) {
console.error(`[IdentityManager] Error:`, error.message);
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
return null;
}
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
const delay = state.config.retryDelay * attempt;
await new Promise(resolve => {
CleanupManager.setTimeout(resolve, delay);
});
return fetchWithRetry(address, attempt + 1);
}
}
@@ -188,5 +227,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
console.log('IdentityManager initialized');

View File

@@ -11,15 +11,15 @@ const MemoryManager = (function() {
debug: false,
protectedWebSockets: ['wsPort', 'ws_port'],
interactiveSelectors: [
'tr:hover',
'[data-tippy-root]:hover',
'.tooltip:hover',
'[data-tooltip-trigger-id]:hover',
'tr:hover',
'[data-tippy-root]:hover',
'.tooltip:hover',
'[data-tooltip-trigger-id]:hover',
'[data-tooltip-target]:hover'
],
protectedContainers: [
'#sent-tbody',
'#received-tbody',
'#sent-tbody',
'#received-tbody',
'#offers-body'
]
};
@@ -74,10 +74,10 @@ const MemoryManager = (function() {
function shouldSkipCleanup() {
if (state.isCleanupRunning) return true;
const selector = config.interactiveSelectors.join(', ');
const hoveredElements = document.querySelectorAll(selector);
return hoveredElements.length > 0;
}
@@ -130,7 +130,7 @@ const MemoryManager = (function() {
disconnectedRemoved: disconnectedResult,
memoryBefore: startMemory ? startMemory.usedMB : null,
memoryAfter: endMemory ? endMemory.usedMB : null,
memorySaved: startMemory && endMemory ?
memorySaved: startMemory && endMemory ?
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
};
@@ -162,7 +162,7 @@ const MemoryManager = (function() {
tippyRoots.forEach(root => {
const tooltipId = root.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
if (!trigger || !document.body.contains(trigger)) {
@@ -228,7 +228,7 @@ const MemoryManager = (function() {
return true;
}
function checkMemoryUsage() {
const result = {
usedJSHeapSize: 0,
@@ -288,7 +288,7 @@ const MemoryManager = (function() {
setTimeout(() => {
processingScheduled = false;
lastProcessTime = Date.now();
if (state.isCleanupRunning) {
return;
}
@@ -298,7 +298,7 @@ const MemoryManager = (function() {
tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length;
if (tooltipCount > config.maxTooltipsThreshold &&
if (tooltipCount > config.maxTooltipsThreshold &&
(Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) {
removeOrphanedTooltips();
@@ -429,7 +429,7 @@ const MemoryManager = (function() {
return true;
}
function initialize(options = {}) {
preserveTooltipFunctions();
@@ -492,8 +492,8 @@ const MemoryManager = (function() {
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}%)` :
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);
@@ -569,7 +569,7 @@ const MemoryManager = (function() {
})();
document.addEventListener('DOMContentLoaded', function() {
const isDevMode = window.location.hostname === 'localhost' ||
const isDevMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
MemoryManager.initialize({

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

@@ -166,7 +166,7 @@ const SummaryManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -205,8 +205,16 @@ const SummaryManager = (function() {
}
function fetchSummaryDataWithTimeout() {
if (window.ApiManager) {
return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
return fetch(config.summaryEndpoint, {
signal: controller.signal,
@@ -217,7 +225,11 @@ const SummaryManager = (function() {
}
})
.then(response => {
clearTimeout(timeoutId);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
@@ -226,7 +238,11 @@ const SummaryManager = (function() {
return response.json();
})
.catch(error => {
clearTimeout(timeoutId);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
throw error;
});
}
@@ -261,9 +277,12 @@ const SummaryManager = (function() {
}
if (data.event) {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
@@ -272,7 +291,7 @@ const SummaryManager = (function() {
};
webSocket.onclose = () => {
setTimeout(setupWebSocket, 5000);
CleanupManager.setTimeout(setupWebSocket, 5000);
};
}
@@ -300,7 +319,7 @@ const SummaryManager = (function() {
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
refreshTimer = CleanupManager.setInterval(() => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
@@ -334,9 +353,12 @@ const SummaryManager = (function() {
wsManager.addMessageHandler('message', (data) => {
if (data.event) {
this.fetchSummaryData()
.then(() => {})
.catch(() => {});
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
this.fetchSummaryData()
.then(() => {})
.catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
@@ -380,7 +402,7 @@ const SummaryManager = (function() {
}
return new Promise(resolve => {
setTimeout(() => {
CleanupManager.setTimeout(() => {
resolve(this.fetchSummaryData());
}, config.retryDelay);
});
@@ -440,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');

View File

@@ -14,6 +14,9 @@ const TooltipManager = (function() {
this.debug = false;
this.tooltipData = new WeakMap();
this.resources = {};
this.creationQueue = [];
this.batchSize = 5;
this.isProcessingQueue = false;
if (window.CleanupManager) {
CleanupManager.registerResource(
@@ -48,40 +51,69 @@ const TooltipManager = (function() {
this.performPeriodicCleanup(true);
}
const createTooltip = () => {
if (!document.body.contains(element)) return;
this.creationQueue.push({ element, content, options });
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltipInstance(element, content, options);
} else {
let retryCount = 0;
const maxRetries = 3;
if (!this.isProcessingQueue) {
this.processCreationQueue();
}
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltipInstance(element, content, options);
}
} else {
retryCount++;
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.requestAnimationFrame(createTooltip);
return null;
}
processCreationQueue() {
if (this.creationQueue.length === 0) {
this.isProcessingQueue = false;
return;
}
this.isProcessingQueue = true;
const batch = this.creationQueue.splice(0, this.batchSize);
CleanupManager.requestAnimationFrame(() => {
batch.forEach(({ element, content, options }) => {
this.createTooltipSync(element, content, options);
});
if (this.creationQueue.length > 0) {
CleanupManager.setTimeout(() => {
this.processCreationQueue();
}, 0);
} else {
this.isProcessingQueue = false;
}
});
}
createTooltipSync(element, content, options) {
if (!document.body.contains(element)) return;
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltipInstance(element, content, options);
} else {
let retryCount = 0;
const maxRetries = 3;
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltipInstance(element, content, options);
}
} else {
retryCount++;
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
}
createTooltipInstance(element, content, options = {}) {
if (!element || !document.body.contains(element)) {
return null;
@@ -185,12 +217,15 @@ const TooltipManager = (function() {
}
}
);
return tippyInstance[0];
}
return null;
} catch (error) {
if (window.ErrorHandler) {
return window.ErrorHandler.handleError(error, 'TooltipManager.createTooltipInstance', null);
}
console.error('Error creating tooltip:', error);
return null;
}
@@ -199,7 +234,7 @@ const TooltipManager = (function() {
destroy(element) {
if (!element) return;
try {
const destroyFn = () => {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!tooltipId) return;
@@ -224,8 +259,16 @@ const TooltipManager = (function() {
this.tooltipData.delete(element);
tooltipInstanceMap.delete(element);
} catch (error) {
console.error('Error destroying tooltip:', error);
};
if (window.ErrorHandler) {
window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
} else {
try {
destroyFn();
} catch (error) {
console.error('Error destroying tooltip:', error);
}
}
}
@@ -248,7 +291,7 @@ const TooltipManager = (function() {
this.log('Running tooltip cleanup');
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
(document.querySelector('[data-tippy-root]:hover') || document.querySelector('[data-tooltip-trigger-id]:hover'))) {
console.log('Skipping tooltip cleanup - tooltip is being hovered');
return;
@@ -299,7 +342,7 @@ const TooltipManager = (function() {
this.log('Cleaning up all tooltips');
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
document.querySelector('#offers-body tr:hover')) {
this.log('Skipping all tooltips cleanup on offers/bids page with row hover');
return;
@@ -328,7 +371,7 @@ const TooltipManager = (function() {
if (!isHovered(trigger)) {
trigger.removeAttribute('data-tooltip-trigger-id');
trigger.removeAttribute('aria-describedby');
if (trigger._tippy) {
try {
trigger._tippy.destroy();
@@ -350,7 +393,7 @@ const TooltipManager = (function() {
if (!closestHoveredRow) {
const style = window.getComputedStyle(tooltip);
const isVisible = style.display !== 'none' &&
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
@@ -372,8 +415,8 @@ const TooltipManager = (function() {
tippyElements.forEach(element => {
const tooltipId = element.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) :
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) :
null;
if (!trigger || !document.body.contains(trigger)) {
@@ -489,7 +532,7 @@ const TooltipManager = (function() {
performPeriodicCleanup(force = false) {
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
!force) {
return;
}
@@ -738,10 +781,40 @@ const TooltipManager = (function() {
}
}
initializeLazyTooltips(selector = '[data-tooltip-target]') {
const initializedTooltips = new Set();
const initializeTooltip = (element) => {
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId || initializedTooltips.has(targetId)) return;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
initializedTooltips.add(targetId);
}
};
document.addEventListener('mouseover', (e) => {
const target = e.target.closest(selector);
if (target) {
initializeTooltip(target);
}
}, { passive: true, capture: true });
this.log('Lazy tooltip initialization enabled');
}
dispose() {
this.log('Disposing TooltipManager');
try {
this.creationQueue = [];
this.isProcessingQueue = false;
this.cleanup();
Object.values(this.resources).forEach(resourceId => {
@@ -829,7 +902,12 @@ const TooltipManager = (function() {
const manager = this.getInstance();
return manager.initializeTooltips(...args);
},
initializeLazyTooltips: function(...args) {
const manager = this.getInstance();
return manager.initializeLazyTooltips(...args);
},
setDebugMode: function(enabled) {
const manager = this.getInstance();
return manager.setDebugMode(enabled);

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

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

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

@@ -16,13 +16,7 @@ const AmmCounterManager = (function() {
}
function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmCounter] ${message}`, data);
// } else {
// console.log(`[AmmCounter] ${message}`);
// }
// }
}
function updateAmmCounter(count, status) {
@@ -103,7 +97,7 @@ const AmmCounterManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -148,7 +142,7 @@ const AmmCounterManager = (function() {
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
return new Promise(resolve => {
setTimeout(() => {
CleanupManager.setTimeout(() => {
resolve(fetchAmmStatus());
}, config.retryDelay);
});
@@ -168,7 +162,7 @@ const AmmCounterManager = (function() {
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
refreshTimer = CleanupManager.setInterval(() => {
fetchAmmStatus()
.then(() => {})
.catch(() => {});
@@ -251,5 +245,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (!window.ammCounterManagerInitialized) {
window.AmmCounterManager = AmmCounterManager.initialize();
window.ammCounterManagerInitialized = true;
if (window.CleanupManager) {
CleanupManager.registerResource('ammCounter', window.AmmCounterManager, (mgr) => {
if (mgr && mgr.dispose) mgr.dispose();
});
}
}
});

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);
})();

View File

@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
}
function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmTables] ${message}`, data);
// } else {
// console.log(`[AmmTables] ${message}`);
// }
// }
}
function initializeTabs() {
@@ -67,53 +61,8 @@ const AmmTablesManager = (function() {
}
function getCoinDisplayName(coinId) {
if (config.debug) {
console.log('[AMM Tables] getCoinDisplayName called with:', coinId, typeof coinId);
}
if (typeof coinId === 'string') {
const lowerCoinId = coinId.toLowerCase();
if (lowerCoinId === 'part_anon' ||
lowerCoinId === 'particl_anon' ||
lowerCoinId === 'particl anon') {
if (config.debug) {
console.log('[AMM Tables] Matched Particl Anon variant:', coinId);
}
return 'Particl Anon';
}
if (lowerCoinId === 'part_blind' ||
lowerCoinId === 'particl_blind' ||
lowerCoinId === 'particl blind') {
if (config.debug) {
console.log('[AMM Tables] Matched Particl Blind variant:', coinId);
}
return 'Particl Blind';
}
if (lowerCoinId === 'ltc_mweb' ||
lowerCoinId === 'litecoin_mweb' ||
lowerCoinId === 'litecoin mweb') {
if (config.debug) {
console.log('[AMM Tables] Matched Litecoin MWEB variant:', coinId);
}
return 'Litecoin MWEB';
}
}
if (window.CoinManager && window.CoinManager.getDisplayName) {
const displayName = window.CoinManager.getDisplayName(coinId);
if (displayName) {
if (config.debug) {
console.log('[AMM Tables] CoinManager returned:', displayName);
}
return displayName;
}
}
if (config.debug) {
console.log('[AMM Tables] Returning coin name as-is:', coinId);
return window.CoinManager.getDisplayName(coinId) || coinId;
}
return coinId;
}
@@ -128,13 +77,13 @@ const AmmTablesManager = (function() {
<td class="py-0 px-0 text-right text-sm">
<div class="flex items-center justify-center monospace">
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12" src="/static/images/coins/${fromImage}" alt="${fromDisplayName}">
<img class="h-12" src="/static/images/coins/${toImage}" alt="${toDisplayName}">
</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12" src="/static/images/coins/${toImage}" alt="${toDisplayName}">
<img class="h-12" src="/static/images/coins/${fromImage}" alt="${fromDisplayName}">
</span>
</div>
</td>
@@ -309,7 +258,9 @@ const AmmTablesManager = (function() {
`;
});
offersBody.innerHTML = tableHtml;
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
offersBody.innerHTML = tableHtml;
}
}
function renderBidsTable(stateData) {
@@ -441,7 +392,9 @@ const AmmTablesManager = (function() {
`;
});
bidsBody.innerHTML = tableHtml;
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
bidsBody.innerHTML = tableHtml;
}
}
function formatDuration(seconds) {
@@ -540,7 +493,6 @@ const AmmTablesManager = (function() {
coinPrice = window.latestPrices[coinName.toUpperCase()];
}
if (!coinPrice || isNaN(coinPrice)) {
return null;
}
@@ -550,6 +502,9 @@ const AmmTablesManager = (function() {
function formatUSDPrice(usdValue) {
if (!usdValue || isNaN(usdValue)) return '';
if (window.config && window.config.utils && window.config.utils.formatPrice) {
return `($${window.config.utils.formatPrice('USD', usdValue)} USD)`;
}
return `($${usdValue.toFixed(2)} USD)`;
}
@@ -724,6 +679,399 @@ const AmmTablesManager = (function() {
}
}
function shouldDropdownOptionsShowBalance(select) {
const isMakerDropdown = select.id.includes('coin-from');
const isTakerDropdown = select.id.includes('coin-to');
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let isBidModal = false;
if (addModalVisible) {
const dataType = addModal.getAttribute('data-amm-type');
if (dataType) {
isBidModal = dataType === 'bid';
} else {
const modalTitle = document.getElementById('add-modal-title');
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
}
} else if (editModalVisible) {
const dataType = editModal.getAttribute('data-amm-type');
if (dataType) {
isBidModal = dataType === 'bid';
} else {
const modalTitle = document.getElementById('edit-modal-title');
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
}
}
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
return result;
}
function refreshDropdownOptions() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select || select.style.display !== 'none') return;
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdown = wrapper.querySelector('[role="listbox"]');
if (!dropdown) return;
const options = dropdown.querySelectorAll('[data-value]');
options.forEach(optionElement => {
const coinValue = optionElement.getAttribute('data-value');
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
if (!originalOption) return;
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
if (!textContainer) return;
textContainer.innerHTML = '';
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
const fullText = originalOption.textContent.trim();
const balance = originalOption.getAttribute('data-balance') || '0.00000000';
console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
if (shouldShowBalance) {
textContainer.className = 'flex flex-col';
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
const coinNameSpan = document.createElement('span');
coinNameSpan.textContent = coinName;
coinNameSpan.className = 'text-gray-900 dark:text-white';
const balanceSpan = document.createElement('span');
balanceSpan.textContent = `Balance: ${balance}`;
balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
textContainer.appendChild(coinNameSpan);
textContainer.appendChild(balanceSpan);
} else {
textContainer.className = 'flex items-center';
const coinNameSpan = document.createElement('span');
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
coinNameSpan.textContent = coinName;
coinNameSpan.className = 'text-gray-900 dark:text-white';
textContainer.appendChild(coinNameSpan);
}
});
});
}
function refreshDropdownBalances() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select || select.style.display !== 'none') return;
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdownItems = wrapper.querySelectorAll('[data-value]');
dropdownItems.forEach(item => {
const value = item.getAttribute('data-value');
const option = select.querySelector(`option[value="${value}"]`);
if (option) {
const balance = option.getAttribute('data-balance') || '0.00000000';
const pendingBalance = option.getAttribute('data-pending-balance') || '';
const balanceDiv = item.querySelector('.text-xs');
if (balanceDiv) {
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = item.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
pendingDiv = document.createElement('div');
pendingDiv.className = 'text-green-500 text-xs';
balanceDiv.parentNode.appendChild(pendingDiv);
}
pendingDiv.textContent = `+${pendingBalance} pending`;
} else if (pendingDiv) {
pendingDiv.remove();
}
}
}
});
const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
const textContainer = wrapper.querySelector('button .flex-grow');
const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
if (balanceDiv) {
const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = textContainer.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
pendingDiv = document.createElement('div');
pendingDiv.className = 'text-green-500 text-xs';
textContainer.appendChild(pendingDiv);
}
pendingDiv.textContent = `+${pendingBalance} pending`;
} else if (pendingDiv) {
pendingDiv.remove();
}
}
}
});
}
function refreshOfferDropdownBalanceDisplay() {
refreshDropdownBalances();
}
function refreshBidDropdownBalanceDisplay() {
refreshDropdownBalances();
}
function refreshDropdownBalanceDisplay(modalType = null) {
if (modalType === 'offer') {
refreshOfferDropdownBalanceDisplay();
} else if (modalType === 'bid') {
refreshBidDropdownBalanceDisplay();
} else {
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let detectedType = null;
if (addModalVisible) {
detectedType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
detectedType = editModal.getAttribute('data-amm-type');
}
if (detectedType === 'offer') {
refreshOfferDropdownBalanceDisplay();
} else if (detectedType === 'bid') {
refreshBidDropdownBalanceDisplay();
}
}
}
function updateDropdownsForModalType(modalPrefix) {
const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
if (!coinFromSelect || !coinToSelect) return;
const balanceData = {};
Array.from(coinFromSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
balanceData[option.value] = balance;
}
});
Array.from(coinToSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
balanceData[option.value] = balance;
}
});
updateDropdownOptions(coinFromSelect, balanceData);
updateDropdownOptions(coinToSelect, balanceData);
}
function updateDropdownOptions(select, balanceData, pendingData = {}) {
Array.from(select.options).forEach(option => {
const coinName = option.value;
const balance = balanceData[coinName] || '0.00000000';
const pending = pendingData[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
option.textContent = coinName;
});
}
function createSimpleDropdown(select, showBalance = false) {
if (!select) return;
const existingWrapper = select.parentNode.querySelector('.relative');
if (existingWrapper) {
existingWrapper.remove();
select.style.display = '';
}
select.style.display = 'none';
const wrapper = document.createElement('div');
wrapper.className = 'relative';
const button = document.createElement('button');
button.type = 'button';
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
button.style.minHeight = '60px';
const displayContent = document.createElement('div');
displayContent.className = 'flex items-center';
const icon = document.createElement('img');
icon.className = 'w-5 h-5 mr-2';
icon.alt = '';
const textContainer = document.createElement('div');
textContainer.className = 'flex-grow text-left';
const arrow = document.createElement('div');
arrow.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`;
displayContent.appendChild(icon);
displayContent.appendChild(textContainer);
button.appendChild(displayContent);
button.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
item.setAttribute('data-value', option.value);
const itemIcon = document.createElement('img');
itemIcon.className = 'w-5 h-5 mr-2';
itemIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
itemIcon.alt = '';
const itemText = document.createElement('div');
const coinName = option.textContent.trim();
const balance = option.getAttribute('data-balance') || '0.00000000';
const pendingBalance = option.getAttribute('data-pending-balance') || '';
if (showBalance) {
itemText.className = 'flex flex-col';
let html = `
<div class="text-gray-900 dark:text-white">${coinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
`;
if (pendingBalance && parseFloat(pendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
}
itemText.innerHTML = html;
} else {
itemText.className = 'text-gray-900 dark:text-white';
itemText.textContent = coinName;
}
item.appendChild(itemIcon);
item.appendChild(itemText);
item.addEventListener('click', function() {
select.value = this.getAttribute('data-value');
const selectedOption = select.options[select.selectedIndex];
const selectedCoinName = selectedOption.textContent.trim();
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
icon.src = itemIcon.src;
if (showBalance) {
let html = `
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
textContainer.innerHTML = html;
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
} else {
textContainer.textContent = selectedCoinName;
textContainer.className = 'flex-grow text-left';
}
dropdown.classList.add('hidden');
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
});
dropdown.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
const selectedCoinName = selectedOption.textContent.trim();
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
if (showBalance) {
let html = `
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
textContainer.innerHTML = html;
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
} else {
textContainer.textContent = selectedCoinName;
textContainer.className = 'flex-grow text-left';
}
}
button.addEventListener('click', function() {
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
dropdown.classList.add('hidden');
}
});
wrapper.appendChild(button);
wrapper.appendChild(dropdown);
select.parentNode.insertBefore(wrapper, select);
}
function setupButtonHandlers() {
const addOfferButton = document.getElementById('add-new-offer-btn');
if (addOfferButton) {
@@ -844,6 +1192,36 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('add');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
updateOfferDropdownBalances(balanceData);
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
updateBidDropdownBalances(balanceData);
}
})
.catch(error => {
console.error('Error updating dropdown balances:', error);
});
}
}, 50);
document.getElementById('add-amm-type').value = type;
document.getElementById('add-amm-name').value = 'Unnamed Offer';
@@ -940,11 +1318,6 @@ const AmmTablesManager = (function() {
if (type === 'offer') {
setupBiddingControls('add');
}
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
}
function closeAddModal() {
@@ -1056,10 +1429,10 @@ const AmmTablesManager = (function() {
newItem.min_swap_amount = parseFloat(minSwapAmount);
}
const amountStep = document.getElementById('add-offer-amount-step').value;
const amountStep = parseFloat(document.getElementById('add-offer-amount-step').value);
const offerAmount = parseFloat(document.getElementById('add-amm-amount').value);
if (!amountStep || amountStep.trim() === '') {
if (!amountStep) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
} else {
@@ -1069,8 +1442,7 @@ const AmmTablesManager = (function() {
}
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
const parsedValue = parseFloat(amountStep);
if (parsedValue <= 0) {
if (amountStep <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
} else {
@@ -1078,7 +1450,7 @@ const AmmTablesManager = (function() {
}
return;
}
if (parsedValue < 0.001) {
if (amountStep < 0.001) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
} else {
@@ -1086,15 +1458,15 @@ const AmmTablesManager = (function() {
}
return;
}
if (parsedValue > offerAmount) {
if (amountStep > offerAmount) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
} else {
alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
}
return;
}
newItem.amount_step = parsedValue.toString();
newItem.amount_step = amountStep;
console.log(`Offer Size Increment set to: ${newItem.amount_step}`);
} else {
if (window.showErrorModal) {
@@ -1270,6 +1642,36 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('edit');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
updateOfferDropdownBalances(balanceData);
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
updateBidDropdownBalances(balanceData);
}
})
.catch(error => {
console.error('Error updating dropdown balances:', error);
});
}
}, 50);
document.getElementById('edit-amm-type').value = type;
document.getElementById('edit-amm-id').value = id || '';
document.getElementById('edit-amm-original-name').value = name;
@@ -1283,8 +1685,12 @@ const AmmTablesManager = (function() {
coinFromSelect.value = item.coin_from || '';
coinToSelect.value = item.coin_to || '';
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
if (coinFromSelect) {
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
if (coinToSelect) {
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
document.getElementById('edit-amm-amount').value = item.amount || '';
@@ -1371,11 +1777,6 @@ const AmmTablesManager = (function() {
setupBiddingControls('edit');
populateBiddingControls('edit', item);
}
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
} catch (error) {
alert(`Error processing the configuration: ${error.message}`);
debugLog('Error opening edit modal:', error);
@@ -1389,7 +1790,6 @@ const AmmTablesManager = (function() {
});
}
function closeEditModal() {
const modal = document.getElementById('edit-amm-modal');
if (modal) {
@@ -1502,10 +1902,10 @@ const AmmTablesManager = (function() {
updatedItem.min_swap_amount = parseFloat(minSwapAmount);
}
const amountStep = document.getElementById('edit-offer-amount-step').value;
const amountStep = parseFloat(document.getElementById('edit-offer-amount-step').value);
const offerAmount = parseFloat(document.getElementById('edit-amm-amount').value);
if (!amountStep || amountStep.trim() === '') {
if (!amountStep) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
} else {
@@ -1515,8 +1915,7 @@ const AmmTablesManager = (function() {
}
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
const parsedValue = parseFloat(amountStep);
if (parsedValue <= 0) {
if (amountStep <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
} else {
@@ -1524,7 +1923,7 @@ const AmmTablesManager = (function() {
}
return;
}
if (parsedValue < 0.001) {
if (amountStep < 0.001) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
} else {
@@ -1532,15 +1931,15 @@ const AmmTablesManager = (function() {
}
return;
}
if (parsedValue > offerAmount) {
if (amountStep > offerAmount) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', `Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
} else {
alert(`Offer Size Increment (${parsedValue}) cannot be greater than the offer amount (${offerAmount}).`);
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
}
return;
}
updatedItem.amount_step = parsedValue.toString();
updatedItem.amount_step = amountStep;
console.log(`Offer Size Increment set to: ${updatedItem.amount_step}`);
} else {
if (window.showErrorModal) {
@@ -1810,7 +2209,7 @@ const AmmTablesManager = (function() {
}
}
function initializeCustomSelects() {
function initializeCustomSelects(modalType = null) {
const coinSelects = [
document.getElementById('add-amm-coin-from'),
document.getElementById('add-amm-coin-to'),
@@ -1823,116 +2222,13 @@ const AmmTablesManager = (function() {
document.getElementById('edit-offer-swap-type')
];
function createCoinDropdown(select) {
if (!select) return;
const wrapper = document.createElement('div');
wrapper.className = 'relative';
const display = document.createElement('div');
display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
const icon = document.createElement('img');
icon.className = 'w-5 h-5 mr-2';
icon.alt = '';
const text = document.createElement('span');
text.className = 'flex-grow';
const arrow = document.createElement('span');
arrow.className = 'ml-2';
arrow.innerHTML = `
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
`;
display.appendChild(icon);
display.appendChild(text);
display.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
item.setAttribute('data-value', option.value);
item.setAttribute('data-symbol', option.getAttribute('data-symbol') || '');
const optionIcon = document.createElement('img');
optionIcon.className = 'w-5 h-5 mr-2';
optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
optionIcon.alt = '';
const optionText = document.createElement('span');
optionText.textContent = option.textContent.trim();
item.appendChild(optionIcon);
item.appendChild(optionText);
item.addEventListener('click', function() {
select.value = this.getAttribute('data-value');
text.textContent = optionText.textContent;
icon.src = optionIcon.src;
dropdown.classList.add('hidden');
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') {
const coinFrom = document.getElementById('add-amm-coin-from');
const coinTo = document.getElementById('add-amm-coin-to');
const swapType = document.getElementById('add-offer-swap-type');
if (coinFrom && coinTo && swapType) {
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
}
} else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') {
const coinFrom = document.getElementById('edit-amm-coin-from');
const coinTo = document.getElementById('edit-amm-coin-to');
const swapType = document.getElementById('edit-offer-swap-type');
if (coinFrom && coinTo && swapType) {
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
}
}
});
dropdown.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
text.textContent = selectedOption.textContent.trim();
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
display.addEventListener('click', function(e) {
e.stopPropagation();
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', function() {
dropdown.classList.add('hidden');
});
wrapper.appendChild(display);
wrapper.appendChild(dropdown);
select.parentNode.insertBefore(wrapper, select);
select.style.display = 'none';
select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
text.textContent = selectedOption.textContent.trim();
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
});
}
function createSwapTypeDropdown(select) {
if (!select) return;
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
return;
}
const wrapper = document.createElement('div');
wrapper.className = 'relative';
@@ -1982,7 +2278,9 @@ const AmmTablesManager = (function() {
});
const selectedOption = select.options[select.selectedIndex];
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
display.addEventListener('click', function(e) {
if (select.disabled) return;
@@ -2002,7 +2300,9 @@ const AmmTablesManager = (function() {
select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
});
const observer = new MutationObserver(function(mutations) {
@@ -2024,7 +2324,18 @@ const AmmTablesManager = (function() {
}
}
coinSelects.forEach(select => createCoinDropdown(select));
coinSelects.forEach(select => {
if (!select) return;
let showBalance = false;
if (modalType === 'offer' && select.id.includes('coin-from')) {
showBalance = true;
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
showBalance = true;
}
createSimpleDropdown(select, showBalance);
});
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
}
@@ -2303,19 +2614,27 @@ const AmmTablesManager = (function() {
if (refreshButton) {
refreshButton.addEventListener('click', async function() {
if (refreshButton.disabled) return;
const icon = refreshButton.querySelector('svg');
refreshButton.disabled = true;
if (icon) {
icon.classList.add('animate-spin');
}
await initializePrices();
updateTables();
setTimeout(() => {
if (icon) {
icon.classList.remove('animate-spin');
}
}, 1000);
try {
await initializePrices();
updateTables();
} finally {
setTimeout(() => {
if (icon) {
icon.classList.remove('animate-spin');
}
refreshButton.disabled = false;
}, 500);
}
});
}
@@ -2328,7 +2647,11 @@ const AmmTablesManager = (function() {
return {
updateTables,
startRefreshTimer,
stopRefreshTimer
stopRefreshTimer,
refreshDropdownBalanceDisplay,
refreshOfferDropdownBalanceDisplay,
refreshBidDropdownBalanceDisplay,
refreshDropdownOptions
};
}

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);
}
@@ -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

@@ -66,7 +66,7 @@ const BidExporter = {
link.click();
document.body.removeChild(link);
setTimeout(() => {
CleanupManager.setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
@@ -104,7 +104,7 @@ const BidExporter = {
};
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
CleanupManager.setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
const exportAllButton = document.getElementById('exportAllBids');
if (exportAllButton) {
@@ -147,7 +147,7 @@ window.cleanup = function() {
if (exportAllButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportAllButton, 'click');
}
if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click');
}

View File

@@ -32,7 +32,7 @@ document.addEventListener('tabactivated', function(event) {
if (event.detail && event.detail.tabId) {
const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' :
(event.detail.tabId === '#sent' ? 'sent' : 'received'));
//console.log('Tab activation event received for:', tabType);
state.currentTab = tabType;
updateBidsTable();
}
@@ -190,8 +190,7 @@ const EventManager = {
};
function cleanup() {
//console.log('Starting comprehensive cleanup process for bids table');
try {
if (searchTimeout) {
clearTimeout(searchTimeout);
@@ -326,8 +325,7 @@ window.cleanupBidsTable = cleanup;
CleanupManager.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
//console.log('Page hidden - pausing WebSocket and optimizing memory');
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
WebSocketManager.pause();
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
@@ -351,7 +349,7 @@ CleanupManager.addListener(document, 'visibilitychange', () => {
const lastUpdateTime = state.lastRefresh || 0;
const now = Date.now();
const refreshInterval = 5 * 60 * 1000; // 5 minutes
const refreshInterval = 5 * 60 * 1000;
if (now - lastUpdateTime > refreshInterval) {
setTimeout(() => {
@@ -490,13 +488,7 @@ function coinMatches(offerCoin, filterCoin) {
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
if (window.CoinUtils && window.CoinUtils.isSameCoin(offerCoin, filterCoin)) {
return true;
}
@@ -926,6 +918,12 @@ const forceTooltipDOMCleanup = () => {
foundCount += allTooltipElements.length;
allTooltipElements.forEach(element => {
const isInTooltipContainer = element.closest('.tooltip-container');
if (isInTooltipContainer) {
return;
}
const isDetached = !document.body.contains(element) ||
element.classList.contains('hidden') ||
element.style.display === 'none';
@@ -1012,7 +1010,7 @@ const forceTooltipDOMCleanup = () => {
});
}
if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
}
}
@@ -1146,7 +1144,7 @@ const createTableRow = async (bid) => {
`;
}
return `
const rowHtml = `
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<!-- Time Column -->
<td class="py-3 pl-6 pr-3">
@@ -1232,13 +1230,16 @@ const createTableRow = async (bid) => {
</div>
</td>
</tr>
`;
<!-- Tooltips -->
const tooltipIdentityHtml = `
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
${tooltipContent}
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
`;
const tooltipStatusHtml = `
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="text-white">
<p class="font-bold mb-2">Transaction Status</p>
@@ -1256,6 +1257,12 @@ const createTableRow = async (bid) => {
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
`;
return {
rowHtml,
tooltipIdentityHtml,
tooltipStatusHtml
};
};
function cleanupOffscreenTooltips() {
@@ -1323,8 +1330,6 @@ async function fetchBids(type = state.currentTab) {
const withExpiredSelect = document.getElementById('with_expired');
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
//console.log(`Fetching ${type} bids, include expired:`, includeExpired);
const timeoutId = setTimeout(() => {
if (activeFetchController) {
activeFetchController.abort();
@@ -1372,8 +1377,6 @@ async function fetchBids(type = state.currentTab) {
}
}
//console.log(`Received raw ${type} data:`, data.length, 'bids');
state.filters.with_expired = includeExpired;
let processedData;
@@ -1410,7 +1413,8 @@ const updateTableContent = async (type) => {
}
cleanupTooltips();
forceTooltipDOMCleanup();
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading bids...</div></td></tr>';
tooltipIdsToCleanup.clear();
@@ -1421,47 +1425,55 @@ const updateTableContent = async (type) => {
const currentPageData = filteredData.slice(startIndex, endIndex);
//console.log('Updating table content:', {
// type: type,
// totalFilteredBids: filteredData.length,
// currentPageBids: currentPageData.length,
// startIndex: startIndex,
// endIndex: endIndex
//});
let tooltipContainerId = `tooltip-container-${type}`;
let tooltipContainer = document.getElementById(tooltipContainerId);
if (!tooltipContainer) {
tooltipContainer = document.createElement('div');
tooltipContainer.id = tooltipContainerId;
tooltipContainer.className = 'tooltip-container';
document.body.appendChild(tooltipContainer);
} else {
tooltipContainer.innerHTML = '';
}
try {
if (currentPageData.length > 0) {
const BATCH_SIZE = 10;
let allRows = [];
let allTooltips = [];
for (let i = 0; i < currentPageData.length; i += BATCH_SIZE) {
const batch = currentPageData.slice(i, i + BATCH_SIZE);
const rowPromises = batch.map(bid => createTableRow(bid));
const rows = await Promise.all(rowPromises);
allRows = allRows.concat(rows);
const rowData = await Promise.all(rowPromises);
if (i + BATCH_SIZE < currentPageData.length) {
await new Promise(resolve => setTimeout(resolve, 5));
}
rowData.forEach(data => {
allRows.push(data.rowHtml);
allTooltips.push(data.tooltipIdentityHtml);
allTooltips.push(data.tooltipStatusHtml);
});
}
const scrollPosition = tbody.parentElement?.scrollTop || 0;
tbody.innerHTML = allRows.join('');
tooltipContainer.innerHTML = allTooltips.join('');
if (tbody.parentElement && scrollPosition > 0) {
tbody.parentElement.scrollTop = scrollPosition;
}
if (document.visibilityState === 'visible') {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
initializeTooltips();
setTimeout(() => {
initializeTooltips();
setTimeout(() => {
forceTooltipDOMCleanup();
}, 100);
}, 10);
setTimeout(() => {
forceTooltipDOMCleanup();
}, 100);
});
});
}
} else {
tbody.innerHTML = `
@@ -1495,7 +1507,7 @@ const initializeTooltips = () => {
const tooltipTriggers = document.querySelectorAll(selector);
const tooltipCount = tooltipTriggers.length;
if (tooltipCount > 50) {
//console.log(`Optimizing ${tooltipCount} tooltips`);
const viewportMargin = 200;
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
const rect = trigger.getBoundingClientRect();
@@ -1595,13 +1607,6 @@ const updatePaginationControls = (type) => {
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
const bidsCount = elements[`${type}BidsCount`];
//console.log('Pagination controls update:', {
// type: type,
// totalBids: data.length,
// totalPages: totalPages,
// currentPage: state.currentPage[type]
//});
if (state.currentPage[type] > totalPages) {
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
}
@@ -2077,7 +2082,7 @@ const setupEventListeners = () => {
function setupMemoryMonitoring() {
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
const intervalId = setInterval(() => {
const intervalId = CleanupManager.setInterval(() => {
if (document.hidden) {
console.log('Tab hidden - running memory optimization');
@@ -2110,9 +2115,9 @@ function setupMemoryMonitoring() {
}
}, MEMORY_CHECK_INTERVAL);
document.addEventListener('beforeunload', () => {
CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => {
clearInterval(intervalId);
}, { once: true });
});
}
function initialize() {

View File

@@ -7,7 +7,7 @@
originalOnload();
}
setTimeout(function() {
CleanupManager.setTimeout(function() {
initBidsTabNavigation();
handleInitialNavigation();
}, 100);
@@ -15,8 +15,14 @@
document.addEventListener('DOMContentLoaded', function() {
initBidsTabNavigation();
if (window.CleanupManager) {
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
window.removeEventListener('hashchange', handleHashChange);
});
}
});
window.addEventListener('hashchange', handleHashChange);
window.bidsTabNavigationInitialized = false;
@@ -43,7 +49,7 @@
});
window.bidsTabNavigationInitialized = true;
console.log('Bids tab navigation initialized');
}
function handleInitialNavigation() {
@@ -52,16 +58,16 @@
}
const tabToActivate = localStorage.getItem('bidsTabToActivate');
if (tabToActivate) {
//console.log('Activating tab from localStorage:', tabToActivate);
localStorage.removeItem('bidsTabToActivate');
activateTabWithRetry('#' + tabToActivate);
} else if (window.location.hash) {
//console.log('Activating tab from hash:', window.location.hash);
activateTabWithRetry(window.location.hash);
} else {
//console.log('Activating default tab: #all');
activateTabWithRetry('#all');
}
}
@@ -70,13 +76,13 @@
if (window.location.pathname !== '/bids') {
return;
}
const hash = window.location.hash;
if (hash) {
//console.log('Hash changed, activating tab:', hash);
activateTabWithRetry(hash);
} else {
//console.log('Hash cleared, activating default tab: #all');
activateTabWithRetry('#all');
}
}
@@ -85,28 +91,24 @@
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
//console.log('Invalid tab ID, defaulting to #all');
activateTabWithRetry('#all');
return;
}
const tabButtonId = normalizedTabId === '#all' ? 'all-tab' :
const tabButtonId = normalizedTabId === '#all' ? 'all-tab' :
(normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab');
const tabButton = document.getElementById(tabButtonId);
if (!tabButton) {
if (retryCount < 5) {
//console.log('Tab button not found, retrying...', retryCount + 1);
setTimeout(() => {
CleanupManager.setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100);
} else {
//console.error('Failed to find tab button after retries');
}
return;
}
//console.log('Activating tab:', normalizedTabId);
tabButton.click();
@@ -115,7 +117,7 @@
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 => {
@@ -133,7 +135,7 @@
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');
@@ -146,7 +148,7 @@
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');
@@ -162,28 +164,28 @@
}
function triggerDataLoad(tabId) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
if (window.state) {
window.state.currentTab = tabId === '#all' ? 'all' :
window.state.currentTab = tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received');
if (typeof window.updateBidsTable === 'function') {
//console.log('Triggering data load for', tabId);
window.updateBidsTable();
}
}
const event = new CustomEvent('tabactivated', {
detail: {
detail: {
tabId: tabId,
type: tabId === '#all' ? 'all' :
type: tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received')
}
});
document.dispatchEvent(event);
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.cleanup();
if (typeof window.initializeTooltips === 'function') {
window.initializeTooltips();
@@ -195,10 +197,10 @@
function navigateToTabDirectly(tabId) {
const oldScrollPosition = window.scrollY;
activateTabWithRetry(tabId);
setTimeout(function() {
CleanupManager.setTimeout(function() {
window.scrollTo(0, oldScrollPosition);
}, 0);
}

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 {
@@ -443,7 +467,40 @@ const UIEnhancer = {
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
const parts = name.split(' - Balance: ');
const coinName = parts[0];
const balanceInfo = parts[1] || '';
selectNameElement.innerHTML = '';
selectNameElement.style.display = 'flex';
selectNameElement.style.flexDirection = 'column';
selectNameElement.style.alignItems = 'flex-start';
selectNameElement.style.lineHeight = '1.2';
const coinNameDiv = document.createElement('div');
coinNameDiv.textContent = coinName;
coinNameDiv.style.fontWeight = 'normal';
coinNameDiv.style.color = 'inherit';
const balanceDiv = document.createElement('div');
balanceDiv.textContent = `Balance: ${balanceInfo}`;
balanceDiv.style.fontSize = '0.75rem';
balanceDiv.style.color = '#6b7280';
balanceDiv.style.marginTop = '1px';
selectNameElement.appendChild(coinNameDiv);
selectNameElement.appendChild(balanceDiv);
} else {
selectNameElement.textContent = name;
selectNameElement.style.display = 'block';
selectNameElement.style.flexDirection = '';
selectNameElement.style.alignItems = '';
}
}
updateSelectCache(select);
@@ -537,6 +594,8 @@ function initializeApp() {
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
ErrorModal.init();
}
if (document.readyState === 'loading') {
@@ -544,3 +603,6 @@ if (document.readyState === 'loading') {
} else {
initializeApp();
}
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);

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

@@ -5,8 +5,8 @@ let jsonData = [];
let originalJsonData = [];
let currentSortColumn = 0;
let currentSortDirection = 'desc';
let filterTimeout = null;
let isPaginationInProgress = false;
let autoRefreshInterval = null;
const isSentOffers = window.offersTableConfig.isSentOffers;
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
@@ -28,6 +28,9 @@ window.tableRateModule = {
processedOffers: new Set(),
getCachedValue(key) {
if (window.CacheManager) {
return window.CacheManager.get(key);
}
const cachedItem = localStorage.getItem(key);
if (cachedItem) {
const parsedItem = JSON.parse(cachedItem);
@@ -41,6 +44,14 @@ window.tableRateModule = {
},
setCachedValue(key, value, resourceType = null) {
if (window.CacheManager) {
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
900000;
window.CacheManager.set(key, value, ttl);
return;
}
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
@@ -65,26 +76,6 @@ window.tableRateModule = {
return true;
},
formatUSD(value) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(8) + ' USD';
} else if (Math.abs(value) < 0.01) {
return value.toFixed(8) + ' USD';
} else {
return value.toFixed(2) + ' USD';
}
},
formatNumber(value, decimals) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(decimals);
} else if (Math.abs(value) < 0.01) {
return value.toFixed(decimals);
} else {
return value.toFixed(Math.min(2, decimals));
}
},
getFallbackValue(coinSymbol) {
if (!coinSymbol) return null;
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
@@ -151,6 +142,41 @@ function initializeTooltips() {
}
}
function initializeTooltipsInBatches() {
if (!window.TooltipManager) return;
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
const BATCH_SIZE = 5;
let currentIndex = 0;
function processBatch() {
const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length);
for (let i = currentIndex; i < endIndex; i++) {
const element = tooltipElements[i];
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId) continue;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
}
currentIndex = endIndex;
if (currentIndex < tooltipElements.length) {
CleanupManager.setTimeout(processBatch, 0);
}
}
if (tooltipElements.length > 0) {
CleanupManager.setTimeout(processBatch, 0);
}
}
function getValidOffers() {
if (!jsonData) {
return [];
@@ -180,7 +206,6 @@ function saveFilterSettings() {
}));
}
function getSelectedCoins(filterType) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
@@ -188,7 +213,6 @@ function getSelectedCoins(filterType) {
return ['any'];
}
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
const selected = [];
@@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) {
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
}
button.style.width = '210px';
}
@@ -270,7 +293,6 @@ function updateCoinBadges(filterType) {
const coinName = getCoinNameFromValue(coinValue, filterType);
const badge = document.createElement('span');
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
@@ -285,7 +307,6 @@ function updateCoinBadges(filterType) {
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
const coinImage = getCoinImage(coinName);
badge.innerHTML = `
@@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) {
return true;
}
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
return true;
}
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
return true;
}
@@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) {
if (checkbox) {
checkbox.checked = false;
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
applyFilters();
@@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) {
}
}
window.removeCoinFilter = removeCoinFilter;
function filterAndSortData() {
@@ -510,7 +523,6 @@ function filterAndSortData() {
return false;
}
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
const matches = coinMatches(offer.coin_to, coinNames);
@@ -519,7 +531,6 @@ function filterAndSortData() {
}
}
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
const matches = coinMatches(offer.coin_from, coinNames);
@@ -578,7 +589,7 @@ function filterAndSortData() {
if (offer.is_own_offer || isSentOffers) {
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
} else {
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
}
}
}
@@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
if (window.CoinManager) {
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
} else {
if (normalizedCoin === 'zcoin') normalizedCoin = 'firo';
if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash')
normalizedCoin = 'bitcoin-cash';
if (normalizedCoin.includes('particl')) normalizedCoin = 'particl';
if (window.CoinUtils) {
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
}
}
let price = null;
if (latestPrices && latestPrices[normalizedCoin]) {
@@ -706,7 +716,7 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
if (isOwnOffer) {
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
} else {
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
}
if (isNaN(percentDiff)) {
@@ -734,11 +744,38 @@ async function fetchLatestPrices() {
}
}
async function fetchPricesAsync() {
try {
const prices = await window.PriceManager.getPrices(false);
return prices;
} catch (error) {
console.error('Error fetching prices asynchronously:', error);
return null;
}
}
async function fetchOffers() {
const refreshButton = document.getElementById('refreshOffers');
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
const fetchWithRetry = async (url, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`);
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
@@ -751,15 +788,7 @@ async function fetchOffers() {
refreshButton.classList.add('opacity-75', 'cursor-wait');
}
const [offersResponse, pricesData] = await Promise.all([
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
fetchLatestPrices()
]);
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
const data = await offersResponse.json();
if (data.error) {
@@ -789,13 +818,20 @@ async function fetchOffers() {
jsonData = formatInitialData(processedData);
originalJsonData = [...jsonData];
latestPrices = pricesData || getEmptyPriceData();
CacheManager.set('offers_cached', jsonData, 'offers');
applyFilters();
updatePaginationInfo();
fetchPricesAsync().then(prices => {
if (prices) {
latestPrices = prices;
updateProfitLossDisplays();
}
}).catch(error => {
console.error('Error fetching prices after offers refresh:', error);
});
} catch (error) {
console.error('[Debug] Error fetching offers:', error);
NetworkManager.handleNetworkError(error);
@@ -871,27 +907,37 @@ function updateConnectionStatus(status) {
}
function updateRowTimes() {
requestAnimationFrame(() => {
const rows = document.querySelectorAll('[data-offer-id]');
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
const rows = document.querySelectorAll('[data-offer-id]');
const updates = [];
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
updates.push({
postedElement,
expiresElement,
newPostedTime,
newExpiresIn
});
});
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
});
}
function updateLastRefreshTime() {
@@ -975,12 +1021,11 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
return;
}
const formattedPercentDiff = percentDiff.toFixed(2);
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? percentDiff : -percentDiff);
const colorClass = getProfitColorClass(percentDiff);
profitLossElement.textContent = `${percentDiffDisplay}%`;
profitLossElement.textContent = `${percentDiffDisplay.toFixed(2)}%`;
profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
@@ -1098,8 +1143,13 @@ async function updateOffersTable(options = {}) {
return;
}
const isIncrementalUpdate = options.incremental === true;
if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) {
offersBody.innerHTML = '<tr><td colspan="10" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading offers...</div></td></tr>';
}
if (window.TooltipManager) {
window.TooltipManager.cleanup();
requestAnimationFrame(() => window.TooltipManager.cleanup());
}
const validOffers = getValidOffers();
@@ -1139,28 +1189,72 @@ async function updateOffersTable(options = {}) {
if (row) fragment.appendChild(row);
});
if (i + BATCH_SIZE < itemsToDisplay.length) {
await new Promise(resolve => setTimeout(resolve, 16));
}
}
if (offersBody) {
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => cleanupRow(row));
offersBody.textContent = '';
offersBody.appendChild(fragment);
if (isIncrementalUpdate && offersBody.children.length > 0) {
const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]'));
const newRows = Array.from(fragment.querySelectorAll('tr[data-offer-id]'));
const existingMap = new Map(existingRows.map(row => [row.getAttribute('data-offer-id'), row]));
const newMap = new Map(newRows.map(row => [row.getAttribute('data-offer-id'), row]));
existingRows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
if (!newMap.has(offerId)) {
cleanupRow(row);
row.remove();
}
});
newRows.forEach((newRow, index) => {
const offerId = newRow.getAttribute('data-offer-id');
const existingRow = existingMap.get(offerId);
if (existingRow) {
const currentIndex = Array.from(offersBody.children).indexOf(existingRow);
if (currentIndex !== index) {
if (index >= offersBody.children.length) {
offersBody.appendChild(existingRow);
} else {
offersBody.insertBefore(existingRow, offersBody.children[index]);
}
}
} else {
if (index >= offersBody.children.length) {
offersBody.appendChild(newRow);
} else {
offersBody.insertBefore(newRow, offersBody.children[index]);
}
}
});
} else {
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => cleanupRow(row));
offersBody.textContent = '';
offersBody.appendChild(fragment);
}
}
initializeTooltips();
initializeTooltipsInBatches();
requestAnimationFrame(() => {
CleanupManager.setTimeout(() => {
updateRowTimes();
updatePaginationInfo();
updateProfitLossDisplays();
}, 10);
CleanupManager.setTimeout(() => {
if (tableRateModule?.initializeTable) {
tableRateModule.initializeTable();
}
});
}, 50);
lastRefreshTime = Date.now();
updateLastRefreshTime();
@@ -1172,7 +1266,10 @@ async function updateOffersTable(options = {}) {
}
function updateProfitLossDisplays() {
const rows = document.querySelectorAll('[data-offer-id]');
const updates = [];
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
@@ -1180,6 +1277,17 @@ function updateProfitLossDisplays() {
const fromAmount = parseFloat(offer.amount_from) || 0;
const toAmount = parseFloat(offer.amount_to) || 0;
updates.push({
row,
offerId,
offer,
fromAmount,
toAmount
});
});
updates.forEach(({ row, offerId, offer, fromAmount, toAmount }) => {
updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
const rateTooltipId = `tooltip-rate-${offerId}`;
@@ -1495,7 +1603,6 @@ function createRateColumn(offer, coinFrom, coinTo) {
`;
}
function createPercentageColumn(offer) {
return `
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
@@ -1732,45 +1839,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
const getPriceKey = (coin) => {
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') return 'firo';
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
}
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
return 'part';
}
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
const symbol = window.config.coinMappings.nameToSymbol[coin];
if (symbol) {
if (symbol.toUpperCase() === 'BCH') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
}
if (symbol.toUpperCase() === 'PART') {
return 'part';
}
return symbol.toLowerCase();
}
}
return lowerCoin;
return coin.toLowerCase();
};
const fromSymbol = getPriceKey(coinFrom);
@@ -1808,38 +1880,37 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
if (isSentOffers || isOwnOffer) {
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
} else {
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
}
const formattedPercentDiff = percentDiff.toFixed(2);
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? percentDiff : -percentDiff);
const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss";
const profitLabel = percentDiff > 0 ? "Max Profit" : "Max Loss";
const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying";
const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying";
return `
<p class="font-bold mb-1">Profit/Loss Calculation:</p>
<p>You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD) <br/> and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).</p>
<p class="mt-1">Percentage difference: ${percentDiffDisplay}%</p>
<p>${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${Math.abs(profitUSD).toFixed(2)} USD</p>
<p class="mt-1">Percentage difference: ${percentDiffDisplay.toFixed(2)}%</p>
<p>${profitLabel}: ${Math.abs(profitUSD).toFixed(2)} USD</p>
<p class="font-bold mt-2">Calculation:</p>
<p>Percentage = ${(isSentOffers || isOwnOffer) ?
"((To Amount in USD / From Amount in USD) - 1) * 100" :
"((From Amount in USD / To Amount in USD) - 1) * 100"}</p>
"(-((To Amount in USD / From Amount in USD) - 1)) * 100"}</p>
<p>USD ${profitLabel} = To Amount in USD - From Amount in USD</p>
<p class="font-bold mt-1">Interpretation:</p>
${(isSentOffers || isOwnOffer) ? `
<p><span class="text-green-500">Positive percentage:</span> You're selling above market rate (profitable)</p>
<p><span class="text-red-500">Negative percentage:</span> You're selling below market rate (loss)</p>
<p><span class="text-green-500">Green:</span> You're selling above market rate (profitable)</p>
<p><span class="text-red-500">Red:</span> You're selling below market rate (loss)</p>
` : `
<p><span class="text-green-500">Positive percentage:</span> You're buying below market rate (savings)</p>
<p><span class="text-red-500">Negative percentage:</span> You're buying above market rate (premium)</p>
<p><span class="text-green-500">Green:</span> You're buying below market rate (savings)</p>
<p><span class="text-red-500">Red:</span> You're buying above market rate (premium)</p>
`}
<p class="mt-1"><strong>Note:</strong> ${(isSentOffers || isOwnOffer) ?
"As a seller, a positive percentage means <br/> you're selling for more than the current market value." :
"As a buyer, a positive percentage indicates </br> potential savings compared to current market rates."}</p>
"As a seller, a green percentage means <br/> you're selling for more than the current market rate." :
"As a buyer, a green percentage indicates </br> potential savings compared to current market rate."}</p>
<p class="mt-1"><strong>Market Rate:</strong> 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}</p>
<p><strong>Offer Rate:</strong> 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}</p>
`;
@@ -1851,44 +1922,10 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
const getPriceKey = (coin) => {
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') return 'firo';
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
}
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
return 'part';
}
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
const symbol = window.config.coinMappings.nameToSymbol[coin];
if (symbol) {
if (symbol.toUpperCase() === 'BCH') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
}
if (symbol.toUpperCase() === 'PART') {
return 'part';
}
return symbol.toLowerCase();
}
}
return lowerCoin;
return coin.toLowerCase();
};
const fromSymbol = getPriceKey(coinFrom);
@@ -1921,15 +1958,14 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
const rateInUSD = rate * toPriceUSD;
const marketRate = fromPriceUSD / toPriceUSD;
const percentDiff = marketRate ? ((rate - marketRate) / marketRate) * 100 : 0;
const formattedPercentDiff = percentDiff.toFixed(2);
const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
(percentDiff > 0 ? percentDiff : -percentDiff);
const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at";
const action = treatAsSentOffer ? "selling" : "buying";
return `
<p class="font-bold mb-1">Exchange Rate Explanation:</p>
<p>This offer is ${action} ${coinFrom} for ${coinTo} <br/>at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.</p>
<p>This offer is ${action} ${coinFrom} for ${coinTo} <br/>at a rate that is ${percentDiffDisplay.toFixed(2)}% ${aboveOrBelow} market price.</p>
<p class="font-bold mt-1">Exchange Rates:</p>
<p>1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}</p>
<p>1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}</p>
@@ -1961,23 +1997,23 @@ function updateTooltipTargets(row, uniqueId) {
});
}
function applyFilters() {
if (filterTimeout) {
clearTimeout(filterTimeout);
filterTimeout = null;
function applyFilters(options = {}) {
if (window.filterTimeout) {
clearTimeout(window.filterTimeout);
window.filterTimeout = null;
}
try {
filterTimeout = setTimeout(() => {
window.filterTimeout = CleanupManager.setTimeout(() => {
currentPage = 1;
jsonData = filterAndSortData();
updateOffersTable();
updateOffersTable(options);
updateClearFiltersButton();
filterTimeout = null;
window.filterTimeout = null;
}, 250);
} catch (error) {
console.error('Error in filter timeout:', error);
filterTimeout = null;
window.filterTimeout = null;
}
}
@@ -2040,13 +2076,10 @@ function formatTimeLeft(timestamp) {
}
function getDisplayName(coinName) {
if (window.CoinManager) {
if (window.CoinManager && window.CoinManager.getDisplayName) {
return window.CoinManager.getDisplayName(coinName) || coinName;
}
if (coinName.toLowerCase() === 'zcoin') {
return 'Firo';
}
return window.config.coinMappings.nameToDisplayName[coinName] || coinName;
return coinName;
}
function getCoinSymbolLowercase(coin) {
@@ -2088,38 +2121,23 @@ function escapeHtml(unsafe) {
}
function getPriceKey(coin) {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coin);
}
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') {
return 'firo';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin);
}
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
return 'bitcoin-cash';
}
if (lowerCoin === 'part' || lowerCoin === 'particl' ||
lowerCoin.includes('particl')) {
return 'particl';
}
return lowerCoin;
return coin ? coin.toLowerCase() : null;
}
function getCoinSymbol(fullName) {
if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return window.config.coinMappings.nameToSymbol[fullName] || fullName;
if (window.CoinUtils) {
return window.CoinUtils.getCoinSymbol(fullName);
}
return fullName;
}
function initializeTableEvents() {
@@ -2143,7 +2161,6 @@ function initializeTableEvents() {
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
if (coinToButton && coinToDropdown) {
CleanupManager.addListener(coinToButton, 'click', (e) => {
e.stopPropagation();
@@ -2158,7 +2175,6 @@ function initializeTableEvents() {
});
}
if (coinFromButton && coinFromDropdown) {
CleanupManager.addListener(coinFromButton, 'click', (e) => {
e.stopPropagation();
@@ -2223,15 +2239,16 @@ function initializeTableEvents() {
refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed');
if (countdownInterval) clearInterval(countdownInterval);
if (window.countdownInterval) clearInterval(window.countdownInterval);
countdownInterval = setInterval(() => {
window.countdownInterval = CleanupManager.setInterval(() => {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
if (remainingTime <= 0) {
clearInterval(countdownInterval);
clearInterval(window.countdownInterval);
window.countdownInterval = null;
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
@@ -2243,7 +2260,6 @@ function initializeTableEvents() {
return;
}
console.log('Manual refresh initiated');
lastRefreshTime = now;
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
@@ -2270,10 +2286,10 @@ function initializeTableEvents() {
if (!priceData && previousPrices) {
console.log('Using previous price data after failed refresh');
latestPrices = previousPrices;
applyFilters();
applyFilters({ incremental: false });
} else if (priceData) {
latestPrices = priceData;
applyFilters();
applyFilters({ incremental: false });
} else {
throw new Error('Unable to fetch price data');
}
@@ -2281,8 +2297,6 @@ function initializeTableEvents() {
lastRefreshTime = now;
updateLastRefreshTime();
console.log('Manual refresh completed successfully');
} catch (error) {
console.error('Error during manual refresh:', error);
NetworkManager.handleNetworkError(error);
@@ -2323,7 +2337,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2343,7 +2357,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2419,18 +2433,44 @@ function handleTableSort(columnIndex, header) {
clearTimeout(window.sortTimeout);
}
window.sortTimeout = setTimeout(() => {
window.sortTimeout = CleanupManager.setTimeout(() => {
applyFilters();
}, 100);
}
function startAutoRefresh() {
const REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
autoRefreshInterval = CleanupManager.setInterval(async () => {
try {
const response = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
if (response.ok) {
}
} catch (error) {
console.error('[Auto-refresh] Error during background refresh:', error);
}
}, REFRESH_INTERVAL);
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
async function initializeTableAndData() {
loadSavedSettings();
updateClearFiltersButton();
initializeTableEvents();
initializeTooltips();
updateFilterButtonText('coin_to');
updateFilterButtonText('coin_from');
updateCoinBadges('coin_to');
@@ -2530,24 +2570,44 @@ document.addEventListener('DOMContentLoaded', async function() {
if (window.WebSocketManager) {
WebSocketManager.addMessageHandler('message', async (data) => {
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
//console.log('WebSocket event received:', data.event);
try {
const fetchWithRetry = async (url, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
const previousPrices = latestPrices;
const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
const newData = await offersResponse.json();
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
jsonData = formatInitialData(processedNewData);
const newFormattedData = formatInitialData(processedNewData);
const oldOfferIds = originalJsonData.map(o => o.offer_id).sort().join(',');
const newOfferIds = newFormattedData.map(o => o.offer_id).sort().join(',');
const dataChanged = oldOfferIds !== newOfferIds;
if (!dataChanged) {
return;
}
jsonData = newFormattedData;
originalJsonData = [...jsonData];
const previousPrices = latestPrices;
let priceData;
if (window.PriceManager) {
priceData = await window.PriceManager.getPrices(true);
priceData = await window.PriceManager.getPrices(false);
} else {
priceData = await fetchLatestPrices();
}
@@ -2556,12 +2616,10 @@ document.addEventListener('DOMContentLoaded', async function() {
latestPrices = priceData;
CacheManager.set('prices_coingecko', priceData, 'prices');
} else if (previousPrices) {
console.log('Using previous price data after failed refresh');
latestPrices = previousPrices;
}
applyFilters();
applyFilters({ incremental: true, skipSkeleton: true });
updateProfitLossDisplays();
document.querySelectorAll('.usd-value').forEach(usdValue => {
@@ -2572,8 +2630,14 @@ document.addEventListener('DOMContentLoaded', async function() {
if (price !== undefined && price !== null) {
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
if (!isNaN(amount) && amount > 0) {
const usdValue = amount * price;
usdValue.textContent = tableRateModule.formatUSD(usdValue);
const calculatedUSD = amount * price;
const formattedUSD = calculatedUSD < 0.01
? calculatedUSD.toFixed(8) + ' USD'
: calculatedUSD.toFixed(2) + ' USD';
if (usdValue.textContent !== formattedUSD) {
usdValue.textContent = formattedUSD;
}
}
}
}
@@ -2581,7 +2645,6 @@ document.addEventListener('DOMContentLoaded', async function() {
updatePaginationInfo();
//console.log('WebSocket-triggered refresh completed successfully');
} catch (error) {
console.error('Error during WebSocket-triggered refresh:', error);
NetworkManager.handleNetworkError(error);
@@ -2616,9 +2679,7 @@ document.addEventListener('DOMContentLoaded', async function() {
});
}
if (window.config.autoRefreshEnabled) {
startAutoRefresh();
}
startAutoRefresh();
const filterForm = document.getElementById('filterForm');
if (filterForm) {
@@ -2652,20 +2713,10 @@ document.addEventListener('DOMContentLoaded', async function() {
}
});
const rowTimeInterval = setInterval(updateRowTimes, 30000);
if (CleanupManager.registerResource) {
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} else if (CleanupManager.addResource) {
CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} else {
window._cleanupIntervals = window._cleanupIntervals || [];
window._cleanupIntervals.push(rowTimeInterval);
}
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} catch (error) {
console.error('Error during initialization:', error);
@@ -2697,6 +2748,8 @@ function cleanup() {
window.countdownInterval = null;
}
stopAutoRefresh();
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
window._cleanupIntervals.forEach(interval => {
clearInterval(interval);
@@ -2742,7 +2795,6 @@ function cleanup() {
}
}
//console.log('Offers.js cleanup completed');
} catch (error) {
console.error('Error during cleanup:', error);
}

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,29 +53,6 @@ const api = {
}
},
fetchCryptoCompareDataXHR: (coin) => {
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
}
return Api.fetchCryptoCompareData(coin, {
cryptoCompare: apiKeys.cryptoCompare
});
} catch (error) {
logger.error(`CryptoCompare request failed for ${coin}:`, error);
NetworkManager.handleNetworkError(error);
const cachedData = CacheManager.get(`coinData_${coin}`);
if (cachedData) {
logger.info(`Using cached data for ${coin}`);
return cachedData.value;
}
return { error: error.message };
}
},
fetchCoinGeckoDataXHR: async () => {
try {
const priceData = await window.PriceManager.getPrices();
@@ -172,10 +108,7 @@ const api = {
const historicalData = await Api.fetchHistoricalData(
coinSymbols,
window.config.currentResolution,
{
cryptoCompare: window.config.getAPIKeys().cryptoCompare
}
window.config.currentResolution
);
Object.keys(historicalData).forEach(coin => {
@@ -209,8 +142,7 @@ const api = {
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: window.config.rateLimits.coingecko.minInterval,
cryptocompare: window.config.rateLimits.cryptocompare.minInterval
coingecko: window.config.rateLimits.coingecko.minInterval
},
requestQueue: {},
retryDelays: window.config.retryDelays,
@@ -242,7 +174,7 @@ const rateLimiter = {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
@@ -252,7 +184,7 @@ const rateLimiter = {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -260,7 +192,7 @@ const rateLimiter = {
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -303,7 +235,7 @@ const ui = {
if (isError || volume24h === null || volume24h === undefined) {
volumeElement.textContent = 'N/A';
} else {
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
}
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
}
@@ -332,10 +264,10 @@ const ui = {
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;
}
@@ -343,9 +275,9 @@ const ui = {
priceBTC = 0;
}
}
priceChange1d = data.price_change_percentage_24h || 0;
volume24h = data.total_volume || 0;
volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null;
if (isNaN(priceUSD) || isNaN(priceBTC)) {
throw new Error(`Invalid numeric values in data for ${coin}`);
}
@@ -498,7 +430,7 @@ const ui = {
chartContainer.classList.add('blurred');
if (duration > 0) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
ui.hideErrorMessage();
}, duration);
}
@@ -929,8 +861,11 @@ destroyChart: function() {
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
data = allData[coinSymbol];
if (!data || Object.keys(data).length === 0) {
throw new Error(`No data returned for ${coinSymbol}`);
if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) {
console.warn(`No price data available for ${coinSymbol}`);
chartModule.hideChartLoader();
chartModule.showNoDataMessage(coinSymbol);
return;
}
CacheManager.set(cacheKey, data, 'chart');
@@ -960,6 +895,8 @@ destroyChart: function() {
chartModule.initChart();
}
chartModule.hideNoDataMessage();
const chartData = chartModule.prepareChartData(coinSymbol, data);
if (chartData.length > 0 && chartModule.chart) {
chartModule.chart.data.datasets[0].data = chartData;
@@ -1014,6 +951,41 @@ destroyChart: function() {
chart.classList.remove('hidden');
},
showNoDataMessage: function(coinSymbol) {
const chartCanvas = document.getElementById('coin-chart');
if (!chartCanvas) {
return;
}
if (this.chart) {
this.chart.data.datasets[0].data = [];
this.chart.update('none');
}
let messageDiv = document.getElementById('chart-no-data-message');
if (!messageDiv) {
messageDiv = document.createElement('div');
messageDiv.id = 'chart-no-data-message';
messageDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #888; font-size: 14px; z-index: 10;';
chartCanvas.parentElement.style.position = 'relative';
chartCanvas.parentElement.appendChild(messageDiv);
}
messageDiv.innerHTML = `
<div style="padding: 20px; background: rgba(0,0,0,0.05); border-radius: 8px;">
<div style="font-size: 16px; margin-bottom: 8px;">No Price Data Available</div>
</div>
`;
messageDiv.classList.remove('hidden');
},
hideNoDataMessage: function() {
const messageDiv = document.getElementById('chart-no-data-message');
if (messageDiv) {
messageDiv.classList.add('hidden');
}
},
cleanup: function() {
if (this.pendingAnimationFrame) {
cancelAnimationFrame(this.pendingAnimationFrame);
@@ -1136,15 +1108,32 @@ const app = {
if (chartModule.chart) {
window.config.currentResolution = 'day';
await chartModule.updateChart('BTC');
app.updateResolutionButtons('BTC');
let defaultCoin = null;
if (window.config.coins && window.config.coins.length > 0) {
for (const coin of window.config.coins) {
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
if (container) {
defaultCoin = coin.symbol;
break;
}
}
}
if (!defaultCoin) {
defaultCoin = 'BTC';
}
await chartModule.updateChart(defaultCoin);
app.updateResolutionButtons(defaultCoin);
const chartTitle = document.getElementById('chart-title');
if (chartTitle) {
chartTitle.textContent = 'Price Chart (BTC)';
chartTitle.textContent = `Price Chart (${defaultCoin})`;
}
ui.setActiveContainer(`${defaultCoin.toLowerCase()}-container`);
}
ui.setActiveContainer('btc-container');
app.setupEventListeners();
app.initAutoRefresh();
@@ -1182,11 +1171,11 @@ const app = {
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
}
@@ -1214,11 +1203,7 @@ const app = {
} else {
try {
ui.showCoinLoader(coin.symbol);
if (coin.usesCoinGecko) {
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
} else {
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
}
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
if (data.error) {
throw new Error(data.error);
}
@@ -1365,7 +1350,7 @@ const app = {
}
const timeUntilRefresh = nextRefreshTime - now;
app.nextRefreshTime = nextRefreshTime;
app.autoRefreshInterval = setTimeout(() => {
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
if (NetworkManager.isOnline()) {
app.refreshAllData();
} else {
@@ -1377,8 +1362,7 @@ const app = {
},
refreshAllData: async function() {
//console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...');
return;
@@ -1398,7 +1382,7 @@ refreshAllData: async function() {
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
let remainingTime = seconds;
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
@@ -1411,7 +1395,6 @@ refreshAllData: async function() {
return;
}
//console.log('Starting refresh of all data...');
app.isRefreshing = true;
app.updateNextRefreshTime();
ui.showLoader();
@@ -1426,7 +1409,7 @@ refreshAllData: async function() {
console.warn('BTC price update failed, continuing with cached or default value');
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
@@ -1451,11 +1434,11 @@ refreshAllData: async function() {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
} else {
try {
@@ -1478,15 +1461,13 @@ refreshAllData: async function() {
const cacheKey = `coinData_${coin.symbol}`;
CacheManager.set(cacheKey, coinData, 'prices');
//console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
} catch (coinError) {
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
failedCoins.push(coin.symbol);
}
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
if (chartModule.currentCoin) {
try {
@@ -1508,7 +1489,7 @@ refreshAllData: async function() {
let countdown = 5;
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
@@ -1518,8 +1499,7 @@ 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);
NetworkManager.handleNetworkError(error);
@@ -1527,7 +1507,7 @@ refreshAllData: async function() {
let countdown = 10;
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
@@ -1549,7 +1529,6 @@ refreshAllData: async function() {
app.scheduleNextRefresh();
}
//console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
}
},
@@ -1573,7 +1552,7 @@ refreshAllData: async function() {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.add('animate-spin');
setTimeout(() => {
CleanupManager.setTimeout(() => {
svg.classList.remove('animate-spin');
}, 2000);
}

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) => {
@@ -520,8 +520,6 @@ const createSwapTableRow = async (swap) => {
async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
//console.log('Updating swaps table:', { resetPage, refreshData });
if (state.refreshPromise) {
await state.refreshPromise;
return;
@@ -547,19 +545,17 @@ async function updateSwapsTable(options = {}) {
}
const data = await response.json();
//console.log('Received swap data:', data);
state.swapsData = Array.isArray(data)
? data.filter(swap => {
const isActive = isActiveSwap(swap);
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
return isActive;
})
: [];
//console.log('Filtered active swaps:', state.swapsData);
} catch (error) {
//console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
@@ -585,8 +581,6 @@ async function updateSwapsTable(options = {}) {
const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
//console.log('Current page swaps:', currentPageSwaps);
if (elements.swapsBody) {
if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
@@ -607,7 +601,7 @@ async function updateSwapsTable(options = {}) {
});
}
} else {
//console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
@@ -679,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
WebSocketManager.initialize();
setupEventListeners();
await updateSwapsTable({ resetPage: true, refreshData: true });
const autoRefreshInterval = setInterval(async () => {
const autoRefreshInterval = CleanupManager.setInterval(async () => {
await updateSwapsTable({ resetPage: false, refreshData: true });
}, 10000); // 30 seconds
}, 10000);
CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => {
clearInterval(autoRefreshInterval);
});
});

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);

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