361 Commits
v0.14.3 ... dev

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

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

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

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

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

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

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

* Fix GUI Rate tolerance.

* Fix: json/rate

* Fix: Mismatch

* Fix: Use backend handle calc.

* Cleanup

* Fix: format_amount

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

* BLACK + FLAKE8

* Clean-up.

* Fix refresh intervals + Fix pending balance.

* Fix amounts scientific notation (1e-8)

* Better Notifications (Toasts)

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

* Fix MWEB doesnt show as pending + Various fixes.

* Fix: USD values are off with part blind.

* Fix: Percentage change buttons on wallet page.

* Cleanup debug on wallet page.

* Use ZMQ for part balances.

* Fix ZMQ config.

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

* Use coin-manager / clean-up.

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

* Fix amount not empty.

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

* Fix Enabled/Disabled logic.

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

* Fix selenium test for test_settings.py

* Fix + BLACK

* Small fix.

* Fix settings.html + Small fix on tests.

* Fix default state.

* Fix selenium charts.

* Fix switch back tab (tests)

* fix XMR (tests)

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

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

* Better changepassword page + Various fixes.

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

* LINT + Fixes

* Remove unused global variables.

* BLACK

* BLACK

* AMM - Various Fixes/Features/Bug Fixes.

* FLAKE

* FLAKE

* BLACK

* Small fix

* Fix

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

* Updated createoffers.py

* BLACK

* AMM Styling

* Update bid_xmr template confirm model.

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

* Fix: Better redirect.

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

* GUI v3.2.2

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

* Fix math.

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

* Donation page.

* Updated header.html + Typo.

* Update on createoffer.py + BLACK

* AMM: html updates.

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

* AMM: Amount step default 0.001

* Fix global settings.

* Update createoffers.py

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

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

* Fixed debug + New feature attempt bids first.

* Fix: Orderbook (Auto-Accept)

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

* Fix: with_extra_info

* Small fix automation_strat_id

* Various fixes.

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 08:04:10 +00:00
tecnovert
13847e129b Fix adaptor sig swaps to Firo. 2025-05-13 22:09:16 +02:00
tecnovert
f6914d7c30 tests: Disable test_13_locked_xmr when xmr version >= 0.18.4. 2025-05-12 00:21:37 +02:00
tecnovert
2a8ac051fc Fix prefunded ITX. 2025-05-11 14:29:21 +02:00
tecnovert
3ea7a219d1 tests: Fix LTC wallet creation. 2025-05-11 14:29:18 +02:00
tecnovert
80915d9865 Deduplicate getP2SHScriptForHash 2025-05-11 14:29:02 +02:00
tecnovert
38302d2d79 Merge pull request #307 from gerlofvanek/fixes-17
Fix: Price Tiles / (JS) Better memory cleanup/manager / Improved header / ALL tab/table on bids page + Various fixes.
2025-05-10 19:41:53 +00:00
gerlofvanek
e7b47486f5 ALL tab/table on bids page. + Fix bids export. 2025-05-09 20:43:31 +02:00
gerlofvanek
b3c0ad7e9c Add clickable bid counters in the header that navigate to the sent/received tabs + small fix. 2025-05-08 22:31:39 +02:00
gerlofvanek
ece9d7fb4b Removed repeating console.log(s) 2025-05-08 21:09:24 +02:00
gerlofvanek
868b2475c1 Fix: Better memory/tooltip/clean-up managers, various fixes. 2025-05-08 21:01:02 +02:00
tecnovert
27c3b93ff9 Raise version to 0.14.5 2025-05-06 19:18:02 +02:00
tecnovert
7df2f1b290 Add check for minimum sqlite version. 2025-05-05 00:38:02 +02:00
gerlofvanek
d57a148ff4 Fix: Price Tiles volume/btc display + Better memory clean / tooltip manager. 2025-05-04 19:51:22 +02:00
tecnovert
aa898a9601 Merge pull request #306 from tecnovert/sip
UI: Improve swaps in progress.
2025-05-02 09:31:03 +00:00
tecnovert
ec5ea4ca3c Merge pull request #304 from nahuhh/pr_bids-patch
bids: cosmetic fixes
2025-05-02 09:30:51 +00:00
tecnovert
ed18b36da6 Merge pull request #303 from justanwar/firo_v0141401_hardfork
firo: v0.14.14.1 hardfork 2025-05-28 mandatory
2025-05-02 09:30:36 +00:00
tecnovert
058270ec7a tests: Add bid and active pages to test_swap_direction.py 2025-05-01 00:25:39 +02:00
tecnovert
2818afc933 ui: Swap send/receive for sent bids in active.html. 2025-04-29 20:10:15 +02:00
tecnovert
48bfdb7462 Fix js_active amounts for reverse bids. 2025-04-29 19:48:49 +02:00
nahuhh
e14b9b7e6e bids: adjust / consolidate colors 2025-04-24 23:19:19 +00:00
nahuhh
a87180f2ef header: bid totals too high 2025-04-24 23:19:19 +00:00
nahuhh
66d763e8ea bids: fix hidden details column 2025-04-24 23:19:12 +00:00
justanwar
061a09f3fb firo: v0.14.14.1 hardfork 2025-05-28 mandatory 2025-04-21 21:35:53 +08:00
tecnovert
e7af4f9005 Merge pull request #301 from gerlofvanek/smallfix
Fix the rate/amount variable toggles on order creation.
2025-04-19 05:56:50 +00:00
tecnovert
a22274b06d Merge pull request #299 from nahuhh/patch-1
cosmetic issue & some lints
2025-04-19 05:56:26 +00:00
gerlofvanek
3b2b666c75 Fix the rate/amount variable toggles on order creation. 2025-04-18 19:59:41 +02:00
nahuhh
ec092eaa6e cleanup 2025-04-17 02:44:42 +00:00
tecnovert
b605bd4bc3 tests: Limit infinite loop in ci. 2025-04-16 22:11:24 +02:00
tecnovert
934aab9d8a Allow starting with a subset of configured coins. 2025-04-16 20:20:43 +02:00
tecnovert
21c0a534f2 Timeout waiting for mutex on shutdown. 2025-04-16 20:20:43 +02:00
Cryptoguard
b293b5daee Update install.md 2025-04-16 12:47:24 -04:00
tecnovert
8cfc405bc1 guix: Update packed version. 2025-04-14 20:19:58 +02:00
tecnovert
3c18a3ed26 Raise version to 0.14.4. 2025-04-14 20:14:08 +02:00
tecnovert
b826d9658a Merge pull request #297 from tecnovert/wallet_encryption
Add workaround for btc seed changing after encrypting wallet.
2025-04-14 18:03:29 +00:00
tecnovert
d89a58242f tests: Fix test_btc_xmr on windows. 2025-04-14 19:28:49 +02:00
tecnovert
5a4b1c737c Refresh BCH keypool. 2025-04-14 19:28:46 +02:00
tecnovert
6bc654f57e Revert active hdchains. 2025-04-14 19:28:37 +02:00
tecnovert
3e98f174cd Add workaround for btc seed changing after encrypting wallet.
Loses wallet history, rescanblockchain won't work on pruned chains.
2025-04-14 19:27:56 +02:00
tecnovert
550435e15f Merge pull request #292 from tecnovert/persist_prepare_conf
prepare: Make setup config persistent.
2025-04-14 17:19:33 +00:00
tecnovert
232e72882b Merge pull request #296 from nahuhh/xmr_open
xmr: detect corrupt wallets
2025-04-14 17:18:17 +00:00
Gerlof van Ek
9708657411 Merge pull request #290 from cryptoguard/client-auth-webui
Added client authentication.
2025-04-14 19:17:23 +02:00
tecnovert
9387c43ff5 Rename wallet file on error. 2025-04-14 18:23:55 +02:00
tecnovert
d0e35d1846 Apply only when env vars are set. 2025-04-14 18:21:54 +02:00
tecnovert
2af574c828 Merge pull request #298 from tecnovert/wownero
Wownero windows
2025-04-13 19:27:33 +00:00
tecnovert
087dcefb2c Wownero: Fix windows download url.
platform.machine() returns "AMD64" on windows, url expects "x86_64".
2025-04-13 13:20:58 +02:00
tecnovert
6777aff0b9 Merge pull request #295 from tecnovert/multinet
Add simplex chat test.
2025-04-10 23:02:39 +00:00
tecnovert
db2ba19220 Improve checkSplitMessages. 2025-04-11 01:01:23 +02:00
tecnovert
fa0760b172 Add simplex chat test. 2025-04-11 01:00:19 +02:00
Gerlof van Ek
748dd388cb Extra refactor + Various bug/fixes. (#293)
* Refactor + Various Fixes.

* WS / LINT

* Show also failed status.

* Fix sorting market +/-

* Simplified swaps in progress

* Black

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Fixes + GUI: v3.2.1

* Fixes + AutoRefreshEnabled true as default.

* Fix small memory issue since new features added,

---------

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2025-04-10 21:18:03 +02:00
Gerlof van Ek
f15f073b12 Merge pull request #286 from cryptoguard/dev
Offer page: Add info whether an offer is set to auto accept bid or not
2025-04-10 21:17:29 +02:00
cryptoguard
c9ef7bec44 Offer page: Display whether offer is set to automatically accept bids or not 2025-04-10 14:08:32 -04:00
tecnovert
2817d2d8e2 Update http_server.py 2025-04-10 10:33:05 -04:00
cryptoguard
c5908d5e0f Added client authentication 2025-04-10 10:32:42 -04:00
nahuhh
2d88491d48 xmr: detect corrupt wallets 2025-04-08 20:41:41 +01:00
tecnovert
96b44bef27 Merge pull request #294 from nahuhh/monero_v18.4.0
xmr: bump to v0.18.4.0
2025-04-08 10:19:32 +00:00
Cryptoguard
027d5c7adf Added NMC to README.md 2025-04-07 16:39:26 -04:00
nahuhh
25ad396dcf xmr: bump to v0.18.4.0 2025-04-05 22:14:13 +01:00
tecnovert
7972a50341 prepare: Make setup config persistent. 2025-04-02 20:45:30 +02:00
tecnovert
5a202e447c Merge pull request #291 from gerlofvanek/offers-11
Fix: Update orderbook rates.
2025-04-01 21:08:21 +00:00
tecnovert
c28eb9ab9b prepare: Fix addcoin with encrypted wallets.
Add workaround for Dash: sethdseed error if wallet is encrypted.
2025-04-01 22:53:35 +02:00
gerlofvanek
e1a6dbeaed Fix: Update orderbook rates.
- Update rates whenever new offer date is received via WebSocket.
- Ensure rates are updated when the offers table is refreshed.
- Keep profit/loss in sync.
2025-04-01 22:52:31 +02:00
tecnovert
31978d9f2a Merge pull request #288 from tecnovert/xmr_trusted
xmr: Default "trusted_daemon" setting to true.
2025-04-01 19:11:14 +00:00
tecnovert
c205607bb4 xmr: Default "trusted_daemon" setting to true. 2025-04-01 20:49:50 +02:00
tecnovert
aa9babdc69 Merge pull request #289 from tecnovert/namecoin_v28
Namecoin v28
2025-04-01 18:33:52 +00:00
tecnovert
dc44cc5ebe prepare: Compare full PGP fingerprint. 2025-04-01 16:05:52 +02:00
tecnovert
99bc8b6bd2 nmc: Update URL. 2025-04-01 16:05:48 +02:00
tecnovert
6b724ece84 nmc: Add to test_xmr_persistent. 2025-04-01 16:05:41 +02:00
tecnovert
e9ed334a54 nmc: Use descriptor wallets by default. 2025-03-31 00:48:55 +02:00
tecnovert
f263bb53c3 nmc: Update test. 2025-03-31 00:48:52 +02:00
tecnovert
8967f677c3 nmc: Create bdb wallet. 2025-03-31 00:46:56 +02:00
nahuhh
3ffe55e5a2 nmc: prepare 2025-03-31 00:46:56 +02:00
nahuhh
4f11e830af nmc: chainparams 2025-03-31 00:46:55 +02:00
tecnovert
9c252323be Set expect_seed after creation.
BCH must be reseeded manually as sethdseed is disabled during IBD.
2025-03-31 00:46:04 +02:00
tecnovert
a0c31fb87d lint: Changes for flake8 7.2 2025-03-31 00:45:57 +02:00
Gerlof van Ek
447f32d6b2 Merge pull request #283 from nahuhh/offer_debugui 2025-03-27 00:08:06 +01:00
nahuhh
eeade736a4 js: eslints 2025-03-26 23:01:30 +00:00
nahuhh
d15466f656 offer: move amt and rate vari to debug_ui 2025-03-26 22:58:15 +00:00
Gerlof van Ek
d5f48ce6b9 Refactoring + various fixes. (#285) 2025-03-26 23:54:55 +01:00
tecnovert
65cf6789a7 prepare: Add warning if local PGP key not found. 2025-03-26 12:33:33 +02:00
tecnovert
1f6ef7dfc7 doc: Add --upgradecores and --particl_mnemonic. 2025-03-25 10:05:58 +02:00
tecnovert
fbfb4c95ba Hide BrokenPipeErrors. 2025-03-24 16:51:11 +02:00
tecnovert
7c17ff2dd2 Shorten lookupFiatRates debug message. 2025-03-24 14:04:05 +02:00
tecnovert
6d68026808 Merge pull request #277 from basicswap/dependabot/pip/dev/jinja2-3.1.6
build(deps): bump jinja2 from 3.1.5 to 3.1.6
2025-03-24 11:44:15 +00:00
tecnovert
54c8e3fb36 Merge pull request #276 from basicswap/dependabot/pip/dev/black-25.1.0
build(deps): bump black from 24.10.0 to 25.1.0
2025-03-24 11:44:02 +00:00
tecnovert
5e5b404a48 Fix PIVX. 2025-03-24 13:32:36 +02:00
tecnovert
cc57d3537d tests: Complete test_swap_direction. 2025-03-20 00:01:54 +02:00
tecnovert
3e7b3925f6 tests: Add test_prepare to CI. 2025-03-19 19:50:46 +02:00
tecnovert
082a7f3d44 tests: Update test_prepare. 2025-03-19 18:31:24 +02:00
tecnovert
ec31f2eb35 Fix addcoin. 2025-03-19 13:34:23 +02:00
tecnovert
03a8ddc863 Fix can_accept for upgraded dbs. 2025-03-18 20:43:00 +02:00
tecnovert
5270c7da0b Merge pull request #280 from tecnovert/log_rotation
Log rotation.
2025-03-14 07:07:50 +00:00
tecnovert
826527fea9 Log rotation. 2025-03-12 10:41:36 +02:00
Gerlof van Ek
7d5f7e0936 Swaps in Progress update + various fixes. (#278)
* Swaps in Progress fixes.

* Fixes.

* Edit bid only in debug.

* Set edit button in debug_ui
2025-03-10 21:06:10 +00:00
Gerlof van Ek
6f14e24485 Updated wallets/wallet with backend coin prices/cache + various fixes. (#275)
* Updated wallets/wallet with backend coin prices/cache + various fixes.

* WOW fix.
2025-03-10 20:54:38 +00:00
dependabot[bot]
2b93276666 build(deps): bump jinja2 from 3.1.5 to 3.1.6
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 07:36:41 +00:00
dependabot[bot]
2bd82153bd build(deps): bump black from 24.10.0 to 25.1.0
Bumps [black](https://github.com/psf/black) from 24.10.0 to 25.1.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/24.10.0...25.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 07:36:02 +00:00
tecnovert
0cf77a4854 api: Use cryptocompare pricemulti endpoint for multiple coins. 2025-03-05 23:17:31 +02:00
tecnovert
1fc8bcea58 Merge pull request #268 from tecnovert/cache_rates
Cache rates
2025-03-05 14:52:42 +02:00
tecnovert
5bedc6289f Cache coin rates. 2025-03-05 13:14:25 +02:00
tecnovert
3cdab962d3 Deduplicate getCoinIdFromTicker. 2025-03-05 13:14:24 +02:00
tecnovert
0e9bb47902 Merge pull request #270 from tecnovert/ci-selenium
Add Selenium test to CI
2025-03-05 11:12:58 +00:00
tecnovert
e54f57f63a Merge pull request #272 from nahuhh/xmr_refresh
ux: revert xmr universal refresh to fix regression
2025-03-05 11:12:34 +00:00
tecnovert
19968ed496 tests: Run black in CI. 2025-03-05 13:08:42 +02:00
tecnovert
15b2038d65 tests: Add log prefix to BaseTestWithPrepare. 2025-03-05 13:08:42 +02:00
tecnovert
5ce607541e tests: Run selenium test in CI 2025-03-05 13:08:42 +02:00
tecnovert
7c482bab5c Merge pull request #274 from gerlofvanek/wsport
Pricechart + Global Tooltips optimization + wsport fix.
2025-03-05 11:07:04 +00:00
nahuhh
07bd7d3bd0 ux: revert xmr universal refresh to fix regression 2025-03-04 16:28:21 +00:00
gerlofvanek
30270d87f1 Set default fallback wsport + small fix. 2025-03-03 21:22:32 +01:00
gerlofvanek
3489ebe908 Pricechart + Global Tooltips optimization + wsport fix. 2025-03-03 21:09:46 +01:00
tecnovert
a5c3c692a0 Merge pull request #269 from gerlofvanek/version-1
GUI v3.2.0
2025-02-27 16:48:03 +00:00
gerlofvanek
b2df4ea80d GUI v3.2.0 2025-02-27 17:27:50 +01:00
Gerlof van Ek
18a7105f20 New Swaps in Progress page + various fixes + CSV export on bids page. (#267)
* New Swaps in Progress page + various fixes.

* LINT

* Fix small memory leak in bids page.

* Fix coin filter logic.

* Add CSV export on bids page + various fixes.

* Update basicswap/static/js/bids_sentreceived.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/bids_sentreceived.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/bids_sentreceived.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/bids_sentreceived.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Various fixes.

---------

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2025-02-25 19:20:55 +00:00
Gerlof van Ek
fcdb2e7dfe Merge pull request #265 from nahuhh/offertweaks
bids/offers: responsive and styling tweaks
2025-02-22 23:11:34 +01:00
nahuhh
3c5e8481cd bids/offers: responsive and styling tweaks 2025-02-22 22:05:31 +00:00
Gerlof van Ek
97bb615176 New bids pages + various fixes. (#266)
* New bids pages + various fixes.

* LINT

* Fix styling.
2025-02-22 15:55:12 +00:00
tecnovert
f1c2b41714 Add safe_logs option to anonymise logs. (#264)
* Add safe_logs option to anonymise logs.

* Extend logger class.
2025-02-22 15:54:13 +00:00
tecnovert
8d317e4b67 Merge pull request #262 from gerlofvanek/ws
JS: Fix websocket delay / loading tables faster.
2025-02-17 09:57:17 +00:00
tecnovert
45ed2cdb87 Merge pull request #254 from tecnovert/local_pgp
Import signing pubkeys from local filesystem.
2025-02-17 09:57:03 +00:00
tecnovert
d64e3f4be9 Merge pull request #259 from basicswap/dependabot/pip/dev/pyzmq-26.2.1
build(deps): bump pyzmq from 26.2.0 to 26.2.1
2025-02-17 09:56:38 +00:00
gerlofvanek
57d885bc0c JS: Fix websocket delay / loading tables faster. 2025-02-12 20:23:55 +01:00
Gerlof van Ek
205c6e2b58 Merge pull request #260 from gerlofvanek/readme
Updated README.md with DOGE
2025-02-08 22:03:28 +01:00
tecnovert
d95f3ccd24 Fix checkWallets regression, must rename watchonly wallet also. 2025-02-08 00:10:39 +02:00
gerlofvanek
d6a9425b22 Updated README.md with DOGE 2025-02-03 11:07:06 +01:00
dependabot[bot]
e4cc5da490 build(deps): bump pyzmq from 26.2.0 to 26.2.1
Bumps [pyzmq](https://github.com/zeromq/pyzmq) from 26.2.0 to 26.2.1.
- [Release notes](https://github.com/zeromq/pyzmq/releases)
- [Commits](https://github.com/zeromq/pyzmq/compare/v26.2.0...v26.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 07:58:31 +00:00
tecnovert
e177d36bd4 Silence python deprecation warning. 2025-02-02 11:14:01 +02:00
tecnovert
05ffa5e3ac prepare: Can use original UTXO snapshot signature.
Prints UTXO snapshot hashing progress.
Add signature of snapshot hashes.
Add PGP keys for Nicolas Dorier.
2025-02-02 11:14:01 +02:00
tecnovert
6165cbc4c3 Change ADD_PUBKEY_URL to per coin. 2025-02-02 11:14:01 +02:00
tecnovert
7e6f94319d Import signing pubkeys from local filesystem. 2025-02-02 11:14:00 +02:00
tecnovert
71fd3d10aa Merge pull request #252 from tecnovert/descriptors
Add BTC descriptor wallet support.
2025-01-31 19:06:22 +00:00
tecnovert
e4ed9aebdf Merge pull request #256 from tecnovert/scripts
scripts: Periodically prune old state data.
2025-01-31 19:03:14 +00:00
Gerlof van Ek
b97a9f4a27 Merge pull request #258 from nahuhh/pr/scroll
js: contain scroll
2025-01-30 23:03:40 +01:00
nahuhh
510eff6163 js: contain scroll 2025-01-30 21:54:59 +00:00
tecnovert
efb84f58af scripts: Periodically prune old state data.
Set "prune_state_delay" to 0 to disable.
Removes entires over two weeks old by default.
2025-01-30 15:58:54 +02:00
tecnovert
831ef40977 tests: Intercept signals in test_scripts.py 2025-01-30 15:56:25 +02:00
tecnovert
a0456cb689 Avoid reentrant error in signal_handler. 2025-01-30 15:21:56 +02:00
tecnovert
c7818f5fac Merge branch 'dev' 2025-01-30 14:18:24 +02:00
Gerlof van Ek
713577d868 JS/UI: Tooltips + Sorting table + Memory fix and new header. (#253)
* JS/UI: Tooltips + Sorting table + Memory fix and new header.

* LINT

* Light theme fix

* JS: Global / standalone Tooltips.

* Unminimized versions of tippy and popper js libs

* Formatting / Cleanup
2025-01-30 12:16:41 +00:00
tecnovert
37be3bcab5 Add BTC descriptor wallet support.
Set BTC_USE_DESCRIPTORS env var to true to enable descriptors in the prepare script and test_btc_xmr
A separate watchonly wallet is created when using descriptor wallets.
2025-01-29 10:16:07 +02:00
tecnovert
4ae97790aa Merge pull request #255 from tecnovert/ci
Fix CI caching
2025-01-29 07:27:41 +00:00
tecnovert
8928451af0 Merge pull request #251 from tecnovert/wallet_name
Add wallet_name option to basicswap.json.
2025-01-29 07:27:28 +00:00
tecnovert
473e4fd400 Fix CI caching 2025-01-29 09:16:23 +02:00
tecnovert
ff2fc35f72 Add wallet_name option to basicswap.json.
Removed "walletfile" setting for XMR and WOW, replaced with "wallet_name".
Set wallet_name in prepare script with eg: BTC_WALLET_NAME env var.
2025-01-28 09:40:29 +02:00
tecnovert
edb3b19dcf Merge pull request #248 from nahuhh/pr/transient
xmr: make " failed to get earliest fork height" a transient error
2025-01-23 08:53:08 +00:00
nahuhh
aac2f51b88 xmr: make earliest fork height a transient error 2025-01-22 23:30:33 +00:00
nahuhh
57b96cd985 wallet: fix reseed regression 2025-01-22 21:21:55 +02:00
tecnovert
4d5551cd84 Merge pull request #247 from nahuhh/pr/reseed
wallet: fix reseed regression
2025-01-22 19:00:07 +00:00
nahuhh
7ee4720738 wallet: fix reseed regression 2025-01-22 18:24:21 +00:00
tecnovert
c76fe79848 scripts: Fix createoffers, identities api changed. 2025-01-22 20:13:33 +02:00
tecnovert
f13c481b51 tests: Fix test_xmr_persistent with BTC v28. 2025-01-22 20:08:46 +02:00
tecnovert
6f776971b1 Merge pull request #245 from tecnovert/fastsync
prepare: Update BTC fastsync file.
2025-01-22 16:10:05 +00:00
tecnovert
c79ed493aa Add estimated tx fee to amount check when posting bid.
Add more log messages around balance checks.
2025-01-22 18:07:20 +02:00
tecnovert
b6709d0cdc prepare: Update BTC fastsync file.
Allow specifying a custom URL to look for the snapshot signature with: BITCOIN_FASTSYNC_SIG_URL.

Reduce gnupg module logging level.
2025-01-22 00:48:27 +02:00
tecnovert
c945e267e7 Merge pull request #239 from nahuhh/pr/filters
offers: align filters
2025-01-21 19:12:20 +00:00
tecnovert
ef65420978 Merge pull request #235 from nahuhh/pr/wallet
wallet: resposive ui & cleanup
2025-01-21 19:11:56 +00:00
tecnovert
6da4bf6aaf Merge pull request #225 from nahuhh/cores
help: add --upgradecores
2025-01-21 19:11:29 +00:00
tecnovert
6e56b7f421 Merge pull request #224 from nahuhh/pr/minbid
ui: reword min bid -> min purchase
2025-01-21 19:11:13 +00:00
Gerlof van Ek
f084c6f538 JS/UI: Fix scrolling lag / tooltips + Various fixes and cleanup. (#236)
* JS/UI: Fix scrolling lag + Various fixes and cleanup.

* Fix clear button

* JS: Fix when page is hidden, reconnect and proper pause/resume logic.

* JS: Fix tooltips bugs.

* JS: Various fixes.

* JS: Fix fetch system.

* JS: Cleanup
2025-01-21 19:10:52 +00:00
nahuhh
443bd6917f prepare: fix mweb wallet generation (#238)
* prepare: fix mweb wallet generation

* Restore interface_type on LTC MWEB and send it through initialiseWallet.

---------

Co-authored-by: tecnovert <tecnovert@tecnovert.net>
2025-01-21 19:09:04 +00:00
nahuhh
b55d126a0a ui: reword min bid -> min purchase 2025-01-21 13:23:31 +00:00
nahuhh
586ff3288f offers: align filters 2025-01-21 11:53:38 +00:00
nahuhh
0398fce5a8 wallet: responsive 2025-01-20 22:50:35 +00:00
nahuhh
ef082ff7be xmr: remove inaccurate fee rate, hide sweep all checkbox 2025-01-20 22:50:35 +00:00
nahuhh
168284ce25 wallet: cleanup, deduplicate, djlints 2025-01-20 22:50:27 +00:00
Gerlof van Ek
e797e23625 Merge pull request #234 from gerlofvanek/decimals
JS: Decimals
2025-01-18 22:22:28 +01:00
gerlofvanek
d3fcdc8052 JS: Decimals 2025-01-18 22:21:24 +01:00
Gerlof van Ek
2c176a8c86 Merge pull request #232 from gerlofvanek/cleanup-4
JS: Final tweaks 429
2025-01-18 21:06:54 +01:00
gerlofvanek
e92d5560af JS: Final tweaks 429 2025-01-18 20:53:13 +01:00
Gerlof van Ek
0171ad6889 Merge pull request #231 from gerlofvanek/eslint
Fix: Eslint.
2025-01-18 20:38:02 +01:00
gerlofvanek
5d381d4b73 Fix: Eslint. 2025-01-18 20:22:51 +01:00
Gerlof van Ek
9e24d9a12a Merge pull request #230 from nahuhh/pr/overflow
ui: missing character
2025-01-18 18:58:19 +01:00
nahuhh
21ef6f3129 ui: missing character 2025-01-18 17:31:40 +00:00
Gerlof van Ek
67d808cbe4 JS: Enhanced 429 fix + better error handle. Updated refresh button. (#229)
* JS: Enhanced 429 fix + better error handle. Updated refresh button.

* Update offerstable.js
2025-01-18 18:14:49 +01:00
Gerlof van Ek
5d1bed6423 Merge pull request #228 from nahuhh/pr/overflow
ui: responsive offers page
2025-01-18 18:13:47 +01:00
nahuhh
edc11b4c96 ui: offers responsive ui
ui: offers avoid squishing tiles
2025-01-18 16:55:48 +00:00
Gerlof van Ek
5daf591985 Merge pull request #226 from gerlofvanek/cleanup-2
JS: Fix HTTP Error 429
2025-01-17 22:50:39 +01:00
Gerlof van Ek
aee66712b8 Update pricechart.js 2025-01-17 22:14:19 +01:00
Gerlof van Ek
8de365f9d3 Update offerstable.js 2025-01-17 22:13:37 +01:00
gerlofvanek
765ef9571a JS: Fix HTTP Error 429 2025-01-17 20:15:58 +01:00
nahuhh
c575625097 help: add --upgradecores 2025-01-17 14:54:56 +00:00
tecnovert
fe02441619 Merge pull request #223 from gerlofvanek/cleanup-1
JS: Cleanup + Fixes
2025-01-17 10:46:07 +00:00
gerlofvanek
c992ef571a JS: Cleanup + Fixes 2025-01-17 11:34:28 +01:00
tecnovert
5f275132de Merge pull request #218 from basicswap/dependabot/pip/dev/python-gnupg-0.5.4
build(deps): bump python-gnupg from 0.5.3 to 0.5.4
2025-01-17 07:42:13 +00:00
tecnovert
64151f4203 Merge pull request #216 from nahuhh/bch
bch: v28.0.1
2025-01-17 07:39:51 +00:00
tecnovert
734214af53 Merge pull request #222 from nahuhh/pr/overflow
offers: fix overflow bar
2025-01-17 07:39:09 +00:00
tecnovert
1cb8ffb632 Merge pull request #220 from nahuhh/pr/eslint
lint: eslinting suggestions
2025-01-17 07:34:36 +00:00
nahuhh
40d06df325 offers: fix overflow bar 2025-01-17 02:55:20 +00:00
nahuhh
62031173f5 lint: manual eslint 2025-01-16 21:36:08 +00:00
nahuhh
f473d66de5 lint: auto eslint 2025-01-16 21:26:20 +00:00
tecnovert
e548cf2b3b Merge pull request #219 from gerlofvanek/memory
Fix potential sources of mem leaks + Various related fixes.
2025-01-16 19:28:13 +00:00
gerlofvanek
d1baf4bc10 Improve identity fetching + hor/ver bar fix. 2025-01-16 19:14:58 +01:00
gerlofvanek
3b8e084b2e Simplified API requests and remove debug. 2025-01-16 16:34:36 +01:00
gerlofvanek
0a697c61e8 Fix scroll up / down memory increase bug. 2025-01-16 16:26:48 +01:00
gerlofvanek
5af59dd8da Pricechart fix potential mem leaks. 2025-01-16 14:17:46 +01:00
gerlofvanek
a75cd28995 Reduced retry delay. 2025-01-16 13:23:18 +01:00
gerlofvanek
f40d98ef23 Fix API issue with Firo and various small fixes. 2025-01-16 13:08:52 +01:00
gerlofvanek
b14fba0e1f Fix small API bug and better status feedback. 2025-01-16 12:00:50 +01:00
gerlofvanek
4d928dc98e Better error handling API / Tooltips: Rate, Market. 2025-01-16 11:38:02 +01:00
gerlofvanek
1845f802a2 Update cleanup. 2025-01-16 10:25:25 +01:00
gerlofvanek
7ec9dfa35a Fix manual refresh button. 2025-01-16 01:28:10 +01:00
gerlofvanek
b70e46ffc1 Fix potential sources of mem leaks. 2025-01-16 01:17:23 +01:00
dependabot[bot]
07de2d61af build(deps): bump python-gnupg from 0.5.3 to 0.5.4
Bumps [python-gnupg](https://github.com/vsajip/python-gnupg) from 0.5.3 to 0.5.4.
- [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.3...0.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-15 16:59:52 +00:00
nahuhh
b87e034719 bch: v28.0.1 2025-01-14 12:49:31 +00:00
235 changed files with 56116 additions and 16738 deletions

3
.djlintrc Normal file
View File

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

View File

@@ -9,6 +9,9 @@ concurrency:
env:
BIN_DIR: /tmp/cached_bin
TEST_RELOAD_PATH: /tmp/test_basicswap
BSX_SELENIUM_DRIVER: firefox-ci
XMR_RPC_USER: xmr_user
XMR_RPC_PWD: xmr_pwd
jobs:
ci:
@@ -24,19 +27,36 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
if [ $(dpkg-query -W -f='${Status}' firefox 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
install -d -m 0755 /etc/apt/keyrings
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | sudo tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
echo "Package: *" | sudo tee /etc/apt/preferences.d/mozilla
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
sudo apt-get update
sudo apt-get install -y firefox
fi
python -m pip install --upgrade pip
pip install flake8 codespell pytest
pip install python-gnupg
pip install -e .[dev]
pip install -r requirements.txt --require-hashes
- name: Install
run: |
pip install .
- name: Running flake8
# Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt
- name: Run flake8
run: |
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Running codespell
- name: Run codespell
run: |
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
- name: Running test_other
- name: Run black
run: |
black --check --diff --exclude="contrib" .
- name: Run test_other
run: |
pytest tests/basicswap/test_other.py
- name: Cache coin cores
@@ -44,26 +64,57 @@ jobs:
uses: actions/cache@v3
env:
cache-name: cache-cores
CACHE_KEY: $(printf $(python bin/basicswap-prepare.py --version --withcoins=bitcoin) | sha256sum | head -c 64)
with:
path: $BIN_DIR
key: $CACHE_KEY
path: /tmp/cached_bin
key: cores-${{ runner.os }}-${{ hashFiles('**/core_versions.txt') }}
- if: ${{ steps.cache-yarn.outputs.cache-hit != 'true' }}
- if: ${{ steps.cache-cores.outputs.cache-hit != 'true' }}
name: Running basicswap-prepare
run: |
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
- name: Running test_xmr
- name: Run test_prepare
run: |
export PYTHONPATH=$(pwd)
export PARTICL_BINDIR="$BIN_DIR/particl";
export BITCOIN_BINDIR="$BIN_DIR/bitcoin";
export XMR_BINDIR="$BIN_DIR/monero";
export TEST_BIN_PATH="$BIN_DIR"
export TEST_PATH=/tmp/test_prepare
pytest tests/basicswap/extended/test_prepare.py
- name: Run test_xmr
run: |
export PYTHONPATH=$(pwd)
export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero"
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
- name: Running test_encrypted_xmr_reload
- name: Run test_encrypted_xmr_reload
run: |
export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Run selenium tests
run: |
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
export PYTHONPATH=$(pwd)
python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & TEST_NETWORK_PID=$!
echo "Starting test_xmr_persistent, PID $TEST_NETWORK_PID"
i=0
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
do
tail -n 1 /tmp/log.txt
sleep 2
((++i))
if [ $i -ge 60 ]; then
echo "Timed out waiting for test_xmr_persistent, PID $TEST_NETWORK_PID"
kill $TEST_NETWORK_PID
(exit 1) # Fail test
break
fi
done
echo "Running test_settings.py"
python tests/basicswap/selenium/test_settings.py
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID

6
.gitignore vendored
View File

@@ -1,5 +1,6 @@
old/
build/
venv/
*.pyc
__pycache__
/dist/
@@ -8,6 +9,9 @@ __pycache__
/*.eggs
.tox
.eggs
.ruff_cache
.pytest_cache
.vectorcode
*~
# geckodriver.log
@@ -15,4 +19,4 @@ __pycache__
docker/.env
# vscode dev container settings
compose-dev.yaml
compose-dev.yaml

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

@@ -112,6 +112,18 @@ BasicSwap is compatible with the following digital assets.
<td>PART
</td>
</tr>
<tr>
<td>Dogecoin
</td>
<td>DOGE
</td>
</tr>
<tr>
<td>Namecoin
</td>
<td>NMC
</td>
</tr>
</table>
If youd like to add a cryptocurrency to BasicSwap, refer to how other cryptocurrencies have been integrated to the DEX by following [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html).

View File

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

View File

@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import time
import shlex
import socks
import random
import socket
import urllib
import logging
import threading
import traceback
import os
import random
import shlex
import socket
import socks
import subprocess
import sys
import threading
import time
import traceback
import urllib
from sockshandler import SocksiPyHandler
@@ -28,6 +29,10 @@ from .rpc import (
from .util import (
TemporaryError,
)
from .util.logging import (
BSXLogger,
LogCategories as LC,
)
from .chainparams import (
Coins,
chainparams,
@@ -39,9 +44,9 @@ def getaddrinfo_tor(*args):
class BaseApp(DBMethods):
def __init__(self, fp, 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.fp = fp
self.fail_code = 0
self.mock_time_offset = 0
@@ -57,7 +62,7 @@ class BaseApp(DBMethods):
self._network = None
self.prepareLogging()
self.log.info("Network: {}".format(self.chain))
self.log.info(f"Network: {self.chain}")
self.use_tor_proxy = self.settings.get("use_tor", False)
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
@@ -67,24 +72,67 @@ class BaseApp(DBMethods):
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
self._force_db_upgrade = False
self._enabled_log_categories = set()
for category in self.settings.get("enabled_log_categories", []):
category = category.lower()
if category == "net":
self._enabled_log_categories.add(LC.NET)
else:
self.log.warning(
f'Unknown entry "{category}" in "enabled_log_categories"'
)
if len(self._enabled_log_categories) > 0:
self.log.info(
"Enabled logging categories: {}".format(
",".join(sorted([c.name for c in self._enabled_log_categories]))
)
)
super().__init__(
data_dir=data_dir,
settings=settings,
chain=chain,
log_name=log_name,
**kwargs,
)
def __del__(self):
if self.fp:
self.fp.close()
def stopRunning(self, with_code=0):
self.fail_code = with_code
with self.mxDB:
# Wait for lock to shutdown gracefully.
if self.mxDB.acquire(timeout=5):
self.chainstate_delay_event.set()
self.delay_event.set()
self.mxDB.release()
else:
# Waiting for lock timed out, stop anyway
self.chainstate_delay_event.set()
self.delay_event.set()
def openLogFile(self):
self.fp = open(os.path.join(self.data_dir, "basicswap.log"), "a")
def prepareLogging(self):
logging.setLoggerClass(BSXLogger)
self.log = logging.getLogger(self.log_name)
self.log.propagate = False
self.openLogFile()
# Remove any existing handlers
self.log.handlers = []
formatter = logging.Formatter(
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
)
stream_stdout = logging.StreamHandler()
stream_stdout = logging.StreamHandler(sys.stdout)
if self.log_name != "BasicSwap":
stream_stdout.setFormatter(
logging.Formatter(
@@ -94,6 +142,7 @@ class BaseApp(DBMethods):
)
else:
stream_stdout.setFormatter(formatter)
self.log_formatter = formatter
stream_fp = logging.StreamHandler(self.fp)
stream_fp.setFormatter(formatter)
@@ -128,7 +177,7 @@ class BaseApp(DBMethods):
for c, params in chainparams.items():
if coin_name.lower() == params["name"].lower():
return c
raise ValueError("Unknown coin: {}".format(coin_name))
raise ValueError(f"Unknown coin: {coin_name}")
def callrpc(self, method, params=[], wallet=None):
cc = self.coin_clients[Coins.PART]
@@ -213,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

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,12 +9,14 @@
import struct
import hashlib
from enum import IntEnum, auto
from html import escape as html_escape
from .util.address import (
encodeAddress,
decodeAddress,
)
from .chainparams import (
chainparams,
Fiat,
)
@@ -34,6 +36,16 @@ class KeyTypes(IntEnum):
KAF = 6
class MessageNetworks(IntEnum):
SMSG = auto()
SIMPLEX = auto()
class MessageNetworkLinkTypes(IntEnum):
RECEIVED_ON = auto()
SENT_ON = auto()
class MessageTypes(IntEnum):
OFFER = auto()
BID = auto()
@@ -51,12 +63,18 @@ class MessageTypes(IntEnum):
ADS_BID_LF = auto()
ADS_BID_ACCEPT_FL = auto()
CONNECT_REQ = auto()
PORTAL_OFFER = auto()
PORTAL_SEND = auto()
class AddressTypes(IntEnum):
OFFER = auto()
BID = auto()
RECV_OFFER = auto()
SEND_OFFER = auto()
PORTAL_LOCAL = auto()
PORTAL = auto()
class SwapTypes(IntEnum):
@@ -109,6 +127,7 @@ class BidStates(IntEnum):
BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
CONNECT_REQ_SENT = 34
class TxStates(IntEnum):
@@ -191,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):
@@ -224,6 +245,12 @@ class NotificationTypes(IntEnum):
OFFER_RECEIVED = auto()
BID_RECEIVED = auto()
BID_ACCEPTED = auto()
SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto()
class ConnectionRequestTypes(IntEnum):
BID = 1
class AutomationOverrideOptions(IntEnum):
@@ -337,6 +364,8 @@ def strBidState(state):
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
if state == BidStates.CONNECT_REQ_SENT:
return "Connect request sent"
return "Unknown" + " " + str(state)
@@ -379,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):
@@ -410,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:
@@ -520,7 +552,7 @@ def getLastBidState(packed_states):
return BidStates.BID_STATE_UNKNOWN
def strSwapType(swap_type):
def strSwapType(swap_type) -> str:
if swap_type == SwapTypes.SELLER_FIRST:
return "seller_first"
if swap_type == SwapTypes.XMR_SWAP:
@@ -528,7 +560,7 @@ def strSwapType(swap_type):
return None
def strSwapDesc(swap_type):
def strSwapDesc(swap_type) -> str:
if swap_type == SwapTypes.SELLER_FIRST:
return "Secret Hash"
if swap_type == SwapTypes.XMR_SWAP:
@@ -536,6 +568,31 @@ def strSwapDesc(swap_type):
return None
def fiatTicker(fiat_ind: int) -> str:
try:
return Fiat(fiat_ind).name
except Exception as e: # noqa: F841
raise ValueError(f"Unknown fiat ind {fiat_ind}")
def fiatFromTicker(ticker: str) -> int:
ticker_uc = ticker.upper()
for entry in Fiat:
if entry.name == ticker_uc:
return entry
raise ValueError(f"Unknown fiat {ticker}")
def get_api_key_setting(
settings, setting_name: str, default_value: str = "", escape: bool = False
):
setting_name_enc: str = setting_name + "_enc"
if setting_name_enc in settings:
rv = bytes.fromhex(settings[setting_name_enc]).decode("utf-8")
return html_escape(rv) if escape else rv
return settings.get(setting_name, default_value)
inactive_states = [
BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR,
@@ -554,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

File diff suppressed because it is too large Load Diff

View File

@@ -6,66 +6,107 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import json
import logging
import os
import shutil
import signal
import logging
import traceback
import subprocess
import sys
import traceback
import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
if not len(initial_logger.handlers):
initial_logger.addHandler(initial_logger.StreamHandler(sys.stdout))
logger = initial_logger
swap_client = None
class Daemon:
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v["name"]:
return True
return False
def signal_handler(sig, frame):
global swap_client
logger.info("Signal %d detected, ending program." % (sig))
if swap_client is not None:
os.write(
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
)
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
logger.info(f"AMM signal shutdown: {msg}")
else:
logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}")
swap_client.stopRunning()
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
try:
datadir = part_settings.get("datadir")
if not datadir:
return
config_path = os.path.join(datadir, "particl.conf")
if not os.path.exists(config_path):
return
with open(config_path, "r") as f:
config_content = f.read()
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
zmq_port = swap_settings.get("zmqport", 14792)
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
if "zmqpubhashwtx=" not in config_content:
with open(config_path, "a") as f:
f.write(f"{expected_line}\n")
elif expected_line not in config_content:
lines = config_content.split("\n")
updated_lines = []
for line in lines:
if line.startswith("zmqpubhashwtx="):
updated_lines.append(expected_line)
else:
updated_lines.append(line)
with open(config_path, "w") as f:
f.write("\n".join(updated_lines))
except Exception as e:
logger.debug(f"Error checking PART ZMQ config: {e}")
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
coin_name = extra_config.get("coin_name", "")
# Rewrite litecoin.conf
# TODO: Remove
needs_rewrite: bool = False
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
needs_rewrite: bool = False
add_changetype: bool = True
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if line.endswith("=onion"):
needs_rewrite = True
break
@@ -81,6 +122,29 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
fp_to.write(line.strip()[:-6] + "\n")
else:
fp_to.write(line)
if add_changetype:
fp_to.write("changetype=bech32\n")
add_changetype = False
if add_changetype:
logger.info("Adding changetype to litecoin.conf")
with open(ltc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
# Rewrite bitcoin.conf
# TODO: Remove
btc_conf_path = os.path.join(datadir_path, "bitcoin.conf")
if coin_name == "bitcoin" and os.path.exists(btc_conf_path):
add_changetype: bool = True
with open(btc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if add_changetype:
logger.info("Adding changetype to bitcoin.conf")
with open(btc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
args = [
daemon_bin,
@@ -89,7 +153,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
if add_datadir:
args.append("-datadir=" + datadir_path)
args += opts
logger.info("Starting node {}".format(daemon_bin))
logger.info(f"Starting node {daemon_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
opened_files = []
@@ -120,6 +184,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
cwd=datadir_path,
),
opened_files,
os.path.basename(daemon_bin),
)
@@ -135,7 +200,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
"--non-interactive",
"--config-file=" + os.path.join(datadir_path, config_filename),
] + opts
logger.info("Starting node {}".format(daemon_bin))
logger.info(f"Starting node {daemon_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -150,6 +215,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
cwd=datadir_path,
),
[file_stdout, file_stderr],
os.path.basename(daemon_bin),
)
@@ -198,7 +264,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
):
fp_to.write(line)
logger.info("Starting wallet daemon {}".format(wallet_bin))
logger.info(f"Starting wallet daemon {wallet_bin}")
logger.debug("Arguments {}".format(" ".join(args)))
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
@@ -213,28 +279,10 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
cwd=data_dir,
),
[wallet_stdout, wallet_stderr],
os.path.basename(wallet_bin),
)
def ws_new_client(client, server):
if swap_client:
swap_client.log.debug(f'ws_new_client {client["id"]}')
def ws_client_left(client, server):
if client is None:
return
if swap_client:
swap_client.log.debug(f'ws_client_left {client["id"]}')
def ws_message_received(client, server, message):
if len(message) > 200:
message = message[:200] + ".."
if swap_client:
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get(
"core_binname", chainparams[coin_id].get("core_binname", default_name)
@@ -251,7 +299,7 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
extra_args = []
if "config_filename" in coin_settings:
extra_args.append("--conf=" + coin_settings["config_filename"])
if "port" in coin_settings:
if "port" in coin_settings and coin_id != Coins.BTC:
if prepare is False and use_tor_proxy:
if coin_id == Coins.BCH:
# Without this BCH (27.1) will bind to the default BTC port, even with proxy set
@@ -263,17 +311,46 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
# As BCH may use port 8334, disable it here.
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
if (
prepare is False
and use_tor_proxy is False
and coin_id in (Coins.BTC, Coins.NMC)
):
port: int = coin_settings.get("port", 8333)
extra_args.append(f"--bind=0.0.0.0:{port}")
return extra_args
def runClient(fp, data_dir, chain, start_only_coins):
def mainLoop(daemons, update: bool = True):
while not swap_client.delay_event.wait(0.5):
if update:
swap_client.update()
else:
pass
for daemon in daemons:
if daemon.running is False:
continue
poll = daemon.handle.poll()
if poll is None:
pass # Process is running
else:
daemon.running = False
swap_client.log.error(
f"Process {daemon.handle.pid} for {daemon.name} terminated unexpectedly returning {poll}."
)
def runClient(
data_dir: str,
chain: str,
start_only_coins: bool,
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
global swap_client, logger
daemons = []
pids = []
threads = []
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
pids_path = os.path.join(data_dir, ".pids")
@@ -294,30 +371,76 @@ def runClient(fp, data_dir, chain, start_only_coins):
with open(settings_path) as fs:
settings = json.load(fs)
swap_client = BasicSwap(fp, data_dir, settings, chain)
swap_client = BasicSwap(
data_dir, settings, chain, log_name=log_prefix, extra_opts=extra_opts
)
logger = swap_client.log
if os.path.exists(pids_path):
with open(pids_path) as fd:
for ln in fd:
# TODO: try close
logger.warning("Found pid for daemon {} ".format(ln.strip()))
logger.warning("Found pid for daemon {}".format(ln.strip()))
# Ensure daemons are stopped
swap_client.stopDaemons()
# Settings may have been modified
settings = swap_client.settings
try:
# Try start daemons
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
)
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
continue
if (
len(swap_client.with_coins_override) > 0
and c not in swap_client.with_coins_override
) or c in swap_client.without_coins_override:
if v.get("manage_daemon", False) or v.get(
"manage_wallet_daemon", False
):
logger.warning(
f"Not starting coin {c.capitalize()}, disabled by arguments."
)
continue
try:
coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id)
except Exception as e: # noqa: F841
logger.warning("Not starting unknown coin: {}".format(c))
logger.warning(f"Not starting unknown coin: {c}")
continue
if c in ("monero", "wownero"):
if v["manage_daemon"] is True:
@@ -326,7 +449,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -374,7 +497,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
)
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
continue # /monero
@@ -393,6 +516,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
"stdout_to_file": True,
"stdout_filename": "dcrd_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -404,7 +528,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
)
)
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -421,6 +545,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
"stdout_to_file": True,
"stdout_filename": "dcrwallet_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -432,22 +557,34 @@ def runClient(fp, data_dir, chain, start_only_coins):
)
)
pid = daemons[-1].handle.pid
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
continue # /decred
if v["manage_daemon"] is True:
if c == "particl" and swap_client._zmq_queue_enabled:
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs(coin_id, v, use_tor_proxy=swap_client.use_tor_proxy)
extra_opts = getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
extra_config = {"coin_name": c}
daemons.append(
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
startDaemon(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
pids.append((c, pid))
swap_client.setDaemonPID(c, pid)
swap_client.log.info("Started {} {}".format(filename, pid))
swap_client.log.info(f"Started {filename} {pid}")
if len(pids) > 0:
with open(pids_path, "w") as fd:
for p in pids:
@@ -461,48 +598,12 @@ def runClient(fp, data_dir, chain, start_only_coins):
logger.info(
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
)
while not swap_client.delay_event.wait(0.5):
pass
mainLoop(daemons, update=False)
else:
swap_client.start()
if "htmlhost" in settings:
swap_client.log.info(
"Starting http server at http://%s:%d."
% (settings["htmlhost"], settings["htmlport"])
)
allow_cors = (
settings["allowcors"]
if "allowcors" in settings
else cfg.DEFAULT_ALLOW_CORS
)
thread_http = HttpThread(
fp,
settings["htmlhost"],
settings["htmlport"],
allow_cors,
swap_client,
)
threads.append(thread_http)
thread_http.start()
if "wshost" in settings:
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
swap_client.log.info(f"Starting ws server at {ws_url}.")
swap_client.ws_server = WebsocketServer(
host=settings["wshost"], port=settings["wsport"]
)
swap_client.ws_server.client_port = settings.get(
"wsclientport", settings["wsport"]
)
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
logger.info("Exit with Ctrl + c.")
while not swap_client.delay_event.wait(0.5):
swap_client.update()
mainLoop(daemons)
except Exception as e: # noqa: F841
traceback.print_exc()
@@ -515,23 +616,16 @@ def runClient(fp, data_dir, chain, start_only_coins):
traceback.print_exc()
swap_client.finalise()
swap_client.log.info("Stopping HTTP threads.")
for t in threads:
try:
t.stop()
t.join()
except Exception as e: # noqa: F841
traceback.print_exc()
closed_pids = []
for d in daemons:
swap_client.log.info("Interrupting {}".format(d.handle.pid))
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}")
try:
d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
except Exception as e:
swap_client.log.info("Interrupting %d, error %s", d.handle.pid, str(e))
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
for d in daemons:
try:
d.handle.wait(timeout=120)
@@ -539,8 +633,11 @@ def runClient(fp, data_dir, chain, start_only_coins):
if fp:
fp.close()
closed_pids.append(d.handle.pid)
except Exception as ex:
swap_client.log.error("Error: {}".format(ex))
except Exception as e:
swap_client.log.error(f"Error: {e}")
fail_code: int = swap_client.fail_code
del swap_client
if os.path.exists(pids_path):
with open(pids_path) as fd:
@@ -555,9 +652,18 @@ def runClient(fp, data_dir, chain, start_only_coins):
with open(pids_path, "w") as fd:
fd.write(still_running)
return fail_code
def printVersion():
logger.info("Basicswap version: %s", __version__)
logger.info(
f"Basicswap version: {__version__}",
)
def ensure_coin_valid(coin: str) -> bool:
if isKnownCoinName(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
def printHelp():
@@ -565,26 +671,34 @@ def printHelp():
print("\n--help, -h Print help.")
print("--version, -v Print version.")
print(
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
cfg.BASICSWAP_DATADIR
)
f"--datadir=PATH Path to basicswap data directory, default:{cfg.BASICSWAP_DATADIR}."
)
print("--mainnet Run in mainnet mode.")
print("--testnet Run in testnet mode.")
print("--regtest Run in regtest mode.")
print("--withcoin= Run only with coin/s.")
print("--withoutcoin= Run without coin/s.")
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print("--logprefix Specify log prefix.")
print(
"--forcedbupgrade Recheck database against schema regardless of version."
)
def main():
data_dir = None
chain = "mainnet"
start_only_coins = set()
log_prefix: str = "BasicSwap"
options = dict()
with_coins = set()
without_coins = set()
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != "-":
logger.warning("Unknown argument %s", v)
logger.warning(f"Unknown argument {v}")
continue
s = v.split("=")
@@ -604,19 +718,35 @@ def main():
if name in ("mainnet", "testnet", "regtest"):
chain = name
continue
if name in ("withcoin", "withcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
ensure_coin_valid(coin)
with_coins.add(coin)
continue
if name in ("withoutcoin", "withoutcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
if coin == "particl":
raise ValueError("Particl is required.")
ensure_coin_valid(coin)
without_coins.add(coin)
continue
if name == "forcedbupgrade":
options["force_db_upgrade"] = True
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1])
data_dir = os.path.abspath(os.path.expanduser(s[1]))
continue
if name == "logprefix":
log_prefix = s[1]
continue
if name == "startonlycoin":
for coin in [s.lower() for s in s[1].split(",")]:
if is_known_coin(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
ensure_coin_valid(coin)
start_only_coins.add(coin)
continue
logger.warning("Unknown argument %s", v)
logger.warning(f"Unknown argument {v}")
if os.name == "nt":
logger.warning(
@@ -625,20 +755,23 @@ def main():
if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
logger.info("Using datadir: %s", data_dir)
logger.info("Chain: %s", chain)
logger.info(f"Using datadir: {data_dir}")
logger.info(f"Chain: {chain}")
if not os.path.exists(data_dir):
os.makedirs(data_dir)
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
logger.info(
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
)
runClient(fp, data_dir, chain, start_only_coins)
if len(with_coins) > 0:
with_coins.add("particl")
options["with_coins"] = with_coins
if len(without_coins) > 0:
options["without_coins"] = without_coins
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix, options)
print("Done.")
return swap_client.fail_code if swap_client is not None else 0
return fail_code
if __name__ == "__main__":

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -35,6 +35,12 @@ class Coins(IntEnum):
DOGE = 18
class Fiat(IntEnum):
USD = -1
GBP = -2
EUR = -3
chainparams = {
Coins.PART: {
"name": "particl",
@@ -52,6 +58,8 @@ chainparams = {
"bip44": 44,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x696E82D1,
"ext_secret_key_prefix": 0x8F1DAEB8,
},
"testnet": {
"rpcport": 51935,
@@ -63,6 +71,8 @@ chainparams = {
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0xE1427800,
"ext_secret_key_prefix": 0x04889478,
},
"regtest": {
"rpcport": 51936,
@@ -74,6 +84,8 @@ chainparams = {
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0xE1427800,
"ext_secret_key_prefix": 0x04889478,
},
},
Coins.BTC: {
@@ -91,6 +103,8 @@ chainparams = {
"bip44": 0,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x0488B21E,
"ext_secret_key_prefix": 0x0488ADE4,
},
"testnet": {
"rpcport": 18332,
@@ -102,6 +116,8 @@ chainparams = {
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
"regtest": {
"rpcport": 18443,
@@ -112,6 +128,8 @@ chainparams = {
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
},
Coins.LTC: {
@@ -199,6 +217,7 @@ chainparams = {
"message_magic": "Decred Signed Message:\n",
"blocks_target": 60 * 5,
"decimal_places": 8,
"has_multiwallet": False,
"mainnet": {
"rpcport": 9109,
"pubkey_address": 0x073F,
@@ -238,29 +257,38 @@ chainparams = {
"rpcport": 8336,
"pubkey_address": 52,
"script_address": 13,
"key_prefix": 180,
"hrp": "nc",
"bip44": 7,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x0488B21E, # base58Prefixes[EXT_PUBLIC_KEY]
"ext_secret_key_prefix": 0x0488ADE4,
},
"testnet": {
"rpcport": 18336,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "tn",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "ncrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
},
Coins.XMR: {
@@ -404,6 +432,7 @@ chainparams = {
"has_cltv": False,
"has_csv": False,
"has_segwit": False,
"has_multiwallet": False,
"mainnet": {
"rpcport": 8888,
"pubkey_address": 82,
@@ -443,6 +472,7 @@ chainparams = {
"decimal_places": 8,
"has_csv": True,
"has_segwit": True,
"has_multiwallet": False,
"mainnet": {
"rpcport": 44444,
"pubkey_address": 53,
@@ -519,10 +549,13 @@ chainparams = {
},
},
}
name_map = {}
ticker_map = {}
for c, params in chainparams.items():
name_map[params["name"].lower()] = c
ticker_map[params["ticker"].lower()] = c
@@ -530,4 +563,15 @@ def getCoinIdFromTicker(ticker: str) -> str:
try:
return ticker_map[ticker.lower()]
except Exception:
raise ValueError("Unknown coin")
raise ValueError(f"Unknown coin {ticker}")
def getCoinIdFromName(name: str) -> str:
try:
return name_map[name.lower()]
except Exception:
raise ValueError(f"Unknown coin {name}")
def isKnownCoinName(name: str) -> bool:
return params["name"].lower() in name_map

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 The Basicswap developers
# Copyright (c) 2019-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -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"))
@@ -40,13 +42,6 @@ DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix)
NAMECOIN_BINDIR = os.path.expanduser(
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
)
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
XMR_BINDIR = os.path.expanduser(
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
)

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

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# 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.
"""Utility functions related to output descriptors"""
import re
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd]
def descsum_polymod(symbols):
"""Internal function that computes the descriptor checksum."""
chk = 1
for value in symbols:
top = chk >> 35
chk = (chk & 0x7ffffffff) << 5 ^ value
for i in range(5):
chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
return chk
def descsum_expand(s):
"""Internal function that does the character to symbol expansion"""
groups = []
symbols = []
for c in s:
if not c in INPUT_CHARSET:
return None
v = INPUT_CHARSET.find(c)
symbols.append(v & 31)
groups.append(v >> 5)
if len(groups) == 3:
symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
groups = []
if len(groups) == 1:
symbols.append(groups[0])
elif len(groups) == 2:
symbols.append(groups[0] * 3 + groups[1])
return symbols
def descsum_create(s):
"""Add a checksum to a descriptor without"""
symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
checksum = descsum_polymod(symbols) ^ 1
return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))
def descsum_check(s, require=True):
"""Verify that the checksum is correct in a descriptor"""
if not '#' in s:
return not require
if s[-9] != '#':
return False
if not all(x in CHECKSUM_CHARSET for x in s[-8:]):
return False
symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]]
return descsum_polymod(symbols) == 1
def drop_origins(s):
'''Drop the key origins from a descriptor'''
desc = re.sub(r'\[.+?\]', '', s)
if '#' in s:
desc = desc[:desc.index('#')]
return descsum_create(desc)

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,20 +5,22 @@
"""Helpful routines for regression testing."""
from base64 import b64encode
from binascii import unhexlify
from decimal import Decimal, ROUND_DOWN
from subprocess import CalledProcessError
import hashlib
import inspect
import json
import logging
import os
import random
import pathlib
import platform
import re
import time
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
from io import BytesIO
from collections.abc import Callable
from typing import Optional
logger = logging.getLogger("TestFramework.utils")
@@ -28,23 +30,46 @@ logger = logging.getLogger("TestFramework.utils")
def assert_approx(v, vexp, vspan=0.00001):
"""Assert that `v` is within `vspan` of `vexp`"""
if isinstance(v, Decimal) or isinstance(vexp, Decimal):
v=Decimal(v)
vexp=Decimal(vexp)
vspan=Decimal(vspan)
if v < vexp - vspan:
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
if v > vexp + vspan:
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = round(tx_size * fee_per_kB / 1000, 8)
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
"""Assert the fee is in range."""
assert isinstance(tx_size, int)
target_fee = get_fee(tx_size, feerate_BTC_kvB)
if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
if fee > (tx_size + 2) * fee_per_kB / 1000:
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
if fee > high_fee:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
def summarise_dict_differences(thing1, thing2):
if not isinstance(thing1, dict) or not isinstance(thing2, dict):
return thing1, thing2
d1, d2 = {}, {}
for k in sorted(thing1.keys()):
if k not in thing2:
d1[k] = thing1[k]
elif thing1[k] != thing2[k]:
d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
for k in sorted(thing2.keys()):
if k not in thing1:
d2[k] = thing2[k]
return d1, d2
def assert_equal(thing1, thing2, *args):
if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
d1,d2 = summarise_dict_differences(thing1, thing2)
raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2))
if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
@@ -79,7 +104,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds):
raise AssertionError("No exception raised")
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
"""Execute a process and asserts the process return code and output.
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
@@ -87,9 +112,9 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds):
no CalledProcessError was raised or if the return code and output are not as expected.
Args:
returncode (int): the process return code.
output (string): [a substring of] the process output.
fun (function): the function to call. This should execute a process.
returncode: the process return code.
output: [a substring of] the process output.
fun: the function to call. This should execute a process.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -104,7 +129,7 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds):
raise AssertionError("No exception raised")
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
@@ -112,11 +137,11 @@ def assert_raises_rpc_error(code, message, fun, *args, **kwds):
no JSONRPCException was raised or if the error code/message are not as expected.
Args:
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required.
fun (function): the function to call. This should be the name of an RPC.
code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
Set to None if checking the error code is not required.
message: [a substring of] the error string returned by the RPC call.
Set to None if checking the error string is not required.
fun: the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -203,29 +228,45 @@ def check_json_precision():
raise RuntimeError("JSON encode/decode loses precision")
def EncodeDecimal(o):
if isinstance(o, Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def ceildiv(a, b):
"""
Divide 2 ints and round up to next int rather than round down
Implementation requires python integers, which have a // operator that does floor division.
Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
"""
assert isinstance(a, int)
assert isinstance(b, int)
return -(-a // b)
def get_fee(tx_size, feerate_btc_kvb):
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
return target_fee_sat / Decimal(1e8) # Return result in BTC
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
"""Sleep until the predicate resolves to be True.
Warning: Note that this method is not recommended to be used in tests as it is
not aware of the context of the test framework. Using the `wait_until()` members
from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
`p2p.py` has a preset lock.
"""
if attempts == float('inf') and timeout == float('inf'):
timeout = 60
timeout = timeout * timeout_factor
@@ -253,6 +294,16 @@ def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=N
raise RuntimeError('Unreachable')
def sha256sum_file(filename):
h = hashlib.sha256()
with open(filename, 'rb') as f:
d = f.read(4096)
while len(d) > 0:
h.update(d)
d = f.read(4096)
return h.digest()
# RPC/P2P connection constants and functions
############################################
@@ -269,15 +320,15 @@ class PortSeed:
n = None
def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, coveragedir: Optional[str]=None) -> coverage.AuthServiceProxyWrapper:
"""
Args:
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
url: URL of the RPC server to call
node_number: the node number (or id) that this calls to
Kwargs:
timeout (int): HTTP timeout in seconds
coveragedir (str): Directory
timeout: HTTP timeout in seconds
coveragedir: Directory
Returns:
AuthServiceProxy. convenience object for making RPC calls.
@@ -288,11 +339,10 @@ def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
proxy_kwargs['timeout'] = int(timeout)
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile)
def p2p_port(n):
@@ -321,38 +371,76 @@ def rpc_url(datadir, i, chain, rpchost):
################
def initialize_datadir(dirname, n, chain):
def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
datadir = get_datadir_path(dirname, n)
if not os.path.isdir(datadir):
os.makedirs(datadir)
# Translate chain name to config name
if chain == 'testnet3':
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir
def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
# Translate chain subdirectory name to config name
if chain == 'testnet':
chain_name_conf_arg = 'testnet'
chain_name_conf_section = 'test'
else:
chain_name_conf_arg = chain
chain_name_conf_section = chain
with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
f.write("{}=1\n".format(chain_name_conf_arg))
f.write("[{}]\n".format(chain_name_conf_section))
with open(config_path, 'w', encoding='utf8') as f:
if chain_name_conf_arg:
f.write("{}=1\n".format(chain_name_conf_arg))
if chain_name_conf_section:
f.write("[{}]\n".format(chain_name_conf_section))
f.write("port=" + str(p2p_port(n)) + "\n")
f.write("rpcport=" + str(rpc_port(n)) + "\n")
# Disable server-side timeouts to avoid intermittent issues
f.write("rpcservertimeout=99000\n")
f.write("rpcdoccheck=1\n")
f.write("fallbackfee=0.0002\n")
f.write("server=1\n")
f.write("keypool=1\n")
f.write("discover=0\n")
f.write("dnsseed=0\n")
f.write("fixedseeds=0\n")
f.write("listenonion=0\n")
# Increase peertimeout to avoid disconnects while using mocktime.
# peertimeout is measured in mock time, so setting it large enough to
# cover any duration in mock time is sufficient. It can be overridden
# in tests.
f.write("peertimeout=999999999\n")
f.write("printtoconsole=0\n")
f.write("upnp=0\n")
f.write("natpmp=0\n")
f.write("shrinkdebugfile=0\n")
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir
f.write("deprecatedrpc=create_bdb\n") # Required to run the tests
# To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync
f.write("unsafesqlitesync=1\n")
if disable_autoconnect:
f.write("connect=0\n")
f.write(extra_config)
def get_datadir_path(dirname, n):
return os.path.join(dirname, "node" + str(n))
return pathlib.Path(dirname) / f"node{n}"
def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]:
"""Return os-specific environment variables that can be set to make the
GetDefaultDataDir() function return a datadir path under the provided
temp_dir, as well as the complete path it would return."""
if platform.system() == "Windows":
env = dict(APPDATA=str(temp_dir))
datadir = temp_dir / "Particl"
else:
env = dict(HOME=str(temp_dir))
if platform.system() == "Darwin":
datadir = temp_dir / "Library/Application Support/Particl"
else:
datadir = temp_dir / ".particl"
return env, datadir
def append_config(datadir, options):
@@ -395,7 +483,7 @@ def delete_cookie_file(datadir, chain):
def softfork_active(node, key):
"""Return whether a softfork is active."""
return node.getblockchaininfo()['softforks'][key]['active']
return node.getdeploymentinfo()['deployments'][key]['active']
def set_node_times(nodes, t):
@@ -403,208 +491,51 @@ def set_node_times(nodes, t):
node.setmocktime(t)
def disconnect_nodes(from_connection, node_num):
def get_peer_ids():
result = []
for peer in from_connection.getpeerinfo():
if "testnode{}".format(node_num) in peer['subver']:
result.append(peer['id'])
return result
peer_ids = get_peer_ids()
if not peer_ids:
logger.warning("disconnect_nodes: {} and {} were not connected".format(
from_connection.index,
node_num,
))
return
for peer_id in peer_ids:
try:
from_connection.disconnectnode(nodeid=peer_id)
except JSONRPCException as e:
# If this node is disconnected between calculating the peer id
# and issuing the disconnect, don't worry about it.
# This avoids a race condition if we're mass-disconnecting peers.
if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
raise
# wait to disconnect
wait_until(lambda: not get_peer_ids(), timeout=5)
def connect_nodes(from_connection, node_num):
ip_port = "127.0.0.1:" + str(p2p_port(node_num))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
# See comments in net_processing:
# * Must have a version message before anything else
# * Must have a verack message before anything else
wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo()))
wait_until(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo()))
def check_node_connections(*, node, num_in, num_out):
info = node.getnetworkinfo()
assert_equal(info["connections_in"], num_in)
assert_equal(info["connections_out"], num_out)
# Transaction/Block functions
#############################
def find_output(node, txid, amount, *, blockhash=None):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1, blockhash)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))
def gather_inputs(from_node, amount_needed, confirmations_required=1):
"""
Return a random set of unspent txouts that are enough to pay amount_needed
"""
assert confirmations_required >= 0
utxo = from_node.listunspent(confirmations_required)
random.shuffle(utxo)
inputs = []
total_in = Decimal("0.00000000")
while total_in < amount_needed and len(utxo) > 0:
t = utxo.pop()
total_in += t["amount"]
inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]})
if total_in < amount_needed:
raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in))
return (total_in, inputs)
def make_change(from_node, amount_in, amount_out, fee):
"""
Create change output(s), return them
"""
outputs = {}
amount = amount_out + fee
change = amount_in - amount
if change > amount * 2:
# Create an extra change output to break up big inputs
change_address = from_node.getnewaddress()
# Split change in two, being careful of rounding:
outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
change = amount_in - amount - outputs[change_address]
if change > 0:
outputs[from_node.getnewaddress()] = change
return outputs
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment * random.randint(0, fee_variants)
(total_in, inputs) = gather_inputs(from_node, amount + fee)
outputs = make_change(from_node, total_in, amount, fee)
outputs[to_node.getnewaddress()] = float(amount)
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransactionwithwallet(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], 0)
return (txid, signresult["hex"], fee)
# Helper to create at least "count" utxos
# Pass in a fee that is sufficient for relay and mining new transactions.
def create_confirmed_utxos(fee, node, count):
to_generate = int(0.5 * count) + 101
while to_generate > 0:
node.generate(min(25, to_generate))
to_generate -= 25
utxos = node.listunspent()
iterations = count - len(utxos)
addr1 = node.getnewaddress()
addr2 = node.getnewaddress()
if iterations <= 0:
return utxos
for i in range(iterations):
t = utxos.pop()
inputs = []
inputs.append({"txid": t["txid"], "vout": t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr1] = satoshi_round(send_value / 2)
outputs[addr2] = satoshi_round(send_value / 2)
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransactionwithwallet(raw_tx)["hex"]
node.sendrawtransaction(signed_tx)
while (node.getmempoolinfo()['size'] > 0):
node.generate(1)
utxos = node.listunspent()
assert len(utxos) >= count
return utxos
# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions).
# to make it large (helper for constructing large transactions). The
# total serialized size of the txouts is about 66k vbytes.
def gen_return_txouts():
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
# So we have big transactions (and therefore can't fit very many into each block)
# create one script_pubkey
script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes
for i in range(512):
script_pubkey = script_pubkey + "01"
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
txouts = []
from .messages import CTxOut
txout = CTxOut()
txout.nValue = 0
txout.scriptPubKey = hex_str_to_bytes(script_pubkey)
for k in range(128):
txouts.append(txout)
from .script import CScript, OP_RETURN
txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))]
assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456)
return txouts
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above.
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
addr = node.getnewaddress()
def create_lots_of_big_transactions(mini_wallet, node, fee, tx_batch_size, txouts, utxos=None):
txids = []
from .messages import CTransaction
for _ in range(num):
t = utxos.pop()
inputs = [{"txid": t["txid"], "vout": t["vout"]}]
outputs = {}
change = t['amount'] - fee
outputs[addr] = satoshi_round(change)
rawtx = node.createrawtransaction(inputs, outputs)
tx = CTransaction()
tx.deserialize(BytesIO(hex_str_to_bytes(rawtx)))
for txout in txouts:
tx.vout.append(txout)
newtx = tx.serialize().hex()
signresult = node.signrawtransactionwithwallet(newtx, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], 0)
txids.append(txid)
use_internal_utxos = utxos is None
for _ in range(tx_batch_size):
tx = mini_wallet.create_self_transfer(
utxo_to_spend=None if use_internal_utxos else utxos.pop(),
fee=fee,
)["tx"]
tx.vout.extend(txouts)
res = node.testmempoolaccept([tx.serialize().hex()])[0]
assert_equal(res['fees']['base'], fee)
txids.append(node.sendrawtransaction(tx.serialize().hex()))
return txids
def mine_large_block(node, utxos=None):
def mine_large_block(test_framework, mini_wallet, node):
# generate a 66k transaction,
# and 14 of them is close to the 1MB block limit
num = 14
txouts = gen_return_txouts()
utxos = utxos if utxos is not None else []
if len(utxos) < num:
utxos.clear()
utxos.extend(node.listunspent())
fee = 100 * node.getnetworkinfo()["relayfee"]
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
node.generate(1)
create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts)
test_framework.generate(node, 1)
def find_vout_for_address(node, txid, addr):
@@ -614,11 +545,6 @@ def find_vout_for_address(node, txid, addr):
"""
tx = node.getrawtransaction(txid, True)
for i in range(len(tx["vout"])):
scriptPubKey = tx["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i
if addr == tx["vout"][i]["scriptPubKey"]["address"]:
return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))

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

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 25
CURRENT_DB_DATA_VERSION = 5
CURRENT_DB_VERSION = 32
CURRENT_DB_DATA_VERSION = 7
class Concepts(IntEnum):
@@ -174,6 +174,7 @@ class Offer(Table):
secret_hash = Column("blob")
addr_from = Column("string")
pk_from = Column("blob")
addr_to = Column("string")
created_at = Column("integer")
expire_at = Column("integer")
@@ -183,6 +184,8 @@ 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")
@@ -192,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
@@ -215,7 +219,9 @@ class Bid(Table):
created_at = Column("integer")
expire_at = Column("integer")
bid_addr = Column("string")
pk_bid_addr = Column("blob")
proof_address = Column("string")
proof_signature = Column("blob")
proof_utxos = Column("blob")
# Address to spend lock tx to - address from wallet if empty TODO
withdraw_to_addr = Column("string")
@@ -229,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")
@@ -377,6 +384,8 @@ class SmsgAddress(Table):
use_type = Column("integer")
note = Column("string")
index = Index("smsgaddresses_address_index", "addr")
class Action(Table):
__tablename__ = "actions"
@@ -482,6 +491,14 @@ class XmrSwap(Table):
b_lock_tx_id = Column("blob")
msg_split_info = Column("string")
def getMsgSplitInfo(self):
if self.msg_split_info is None:
return 16000, 17000
msg_split_info = self.msg_split_info.split(":")
return int(msg_split_info[0]), int(msg_split_info[1])
class XmrSplitData(Table):
__tablename__ = "xmr_split_data"
@@ -602,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")
@@ -644,82 +663,268 @@ class CheckedBlock(Table):
block_time = Column("integer")
class CoinRates(Table):
__tablename__ = "coinrates"
record_id = Column("integer", primary_key=True, autoincrement=True)
currency_from = Column("integer")
currency_to = Column("integer")
rate = Column("string")
source = Column("string")
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"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
name = Column("string")
created_at = Column("integer")
class MessageNetworkLink(Table):
__tablename__ = "message_network_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
network_id = Column("string")
link_type = Column("integer") # MessageNetworkLinkTypes
created_at = Column("integer")
class DirectMessageRoute(Table):
__tablename__ = "direct_message_routes"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
network_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
smsg_addr_local = Column("string")
smsg_addr_remote = Column("string")
# smsg_addr_id_local = Column("integer") # SmsgAddress
# smsg_addr_id_remote = Column("integer") # KnownIdentity
route_data = Column("blob")
created_at = Column("integer")
class DirectMessageRouteLink(Table):
__tablename__ = "direct_message_route_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
direct_message_route_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
created_at = Column("integer")
class NetworkPortal(Table):
__tablename__ = "network_portals"
def set(
self, time_start, time_valid, network_from, network_to, address_from, address_to
):
super().__init__()
self.active_ind = 1
self.time_start = time_start
self.time_valid = time_valid
self.network_from = network_from
self.network_to = network_to
self.address_from = address_from
self.address_to = address_to
self.smsg_difficulty = 0x1EFFFFFF
self.num_refreshes = 0
self.messages_sent = 0
self.responses_seen = 0
self.time_last_used = 0
self.num_issues = 0
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
own_portal = Column("integer")
address_from = Column("string", unique=True)
address_to = Column("string")
network_from = Column("integer")
network_to = Column("integer")
time_start = Column("integer")
time_valid = Column("integer")
smsg_difficulty = Column("integer")
num_refreshes = Column("integer")
messages_sent = Column("integer")
responses_seen = Column("integer")
time_last_used = Column("integer")
num_issues = Column("integer")
created_at = Column("integer")
def extract_schema() -> dict:
g = globals().copy()
tables = {}
for name, obj in g.items():
if not inspect.isclass(obj):
continue
if not hasattr(obj, "__sqlite3_table__"):
continue
if not hasattr(obj, "__tablename__"):
continue
table_name: str = obj.__tablename__
table = {}
columns = {}
primary_key = None
constraints = []
indices = []
for m in inspect.getmembers(obj):
m_name, m_obj = m
if hasattr(m_obj, "__sqlite3_primary_key__"):
primary_key = m_obj
continue
if hasattr(m_obj, "__sqlite3_unique__"):
constraints.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_index__"):
indices.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_column__"):
col_type: str = m_obj.column_type.upper()
if col_type == "BOOL":
col_type = "INTEGER"
columns[m_name] = {
"type": col_type,
"primary_key": m_obj.primary_key,
"unique": m_obj.unique,
}
table["columns"] = columns
if primary_key is not None:
table["primary_key"] = {"column_1": primary_key.column_1}
if primary_key.column_2:
table["primary_key"]["column_2"] = primary_key.column_2
if primary_key.column_3:
table["primary_key"]["column_3"] = primary_key.column_3
for constraint in constraints:
if "constraints" not in table:
table["constraints"] = []
table_constraint = {"column_1": constraint.column_1}
if constraint.column_2:
table_constraint["column_2"] = constraint.column_2
if constraint.column_3:
table_constraint["column_3"] = constraint.column_3
table["constraints"].append(table_constraint)
for i in indices:
if "indices" not in table:
table["indices"] = []
table_index = {"index_name": i.name, "column_1": i.column_1}
if i.column_2 is not None:
table_index["column_2"] = i.column_2
if i.column_3 is not None:
table_index["column_3"] = i.column_3
table["indices"].append(table_index)
tables[table_name] = table
return tables
def create_table(c, table_name, table) -> None:
query: str = f"CREATE TABLE {table_name} ("
for i, (colname, column) in enumerate(table["columns"].items()):
col_type = column["type"]
query += ("," if i > 0 else "") + f" {colname} {col_type} "
if column["primary_key"]:
query += "PRIMARY KEY ASC "
if column["unique"]:
query += "UNIQUE "
if "primary_key" in table:
column_1 = table["primary_key"]["column_1"]
column_2 = table["primary_key"].get("column_2", None)
column_3 = table["primary_key"].get("column_3", None)
query += f", PRIMARY KEY ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ") "
constraints = table.get("constraints", [])
for constraint in constraints:
column_1 = constraint["column_1"]
column_2 = constraint.get("column_2", None)
column_3 = constraint.get("column_3", None)
query += f", UNIQUE ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ") "
query += ")"
c.execute(query)
indices = table.get("indices", [])
for index in indices:
index_name = index["index_name"]
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
c.execute(query)
def create_db_(con, log) -> None:
db_schema = extract_schema()
c = con.cursor()
for table_name, table in db_schema.items():
create_table(c, table_name, table)
def create_db(db_path: str, log) -> None:
con = None
try:
con = sqlite3.connect(db_path)
c = con.cursor()
g = globals().copy()
for name, obj in g.items():
if not inspect.isclass(obj):
continue
if not hasattr(obj, "__sqlite3_table__"):
continue
if not hasattr(obj, "__tablename__"):
continue
table_name: str = obj.__tablename__
query: str = f"CREATE TABLE {table_name} ("
primary_key = None
constraints = []
indices = []
num_columns: int = 0
for m in inspect.getmembers(obj):
m_name, m_obj = m
if hasattr(m_obj, "__sqlite3_primary_key__"):
primary_key = m_obj
continue
if hasattr(m_obj, "__sqlite3_unique__"):
constraints.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_index__"):
indices.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_column__"):
if num_columns > 0:
query += ","
col_type: str = m_obj.column_type.upper()
if col_type == "BOOL":
col_type = "INTEGER"
query += f" {m_name} {col_type} "
if m_obj.primary_key:
query += "PRIMARY KEY ASC "
if m_obj.unique:
query += "UNIQUE "
num_columns += 1
if primary_key is not None:
query += f", PRIMARY KEY ({primary_key.column_1}"
if primary_key.column_2:
query += f", {primary_key.column_2}"
if primary_key.column_3:
query += f", {primary_key.column_3}"
query += ") "
for constraint in constraints:
query += f", UNIQUE ({constraint.column_1}"
if constraint.column_2:
query += f", {constraint.column_2}"
if constraint.column_3:
query += f", {constraint.column_3}"
query += ") "
query += ")"
c.execute(query)
for i in indices:
query: str = f"CREATE INDEX {i.name} ON {table_name} ({i.column_1}"
if i.column_2 is not None:
query += f", {i.column_2}"
if i.column_3 is not None:
query += f", {i.column_3}"
query += ")"
c.execute(query)
create_db_(con, log)
con.commit()
finally:
if con:
@@ -898,6 +1103,7 @@ class DBMethods:
query += f"{key}=:{key}"
cursor.execute(query, values)
return cursor.lastrowid
def query(
self,
@@ -915,15 +1121,12 @@ class DBMethods:
table_name: str = table_class.__tablename__
query: str = "SELECT "
columns = []
for mc in inspect.getmembers(table_class):
mc_name, mc_obj = mc
if not hasattr(mc_obj, "__sqlite3_column__"):
continue
if len(columns) > 0:
query += ", "
query += mc_name
@@ -931,10 +1134,32 @@ class DBMethods:
query += f" FROM {table_name} WHERE 1=1 "
query_data = {}
for ck in constraints:
if not validColumnName(ck):
raise ValueError(f"Invalid constraint column: {ck}")
query += f" AND {ck} = :{ck} "
constraint_value = constraints[ck]
if isinstance(constraint_value, tuple) or isinstance(
constraint_value, list
):
if len(constraint_value) < 2:
raise ValueError(f"Too few constraint values for list: {ck}")
query += f" AND {ck} IN ("
for i, cv in enumerate(constraint_value):
cv_name: str = f"{ck}_{i}"
if i > 0:
query += ","
query += ":" + cv_name
query_data[cv_name] = cv
query += ") "
else:
if constraint_value is None:
query += f" AND {ck} IS NULL "
else:
query += f" AND {ck} = :{ck} "
query_data[ck] = constraint_value
for order_col, order_dir in order_by.items():
if validColumnName(order_col) is False:
@@ -947,7 +1172,6 @@ class DBMethods:
if query_suffix:
query += query_suffix
query_data = constraints.copy()
query_data.update(extra_query_data)
rows = cursor.execute(query, query_data)
for row in rows:

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
class Explorer:
def __init__(self, swapclient, coin_type, base_url):
self.swapclient = swapclient

View File

@@ -1,25 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import json
import shlex
import secrets
import traceback
import threading
import http.client
from urllib import parse
from http.server import BaseHTTPRequestHandler, HTTPServer
import base64
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
from datetime import datetime, timedelta, timezone
from http.cookies import SimpleCookie
from . import __version__
from .util import (
dumpj,
toBool,
LockedCoinError,
format_timestamp,
)
from .chainparams import (
@@ -30,6 +35,7 @@ from .basicswap_util import (
strTxState,
strBidState,
)
from .util.rfc2440 import verify_rfc2440_password
from .js_server import (
js_error,
@@ -47,6 +53,7 @@ from .ui.page_automation import (
page_automation_strategy_new,
)
from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api
from .ui.page_bids import page_bids, page_bid
from .ui.page_offers import page_offers, page_offer, page_newoffer
from .ui.page_tor import page_tor, get_tor_established_state
@@ -57,6 +64,9 @@ from .ui.page_identity import page_identity
from .ui.page_smsgaddresses import page_smsgaddresses
from .ui.page_debug import page_debug
SESSION_COOKIE_NAME = "basicswap_session_id"
SESSION_DURATION_MINUTES = 60
env = Environment(loader=PackageLoader("basicswap", "templates"))
env.filters["formatts"] = format_timestamp
@@ -119,6 +129,58 @@ def parse_cmd(cmd: str, type_map: str):
class HttpHandler(BaseHTTPRequestHandler):
def _get_session_cookie(self):
if "Cookie" in self.headers:
cookie = SimpleCookie(self.headers["Cookie"])
if SESSION_COOKIE_NAME in cookie:
return cookie[SESSION_COOKIE_NAME].value
return None
def _set_session_cookie(self, session_id):
cookie = SimpleCookie()
cookie[SESSION_COOKIE_NAME] = session_id
cookie[SESSION_COOKIE_NAME]["path"] = "/"
cookie[SESSION_COOKIE_NAME]["httponly"] = True
cookie[SESSION_COOKIE_NAME]["samesite"] = "Lax"
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
cookie[SESSION_COOKIE_NAME]["expires"] = expires.strftime(
"%a, %d %b %Y %H:%M:%S GMT"
)
return ("Set-Cookie", cookie.output(header="").strip())
def _clear_session_cookie(self):
cookie = SimpleCookie()
cookie[SESSION_COOKIE_NAME] = ""
cookie[SESSION_COOKIE_NAME]["path"] = "/"
cookie[SESSION_COOKIE_NAME]["httponly"] = True
cookie[SESSION_COOKIE_NAME]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"
return ("Set-Cookie", cookie.output(header="").strip())
def is_authenticated(self):
swap_client = self.server.swap_client
client_auth_hash = swap_client.settings.get("client_auth_hash")
if not client_auth_hash:
return True
session_id = self._get_session_cookie()
if not session_id:
return False
with self.server.session_lock:
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
return False
def log_error(self, format, *args):
super().log_message(format, *args)
@@ -134,14 +196,20 @@ 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(
self, template, args_dict, status_code=200, version=__version__
self,
template,
args_dict,
status_code=200,
version=__version__,
extra_headers=None,
):
swap_client = self.server.swap_client
if swap_client.ws_server:
@@ -150,48 +218,86 @@ 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
# TODO: Cache value?
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())
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
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:
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception:
args_dict["current_status"] = "stopped"
args_dict["amm_active_count"] = 0
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
else:
args_dict["current_status"] = "unknown"
args_dict["amm_active_count"] = 0
if "messages" in args_dict:
messages_with_ids = []
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:
parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/")
if len(url_split) > 1 and url_split[1]:
args_dict["current_page"] = url_split[1]
else:
args_dict["current_page"] = "index"
else:
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
self.putHeaders(status_code, "text/html")
self.putHeaders(status_code, "text/html", extra_headers=extra_headers)
return bytes(
template.render(
title=self.server.title,
@@ -203,6 +309,7 @@ class HttpHandler(BaseHTTPRequestHandler):
)
def render_simple_template(self, template, args_dict):
self.putHeaders(200, "text/html")
return bytes(
template.render(
title=self.server.title,
@@ -211,7 +318,7 @@ class HttpHandler(BaseHTTPRequestHandler):
"UTF-8",
)
def page_info(self, info_str, post_string=None):
def page_info(self, info_str, post_string=None, extra_headers=None):
template = env.get_template("info.html")
swap_client = self.server.swap_client
summary = swap_client.getSummary()
@@ -222,6 +329,7 @@ class HttpHandler(BaseHTTPRequestHandler):
"message_str": info_str,
"summary": summary,
},
extra_headers=extra_headers,
)
def page_error(self, error_str, post_string=None):
@@ -237,6 +345,100 @@ class HttpHandler(BaseHTTPRequestHandler):
},
)
def page_login(self, url_split, post_string):
swap_client = self.server.swap_client
template = env.get_template("login.html")
err_messages = []
extra_headers = []
is_json_request = "application/json" in self.headers.get("Content-Type", "")
security_warning = None
if self.server.host_name not in ("127.0.0.1", "localhost"):
security_warning = "WARNING: Server is accessible on the network. Sending password over plain HTTP is insecure. Use HTTPS (e.g., via reverse proxy) for non-local access."
if not is_json_request:
err_messages.append(security_warning)
if post_string:
password = None
if is_json_request:
try:
json_data = json.loads(post_string.decode("utf-8"))
password = json_data.get("password")
except Exception as e:
swap_client.log.error(f"Error parsing JSON login data: {e}")
else:
try:
form_data = parse.parse_qs(post_string.decode("utf-8"))
password = form_data.get("password", [None])[0]
except Exception as e:
swap_client.log.error(f"Error parsing form login data: {e}")
client_auth_hash = swap_client.settings.get("client_auth_hash")
if (
client_auth_hash
and password is not None
and verify_rfc2440_password(client_auth_hash, password)
):
session_id = secrets.token_urlsafe(32)
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
with self.server.session_lock:
self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
if is_json_request:
response_data = {"success": True, "session_id": session_id}
if security_warning:
response_data["warning"] = security_warning
self.putHeaders(
200, "application/json", extra_headers=[cookie_header]
)
return json.dumps(response_data).encode("utf-8")
else:
self.send_response(302)
self.send_header("Location", "/offers")
self.send_header(cookie_header[0], cookie_header[1])
self.end_headers()
return b""
else:
if is_json_request:
self.putHeaders(401, "application/json")
return json.dumps({"error": "Invalid password"}).encode("utf-8")
else:
err_messages.append("Invalid password.")
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
if (
not is_json_request
and swap_client.settings.get("client_auth_hash")
and self.is_authenticated()
):
self.send_response(302)
self.send_header("Location", "/offers")
self.end_headers()
return b""
return self.render_template(
template,
{
"title_str": "Login",
"err_messages": err_messages,
"summary": {},
"encrypted": False,
"locked": False,
},
status_code=401 if post_string and not is_json_request else 200,
extra_headers=extra_headers,
)
def page_shutdown_ping(self, url_split, post_string):
if not self.server.stop_event.is_set():
raise ValueError("Unexpected shutdown ping.")
self.putHeaders(401, "application/json")
return json.dumps({"ack": True}).encode("utf-8")
def page_explorers(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -250,14 +452,10 @@ class HttpHandler(BaseHTTPRequestHandler):
form_data = self.checkForm(post_string, "explorers", err_messages)
if form_data:
explorer = form_data[b"explorer"][0].decode("utf-8")
action = form_data[b"action"][0].decode("utf-8")
explorer = get_data_entry(form_data, "explorer")
action = get_data_entry(form_data, "action")
args = get_data_entry_or(form_data, "args", "")
args = (
""
if b"args" not in form_data
else form_data[b"args"][0].decode("utf-8")
)
try:
c, e = explorer.split("_")
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
@@ -410,7 +608,6 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.render_template(
template,
{
"refresh": 30,
"active_swaps": [
(
s[0].hex(),
@@ -447,16 +644,52 @@ class HttpHandler(BaseHTTPRequestHandler):
def page_shutdown(self, url_split, post_string):
swap_client = self.server.swap_client
extra_headers = []
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()
with self.server.session_lock:
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
swap_client.log.info("Web shutdown stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
swap_client.log.info(f"AMM web shutdown: {msg}")
else:
swap_client.log.warning(f"AMM web shutdown warning: {msg}")
except Exception as e:
swap_client.log.error(f"Error stopping AMM in web shutdown: {e}")
swap_client.stopRunning()
return self.page_info("Shutting down")
return self.page_info("Shutting down", extra_headers=extra_headers)
def page_donation(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
template = env.get_template("donation.html")
return self.render_template(
template,
{
"summary": summary,
},
)
def page_index(self, url_split):
swap_client = self.server.swap_client
@@ -477,41 +710,109 @@ class HttpHandler(BaseHTTPRequestHandler):
},
)
def putHeaders(self, status_code, content_type):
def putHeaders(self, status_code, content_type, extra_headers=None):
self.send_response(status_code)
if self.server.allow_cors:
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Type", content_type)
if extra_headers:
for header_tuple in extra_headers:
self.send_header(header_tuple[0], header_tuple[1])
self.end_headers()
def handle_http(self, status_code, path, post_string="", is_json=False):
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
swap_client = self.server.swap_client
parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/")
if post_string == "" and len(parsed.query) > 0:
post_string = parsed.query
if len(url_split) > 1 and url_split[1] == "json":
page = url_split[1] if len(url_split) > 1 else ""
exempt_pages = ["login", "static", "error", "info"]
auth_header = self.headers.get("Authorization")
basic_auth_ok = False
if auth_header and auth_header.startswith("Basic "):
try:
self.putHeaders(status_code, "text/plain")
encoded_creds = auth_header.split(" ", 1)[1]
decoded_creds = base64.b64decode(encoded_creds).decode("utf-8")
_, password = decoded_creds.split(":", 1)
client_auth_hash = swap_client.settings.get("client_auth_hash")
if client_auth_hash and verify_rfc2440_password(
client_auth_hash, password
):
basic_auth_ok = True
else:
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"error": "Invalid Basic Auth credentials"}).encode(
"utf-8"
)
)
return b""
except Exception as e:
swap_client.log.error(f"Error processing Basic Auth header: {e}")
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"error": "Malformed Basic Auth header"}).encode("utf-8")
)
return b""
if not basic_auth_ok and page not in exempt_pages:
if not self.is_authenticated():
if page == "json":
self.putHeaders(401, "application/json")
self.wfile.write(
json.dumps({"error": "Unauthorized"}).encode("utf-8")
)
return b""
else:
self.send_response(302)
self.send_header("Location", "/login")
clear_cookie_header = self._clear_session_cookie()
self.send_header(clear_cookie_header[0], clear_cookie_header[1])
self.end_headers()
return b""
if not post_string and len(parsed.query) > 0:
post_string = parsed.query
if page == "json":
try:
self.putHeaders(status_code, "json")
func = js_url_to_function(url_split)
return func(self, url_split, post_string, is_json)
except Exception as ex:
if swap_client.debug is True:
if isinstance(ex, LockedCoinError):
clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked"
swap_client.log.warning(clean_msg)
return js_error(self, clean_msg)
elif swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex))
if len(url_split) > 1 and url_split[1] == "static":
if page == "static":
try:
static_path = os.path.join(os.path.dirname(__file__), "static")
content = None
mime_type = ""
filepath = ""
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
with open(
os.path.join(static_path, "sequence_diagrams", url_split[3]),
"rb",
) as fp:
self.putHeaders(status_code, "image/svg+xml")
return fp.read()
filepath = os.path.join(
static_path, "sequence_diagrams", url_split[3]
)
mime_type = "image/svg+xml"
elif len(url_split) > 3 and url_split[2] == "images":
filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "images", filename)
_, extension = os.path.splitext(filename)
mime_type = {
".svg": "image/svg+xml",
@@ -520,25 +821,25 @@ class HttpHandler(BaseHTTPRequestHandler):
".gif": "image/gif",
".ico": "image/x-icon",
}.get(extension, "")
if mime_type == "":
raise ValueError("Unknown file type " + filename)
with open(
os.path.join(static_path, "images", filename), "rb"
) as fp:
self.putHeaders(status_code, mime_type)
return fp.read()
elif len(url_split) > 3 and url_split[2] == "css":
filename = os.path.join(*url_split[3:])
with open(os.path.join(static_path, "css", filename), "rb") as fp:
self.putHeaders(status_code, "text/css; charset=utf-8")
return fp.read()
filepath = os.path.join(static_path, "css", filename)
mime_type = "text/css; charset=utf-8"
elif len(url_split) > 3 and url_split[2] == "js":
filename = os.path.join(*url_split[3:])
with open(os.path.join(static_path, "js", filename), "rb") as fp:
self.putHeaders(status_code, "application/javascript")
return fp.read()
filepath = os.path.join(static_path, "js", filename)
mime_type = "application/javascript"
else:
return self.page_404(url_split)
if mime_type == "" or not filepath:
raise ValueError("Unknown file type or path")
with open(filepath, "rb") as fp:
content = fp.read()
self.putHeaders(status_code, mime_type)
return content
except FileNotFoundError:
return self.page_404(url_split)
except Exception as ex:
@@ -550,6 +851,10 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 1:
page = url_split[1]
if page == "login":
return self.page_login(url_split, post_string)
if page == "shutdown_ping":
return self.page_shutdown_ping(url_split, post_string)
if page == "active":
return self.page_active(url_split, post_string)
if page == "wallets":
@@ -578,14 +883,14 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_offers(self, url_split, post_string, sent=True)
if page == "bid":
return page_bid(self, url_split, post_string)
if page == "receivedbids":
return page_bids(self, url_split, post_string, received=True)
if page == "sentbids":
return page_bids(self, url_split, post_string, sent=True)
if page == "bids":
return page_bids(self, url_split, post_string)
if page == "availablebids":
return page_bids(self, url_split, post_string, available=True)
if page == "watched":
return self.page_watched(url_split, post_string)
if page == "donation":
return self.page_donation(url_split, post_string)
if page == "smsgaddresses":
return page_smsgaddresses(self, url_split, post_string)
if page == "identity":
@@ -598,6 +903,41 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_automation_strategy(self, url_split, post_string)
if page == "newautomationstrategy":
return page_automation_strategy_new(self, url_split, post_string)
if page == "amm":
if len(url_split) > 2 and url_split[2] == "status":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
status_data = amm_status_api(
swap_client, self.path, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(status_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "autostart":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
autostart_data = amm_autostart_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(autostart_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "debug":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
debug_data = amm_debug_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(debug_data).encode("utf-8")
return page_amm(self, url_split, post_string)
if page == "shutdown":
return self.page_shutdown(url_split, post_string)
if page == "changepassword":
@@ -617,15 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_error(str(ex))
def do_GET(self):
response = self.handle_http(200, self.path)
self.wfile.write(response)
try:
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
finally:
pass
def do_POST(self):
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
try:
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)
self.wfile.write(response)
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")
@@ -638,12 +991,13 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
class HttpThread(threading.Thread, HTTPServer):
def __init__(self, fp, host_name, port_no, allow_cors, swap_client):
class HttpThread(threading.Thread, ThreadingHTTPServer):
daemon_threads = True
def __init__(self, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
self.stop_event = threading.Event()
self.fp = fp
self.host_name = host_name
self.port_no = port_no
self.allow_cors = allow_cors
@@ -651,27 +1005,36 @@ class HttpThread(threading.Thread, HTTPServer):
self.title = "BasicSwap - " + __version__
self.last_form_id = dict()
self.session_tokens = dict()
self.active_sessions = {}
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()
# Send fake request
conn = http.client.HTTPConnection(self.host_name, self.port_no)
conn.connect()
conn.request("GET", "/none")
response = conn.getresponse()
_ = response.read()
conn.close()
try:
conn = http.client.HTTPConnection(self.host_name, self.port_no, timeout=0.5)
conn.request("GET", "/shutdown_ping")
conn.close()
except Exception:
pass
def serve_forever(self):
self.timeout = 1
while not self.stop_event.is_set():
self.handle_request()
self.socket.close()
self.swap_client.log.info("HTTP server stopped.")
def run(self):
self.serve_forever()

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

@@ -1,13 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from typing import Union
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn
from basicswap.util import b2h, b2i, ensure, i2h
from basicswap.util import b2i, ensure, i2b
from basicswap.util.script import decodePushData, decodeScriptNum
from .btc import BTCInterface, ensure_op, findOutput
from basicswap.rpc import make_rpc_func
@@ -79,6 +79,7 @@ class BCHInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
@@ -106,6 +107,31 @@ class BCHInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def createWallet(self, wallet_name: str, password: str = ""):
self.rpc("createwallet", [wallet_name, False])
if password != "":
self.rpc(
"encryptwallet",
[
password,
],
override_wallet=wallet_name,
)
def newKeypool(self) -> None:
self._log.debug("Refreshing keypool.")
# Use up current keypool
wi = self.rpc_wallet("getwalletinfo")
keypool_size: int = wi["keypoolsize"]
for i in range(keypool_size):
_ = self.rpc_wallet("getnewaddress")
keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"]
for i in range(keypoolsize_hd_internal):
_ = self.rpc_wallet("getrawchangeaddress")
self.rpc_wallet("keypoolrefill")
# returns pkh
def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload)
@@ -454,11 +480,14 @@ class BCHInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
size,
pay_fee,
"createSCLockSpendTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {size}, {pay_fee}"
),
)
)
return tx.serialize_without_witness()
@@ -506,11 +535,14 @@ class BCHInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockRefundTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue
@@ -582,11 +614,14 @@ class BCHInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockRefundSpendToFTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize_without_witness()
@@ -780,10 +815,18 @@ class BCHInterface(BTCInterface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock tx: {}.".format(b2h(txid)))
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)
@@ -835,7 +878,7 @@ class BCHInterface(BTCInterface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock refund tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock refund tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "nLockTime not 0")
@@ -881,7 +924,7 @@ class BCHInterface(BTCInterface):
size = self.getTxSize(tx)
vsize = size
self._log.info(
self._log.info_s(
"tx amount, vsize, fee: %ld, %ld, %ld", locked_coin, vsize, fee_paid
)
@@ -905,7 +948,7 @@ class BCHInterface(BTCInterface):
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock refund spend tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock refund spend tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "nLockTime not 0")
@@ -947,9 +990,7 @@ class BCHInterface(BTCInterface):
size = self.getTxSize(tx)
vsize = size
self._log.info(
"tx amount, vsize, fee: %ld, %ld, %ld", tx_value, vsize, fee_paid
)
self._log.info_s(f"tx amount, vsize, fee: {tx_value}, {vsize}, {fee_paid}")
return True
@@ -962,7 +1003,7 @@ class BCHInterface(BTCInterface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock spend tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock spend tx: {}.".format(self._log.id(txid)))
ensure(tx.nVersion == self.txVersion(), "Bad version")
ensure(tx.nLockTime == 0, "nLockTime not 0")
@@ -995,7 +1036,7 @@ class BCHInterface(BTCInterface):
size = self.getTxSize(tx)
vsize = size
self._log.info(
self._log.info_s(
"tx amount, vsize, fee: %ld, %ld, %ld", tx.vout[0].nValue, vsize, fee_paid
)
@@ -1115,11 +1156,14 @@ class BCHInterface(BTCInterface):
tx.rehash()
self._log.info(
"createMercyTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
1,
vsize,
pay_fee,
"createMercyTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {1}, {vsize}, {pay_fee}"
),
)
)
txHex = tx.serialize_without_witness()

File diff suppressed because it is too large Load Diff

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

@@ -47,7 +47,7 @@ class DASHInterface(BTCInterface):
def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic("english").to_mnemonic(key)
def initialiseWallet(self, key_bytes: bytes) -> None:
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
self._have_checked_seed = False
if self._wallet_v20_compatible:
self._log.warning("Generating wallet compatible with v20 seed.")
@@ -66,7 +66,11 @@ class DASHInterface(BTCInterface):
def checkExpectedSeed(self, expect_seedid: str) -> bool:
self._expect_seedid_hex = expect_seedid
rv = self.rpc_wallet("dumphdinfo")
try:
rv = self.rpc_wallet("dumphdinfo")
except Exception as e:
self._log.debug(f"DASH dumphdinfo failed {e}.")
return False
if rv["mnemonic"] != "":
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
@@ -111,18 +115,45 @@ class DASHInterface(BTCInterface):
return None
def unlockWallet(self, password: str):
super().unlockWallet(password)
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
super().unlockWallet(password, check_seed)
if self._wallet_v20_compatible:
# Store password for initialiseWallet
self._wallet_passphrase = password
if not self._have_checked_seed:
try:
self._sc.checkWalletSeed(self.coin_type())
except Exception as ex:
# dumphdinfo can fail if the wallet is not initialised
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
def lockWallet(self):
super().lockWallet()
self._wallet_passphrase = ""
def encryptWallet(
self, old_password: str, new_password: str, check_seed: bool = True
):
if old_password != "":
self.unlockWallet(old_password, check_seed=False)
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [new_password])
if check_seed is False or seed_id_before == "Not found":
return
self.unlockWallet(new_password, check_seed=False)
seed_id_after: str = self.getWalletSeedID()
self.lockWallet()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
if self.isWalletEncrypted():
raise ValueError("Old password must be set")
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])

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 (
@@ -27,7 +29,6 @@ from basicswap.interface.btc import (
)
from basicswap.util import (
ensure,
b2h,
b2i,
i2b,
i2h,
@@ -211,6 +212,10 @@ class DCRInterface(Secp256k1Interface):
def txoType():
return CTxOut
@staticmethod
def est_lock_tx_vsize() -> int:
return 224
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 327
@@ -273,6 +278,9 @@ class DCRInterface(Secp256k1Interface):
self._connection_type = coin_settings["connection_type"]
self._altruistic = coin_settings.get("altruistic", True)
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
def open_rpc(self):
return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host)
@@ -326,14 +334,14 @@ class DCRInterface(Secp256k1Interface):
def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet:
self.rpc_wallet("getinfo")
self.rpc_wallet("walletislocked")
else:
self.rpc("getblockchaininfo")
def getChainHeight(self) -> int:
return self.rpc("getblockcount")
def initialiseWallet(self, key: bytes) -> None:
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
# Load with --create
pass
@@ -348,7 +356,9 @@ class DCRInterface(Secp256k1Interface):
walletislocked = self.rpc_wallet("walletislocked")
return True, walletislocked
def changeWalletPassword(self, old_password: str, new_password: str):
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
# Read initial pwd from settings
@@ -362,14 +372,15 @@ class DCRInterface(Secp256k1Interface):
# Clear initial password
self._sc.editSettings(self.coin_name().lower(), {"wallet_pwd": ""})
def unlockWallet(self, password: str):
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "":
return
self._log.info("unlockWallet - {}".format(self.ticker()))
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
self._sc.checkWalletSeed(self.coin_type())
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):
self._log.info("lockWallet - {}".format(self.ticker()))
@@ -1076,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,))
@@ -1123,11 +1133,14 @@ class DCRInterface(Secp256k1Interface):
fee_info["size"] = size
self._log.info(
"createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
tx.TxHash().hex(),
tx_fee_rate,
size,
pay_fee,
"createSCLockSpendTx {}{}.".format(
self._log.id(tx.TxHash()),
(
""
if self._log.safe_logs
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
),
)
)
return tx.serialize(TxSerializeType.NoWitness)
@@ -1167,11 +1180,14 @@ class DCRInterface(Secp256k1Interface):
tx.vout[0].value = locked_coin - pay_fee
self._log.info(
"createSCLockRefundTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
tx.TxHash().hex(),
tx_fee_rate,
size,
pay_fee,
"createSCLockRefundTx {}{}.".format(
self._log.id(tx.TxHash()),
(
""
if self._log.safe_logs
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
),
)
)
return tx.serialize(TxSerializeType.NoWitness), refund_script, tx.vout[0].value
@@ -1215,11 +1231,14 @@ class DCRInterface(Secp256k1Interface):
tx.vout[0].value = locked_coin - pay_fee
self._log.info(
"createSCLockRefundSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
tx.TxHash().hex(),
tx_fee_rate,
size,
pay_fee,
"createSCLockRefundSpendTx {}{}.".format(
self._log.id(tx.TxHash()),
(
""
if self._log.safe_logs
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
),
)
)
return tx.serialize(TxSerializeType.NoWitness)
@@ -1244,7 +1263,7 @@ class DCRInterface(Secp256k1Interface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
ensure(tx.version == self.txVersion(), "Bad version")
ensure(tx.locktime == 0, "Bad locktime")
@@ -1320,7 +1339,7 @@ class DCRInterface(Secp256k1Interface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock spend tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock spend tx: {}.".format(self._log.id(txid)))
ensure(tx.version == self.txVersion(), "Bad version")
ensure(tx.locktime == 0, "Bad locktime")
@@ -1390,7 +1409,7 @@ class DCRInterface(Secp256k1Interface):
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock refund tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock refund tx: {}.".format(self._log.id(txid)))
ensure(tx.version == self.txVersion(), "Bad version")
ensure(tx.locktime == 0, "locktime not 0")
@@ -1453,7 +1472,7 @@ class DCRInterface(Secp256k1Interface):
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info("Verifying lock refund spend tx: {}.".format(b2h(txid)))
self._log.info("Verifying lock refund spend tx: {}.".format(self._log.id(txid)))
ensure(tx.version == self.txVersion(), "Bad version")
ensure(tx.locktime == 0, "locktime not 0")
@@ -1539,11 +1558,14 @@ class DCRInterface(Secp256k1Interface):
tx.vout[0].value = locked_amount - pay_fee
self._log.info(
"createSCLockRefundSpendToFTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
tx.TxHash().hex(),
tx_fee_rate,
size,
pay_fee,
"createSCLockRefundSpendToFTx {}{}.".format(
self._log.id(tx.TxHash()),
(
""
if self._log.safe_logs
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
),
)
)
return tx.serialize(TxSerializeType.NoWitness)
@@ -1588,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)
@@ -1712,7 +1734,7 @@ class DCRInterface(Secp256k1Interface):
witness_bytes = 115
size = len(tx.serialize()) + witness_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(
self._log.info_s(
f"BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {size}, {pay_fee}."
)
return pay_fee

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

@@ -42,7 +42,6 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
host = host
def rpc_func(method, params=None):
nonlocal port, auth, host
return callrpc(port, auth, method, params, host)
return rpc_func

View File

@@ -24,6 +24,10 @@ class DOGEInterface(BTCInterface):
def coin_type():
return Coins.DOGE
@staticmethod
def est_lock_tx_vsize() -> int:
return 192
@staticmethod
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 192

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -44,17 +44,40 @@ class FIROInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
def getExchangeName(self, exchange_name: str) -> str:
return "zcoin"
def initialiseWallet(self, key):
def initialiseWallet(self, key, restore_time: int = -1):
# load with -hdseed= parameter
pass
def checkWallets(self) -> int:
return 1
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
# Firo shuts down after encryptwallet
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found":
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
def getNewAddress(self, use_segwit, label="swap_receive"):
return self.rpc("getnewaddress", [label])
# addr_plain = self.rpc('getnewaddress', [label])
@@ -102,7 +125,9 @@ class FIROInterface(BTCInterface):
if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, "bid")
self._log.info("Imported watch-only addr: {}".format(dest_address))
self._log.info(
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
)
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from

View File

@@ -18,7 +18,7 @@ class LTCInterface(BTCInterface):
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
self._rpc_wallet_mweb = "mweb"
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet_mweb = make_rpc_func(
self._rpcport,
self._rpcauth,
@@ -94,10 +94,11 @@ class LTCInterfaceMWEB(LTCInterface):
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
self._rpc_wallet = "mweb"
self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
)
self.rpc_wallet_watch = self.rpc_wallet
def chainparams(self):
return chainparams[Coins.LTC]
@@ -128,7 +129,7 @@ class LTCInterfaceMWEB(LTCInterface):
self._log.info("init_wallet - {}".format(self.ticker()))
self._log.info("Creating mweb wallet for {}.".format(self.coin_name()))
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
@@ -137,7 +138,7 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc_wallet("walletpassphrase", [password, 100000000])
if self.getWalletSeedID() == "Not found":
self._sc.initialiseWallet(self.coin_type())
self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"])
@@ -146,7 +147,7 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str):
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "":
return
self._log.info("unlockWallet - {}".format(self.ticker()))
@@ -156,5 +157,5 @@ class LTCInterfaceMWEB(LTCInterface):
else:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
self._sc.checkWalletSeed(self.coin_type())
if check_seed:
self._sc.checkWalletSeed(self.coin_type())

View File

@@ -41,7 +41,6 @@ from basicswap.util.address import (
from basicswap.util import (
b2i,
i2b,
i2h,
ensure,
)
from basicswap.basicswap_util import (
@@ -80,12 +79,16 @@ class NAVInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return True
def initialiseWallet(self, key):
def initialiseWallet(self, key, restore_time: int = -1):
# Load with -importmnemonic= parameter
pass
@@ -549,7 +552,9 @@ class NAVInterface(BTCInterface):
if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, "bid")
self._log.info("Imported watch-only addr: {}".format(dest_address))
self._log.info(
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
)
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from
@@ -813,11 +818,14 @@ class NAVInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockRefundTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize(), refund_script, tx.vout[0].nValue
@@ -868,11 +876,14 @@ class NAVInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockRefundSpendTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()
@@ -925,11 +936,14 @@ class NAVInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockRefundSpendToFTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()
@@ -972,11 +986,14 @@ class NAVInterface(BTCInterface):
tx.rehash()
self._log.info(
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
"createSCLockSpendTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,39 +14,3 @@ class NMCInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.NMC
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
ro = self.rpc(
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
) # TODO: Use combo(address) where possible
self._log.debug("[rm] scantxoutset end")
return_txid = True if txid is None else False
for o in ro["unspents"]:
if txid and o["txid"] != txid.hex():
continue
# Verify amount
if self.make_int(o["amount"]) != int(bid_amount):
self._log.warning(
"Found output to lock tx address of incorrect value: %s, %s",
str(o["amount"]),
o["txid"],
)
continue
rv = {"depth": 0, "height": o["height"]}
if o["height"] > 0:
rv["depth"] = ro["height"] - o["height"]
if find_index:
rv["index"] = o["vout"]
if return_txid:
rv["txid"] = o["txid"]
return rv

View File

@@ -8,13 +8,13 @@
import hashlib
from enum import IntEnum
from typing import List
from basicswap.contrib.test_framework.messages import (
CTxOutPart,
)
from basicswap.contrib.test_framework.script import (
CScript,
OP_0,
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
@@ -25,7 +25,6 @@ from basicswap.util import (
TemporaryError,
)
from basicswap.util.script import (
getP2WSH,
getCompactSizeLen,
getWitnessElementLen,
)
@@ -66,6 +65,10 @@ class PARTInterface(BTCInterface):
def txVersion() -> int:
return 0xA0
@staticmethod
def est_lock_tx_vsize() -> int:
return 138
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 200
@@ -106,7 +109,7 @@ class PARTInterface(BTCInterface):
)
return index_info["spentindex"]
def initialiseWallet(self, key: bytes) -> None:
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
raise ValueError("TODO")
def withdrawCoin(self, value, addr_to, subfee):
@@ -132,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"]
@@ -185,6 +193,27 @@ class PARTInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def combine_non_segwit_prevouts(self):
raise RuntimeError("No non-segwit outputs found.")
def signMessage(self, address: str, message: str) -> str:
args = [address, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc_wallet("signmessage", args)
def signMessageWithKey(self, key_wif: str, message: str) -> str:
args = [key_wif, message]
if self.getDaemonVersion() > 23020700:
message_magic: str = self.chainparams()["message_magic"]
args += [
message_magic,
]
return self.rpc("signmessagewithprivkey", args)
class PARTInterfaceBlind(PARTInterface):
@@ -195,6 +224,10 @@ class PARTInterfaceBlind(PARTInterface):
def balance_type():
return BalanceTypes.BLIND
@staticmethod
def est_lock_tx_vsize() -> int:
return 980
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 1032
@@ -203,6 +236,15 @@ class PARTInterfaceBlind(PARTInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 980
@staticmethod
def compareFeeRates(actual: int, expected: int) -> bool:
# Allow the fee to be up to 10% larger than expected
if actual < expected - 20:
return False
if actual > expected + expected * 0.1:
return False
return True
def coin_name(self) -> str:
return super().coin_name() + " Blind"
@@ -248,7 +290,7 @@ class PARTInterfaceBlind(PARTInterface):
ephemeral_pubkey = self.getPubkey(ephemeral_key)
assert len(ephemeral_pubkey) == 33
nonce = self.getScriptLockTxNonce(vkbv)
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(script))
inputs = []
outputs = [
{
@@ -322,7 +364,7 @@ class PARTInterfaceBlind(PARTInterface):
locked_coin = input_blinded_info["amount"]
tx_lock_id = lock_tx_obj["txid"]
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(refund_script))
inputs = [
{
@@ -469,10 +511,19 @@ class PARTInterfaceBlind(PARTInterface):
):
lock_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
lock_txid_hex = lock_tx_obj["txid"]
self._log.info("Verifying lock tx: {}.".format(lock_txid_hex))
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)
@@ -487,7 +538,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_txo_scriptpk = bytes.fromhex(
lock_tx_obj["vout"][lock_output_n]["scriptPubKey"]["hex"]
)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
script_pk = self.getP2WSHScriptDest(script_out)
ensure(lock_txo_scriptpk == script_pk, "Bad output script")
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, "Bad script leader pubkey")
@@ -533,7 +584,9 @@ class PARTInterfaceBlind(PARTInterface):
):
lock_refund_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
lock_refund_txid_hex = lock_refund_tx_obj["txid"]
self._log.info("Verifying lock refund tx: {}.".format(lock_refund_txid_hex))
self._log.info(
"Verifying lock refund tx: {}.".format(self._log.id(lock_refund_txid_hex))
)
ensure(lock_refund_tx_obj["version"] == self.txVersion(), "Bad version")
ensure(lock_refund_tx_obj["locktime"] == 0, "Bad nLockTime")
@@ -562,7 +615,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_txo_scriptpk = bytes.fromhex(
lock_refund_tx_obj["vout"][lock_refund_output_n]["scriptPubKey"]["hex"]
)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
script_pk = self.getP2WSHScriptDest(script_out)
ensure(lock_refund_txo_scriptpk == script_pk, "Bad output script")
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, "Bad script pubkey")
@@ -622,7 +675,9 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_spend_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj["txid"]
self._log.info(
"Verifying lock refund spend tx: {}.".format(lock_refund_spend_txid_hex)
"Verifying lock refund spend tx: {}.".format(
self._log.id(lock_refund_spend_txid_hex)
)
)
ensure(lock_refund_spend_tx_obj["version"] == self.txVersion(), "Bad version")
@@ -668,6 +723,7 @@ class PARTInterfaceBlind(PARTInterface):
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_paid = self.make_int(lock_refund_spend_tx_obj["vout"][0]["ct_fee"])
fee_rate_paid = fee_paid * 1000 // vsize
ensure(
self.compareFeeRates(fee_rate_paid, feerate),
@@ -781,11 +837,14 @@ class PARTInterfaceBlind(PARTInterface):
)
actual_tx_fee_rate = pay_fee * 1000 // vsize
self._log.info(
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
lock_spend_tx_obj["txid"],
actual_tx_fee_rate,
vsize,
pay_fee,
"createSCLockSpendTx {}{}.".format(
self._log.id(lock_spend_tx_obj["txid"]),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {actual_tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
fee_info["vsize"] = vsize
@@ -800,7 +859,9 @@ class PARTInterfaceBlind(PARTInterface):
):
lock_spend_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
lock_spend_txid_hex = lock_spend_tx_obj["txid"]
self._log.info("Verifying lock spend tx: {}.".format(lock_spend_txid_hex))
self._log.info(
"Verifying lock spend tx: {}.".format(self._log.id(lock_spend_txid_hex))
)
ensure(lock_spend_tx_obj["version"] == self.txVersion(), "Bad version")
ensure(lock_spend_tx_obj["locktime"] == 0, "Bad nLockTime")
@@ -1014,10 +1075,11 @@ class PARTInterfaceBlind(PARTInterface):
self,
kbv,
Kbs,
cb_swap_value,
cb_block_confirmed,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1048,7 +1110,10 @@ class PARTInterfaceBlind(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "blind", "Output is not anon")
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")
@@ -1220,6 +1285,10 @@ class PARTInterfaceAnon(PARTInterface):
def balance_type():
return BalanceTypes.ANON
@staticmethod
def est_lock_tx_vsize() -> int:
return 1153
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
raise ValueError("Not possible")
@@ -1266,7 +1335,14 @@ class PARTInterfaceAnon(PARTInterface):
return bytes.fromhex(txid)
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
self,
kbv,
Kbs,
cb_swap_value,
cb_block_confirmed,
restore_height,
bid_sender,
check_amount: bool = True,
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1298,7 +1374,10 @@ class PARTInterfaceAnon(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "anon", "Output is not anon")
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")

View File

@@ -33,9 +33,36 @@ class PIVXInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def checkWallets(self) -> int:
return 1
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found":
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if chain_client_settings.get("manage_daemon", False) is False:
self._log.warning(
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
)
return
def signTxWithWallet(self, tx):
rv = self.rpc("signrawtransaction", [tx.hex()])

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -28,4 +29,28 @@ 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):
params = {"filename": filename}
if self._wallet_password is not None:
params["password"] = self._wallet_password
try:
self.rpc_wallet("open_wallet", params)
except Exception as e:
if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}")
return # bypass refresh error to allow startup with a busy daemon
try:
# TODO Remove `store` after upstream fix to autosave on close_wallet
self.rpc_wallet("store")
self.rpc_wallet("close_wallet")
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
self.rpc_wallet("open_wallet", params)
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")

View File

@@ -2,14 +2,15 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
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,
@@ -34,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():
@@ -71,15 +75,28 @@ class XMRInterface(CoinInterface):
def xmr_swap_a_lock_spend_tx_vsize() -> int:
raise ValueError("Not possible")
@staticmethod
def est_lock_tx_vsize() -> int:
# TODO: Estimate with ringsize
return 1604
@staticmethod
def xmr_swap_b_lock_spend_tx_vsize() -> int:
# TODO: Estimate with ringsize
return 1604
def is_transient_error(self, ex) -> bool:
# str_error: str = str(ex).lower()
# if "failed to get output distribution" in str_error:
# return True
str_error: str = str(ex).lower()
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)
def __init__(self, coin_settings, network, swap_client=None):
@@ -94,6 +111,7 @@ class XMRInterface(CoinInterface):
self._log = self._sc.log if self._sc and self._sc.log else logging
self._wallet_password = None
self._have_checked_seed = False
self._wallet_filename = coin_settings.get("wallet_name", "swap_wallet")
daemon_login = None
if coin_settings.get("rpcuser", "") != "":
@@ -139,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"],
@@ -170,14 +190,23 @@ class XMRInterface(CoinInterface):
ensure(new_priority >= 0 and new_priority < 4, "Invalid fee_priority value")
self._fee_priority = new_priority
def setWalletFilename(self, wallet_filename):
self._wallet_filename = wallet_filename
def createWallet(self, params):
if self._wallet_password is not None:
params["password"] = self._wallet_password
rv = self.rpc_wallet("generate_from_keys", params)
self._log.info("generate_from_keys %s", dumpj(rv))
if "address" in rv:
new_address: str = rv["address"]
is_watch_only: bool = "Watch-only" in rv.get("info", "")
self._log.info(
"Generated{} {} wallet: {}".format(
" watch-only" if is_watch_only else "",
self.coin_name(),
self._log.addr(new_address),
)
)
else:
self._log.debug("generate_from_keys %s", dumpj(rv))
raise ValueError("generate_from_keys failed")
def openWallet(self, filename):
params = {"filename": filename}
@@ -186,25 +215,57 @@ class XMRInterface(CoinInterface):
try:
self.rpc_wallet("open_wallet", params)
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
self.rpc_wallet("refresh")
except Exception as e:
if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}")
return # bypass refresh error to allow startup with a busy daemon
try:
# TODO Remove `store` after upstream fix to autosave on close_wallet
self.rpc_wallet("store")
self.rpc_wallet("close_wallet")
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
return # Bypass refresh error to allow startup with a busy daemon
if any(
x in str(e)
for x in (
"invalid signature",
"std::bad_alloc",
"basic_string::_M_replace_aux",
)
):
self._log.error(f"{self.coin_name()} wallet is corrupt.")
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if chain_client_settings.get("manage_wallet_daemon", False):
self._log.info(f"Renaming {self.coin_name()} wallet cache file.")
walletpath = os.path.join(
chain_client_settings.get("datadir", "none"),
"wallets",
filename,
)
if not os.path.isfile(walletpath):
self._log.warning(
f"Could not find {self.coin_name()} wallet cache file."
)
raise
bkp_path = walletpath + ".corrupt"
for i in range(100):
if not os.path.exists(bkp_path):
break
bkp_path = walletpath + f".corrupt{i}"
if os.path.exists(bkp_path):
self._log.error(
f"Could not find backup path for {self.coin_name()} wallet."
)
raise
os.rename(walletpath, bkp_path)
# Drop through to open_wallet
else:
raise
else:
try:
self.rpc_wallet("close_wallet")
self._log.debug(f"Closing {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
self.rpc_wallet("open_wallet", params)
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
self.rpc_wallet("refresh")
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
self._log.debug(f"Attempting to open {self.coin_name()} wallet")
def initialiseWallet(
self, key_view: bytes, key_spend: bytes, restore_height=None
@@ -239,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,
@@ -271,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:
@@ -290,6 +362,8 @@ class XMRInterface(CoinInterface):
raise e
rv = {}
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
balance_info = self.rpc_wallet("get_balance")
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
@@ -328,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()
@@ -339,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)
@@ -355,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
@@ -391,6 +456,8 @@ class XMRInterface(CoinInterface):
) -> bytes:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
@@ -403,14 +470,24 @@ class XMRInterface(CoinInterface):
params["priority"] = self._fee_priority
rv = self.rpc_wallet("transfer", params)
self._log.info(
"publishBLockTx %s to address_b58 %s", rv["tx_hash"], shared_addr
"publishBLockTx {} to address_b58 {}".format(
self._log.id(rv["tx_hash"]),
self._log.addr(shared_addr),
)
)
tx_hash = bytes.fromhex(rv["tx_hash"])
return tx_hash
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
@@ -430,6 +507,9 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
"""
# Debug
try:
@@ -459,7 +539,7 @@ class XMRInterface(CoinInterface):
)
rv = -1
continue
if transfer["amount"] == cb_swap_value:
if transfer["amount"] == cb_swap_value or check_amount is False:
return {
"txid": transfer["tx_hash"],
"amount": transfer["amount"],
@@ -478,14 +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}"
)
@@ -549,6 +629,8 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet("get_balance")
if rv["balance"] < cb_swap_value:
self._log.warning("Balance is too low, checking for existing spend.")
@@ -603,6 +685,8 @@ class XMRInterface(CoinInterface):
) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
if sweepall:
balance = self.rpc_wallet("get_balance")
@@ -682,6 +766,9 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet(
"get_transfers",
{"in": True, "out": True, "pending": True, "failed": True},
@@ -694,11 +781,15 @@ class XMRInterface(CoinInterface):
def getSpendableBalance(self) -> int:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
balance_info = self.rpc_wallet("get_balance")
return balance_info["unlocked_balance"]
def changeWalletPassword(self, old_password, new_password):
def changeWalletPassword(
self, old_password, new_password, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
orig_password = self._wallet_password
if old_password != "":
@@ -713,11 +804,11 @@ class XMRInterface(CoinInterface):
self._wallet_password = orig_password
raise e
def unlockWallet(self, password: str) -> None:
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
self._log.info("unlockWallet - {}".format(self.ticker()))
self._wallet_password = password
if not self._have_checked_seed:
if check_seed and not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None:

View File

@@ -14,13 +14,18 @@ from .util import (
toBool,
)
from .basicswap_util import (
fiatFromTicker,
strBidState,
strTxState,
SwapTypes,
NotificationTypes as NT,
)
from .chainparams import (
Coins,
chainparams,
Fiat,
getCoinIdFromTicker,
getCoinIdFromName,
)
from .ui.util import (
PAGE_LIMIT,
@@ -32,12 +37,12 @@ from .ui.util import (
get_data_entry,
get_data_entry_or,
have_data_entry,
tickerToCoinId,
listOldBidStates,
checkAddressesOwned,
)
from .ui.page_offers import postNewOffer
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
from .db import Concepts
def getFormData(post_string: str, is_json: bool):
@@ -118,12 +123,151 @@ 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()
if len(url_split) > 3:
ticker_str = url_split[3]
coin_type = tickerToCoinId(ticker_str)
coin_type = getCoinIdFromTicker(ticker_str)
if len(url_split) > 4:
cmd = url_split[4]
@@ -162,12 +306,13 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
)
raise ValueError("Unknown command")
if coin_type == Coins.LTC_MWEB:
coin_type = Coins.LTC
rv = swap_client.getWalletInfo(coin_type)
if not rv:
raise ValueError(f"getWalletInfo failed for coin: {coin_type}")
rv.update(swap_client.getBlockchainInfo(coin_type))
ci = swap_client.ci(coin_type)
checkAddressesOwned(swap_client, ci, rv)
@@ -178,7 +323,19 @@ def js_wallets(self, url_split, post_string, is_json):
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
offer_id = None
if len(url_split) > 3:
if url_split[3] == "new":
@@ -201,6 +358,12 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
if offer_id:
filters["offer_id"] = offer_id
parsed_url = urllib.parse.urlparse(self.path)
query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {}
if "with_extra_info" in query_params:
with_extra_info = toBool(query_params["with_extra_info"][0])
if post_string != "":
post_data = getFormData(post_string, is_json)
filters["coin_from"] = setCoinFilter(post_data, "coin_from")
@@ -253,7 +416,9 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_revoked": True if o.active_ind == 2 else False,
"is_public": o.addr_to == swap_client.network_addr
or o.addr_to.strip() == "",
"message_nets": o.message_nets,
}
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if with_extra_info:
offer_data["amount_negotiable"] = o.amount_negotiable
offer_data["rate_negotiable"] = o.rate_negotiable
@@ -268,6 +433,24 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
offer_data["feerate_from"] = o.from_feerate
offer_data["feerate_to"] = o.to_feerate
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
if o.was_sent:
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id)
if strategy:
offer_data["local_automation_strat_id"] = strategy[0]
swap_client.log.debug(
f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}"
)
else:
offer_data["local_automation_strat_id"] = 0
except Exception as e:
swap_client.log.debug(
f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}"
)
offer_data["local_automation_strat_id"] = 0
rv.append(offer_data)
return bytes(json.dumps(rv), "UTF-8")
@@ -320,18 +503,35 @@ def formatBids(swap_client, bids, filters) -> bytes:
with_extra_info = filters.get("with_extra_info", False)
rv = []
for b in bids:
ci_from = swap_client.ci(b[9])
offer = swap_client.getOffer(b[3])
ci_to = swap_client.ci(offer.coin_to) if offer else None
bid_rate: int = 0 if b[10] is None else b[10]
amount_to = None
if ci_to:
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(),
"offer_id": b[3].hex(),
"created_at": b[0],
"expire_at": b[1],
"coin_from": b[9],
"amount_from": swap_client.ci(b[9]).format_amount(b[4]),
"bid_rate": swap_client.ci(b[14]).format_amount(b[10]),
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name() if ci_to else "Unknown",
"amount_from": ci_from.format_amount(b[4]),
"amount_to": amount_to,
"bid_rate": swap_client.ci(b[14]).format_amount(bid_rate),
"bid_state": strBidState(b[5]),
"addr_from": b[11],
"addr_to": offer.addr_to if offer else None,
}
if with_extra_info:
bid_data["addr_from"] = b[11]
bid_data.update(
{"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])}
)
rv.append(bid_data)
return bytes(json.dumps(rv), "UTF-8")
@@ -493,7 +693,19 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
post_data = getFormData(post_string, is_json)
offer_id, filters = parseBidFilters(post_data)
@@ -589,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)
@@ -610,7 +820,19 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
def js_index(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
return bytes(json.dumps(swap_client.getSummary()), "UTF-8")
@@ -620,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,
@@ -630,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:
@@ -641,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()
@@ -654,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()
@@ -776,7 +1100,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
"label": strat_data.label,
"type_ind": strat_data.type_ind,
"only_known_identities": strat_data.only_known_identities,
"data": json.loads(strat_data.data.decode("utf-8")),
"data": json.loads(strat_data.data.decode("UTF-8")),
"note": "" if strat_data.note is None else strat_data.note,
}
return bytes(json.dumps(rv), "UTF-8")
@@ -804,7 +1128,7 @@ def js_validateamount(self, url_split, post_string: str, is_json: bool) -> bytes
f"Unknown rounding method, must be one of {valid_round_methods}"
)
coin_type = tickerToCoinId(ticker_str)
coin_type = getCoinIdFromTicker(ticker_str)
ci = swap_client.ci(coin_type)
r = 0
@@ -830,7 +1154,21 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
coin = getCoinType(get_data_entry(post_data, "coin"))
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:
coin = getCoinType(coin_in)
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.")
@@ -858,14 +1196,22 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
expect_seedid = swap_client.getStringKV(
"main_wallet_seedid_" + ci.coin_name().lower()
)
try:
wallet_seed_id = ci.getWalletSeedID()
except Exception as e:
wallet_seed_id = f"Error: {e}"
rv.update(
{
"seed": seed_key.hex(),
"seed_id": seed_id.hex(),
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
"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),
@@ -900,7 +1246,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = getFormData(post_string, is_json)
password = get_data_entry(post_data, "password")
password: str = get_data_entry(post_data, "password")
if have_data_entry(post_data, "coin"):
coin = getCoinType(str(get_data_entry(post_data, "coin")))
@@ -935,7 +1281,7 @@ def js_404(self, url_split, post_string, is_json) -> bytes:
def js_help(self, url_split, post_string, is_json) -> bytes:
# TODO: Add details and examples
commands = []
for k in pages:
for k in endpoints:
commands.append(k)
return bytes(json.dumps({"commands": commands}), "UTF-8")
@@ -943,26 +1289,289 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
def js_readurl(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 have_data_entry(post_data, "url"):
url = get_data_entry(post_data, "url")
default_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
response = swap_client.readURL(url, headers=default_headers)
try:
error = json.loads(response.decode())
if "Error" in error:
return json.dumps({"Error": error["Error"]}).encode()
except json.JSONDecodeError:
pass
return response
raise ValueError("Requires URL.")
if not have_data_entry(post_data, "url"):
raise ValueError("Requires URL.")
url = get_data_entry(post_data, "url")
default_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
response = swap_client.readURL(url, headers=default_headers)
try:
error = json.loads(response.decode())
if "Error" in error:
return json.dumps({"Error": error["Error"]}).encode()
except json.JSONDecodeError:
pass
return response
pages = {
def js_active(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
all_bids = []
try:
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
try:
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
if offer.bid_reversed:
amount_from: int = bid.amount_to
amount_to: int = bid.amount
bid_rate: int = ci_from.make_int(amount_to / amount_from, r=1)
else:
amount_from: int = bid.amount
amount_to: int = bid.amount_to
bid_rate: int = bid.rate
swap_data = {
"bid_id": bid_id.hex(),
"offer_id": offer.offer_id.hex(),
"created_at": bid.created_at,
"expire_at": bid.expire_at,
"bid_state": strBidState(bid.state),
"tx_state_a": None,
"tx_state_b": None,
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name(),
"amount_from": ci_from.format_amount(amount_from),
"amount_to": ci_to.format_amount(amount_to),
"rate": bid_rate,
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
"was_sent": bid.was_sent,
}
if offer.swap_type == SwapTypes.XMR_SWAP:
swap_data["tx_state_a"] = (
strTxState(bid.xmr_a_lock_tx.state)
if bid.xmr_a_lock_tx
else None
)
swap_data["tx_state_b"] = (
strTxState(bid.xmr_b_lock_tx.state)
if bid.xmr_b_lock_tx
else None
)
else:
swap_data["tx_state_a"] = bid.getITxState()
swap_data["tx_state_b"] = bid.getPTxState()
all_bids.append(swap_data)
except Exception:
pass
except Exception:
return bytes(json.dumps([]), "UTF-8")
return bytes(json.dumps(all_bids), "UTF-8")
def js_coinprices(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.")
currency_to = Fiat.USD
if have_data_entry(post_data, "currency_to"):
currency_to = fiatFromTicker(get_data_entry(post_data, "currency_to"))
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
coinprices = swap_client.lookupFiatRates(
coin_ids, currency_to=currency_to, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in coinprices.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"currency": currency_to.name, "source": rate_source, "rates": rv}),
"UTF-8",
)
def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
volume_data = swap_client.lookupVolume(
coin_ids, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in volume_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "data": rv}),
"UTF-8",
)
def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 3600))
days: int = int(get_data_entry_or(post_data, "days", 1))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
historical_data = swap_client.lookupHistoricalData(
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in historical_data.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"source": rate_source, "days": days, "data": rv}),
"UTF-8",
)
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
filters = {
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
}
if have_data_entry(post_data, "sort_by"):
sort_by = get_data_entry(post_data, "sort_by")
ensure(
sort_by
in [
"created_at",
],
"Invalid sort by",
)
filters["sort_by"] = sort_by
if have_data_entry(post_data, "sort_dir"):
sort_dir = get_data_entry(post_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(post_data, "offset"):
filters["offset"] = int(get_data_entry(post_data, "offset"))
if have_data_entry(post_data, "limit"):
filters["limit"] = int(get_data_entry(post_data, "limit"))
ensure(filters["limit"] > 0, "Invalid limit")
if have_data_entry(post_data, "address_from"):
filters["address_from"] = get_data_entry(post_data, "address_from")
if have_data_entry(post_data, "address_to"):
filters["address_to"] = get_data_entry(post_data, "address_to")
action = get_data_entry_or(post_data, "action", None)
message_routes = swap_client.listMessageRoutes(filters, action)
return bytes(json.dumps(message_routes), "UTF-8")
endpoints = {
"coins": js_coins,
"walletbalances": js_walletbalances,
"wallets": js_wallets,
"offers": js_offers,
"sentoffers": js_sentoffers,
@@ -975,6 +1584,8 @@ pages = {
"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,
@@ -986,10 +1597,15 @@ pages = {
"lock": js_lock,
"help": js_help,
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
"coinvolume": js_coinvolume,
"coinhistory": js_coinhistory,
"messageroutes": js_messageroutes,
}
def js_url_to_function(url_split):
if len(url_split) > 2:
return pages.get(url_split[2], js_404)
return endpoints.get(url_split[2], js_404)
return js_index

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,17 +6,17 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
Handshake procedure:
node0 connecting to node1
node0 send_handshake
node1 process_handshake
node1 send_ping - With a version field
node0 recv_ping
Both nodes are initialised
Handshake procedure:
node0 connecting to node1
node0 send_handshake
node1 process_handshake
node1 send_ping - With a version field
node0 recv_ping
Both nodes are initialised
XChaCha20_Poly1305 mac is 16bytes
XChaCha20_Poly1305 mac is 16bytes
"""
import time

View File

@@ -0,0 +1,516 @@
#!/usr/bin/env python
# -*- 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 base64
import json
import threading
import traceback
import websocket
from queue import Queue, Empty
from basicswap.util.smsg import (
smsgEncrypt,
smsgDecrypt,
smsgGetID,
)
from basicswap.chainparams import (
Coins,
)
from basicswap.util.address import (
decodeWif,
)
from basicswap.basicswap_util import AddressTypes
def encode_base64(data: bytes) -> str:
return base64.b64encode(data).decode("utf-8")
def decode_base64(encoded_data: str) -> bytes:
return base64.b64decode(encoded_data)
class WebSocketThread(threading.Thread):
def __init__(self, url: str, tag: str = None, logger=None):
super().__init__()
self.url: str = url
self.tag = tag
self.logger = logger
self.ws = None
self.mutex = threading.Lock()
self.corrId: int = 0
self.connected: bool = False
self.delay_event = threading.Event()
self.recv_queue = Queue()
self.cmd_recv_queue = Queue()
self.delayed_events_queue = Queue()
self.ignore_events: bool = False
self.num_messages_received: int = 0
def disable_debug_mode(self):
self.ignore_events = False
for i in range(100):
try:
message = self.delayed_events_queue.get(block=False)
except Empty:
break
self.recv_queue.put(message)
def on_message(self, ws, message):
if self.logger:
self.logger.debug("Simplex received msg")
else:
print(f"{self.tag} - Received msg")
if message.startswith('{"corrId"'):
self.cmd_recv_queue.put(message)
else:
self.num_messages_received += 1
self.recv_queue.put(message)
def queue_get(self):
try:
return self.recv_queue.get(block=False)
except Empty:
return None
def cmd_queue_get(self):
try:
return self.cmd_recv_queue.get(block=False)
except Empty:
return None
def on_error(self, ws, error):
if self.logger:
self.logger.error(f"Simplex ws - {error}")
else:
print(f"{self.tag} - Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
if self.logger:
self.logger.info(f"Simplex ws - Closed: {close_status_code}, {close_msg}")
else:
print(f"{self.tag} - Closed: {close_status_code}, {close_msg}")
def on_open(self, ws):
if self.logger:
self.logger.info("Simplex ws - Connection opened")
else:
print(f"{self.tag}: WebSocket connection opened")
self.connected = True
def send_command(self, cmd_str: str):
with self.mutex:
self.corrId += 1
if self.logger:
self.logger.debug(f"Simplex sent command {self.corrId}")
else:
print(f"{self.tag}: sent command {self.corrId}")
cmd = json.dumps({"corrId": str(self.corrId), "cmd": cmd_str})
self.ws.send(cmd)
return self.corrId
def wait_for_command_response(self, cmd_id, num_tries: int = 200):
cmd_id = str(cmd_id)
for i in range(num_tries):
message = self.cmd_queue_get()
if message is not None:
data = json.loads(message)
if "corrId" in data:
if data["corrId"] == cmd_id:
return data
self.delay_event.wait(0.5)
raise ValueError(
f"wait_for_command_response timed-out waiting for ID: {cmd_id}"
)
def run(self):
self.ws = websocket.WebSocketApp(
self.url,
on_message=self.on_message,
on_error=self.on_error,
on_open=self.on_open,
on_close=self.on_close,
)
while not self.delay_event.is_set():
self.ws.run_forever()
self.delay_event.wait(0.5)
def stop(self):
self.delay_event.set()
if self.ws:
self.ws.close()
def waitForResponse(ws_thread, sent_id, delay_event):
sent_id = str(sent_id)
for i in range(200):
message = ws_thread.cmd_queue_get()
if message is not None:
data = json.loads(message)
if "corrId" in data:
if data["corrId"] == sent_id:
return data
delay_event.wait(0.5)
raise ValueError(f"waitForResponse timed-out waiting for ID: {sent_id}")
def waitForConnected(ws_thread, delay_event):
for i in range(100):
if ws_thread.connected:
return True
delay_event.wait(0.5)
raise ValueError("waitForConnected timed-out.")
def encryptMsg(
self,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp=None,
deterministic=False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("encryptMsg")
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
smsg_msg: bytes = smsgEncrypt(
privkey_from,
pubkey_to,
payload,
timestamp,
deterministic,
msg_valid,
difficulty_target=difficulty_target,
)
return smsg_msg
def sendSimplexMsg(
self,
network,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp: int = None,
deterministic: bool = False,
to_user_name: str = None,
return_msg: bool = False,
difficulty_target=0x1EFFFFFF,
) -> bytes:
self.log.debug("sendSimplexMsg")
smsg_msg: bytes = encryptMsg(
self,
addr_from,
addr_to,
payload,
msg_valid,
cursor,
timestamp,
deterministic,
difficulty_target,
)
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
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
def decryptSimplexMsg(self, msg_data):
ci_part = self.ci(Coins.PART)
# Try with the network key first
network_key: bytes = decodeWif(self.network_key)
try:
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pubkey_from"])
)
decrypted["to"] = self.network_addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
# Try with all active bid/offer addresses
query: str = """SELECT DISTINCT address FROM (
SELECT b.bid_addr AS address FROM bids b
JOIN bidstates s ON b.state = s.state_id
WHERE b.active_ind = 1
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
UNION
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
UNION
SELECT addr AS address FROM smsgaddresses WHERE active_ind = 1 AND use_type = :local_portal
)"""
now: int = self.getTime()
try:
cursor = self.openDB()
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)
return decrypted
def parseSimplexMsg(self, chat_item):
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
dir_type = item_status["type"]
if dir_type not in ("sndRcvd", "rcvNew"):
return None
snd_progress = item_status.get("sndProgress", None)
if snd_progress and snd_progress != "complete":
item_id = chat_item["chatItem"]["meta"]["itemId"]
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
return None
conn_id = None
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
chat_type: str = chat_item["chatInfo"]["type"]
if chat_type == "group":
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
self.num_group_simplex_messages_received += 1
elif chat_type == "direct":
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
self.num_direct_simplex_messages_received += 1
else:
return None
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
try:
msg_data: bytes = decode_base64(msg_content)
decrypted_msg = decryptSimplexMsg(self, msg_data)
if decrypted_msg is None:
return None
decrypted_msg["chat_type"] = chat_type
decrypted_msg["chat_name"] = chat_name
decrypted_msg["conn_id"] = conn_id
decrypted_msg["msg_dir"] = msg_dir
return decrypted_msg
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
pass
return None
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
if ws_thread.ignore_events:
if msg_type not in ("contactConnected", "contactDeletedByContact"):
return False
ws_thread.delayed_events_queue.put(json.dumps(data))
return True
if msg_type == "contactConnected":
self.processContactConnected(data)
elif msg_type == "contactDeletedByContact":
self.processContactDisconnected(data)
else:
return False
return True
def readSimplexMsgs(self, network):
ws_thread = network["ws_thread"]
for i in range(100):
message = ws_thread.queue_get()
if message is None:
break
if self.delay_event.is_set():
break
data = json.loads(message)
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
try:
msg_type: str = getResponseData(data, "type")
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
for chat_item in getResponseData(data, "chatItems"):
decrypted_msg = parseSimplexMsg(self, chat_item)
if decrypted_msg is None:
continue
self.processMsg(decrypted_msg)
elif msg_type == "chatError":
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
pass
elif processEvent(self, ws_thread, msg_type, data):
pass
else:
self.log.debug(f"simplex: Unknown msg_type: {msg_type}")
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
except Exception as e:
self.log.debug(f"readSimplexMsgs error: {e}")
if self.debug:
self.log.error(traceback.format_exc())
self.delay_event.wait(0.05)
def getResponseData(data, tag=None):
for pretag in ("Right", "Left"):
if pretag in data["resp"]:
if tag:
return data["resp"][pretag][tag]
return data["resp"][pretag]
if tag:
return data["resp"][tag]
return data["resp"]
def getNewSimplexLink(data):
response_data = getResponseData(data)
if "connLinkContact" in response_data:
return response_data["connLinkContact"]["connFullLink"]
return response_data["connReqContact"]
def getJoinedSimplexLink(data):
response_data = getResponseData(data)
if "connLinkInvitation" in response_data:
return response_data["connLinkInvitation"]["connFullLink"]
return response_data["connReqInvitation"]
def initialiseSimplexNetwork(self, network_config) -> None:
self.log.debug("initialiseSimplexNetwork")
client_host: str = network_config.get("client_host", "127.0.0.1")
ws_port: str = network_config.get("ws_port")
ws_thread = WebSocketThread(f"ws://{client_host}:{ws_port}", logger=self.log)
self.threads.append(ws_thread)
ws_thread.start()
waitForConnected(ws_thread, self.delay_event)
sent_id = ws_thread.send_command("/groups")
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if len(getResponseData(response, "groups")) < 1:
sent_id = ws_thread.send_command("/c " + network_config["group_link"])
response = waitForResponse(ws_thread, sent_id, self.delay_event)
assert "groupLinkId" in getResponseData(response, "connection")
add_network = {
"type": "simplex",
"ws_thread": ws_thread,
}
if "bridged" in network_config:
add_network["bridged"] = network_config["bridged"]
self.active_networks.append(add_network)
def closeSimplexChat(self, net_i, connId) -> bool:
try:
cmd_id = net_i.send_command("/chats")
response = net_i.wait_for_command_response(cmd_id, num_tries=500)
remote_name = None
for chat in getResponseData(response, "chats"):
if (
"chatInfo" not in chat
or "type" not in chat["chatInfo"]
or chat["chatInfo"]["type"] != "direct"
):
continue
try:
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
break
except Exception as e:
self.log.debug(f"Error parsing chat: {e}")
if remote_name is None:
self.log.warning(
f"Unable to find remote name for simplex direct chat, ID: {connId}"
)
return False
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
cmd_id = net_i.send_command(f"/delete @{remote_name}")
cmd_response = net_i.wait_for_command_response(cmd_id)
if getResponseData(cmd_response, "type") != "contactDeleted":
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
self.log.debug(
"cmd_response: {}".format(json.dumps(cmd_response, indent=4))
)
return False
except Exception as e:
self.log.warning(f"Error deleting simplex chat, ID: {connId} - {e}")
return False
return True

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python
# -*- 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 os
import select
import sqlite3
import subprocess
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
if os.name == "nt":
str_args = " ".join(args)
p = subprocess.Popen(
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
)
else:
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
def readOutput():
buf = os.read(pipe_r, 1024).decode("utf-8")
response = None
# logger.debug(f"simplex-chat output: {buf}")
if "display name:" in buf:
logger.debug("Setting display name")
response = b"user\n"
else:
logger.debug(f"Unexpected output: {buf}")
return
if response is not None:
p.stdin.write(response)
p.stdin.flush()
try:
start_time: int = time.time()
max_wait_seconds: int = 60
while p.poll() is None:
if time.time() > start_time + max_wait_seconds:
raise RuntimeError("Timed out")
if os.name == "nt":
readOutput()
delay_event.wait(0.1)
continue
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
readOutput()
delay_event.wait(0.1)
except Exception as e:
logger.error(f"initSimplexClient: {e}")
finally:
if p.poll() is None:
p.terminate()
os.close(pipe_r)
os.close(pipe_w)
p.stdin.close()
def startSimplexClient(
bin_path: str,
data_path: str,
server_address: str,
websocket_port: int,
logger,
delay_event,
socks_proxy=None,
log_level: str = "debug",
) -> Daemon:
logger.info("Starting Simplex client")
if not os.path.exists(data_path):
os.makedirs(data_path)
simplex_data_prefix = os.path.join(data_path, "simplex_client_data")
simplex_db_path = simplex_data_prefix + "_chat.db"
args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)]
if socks_proxy:
args += ["--socks-proxy", socks_proxy]
if not os.path.exists(simplex_db_path):
# Database doesn't exist - safe to add server during initialization
logger.info("Database not found, initializing Simplex client")
init_args = args + ["-e", "/help"] # Run command to exit client
init_args += ["-s", server_address]
initSimplexClient(init_args, logger, delay_event)
else:
# Database exists - only add server if it's not already there
if not serverExistsInDatabase(simplex_db_path, server_address, logger):
logger.debug(f"Adding server to Simplex CLI args: {server_address}")
args += ["-s", server_address]
else:
logger.debug("Server already exists, not adding to CLI args")
args += ["-l", log_level]
opened_files = []
stdout_dest = open(
os.path.join(data_path, "simplex_stdout.log"),
"w",
)
opened_files.append(stdout_dest)
stderr_dest = stdout_dest
return Daemon(
subprocess.Popen(
args,
shell=False,
stdin=subprocess.PIPE,
stdout=stdout_dest,
stderr=stderr_dest,
cwd=data_path,
),
opened_files,
"simplex-chat",
)

20
basicswap/network/util.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- 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.
from basicswap.util.address import b58decode
def getMsgPubkey(self, msg) -> bytes:
if "pubkey_from" in msg:
return bytes.fromhex(msg["pubkey_from"])
rv = self.callrpc(
"smsggetpubkey",
[
msg["from"],
],
)
return b58decode(rv["publickey"])

View File

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

View File

@@ -0,0 +1,25 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQMuBFmZ6L4RCACuqDDCIe2bzKznyKVN1aInzRQnSxdGTXuw0mcDz5HYudAhBjR8
gY6sxCRPNxvZCJVDZDpCygXMhWZlJtWLR8KMTCXxC4HLXXOY4RxQ5KGnYWxEAcKY
deq1ymmuOuMUp7ltRTSyWcBKbR9xTd2vW/+0W7GQIOxUW/aiT1V0x3cky+6kqaec
BorP3+uxJcx0Q8WdlS/6N4x3pBv/lfsdrZSaDD8fU/29pQGMDUEnupKoWJVVei6r
G+vxLHEtIFYYO8VWjZntymw3dl+aogrjyuxqWzl8mfPi9M/DgiRb4pJnH2yOGDI6
Lvg+oo9E79Vwi98UjYSicsB1dtcptKiA96UXAQD/hDB+dil7/SX/SDTlaw/+uTdd
Xg0No63dbN++iY4k3Qf/Xk1ZzbuDviLhe+zEhlJOw6TaMlxfwwQOtxEJXILS5uIL
jYlGcDbBtJh3p4qUoUduDOgjumJ9m47XqIq81rQ0pqzzGMbK1Y82NQjX5Sn8yTm9
p1hmOZ/uX9vCrUSbYBjxJXyQ1OXlerlLRLfBf5WQ0+LO+0cmgtCyX0zV4oGK7vph
XEm7lar7AezOOXaSrWAB+CTPUdJF1E7lcJiUuMVcqMx8pphrH+rfcsqPtN6tkyUD
TmPDpc5ViqFFelEEQnKSlmAY+3iCNZ3y/VdPPhuJ2lAsL3tm9MMh2JGV378LG45a
6SOkQrC977Qq1dhgJA+PGJxQvL2RJWsYlJwp79+Npgf9EfFaJVNzbdjGVq1XmNie
MZYqHRfABkyK0ooDxSyzJrq4vvuhWKInS4JhpKSabgNSsNiiaoDR+YYMHb0H8GRR
Za6JCmfU8w97R41UTI32N7dhul4xCDs5OV6maOIoNts20oigNGb7TKH9b5N7sDJB
zh3Of/fHCChO9Y2chbzU0bERfcn+evrWBf/9XdQGQ3ggoLbOtGpcUQuB/7ofTcBZ
awL6K4VJ2Qlb8DPlRgju6uU9AR/KTYeAlVFC8FX7R0FGgPRcJ3GNkNHGqrbuQ72q
AOhYOPx9nRrU5u+E2J325vOabLnLbOazze3j6LFPSFV4vfmTO9exYlwhz3g+lFAd
CrQ2Q2FsaW4gQ3VsaWFudSAoTmlsYWNUaGVHcmltKSA8Y2FsaW4uY3VsaWFudUBn
bWFpbC5jb20+iHoEExEIACIFAlmZ6L4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B
AheAAAoJECGBClQgMcAsU5cBAO/ngONpHsxny2uTV4ge2f+5V2ajTjcIfN2jUZtg
31jJAQCl1NcrwcIu98+aM2IyjB1UFXkoaMANpr8L9jBopivRGQ==
=cf8I
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGdeBqoBEADuBizUBhm1m34OQ0rnqUONvkfL3tGsriWuX0n3Bq9rhf3I3kZk
5fi+R0Jj6jmz+sbUYRULU35S6eeIY77eYiveWl81H+3JAu8kmo/S6gegINnsPM/g
F7X2P757gQdHMAE0olME3iGfXNpLg/e0OBv3j45eimjvUejgE7eI0e4gjajq8hyf
bizMrGT+5I2PE0g3h07NqN3OuI5xVYujuZp41EgxY99QgYm5qEoU0wMGy8+F7gXV
0htjhvUZcSGGpixP5+kaJJXFAP1TkZ/jqya6vy7LLeEEEuU8eMWhViOmzIjqoOFW
Mq+2rJUrzNEk43tXW5LU+DdGl90HQcXPmQP3aWL27Dx/4AcTMYPDB/0bJrU9qF9Y
9zfJV2HcNMnkhEb9XKDwkA6m3Jx2gfYG6HoMKp6bWSWsODItEgL1taoy35OnaVSM
NWb857DC6p6n+eQUXUNx/1ct4LWmf4lN4Uf61i4mD+hkc4cWmRLAh7vTqMGG4xmb
8Tb3wss8mEXzJvWVP4+bE6EkNPMCVAQleD4ePItaDg3lSJH/cIueIz6NDl5ik07r
AZOZTxhhGU1CD8NkxQKoZLZ6GgjHDEwiUbxaCoD0FAzqtG5/at+jiwyDmCsJ96aE
f0tPLXKOOc62BbqsAUuEOIooGwX/swXrhS4Xvfh8GxBYFBlRponoWXG7XQARAQAB
tEhSb3NlIFR1cmluZyAoUm9zZSBUdXJpbmcncyBzaWduaW5nIGtleSBmb3IgZGV2
IHdvcmsuKSA8cm9zZXR1cmluZ0BwbS5tZT6JAk4EEwEKADgWIQT9g2aoB6mfon/Z
zOqf47/dpsU0lQUCZ14GqgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCf
47/dpsU0lTjfD/9WkMBWlbYhJwRU6JrdZdIPsj2jlMIDYEHXxFo+h1lNn1SLKKrE
4c/+9+H0YGM03pL5ZTtydsxdPMTbAP5l24hBFpokySds3abOcKaPuNcct5BDWiiL
UxsnV3SxCAsN3QcBt+0tYFYP9yIMkko9BRwsY7pSpjZOSCx26jeTKj7M4XQGdcpT
4KMtzXe2s8ss1jLyuaDP2B5ikrFI+IZ5dHVBhohK3ug1y0SzHjfSYeskOEYSgJ/4
uRUJCItWxrkSh16qRz+NFxwsewqIKz8Q0EmpHx4WpAii8z29IFPYKJEqdwcuPyF3
7SiqAow4tY+CtnLAUYEbSiL52e8W/U8KSnrxqhkpMd5wZ28z+k682A5uEQn5YjOy
7dBRjytSC2S87FJ+3zp4OtToDio8Wi0zpZWj/BD5K9raE2ct6Uiw3NG6JI8A7yaJ
pEENfMpxMgKc8G5t8NfiZdDFDw+P+bd6sMAk3q7ZFe/o0zJcsbhtYacBFvwBpeIp
HZnLdUQlKrZoASku7biTZyt7BBJZuNdVv6Q/K+pigJxTYCZNbbx9s/lzS6KGUKuD
yi7n/1qYFXVFktomR+Cm045btVNeAQpnfIKiJS77FNeB5saSWEAOcCMtUkoR74lA
9MGYdeWrPjvdeBu+Muvo/y1h57sVMwvStrXjGrJNs6KBcmvITXrek0osbrkCDQRn
XgaqARAAu8bgP9AbeNatYshdG1xoYv20FeC0MUz0oYu+FvVuhvaAePl/VFFBlh3O
CsCzJ+a+/hyeW22ZGZl62yblvlZcSTw1/WOv5zboFVVLD58/iiz3dCYAUUTQ2OaI
+oMLTCmZ/+GIcuVM1ZZMEohvR9eLcyzY89CgOi8R9+agqTXxNg7Uj43tPkgY2vc0
v66od1SrOAisduXVDAiqTbc6nax9d9aYt27zQlGfuVo5J//rnteHiGA7VphDLlCR
+dra1ZGjbdOieSyhxiEAkBPY2js6UqO/CoRn9uHaTSv4MJqzzMOzLfPni+6y3FqH
qaUoe3vr07Ehf85gBEL4IBiux/WL3Vi1WceqvNkS9aC0MVnnEgHbyAy2R6pWrtN5
vlxdrkqQcnnnYHvOupG5KPsgT/CFK0jGfA23I/dBPuI372EcqFLFpAB4q14cSLQE
ZER81pK7Q445vTv9qQIPu34oq0mg7GWlunduI4v7uGN+oSYIW0kfNLRnM4QjNhTP
07LJZLZoCRW2MyPqTbk8cM0UQDGFOozcjlSgSZSABLdHpnudArl6fzkMi4VH8WNS
JNXvtL2yX8cnOWXuOgK5pFuhr6zeRaHsjlMXgR5ZPSCiq0aMR4upk5n/Mn64qGVm
EnxDEBiGfgL1sl+GGl+rYxvH8vYEEX3fjTtlsaImUzKByfLaY60AEQEAAYkCNgQY
AQoAIBYhBP2DZqgHqZ+if9nM6p/jv92mxTSVBQJnXgaqAhsMAAoJEJ/jv92mxTSV
+0wP/itANwrdF+9kolUUVJg8Vkx7IgIGlcdIiUTxPAu9c8JdTKpziy9q7oVVpzLf
zo+4qgzXGUGuGtcHdM8XSFYQ8CAuuOdvPUvtKbNQiZ1DVjoS/wk4vrzIvLTS1VVd
f4jTgOImx3Tk75/8KX3EpCk26orMMBCHk7nWWia1KF8X2K2Hu1DZ9GqsWlE/uAPN
tS/+ONlbn6tlk1XWDvFC8DkDkRWNRPva++GP5ACylybOHy2rqWKNEtetYflDuMIc
5tkrXZ/rdZgzASKzSrNlEjN2DEBjl15WjUppOPkSc4QPK+SVza6UZJaE7oOrIOqs
tQRchspkyDFreCuK/WZLZC8SUwZ5rzbOsFMLUHeZtFtNkJGxwF1ZUNHbNPPCEaCN
oqNu/nkjxFqeydJfqDM8K8An9dQE2GkUm1nACpuLNgpILXebdG7ItVbbkjosx7HI
0i3BXHeQzT+xY1gmuFFGEVCf9bZVmYspXJaiRGFRfGVyc6mMtdow7urb/A9g5Jqb
Dkc+p29y9hCeOAVZfTY2C/GlWu9X/E64WJ2mQ3ujhtJmSgLM4ieYJU+lxosOC6BW
EjFrTOeLa+myW7qm+/R6Mo/545s1qXvXnDL5Z4aVkSHtUu+fiWBa4f4WaH3mxAAg
XLVwKhulQ3wPaCehbbMPbsQ+091iAOo+hn9s2BPfehM0ltgI
=atlH
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,161 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: https://keybase.io/nicolasdorier
Version: Keybase Go 2.6.0 (linux)
xsFNBFuPQQEBEADWe0DHzPvxOuiRAlUyvoQm/+P6jiCqZ4XjFfPIthPh4lnj9ZC6
oK4XfFgU5Z1YLcXWg/3Ven5GZzcz/V82Q8MoDAuf2cNjmG+hHuoLMCwECGE8GcoN
gqBhNGcUp8UykEUjMx6B+B1kBH/Z563Id82y4MssIWwVZA2roGvrLZKSTA0m7rhu
JHLmO8rOsBZymEtRvGFhnVBTrSw13RIgUpr0D+nYU8s/ahnLwf5EAA0l9AgQcMQ+
VQFMV3zPMnhVHIXpcw1dmfiLMiOHhonQ9uu4x/kLroq2zGRHqetV0Ix9pbx4cxKw
idXt0KbFi2lNX+Xh2s47mC3oJSJyOTLxoIyj073nMPwFE+fZrByop+qYYmLvq9BM
q75ocJIr+O41/IdL0/R4l3rwD+dfwYDHITfwcYMfrI0GZYC8igoeBtQiHx+9bHyV
spmAH6W4pJeo8jkEdWvu8xbBHP37+ELVrabz4DpYnGga1fBGoHGVwTOlIzmtOCJ7
hIS5tpjC0njfiJJRq15bwFeUoWhzr4fngA2pqE5LX1bvH9HwoYJ7nbNZcsXhYFoW
0lXxYJA/6wPoxC5FWFBZ2goq/qPiVLfnp7XPgDJu3UkYn9Mqi1MTJk4nDviUb5iZ
1wFoEFw9QZIpBpIaQKeRCVOa88FGQxP3Ud8CRMsGy1TyOiN/ZkiWxvB1/wARAQAB
zSlOaWNvbGFzIERvcmllciA8bmljb2xhcy5kb3JpZXJAZ21haWwuY29tPsLBeAQT
AQgALAUCW49BAQkQZhh2PvCRhv4CGwMFCR4TOAACGQEECwcJAwUVCAoCAwQWAAEC
AAAmRBAANTErDJqg7Qh2gIEJFS+LVOBF427Bmj+DNTEb/XeMDB1QAbVw/ItM5LEa
WW499HFgG+jBMohIVNcmtKIOGdrQSBc2B8Ox4KUnDLO2TXrzMW+EveMIDjBGjxSZ
n2QAVaeemY19cENZfqmYkBTF2kcJzpzlTLsN9FpjOWYjdebjA/plM8W29rUqLE7R
RRqkayXhkkkou6m3diblDiboWj26V+79Rd4iXYE/S/nzbJfNIUjUTj1geVWVgW+7
Gh26H1c5IkeNrsTx/oSA6PN1Zk8/B8q6ftpt6tN1ksrvW6ErxivaxKQJsxM1RO0f
9tfZlUPCuf6Qsjg/IFayZhzi3U+5KBTpJeupBUPqTDtF8byD/iSi0/s0s3ogEFu7
ibMkmGnPu3W3n74qZpl7dNJysu1J7X1bzbeUb4CTgYl/hmsEu+nj7E82knckNXiI
cqSUlHTGsEywGiEkuGTP2N7qikWdggvDsBVE18OfQnBnzOxEXAVe0rCbRSqtgrqc
CSAG/pXdTfNTAo3ScTJ34DYTrZ3EohUwYuSc77e4nkec6+CdUg/IIGX7rB+Iz6RY
Py/24lRp9AJOG6Pzb3K8evE1o3kZjrU/vYyWEo1kiyJJmQa1toBnvJBVIUrcjk7A
603GGU0yFNXfGG31WxudDNMXaIbFG+s6SUC5H+eA+A9HHMM9/vHOwU0EW49BAQEQ
ALDfCek420s6nTWd0lqhJxpaYbGzw44KekwIyOqiA9BZ9W6/DJ4VJoHHK0tBplhQ
J9yrpfuIPTx+TG/2qShNShWv3zLjtGc1JIjYlJGzofmglo/zXP4HdXIfq5bhC2pP
9F0gVmnVNdSN4nA1/FuMJ3raST23F0Q5hieM2znPRoCxNdy6eGo5+Pn8Hssyvr/1
rRjRmTUIEyB4v5uVlPbqfvEMBtVOy8AS8+sWiW9PCojWV/NQpJ8DEP4NPfZG4sNu
rhUN6wTYTc1YpqHp2ZjSCFgscgXOBXpbhj8wRvfuOR7PQjBMW5Trz1yFvaOXIRHN
Srtoldmt8QyHXwIPVn1Z6byULWGsWw2hSKV4kgCep0djb4cncY04f1hCFHKtycv/
32pKdzya3nd8455wS755L2cQBMRs5tS71EpjkZwiwAHdQ8csXLZ3F+JwveavNp+K
cn4eYhfFx0TejQuryvrPx4le51iH6ozVOM37gIUftNGx537yWYBTBTsspz3fau13
s7NicSKc00GNfdGw2CP5NfcLOosUntk5CK/ZMQcnY2YT2FPdmIdX2iF100Ai+be6
xbbYB3tWbRbnvI5JUIuOPuNeZcFQUEd4mr+XRpGLhzkGi5XqTPaAXiwjfZie7tYO
/ZCuAWmpNo2VWOlBJO/QvN/sHyHwIBAkJ123fQtUystPABEBAAHCwXUEGAEIACkF
AluPQQEJEGYYdj7wkYb+AhsMBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAiKIQANI2
RDk4L33EjOS0abxB8h5tR9ca1P2BIKCnXb/IfiqlDcoKR0RVAy1dOHlmyH/5K7lh
5cp9LsqY3/XuPZoN9MRcWmav6HWWvWKdtpg0RbRqDyiqh0uiwwB8QZ7Hf4uWmLPj
V+tficTqyFhNn7RdU5DrcVhvuueh1fJrTqaizB88QMvYW+xGuuIBYIFrkibH3UFS
/L8Qj7CBgfWNAsC47t8DtBKKX/i07bJnlFyv+0dOpxNAFIROlXw33sbTM8SkZ7jR
jIeKhS+fEowjA8R3rSJLBEadIwUaD+uIACaFVh+o/ogssXWZX3GZ2IgwPhiAFcJT
qDzDu5nsIu8/QwN+TH0zPLoVjfg56HqPAsJHYLOSqO5xCE8lhyQuMh3PPF47kUoS
6QGNkASgSAGEq5RMBpUWqS8TYkYU/mk+b94nJnhhvXQPAEUHIqY7R7EPduHldyBh
e9eF6GZLUj9iA7uUY8m5CrLNl+axKxRhyMqUNOAos58z5bg6pqvrJIy7J26pWjnF
qNj7ylvjGakY3WR+EjPmgU2KGdcKloZLMOOSLq+4kwWPr0+q3dBI0qqXssVPZAtJ
b+lEWZtwBM0n3d8RcNEGywqeZIiAfgvyUQ6rNosDhE51q9nWoJW1i3r9X0ATe+aV
avYCWTKM5AQ7bEIvuVW/4M8PLFClJ2GmI7+YY7gl
=sNb2
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: https://keybase.io/nicolasdorier
Version: Keybase Go 5.0.0 (windows)
xsFNBF3clT4BEAC65tyMgP9NWzaUyNlbvbT8LlFRd/QsbxTElVILwdlypB/HInSt
18P0d5Px381cTN6QQnfRaE5cvbghqL94qVg4Ycc/tW71XxS4GT/xujzbNfol0unC
DAo1NqYWESrIAlosvgZBU2L4M88ASE2psHVdo2Dc6NRmdcit7G/RD9Js4MgGi9Kf
8bu4Xwk+vwGDvHDjPbDjlyx+djkGenQeuBVsIwJqXyFrr4WYkpFfBcGtMiBM986Z
lCMZ/Y8+WeGMHoq16uOuauIiE10RCAjSMkpLbqNcAFY5/qIImaHlQFpUxRewX/04
RQ00QrKYmToMB4VT+b0JSMVpHZAKaITFfSB3QbOSJrblZXyC1cTSGaDnTzhuvVeF
0S1eD1v4ZPDW5egxEKe/ckCxq4O/j39oj3oiYWcVmS+kceiIyETuXlgWyB2meG69
AAFfPisv0jUN/xrQJ7+TNBD86Cs53GvlghqHHWOZyLEDrNlkFOd/f7uN08cYJcCH
HLWwysLxBFhFUE9PXBT+83EkgsU1nCysB7kvodXkAS7rjCtrXuBuE3z3HOyfrQVZ
geOAlyAlLdbL/IQeQWe2k4Mz1ej90k4kqjfzZxSS8zBN3kvBW56/4W1LSA5pPhjl
5BSRUxk/nSrNMfc2u8ZmcD//mNZJ2d9yVJfOAjXJPEDQXAebWRZaWJw/hwARAQAB
zSlOaWNvbGFzIERvcmllciA8bmljb2xhcy5kb3JpZXJAZ21haWwuY29tPsLBeAQT
AQgALAUCXdyVPgkQIj/aad6+qC0CGwMFCR4TOAACGQEECwcJAwUVCAoCAwQWAAEC
AABCERAAFi2eSIRh9kpkERD1NYCMf6NfuPC1y6vf0xNYnIodPkAyv4xthEl4esdJ
xeltVIQ5BcPNUrHitcwO6TmtQa/a/4E8RgFzKDbGo/Wgr7shVAs0YUnQ6Tk07fL6
OVuwRCc1uTpUAgcv8ESNUyUgMeThcTmPChDRhhWn2Imy7pi8NPzM0X+/QCA0yj3p
Fa6Y+03WrqWbv9+OdqRysCwNPtOSAfbT4XXifn4efkOtBk4vx2oGr/NxxUOw5CgR
DAp8hEL76b5yZzvex75JFjCUwKqeYf2GjZrv94XgWXWZderlW2MHM+R/ON2K60/Y
SkafrGg4GdorwJIaLR8OVGV2nuBeUJXg75taOEzTtm8siEmiF1cvlfyEO15lTUuZ
7rIb9CILwCJ79nlON21MFax3bMqWP55GuC8Z79dSl3uSHaJg28NiB1iFVO0xAOlT
wQ++qeWQXpWUviNbHJ57+jgK80PLn6alXvfGSDovNZfO2UvRD5lpDmN6VyqrDB5z
ibPZmfR5SR+G9XqR03i5mG6/ynjWmXDzL4t3trrBPwLeyppvRXA9QY444Tm9OdH/
yj06mNGcQMLqsbd+9KS/veKDl9yJDxhqJe/nauq4vV0a+oMjFGKM+7waLc2n851N
yqdToaKfwt9FocDy4Xh54WPx+xaCfi9tDJMmKPjJP87oys2EdlXOwU0EXdyVPgEQ
AOyufiiUouX9yBrfeLOt3vLMVY3swP1KEosa/EZn+7zNJ+VZzfQFcmrNJ6lfzoIk
WNTYhqhCwPWLyw89wYhXNHEedICzRuOsET2CMP9bYXe0GcMi5vXCOs3QZDD5bNau
VnqnjM/sT25GHJb5IPdE/jOtAO3/WnwtlclfqNBgI1n0UUak4QZM03B7fFmVldXg
G1FydusZ0cH5vn2O8yQkvY7IcgNhgsQRPahrrpfDnfRd/CuX1yP4xbgULrgMjs3P
98HW+vwsx3IS8uFfxMUOftjXBUvCWoz+rc6fNqCS9lUIKdmpN0J+wtvbgcwXlde/
C2j3gzHBg8uGnRyVgygTUZceLeIxYjfwgCoRuGK70EfV4TAKkT9ODivA00D4mQm1
Bkh39hl4dCZ3xMVlVthT4BK1nEEM5DtwRAkVjR7wrv+fHR90yoHH/zDA/wFCGaD+
ML4v3578bctkJcmIJq32pbiP2jS36xnjxSRsDhQcbJjfeSm9qtMAOwF36GyGRVF6
fgxkRh04gzpE7d+fugRM9aTaaSBvr4oU5OmR9Aw066SC0nGGSnGehuvH5Ov/QtpC
Wl95tCviMaW28MSudwdYAfwgzKpCbe6sRi9tH0D6z2ZSLsykwby29wVfdPKVqUZt
LLSHhlRdw/eJDt7vCoxHR/TOJxOQZWCzJma+idz3NBkXABEBAAHCwXUEGAEIACkF
Al3clT4JECI/2mnevqgtAhsMBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAersQAKm/
I45krs/U4OWfru8FA5auuGgdiFThzk2Z+iE3XZ/TcJDSZfcECil8eFvjycL7JSRy
VUDY8GOmxL9oZyW9YY7EuvpsSBq6b7x6r8Cz40hBuP59DD+V1qtIokvc+kh2XJlS
GYKjggKaKTwrUazFtLur+XipPEL6yLYabaJaOiM5sMPmGc8raovIrh5IsVsEgEA2
bLbtaBiQqSR8Czh8pznijT/qw2ZLKqHkD+YQWf0xxwt/jMj/eG0yWzBam7YoqzM9
9GX411vmJNImNnLLrwA+LhN5A+m9oyf2KINHhq9xmyP2cRmXUcLDejMIIaISFWxT
aBrcmDSdztzsDzGaAz389bPUheSnOE6iK3zxbaUx67Tcmt1UjIWEZW1jyO4zmeXI
JG+0rdxZJU+wxa0jZcjF4C4IjgV6mXm+hN8F9jKBXu42ayqBHH2FAQLJQkD7mGSy
YJKo6eiJUfwI6DfDTlYF3QCWGi9bpdKZsaWj6+sgzhsHrENEEd1UnXm3W31wzYew
YtnmykETkCW0tnYf6tW5zJqpH6Y1zTS2+oSE2CRLjIPhWqRw6gfIk7g54mgNXf4D
ppHvGVduPErEE5WWH8iUVWYtk/yA7LhyRfvRjAezjtK7uzqQNqZirQjf6coqrV+Q
/+7CvHSsc6GjkqB7bFx5phZPRpt7OLzVKszDroyv
=ut7t
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: https://keybase.io/nicolasdorier
Version: Keybase Go 5.0.0 (windows)
xsFNBF3ec/EBEAC5sbWmzhP1hoLQ2/gm8Tds+v/p6DmY+vVNIgiBz1/XG+glRkna
qqwmVe71CE+nYtrxlzzc70PfxvrfWzfoavYGMgIkIQhEcst3ST6Qqo7IglAcXL0z
Vwqq5QcmCfyz2kr9wxUUrwofznKQch/7dZATkTl18ci5bzKTgENzHFKJx6EHN6aF
0meUW6tmIVSxva/tmkQK+dZtjfYHZvlDC0AUTNv8nWGEVNtvJvN+KKrXpHjiSjp4
lHGXp6QZEA4Xmbo/5RMoy7FtHAjT8QXG3kmmWAQSN8TYrI0KMWoSIfZMVhytTgqc
1S2G4nmUmkLVJgJ1p2/plLwY3ORpmQHgTrmttYnh/y9h3wNEje/8QQKlLncCLP4b
GVfIfBjuKSoYAU6UqDBV8wgyCbgysdhDDxlt6hkF1lMljc9xlj1pUlYqdMCn8Nvt
rQ21mpaMOcyAKu0qZPgSBJR9W15hdAS7Y3RCHBDi8TraLnl+pvhRy4q2e9qYsMIO
w8kmrRVtXHTdPCyAfVKU93mn8A1MUbISr3f4AmP623NOK8MVP/J0Khx3tHpJ1Hdr
L5Erg0N4n7lA+eUiYthwdxG1JaGQaCRVeqUZJ/TwuLvAsknDOCdZAn/jrjjaxRJ8
EwVnu8kJUuxYIix4CuydLKCS3QXey3jbRccEn8Ybzz4nPcoZoWmJianrRQARAQAB
zS1CVENQYXlTZXJ2ZXIgVmF1bHQgPG5pY29sYXMuZG9yaWVyQGdtYWlsLmNvbT7C
wXgEEwEIACwFAl3ec/EJEGL+hWR97douAhsDBQkeEzgAAhkBBAsHCQMFFQgKAgME
FgABAgAAVGkQABOWW9mCyBOdWaJ7JBFGraUv9qQ3Q9EXFfOCXHDJdiY6WSWyvhMG
0KluY6h0kVMGkc5MXl5D04+UuCrVIn7ucQ3FR5E3pkROJ/ZqGuXXBY/G7JVJsJz2
TGjRD5PxQD2SkfLQ/ZscqhmwcZPtmyVcyfKsLrtSPmDp25xYo/InJ0BDh2M6jvs7
WNRX4O/jQNl2WnAx8e8W/BtTQr23PC5+y6jsi2GVo+ePubqS+nz+O5MD0+0FJ2ov
2i9MAwJZUez4z7w11SRO2QT1MX4FzgIe+YcnnU5DeO+WTQci6cuv2+l1heDysRto
oZlWFL8bNNCKtGC46ZyJ4jmsMUp2eP5st32bpHQPf0yIhFvvKzPkm7u1fZIPPbXM
bmREBJWNiCNWOnCLr7yiO9ATVIzvvnK713oQYHpAHRoIuYgUiVxLVveBSY4ERE8F
IfOu2VUXyi+c/ottTd07dDrLpy8DJ25891ovE883NZcFR/rW1+0ymTDFyl/fPEDM
DNq/NxVKFfrIaGFvRoDLpOJPGbUgHsU3+xxndorFnrWIiOpLk9dIGxKSdVs67Hmx
YiRDuw/2j1QhR4dk1l8ySD75Hs7FFrLrUDfDWbipFHjrKti/V7zgUsgWYxmscAGs
cRd1Q/59vX7GFyyWYvMsEAMob1oIfSA+2SgpVDP55AXoqbo9iWUfJePYzsFNBF3e
c/EBEADQCD6OD21aTYARADbEfnCysxD1l/tDbhmjbJNgw5v5YzvVs2GCovhPzQmC
aLybwzuOvsh+dh2cnOjlWoYaQK/8JXolH0ZAh4z3oJca9UUdcOcBt6poYjPUYCjA
NLNFIS4CH05yr4CECu/GBGM9dSbizmbl/tJ7EcZO8xlxg85XOFT8fz/KhEhElyb8
KrCC46gtWnXYSBQ1XljfcZOUXRhv7ROAe1BAw3j9sdZ34RZ79xXx4rMyna2BBbzn
Gki4hV2qVAgXwcn8gq8Qhux/Y6XeZuJhjFCS6FCk8JgK7BFrThZi2z6FTHFM+7HR
eAkoJBcg/JoqyBauZx0UJ+JckxQb8dqImDiPc+2WJ8ENCTU8xobWAZUT0Hj8HhJi
kQ6URScpty1VushBtU4GHsPfLJoU2mLI7YQQ6b0VJD3ZT3eQuYchNjE44eSGx8M5
XVZjunbrrZjq2gzxd8+iK7vj9mnQ5M/kiFA2ptwPUVHjGmVS/omOI89AtPpLENwC
yFwKqOgOGPy92tVF/FFqKveFnic6U1M/3FWZamU0A3BxUFHrXrY9MWFul9AVLTud
lbrNluOIxmSsRAJXkkTs0JLam4ubgoSAg4XOHe1Y9w/BRC6huIRs72HBNUuDtACS
oMWfPOgt66rl0CW6/qBDh4gSLxxni2PhGehJOEc+ls6K6k+b4QARAQABwsF1BBgB
CAApBQJd3nPxCRBi/oVkfe3aLgIbDAUJHhM4AAQLBwkDBRUICgIDBBYAAQIAACWW
EAB510r8zce3r4bspcj/A/WFAPHgoGlMUeJQkoxsgE3tfcZBLPWkInTGnUHsLPMw
olE+pmqbS3XV3FjC4yGOGPOQYLeF+o/64+EabTzDomi9Hs0rV7GzpuYqSRQ/j8/j
H1qo5iuWwJnvvr5rGy3+mN1O6I88AZDRGHiLS1oG+mFXhNVp0dXPeDMsbGnztgNJ
zmIAWMeWqsC852ZmXa0VosTEE1Jb3s48otblwBwOWzNXBs+J+amuA71DridQYNWR
l3ixirH9/D+tpXOd+zOXwyczoYgf14Yz/lgKT+wlSfOQeMRbqTY5oijIxeLDJbeX
eYZoCss6gX1ue5yqgT0+haI9FAPrnJ/Jq9cPmwXuBmjQ7869JvDWUNgoQ8sP5GoH
vRGjaEzKkH8ibQTLtP2VKPENKsNjikKCaLsmWGvfC1CzAuw0JHQ8fNgwuqIXGs0L
MBCOUgynVqhHQKnApGcbnkCrRjr1wuAydPCQ7xbIaKdhbN3qzj1rUcvkG0GEjs9C
R4VB8G0zcLXMoqwKxPLAeR2cnSiIUW0JEcjxxBb+6poj9kQKaee97cxXP1qq2D8d
hsZHpy1Q/HSyaKYK4gId5/eZ7IsbPH60L61OJ2NC7xRcM9P09/EDz08dbt8IKqrq
bhogEBf9UyDmPn6DW8jC1nkVbE8ODYDaOuLW3PKrthoKVQ==
=n82A
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFlLyYIBEADW3oJnVMDC94g+7OB1/IBUYNevCqJIOAtC0eeFS5g0XGvvqRLx
2NLUqn5te+R7deoGElkZFJLLxFUwEnhqGCRH50Iou5aanUzvgI5fVAbK3k0fp9vc
LKCR0fQVIidcLyqMpkLZo8BSE3+BWxFp/r2OHvh2dYtJC+BZVwblkDS3cqwKvUZx
IocvDs47Wo3tzZfEsqUavbbiGx+Dm0fCV7TVHdVLU7q3bZsHSRiyTUZ2EAApoAmT
ir9csVxv2IM8yf6/HwWi6/Lp7dgSG1+qBZ1lUPPTY+dFLPZyt/kul+vuOj6GLZaU
s3D66d7TaPCHKWAOnP9RHpic/iXODXVXo1KHJfa0x8fW7I+y7/Gb+5x/m4O0Bz2T
BivdrSAuFpXkPqwawlw4CPgI9fc801g83+ZFzD2jJ6qxkEgfnlmf+zGNn5tC4N5j
NRTQ+GyHo1w4824SXcSN590wgz8goGJC3QPJxbifvOA8GzQIVzpxHckofOVyqIEq
qSnkP2xn4mELqD7HcFnoojZBqFbF2cN+oWQ+niLN+v4qrUncpQI9SVWlyp66S+1T
BhBQj2QuX+3B+K27EiDbhNV7EX6xEbGsnB1poMc2aMiz6veybW3GnoM+2ppr8Ko/
12Ij7l+ZA44t6PWUfQQbNSbUk/0Yhd9QJ8VQVck+TaS6gtarTbURlSdmHwARAQAB
tCl0ZWNub3ZlcnQgKGdpdGlhbikgPHRlY25vdmVydEBwYXJ0aWNsLmlvPokCVAQT
AQgAPhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mCAhsDBQkSzAMABQsJCAcC
BhUICQoLAgQWAgMBAh4BAheAAAoJEBPxNlHJzw1rtdYP/22iRX8O2Q++MayPHNx5
XHAlMk9mfi5FB1qJwshtlhda7P9U/hOTi227wH+Mzh5dBje4t2DkoHzxlz/Wr4cQ
QUJMOYd0OEZY6kpAQkvtyYobIb6zlRQK1koAfNMxewmfZZGTlr16IUVCovGSFvZ+
hdYRDEjuHqXjpwBfrxFAy/HCnfY10qSRkJc5w1ypj1IkzlanS+xeRJSDvRTQDAEr
zv3xKcMGjCCHaaCP+tyAaViBaUOlvmZdWwg0gwQCuPLqIh0cfDbcg0quciRIpnyp
zINmfwngZCwXdIYfAzmCzMHw1J3iOiqfqK0EcpHMsL689VyQSPgsoEHtcOGHYjRL
pMPGvRFHtICrnCHENK3IcwFWDGXW+i3zgOlA7g48yYWWvSup+t8I6YT+FeeFlxSO
dj1GdeMA0O7gXZ7znLVduokL2Ef4dZjc+3NwBlFov52vwCZwQMAGsMriwEDB2rDZ
B7YOvAxlUB/kavtx/oE8fV7mZcuwYg02lq4bozF9xlhjOFaRit6xXnLVi0TuI76c
uz67nB9VkWczSLIzCyjNyFpWbx1BMxTYfehZX3+YNajXwG6HdEp9CAYK0u46Guz/
Pth67bbNVYyP/cuOIrv/hqQ6xo4mOBMDDxcCEAXx3rwxfbxNM8vlwrMcpITrtNON
r41bcxUIfMEDefPm5wnXep8W
=szpX
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGROehcBEACXWWc6dHqCos1PmKI32iHi0jP3mYM3jU57YxbjwT78QEtEwSqf
YklpXkgTYq7jexx2JElfegM6w1sPYarq1y051RjnCgzl32da5I506SMvcJTmXumV
Rw6erPeDxAO74PflDSlALgtGOgbKhwwWRudbWgT5hKGkl62qy0mI6GStul0rbT+3
gq77DCGyURfe1PG1pymhO5XVz3WGtOa12NvRA+3wGIcqIji2MbtXuOhGMg//kVI5
m2vcfHyMMuQ01xUXRu57WxRujYaJ1RB4p86JCbDX3YU2XlzTxGAhqChDLuJGqo54
AZMUWDceftXsAoOqH8Hwmm5gFkYSpMt86ZT+umvWygmxohD5k85MuRj4AGagFj/u
CMcQjI/SN1UU/Qozg6VL/5FO8aH9IybDzX7eE3j0V/jTweStw1CIUajYgfemWOWl
whLPBDflRz/8EEqTN0CaSSaiYiULZUiawBO/bRIiCO2Q6QrAi3KpPUhCwiw/Yecd
rAMLH7bytpECDdbNonQ/VMxWwtWJQ87qBtWvHFQxXBKjyuANsKL9X7v3KcYOUdd2
fSt7eqE9GDT4DbK6sTmuTpq2TgHXET0cA39+N2zxTh5xFupI/pi2iAHJ6hgIiQnn
662TngjGOSFvrTV/51Ua0Vx8OCMJJOcRdOVaYzuzg9DsjVcJin3aRqUh4wARAQAB
tCBXb3dhcmlvIDx3b3dhcmlvQHByb3Rvbm1haWwuY29tPokCVAQTAQgAPhYhBKs6
L3JYGPz/J5SEHHk1BLRJxpIgBQJkTnoXAhsDBQkHhh77BQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJEHk1BLRJxpIgwgEP/109vw1WXRh9EaRr3Y1+sBi+/PRQ5TCx
UEcP9ru5sQPJ0BsZK8RYw0BNIfDQX9OB1k/AoiBelL+0EoDKvjXmwz9fPUmSVk5r
3RzfClXTnxn4HXPKkSGMt4WBUnvohTexK7CPkb9xy+K0Jtx8XF1XiQLDFg2a9lBj
IIX2H6aHn4VjdUBv7TrTCAI2Vg0cQUpeJUwyHH+lk0r2WM3zAxzS3Iy2yDDstNT8
audXEX4BtJhyEU1m57jwgscrbTtgwYOAsaRLcnUaAFWhbov3IiGInk7N1fkMsuW5
HE5RcegSZRS3X4o6O/nmwdSjCEB9weydOCPrtfdbvfvuTiMg/jZBikOk/Sj7FM/D
eZKghSHpLbT/V3S76FyIcc/xFkUmR+2fGvCNjJ1Qn2lXTS8xcbyzqR4LZPeUGppV
hvriilLnXSjyc60wuD3kmCCo1Zw4tNL8pr09BtVmScUy6eiwca8LLzvbbivqxF1g
Mrkkv8yQE0ZwO1kgNSn+PSzUPbwAoklcyN5Rhr5DxZh0UudiH5Jt5WWYeE8O2Uc1
si13X575kymGkkeiUcp9WtBkh2uial+RVmTrUTDUTIR2HzT6MAR84/DHlC5dsW8a
h4uDUhzeG2cTxuIfZC881UHKL+xT/I3PPuFdLbU5uoWJpXYpxKYulYWd7LA/k4bi
JWBrQo7VDvvP
=H3wS
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmeP/40ACgkQE/E2UcnP
DWuraw/9HCuAZG+D6xSLWmjA9Dtj9OZMEOIxqvxw+1e2KQ5ek4d1waL63NWFQfMi
fDlKKeFbZoL6Dfjbx0GoUJKTfrIVKog6DlVzIi5PuUwPOCBFuLl0g5kHlC20jbPw
nu7T6fj6/oD/lqo0rzFDkbsX7Fk4GGC7rYLKfdtYhDgMq9ro7QhSxAOJanRyqzXL
dvPNxlyksOyttJLSAZI9BOkrpTWoyb3asOli5oHgdcheHd/2fjby69huS3UWEjdO
9Bm73UFlxF2hxCTc2Fqvvb3SBDmNCLlFM0f+DDJNMJGUQViVCar0YRw3R+/NBo83
ptutp3bpabHijQFEEpIx/19nh9RQMJjaHHHqdPcTeg8bU/Yeq36TI7gsCenK0mQT
75MscvJAG0enoKVrTZez5ner9ZwLOevAKzRe4huRJZZjM8gM6sb2OKslJLqTxEVt
G3b8BLB9IUAxCeyuvGSG/3RV3MgZLnLy5MLYjh72+Kmo6HpuajJwPuvUck5ZYcGE
jjeRFZmqZj0FtCrcfStau/0liyAxU5k/43RwMvujO1uTTgOVHw1QhhMEkZ9bYhhO
JgeCEkwL1Bjjved1NSySjZbt2sFbG89as14ezHxgc4HaujJ6bGkINnkPOPWM1tk4
DjjEO/0PY9i0m/ivQUXf5ZPSnlkAR8x6Ve2S2MvQd7nFoS/YfLs=
=0pTn
-----END PGP SIGNATURE-----

View File

@@ -0,0 +1,21 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
725a049bc5a9fd60b05bba4d4825d35115d99f05ab5b7716d4507c295d05172d utxo-snapshot-bitcoin-mainnet-820852.tar
744c42885df700513331a978b289d9c9d5b27e0cf1147f2f5a287b4492ff940c utxo-snapshot-bitcoin-mainnet-867690.tar
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmedUAYACgkQE/E2UcnP
DWs1Vw/+P3CGP9LLVv2deNocBFunUz+7aDZsQiykSI8ws50ssJ5PsAg5VSl4CbCl
owWOdQVJiDUh7daP0jr+bt3X2FY5ORBb1TGlvfCHE+vLfEFDnTpLXouSCclP0cv8
Ci8zQFKSI5Pf6uSMpALgQZxBgNU/0IegAQbpuJI4nrQXTKHJcMqtw1LtnmcreESO
MsSiGCXnC1R+xGQjptfvbzXaQVrin7ctYA9zjN4CGbjNChzr+ywT8dht2RKoLYyP
OrEys7d8EIaw/ktRvRmyk6O7KmnvUhf0uuFlDq+eTiBIpQoUEovCow1YYKaWkIRB
r4JBJJ34AB+XC2hgi5jpJNub/wKgVBm0iy79zZOSILP3ymbn3iJGg4ifUF0YeZCU
ufYkYi3iTJDpwYr0tylZmBiwsWNcbUhB+WTNX7ogCW70ZuhrF0PJQRPmhI34vsE/
qg3n0/hNNsypy0epRd33KSOvrSmaoTKLtCax9Osnt+F+yTYjD5EPqkQuzlJl+fDe
VvjWO5XHuaRvzijBrJQz6r5V4e/0ioNa8FTRqWmMTO1wHmxF5glpozyKycv9+bsB
IL9F1IQjhPkSVI7Hw8bsURpfH4mV+9eZJJDIvBf1/0gDctsBdsI5+5jxZjup769Q
AmMsGeZoplm/eUofQ9hItWcVitPhisDmC3wDR71UKM0b9FF6IUY=
=YUjt
-----END PGP SIGNATURE-----

View File

@@ -44,7 +44,7 @@ def addLockRefundSigs(self, xmr_swap, ci):
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
self.log.info(f"Manually recovering {bid_id.hex()}")
self.log.info(f"Manually recovering {self.log.id(bid_id)}")
# Manually recover txn if other key is known
try:
use_cursor = self.openDB(cursor)
@@ -119,10 +119,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
lock_tx_vout=lock_tx_vout,
)
self.log.debug(
"Submitted lock B spend txn %s to %s chain for bid %s",
txid.hex(),
ci_follower.coin_name(),
bid_id.hex(),
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
)
self.logBidEvent(
bid.bid_id,
@@ -218,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

@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import json
import shlex
import urllib
import logging
import traceback
import subprocess
import urllib
import http.client
from xmlrpc.client import (
Fault,
Transport,
@@ -17,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
@@ -31,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,
@@ -64,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[:]
@@ -73,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,
@@ -81,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:
@@ -103,8 +166,7 @@ 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("RPC server error " + str(ex) + ", method: " + method)
raise ValueError(f"RPC server error: {ex}, method: {method}")
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
@@ -112,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)
@@ -120,36 +238,7 @@ def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError("RPC error " + str(ex))
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None):
cli_bin = os.path.join(bindir, cli_bin)
args = [
cli_bin,
]
if chain != "mainnet":
args.append("-" + chain)
args.append("-datadir=" + datadir)
if wallet is not None:
args.append("-rpcwallet=" + wallet)
args += shlex.split(cmd)
p = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
out = p.communicate()
if len(out[1]) > 0:
raise ValueError("RPC error " + str(out[1]))
r = out[0].decode("utf-8").strip()
try:
r = json.loads(r)
except Exception:
pass
return r
raise ValueError(f"RPC error: {ex}")
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
@@ -159,7 +248,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
host = host
def rpc_func(method, params=None, wallet_override=None):
nonlocal port, auth, wallet, host
return callrpc(
port,
auth,
@@ -174,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

@@ -309,7 +309,6 @@ def make_xmr_rpc2_func(
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2(
port,
method,
@@ -345,7 +344,6 @@ def make_xmr_rpc_func(
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr(
port,
method,

View File

@@ -1,153 +1,213 @@
/* General Styles */
.bold {
font-weight: bold;
font-weight: bold;
}
.monospace {
font-family: monospace;
font-family: monospace;
}
.floatright {
position: fixed;
top: 1.25rem;
right: 1.25rem;
z-index: 9999;
position: fixed;
top: 1.25rem;
right: 1.25rem;
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;
padding-top: 1.5em;
}
/* Modal Styles */
.modal-highest {
z-index: 9999;
z-index: 9999;
}
/* Animation */
#hide {
-moz-animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
-o-animation: cssAnimation 0s ease-in 15s forwards;
animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
-moz-animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
-o-animation: cssAnimation 0s ease-in 15s forwards;
animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
@keyframes cssAnimation {
to {
width: 0;
height: 0;
overflow: hidden;
}
to {
width: 0;
height: 0;
overflow: hidden;
}
}
@-webkit-keyframes cssAnimation {
to {
width: 0;
height: 0;
visibility: hidden;
}
to {
width: 0;
height: 0;
visibility: hidden;
}
}
/* Custom Select Styles */
.custom-select .select {
appearance: none;
background-image: url('/static/images/other/coin.png');
background-position: 10px center;
background-repeat: no-repeat;
position: relative;
appearance: none;
background-image: url('/static/images/other/coin.png');
background-position: 10px center;
background-repeat: no-repeat;
position: relative;
}
.custom-select select::-webkit-scrollbar {
width: 0;
width: 0;
}
.custom-select .select option {
padding-left: 0;
text-indent: 0;
background-repeat: no-repeat;
background-position: 0 50%;
padding-left: 0;
text-indent: 0;
background-repeat: no-repeat;
background-position: 0 50%;
}
.custom-select .select option.no-space {
padding-left: 0;
padding-left: 0;
}
.custom-select .select option[data-image] {
background-image: url('');
background-image: url('');
}
.custom-select .select-icon {
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
}
.custom-select .select-image {
display: none;
margin-top: 10px;
display: none;
margin-top: 10px;
}
.custom-select .select:focus + .select-dropdown .select-image {
display: block;
display: block;
}
/* Blur and Overlay Styles */
.blurred {
filter: blur(3px);
pointer-events: none;
user-select: none;
filter: blur(3px);
pointer-events: none;
user-select: none;
}
.error-overlay.non-blurred {
filter: none;
pointer-events: auto;
user-select: auto;
filter: none;
pointer-events: auto;
user-select: auto;
}
/* Form Element Styles */
@media screen and (-webkit-min-device-pixel-ratio:0) {
select:disabled,
input:disabled,
textarea:disabled {
opacity: 1 !important;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select:disabled,
input:disabled,
textarea:disabled {
opacity: 1 !important;
}
}
.error {
border: 1px solid red !important;
border: 1px solid red !important;
}
/* Active Container Styles */
.active-container {
position: relative;
border-radius: 10px;
position: relative;
border-radius: 10px;
}
.active-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid rgb(77, 132, 240);
border-radius: inherit;
pointer-events: none;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid rgb(77, 132, 240);
border-radius: inherit;
pointer-events: none;
}
/* Center Spin Animation */
.center-spin {
display: flex;
justify-content: center;
align-items: center;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Hover Container Styles */
@@ -155,215 +215,353 @@
.hover-container:hover #coin_to,
.hover-container:hover #coin_from_button,
.hover-container:hover #coin_from {
border-color: #3b82f6;
border-color: #3b82f6;
}
#coin_to_button, #coin_from_button {
background-repeat: no-repeat;
background-position: center;
background-size: 20px 20px;
#coin_to_button,
#coin_from_button {
background-repeat: no-repeat;
background-position: center;
background-size: 20px 20px;
}
/* Input-like Container Styles */
.input-like-container {
max-width: 100%;
background-color: #ffffff;
width: 360px;
padding: 1rem;
color: #374151;
border-radius: 0.375rem;
font-size: 0.875rem;
line-height: 1.25rem;
outline: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
height: auto;
min-height: 90px;
max-height: 150px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow-y: auto;
max-width: 100%;
background-color: #ffffff;
width: 360px;
padding: 1rem;
color: #374151;
border-radius: 0.375rem;
font-size: 0.875rem;
line-height: 1.25rem;
outline: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
height: auto;
min-height: 90px;
max-height: 150px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow-y: auto;
}
.input-like-container.dark {
background-color: #374151;
color: #ffffff;
background-color: #374151;
color: #ffffff;
}
.input-like-container.copying {
width: inherit;
width: inherit;
}
/* QR Code Styles */
.qrcode {
position: relative;
display: inline-block;
padding: 10px;
overflow: hidden;
position: relative;
display: inline-block;
padding: 10px;
overflow: hidden;
}
.qrcode-border {
border: 2px solid;
background-color: #ffffff;
border-radius: 0px;
border: 2px solid;
background-color: #ffffff;
border-radius: 0px;
}
.qrcode img {
width: 100%;
height: auto;
border-radius: 0px;
width: 100%;
height: auto;
border-radius: 0px;
}
#showQR {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 25px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 25px;
}
.qrcode-container {
margin-top: 25px;
margin-top: 25px;
}
/* Disabled Element Styles */
select.select-disabled,
.disabled-input-enabled,
select.disabled-select-enabled {
opacity: 0.40 !important;
opacity: 0.40 !important;
}
/* Shutdown Modal Styles */
#shutdownModal {
z-index: 50;
z-index: 50;
}
#shutdownModal > div:first-child {
z-index: 40;
z-index: 40;
}
#shutdownModal > div:last-child {
z-index: 50;
z-index: 50;
}
#shutdownModal > div {
transition: opacity 0.3s ease-out;
transition: opacity 0.3s ease-out;
}
#shutdownModal.hidden > div {
opacity: 0;
opacity: 0;
}
#shutdownModal:not(.hidden) > div {
opacity: 1;
opacity: 1;
}
.shutdown-button {
transition: all 0.3s ease;
transition: all 0.3s ease;
}
.shutdown-button.shutdown-disabled {
opacity: 0.6;
cursor: not-allowed;
color: #a0aec0;
opacity: 0.6;
cursor: not-allowed;
color: #a0aec0;
}
.shutdown-button.shutdown-disabled:hover {
background-color: #4a5568;
background-color: #4a5568;
}
.shutdown-button.shutdown-disabled svg {
opacity: 0.5;
opacity: 0.5;
}
/* Loading line animation */
/* Loading Line Animation */
.loading-line {
width: 100%;
height: 2px;
background-color: #ccc;
overflow: hidden;
position: relative;
width: 100%;
height: 2px;
background-color: #ccc;
overflow: hidden;
position: relative;
}
.loading-line::before {
content: '';
display: block;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, #007bff, transparent);
animation: loading 1.5s infinite;
content: '';
display: block;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, #007bff, transparent);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/* Hide the loading line once data is loaded */
.usd-value:not(.loading) .loading-line,
.profit-loss:not(.loading) .loading-line {
display: none;
display: none;
}
.resolution-button {
/* Resolution Button Styles */
.resolution-button {
background: none;
border: none;
color: #4B5563; /* gray-600 */
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #4B5563;
font-size: 0.875rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s;
outline: 2px solid transparent;
outline-offset: 2px;
}
}
.resolution-button:hover {
color: #1F2937; /* gray-800 */
}
.resolution-button:hover {
color: #1F2937;
}
.resolution-button:focus {
outline: 2px solid #3B82F6; /* blue-500 */
}
.resolution-button:focus {
outline: 2px solid #3B82F6;
}
.resolution-button.active {
color: #3B82F6; /* blue-500 */
outline: 2px solid #3B82F6; /* blue-500 */
}
.resolution-button.active {
color: #3B82F6;
outline: 2px solid #3B82F6;
}
.dark .resolution-button {
color: #9CA3AF; /* gray-400 */
}
.dark .resolution-button {
color: #9CA3AF;
}
.dark .resolution-button:hover {
color: #F3F4F6; /* gray-100 */
}
.dark .resolution-button:hover {
color: #F3F4F6;
}
.dark .resolution-button.active {
color: #60A5FA; /* blue-400 */
outline-color: #60A5FA; /* blue-400 */
.dark .resolution-button.active {
color: #60A5FA;
outline-color: #60A5FA;
color: #fff;
}
#toggle-volume.active {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
[data-popper-placement] {
will-change: transform;
transform: translateZ(0);
}
.tooltip {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
/* Toggle Button Styles */
#toggle-volume.active {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
/* Multi-select dropdown styles */
.multi-select-dropdown::-webkit-scrollbar {
width: 12px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: none !important;
border-color: inherit !important;
}
.multi-select-dropdown label:focus-within {
outline: none !important;
box-shadow: none !important;
}
#coin_to_button:focus,
#coin_from_button:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.coin-badge {
background: #3b82f6;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 2px;
}
.coin-badge .remove {
cursor: pointer;
font-weight: bold;
opacity: 0.7;
margin-left: 4px;
}
.coin-badge .remove:hover {
opacity: 1;
}
.multi-select-dropdown {
max-height: 300px;
overflow-y: auto;
z-index: 9999 !important;
position: fixed !important;
min-width: 200px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.multi-select-dropdown::-webkit-scrollbar {
width: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dropdown-container {
position: relative;
z-index: 1;
}
.dropdown-container.open {
z-index: 9999;
}
.filter-button-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.multi-select-dropdown input[type="checkbox"] {
outline: none !important;
box-shadow: none !important;
border: 1px solid #d1d5db;
border-radius: 3px;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.dark .multi-select-dropdown input[type="checkbox"] {
border-color: #6b7280;
background-color: #374151;
}
.dark .multi-select-dropdown input[type="checkbox"]:focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
}
.dark .multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown label {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -1,68 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const selectCache = {};
function updateSelectCache(select) {
const selectedOption = select.options[select.selectedIndex];
const image = selectedOption.getAttribute('data-image');
const name = selectedOption.textContent.trim();
selectCache[select.id] = { image, name };
}
function setSelectData(select) {
const selectedOption = select.options[select.selectedIndex];
const image = selectedOption.getAttribute('data-image') || '';
const name = selectedOption.textContent.trim();
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
const selectImage = select.nextElementSibling.querySelector('.select-image');
if (selectImage) {
selectImage.src = image;
}
const selectNameElement = select.nextElementSibling.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
}
updateSelectCache(select);
}
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
const selectImages = document.querySelectorAll('.custom-select .select-image');
const selectNames = document.querySelectorAll('.custom-select .select-name');
selectIcons.forEach(icon => icon.style.display = 'none');
selectImages.forEach(image => image.style.display = 'none');
selectNames.forEach(name => name.style.display = 'none');
function setupCustomSelect(select) {
const options = select.querySelectorAll('option');
const selectIcon = select.parentElement.querySelector('.select-icon');
const selectImage = select.parentElement.querySelector('.select-image');
options.forEach(option => {
const image = option.getAttribute('data-image');
if (image) {
option.style.backgroundImage = `url(${image})`;
}
});
const storedValue = localStorage.getItem(select.name);
if (storedValue && select.value == '-1') {
select.value = storedValue;
}
select.addEventListener('change', () => {
setSelectData(select);
localStorage.setItem(select.name, select.value);
});
setSelectData(select);
selectIcon.style.display = 'none';
selectImage.style.display = 'none';
}
const customSelects = document.querySelectorAll('.custom-select select');
customSelects.forEach(setupCustomSelect);
});

View File

@@ -0,0 +1,199 @@
document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
if (burger.length && menu.length) {
for (var i = 0; i < burger.length; i++) {
burger[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
const close = document.querySelectorAll('.navbar-close');
const backdrop = document.querySelectorAll('.navbar-backdrop');
if (close.length) {
for (var k = 0; k < close.length; k++) {
close[k].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
if (backdrop.length) {
for (var l = 0; l < backdrop.length; l++) {
backdrop[l].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
const tooltipManager = TooltipManager.initialize();
tooltipManager.initializeTooltips();
setupShutdownModal();
setupDarkMode();
toggleImages();
});
function setupShutdownModal() {
const shutdownButtons = document.querySelectorAll('.shutdown-button');
const shutdownModal = document.getElementById('shutdownModal');
const closeModalButton = document.getElementById('closeShutdownModal');
const confirmShutdownButton = document.getElementById('confirmShutdown');
const shutdownWarning = document.getElementById('shutdownWarning');
function updateShutdownButtons() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
shutdownButtons.forEach(button => {
if (activeSwaps > 0) {
button.classList.add('shutdown-disabled');
button.setAttribute('data-disabled', 'true');
button.setAttribute('title', 'Caution: Swaps in progress');
} else {
button.classList.remove('shutdown-disabled');
button.removeAttribute('data-disabled');
button.removeAttribute('title');
}
});
}
function closeAllDropdowns() {
const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
openDropdowns.forEach(dropdown => {
if (dropdown.style.display !== 'none') {
dropdown.style.display = 'none';
}
});
if (window.Dropdown && window.Dropdown.instances) {
window.Dropdown.instances.forEach(instance => {
if (instance._visible) {
instance.hide();
}
});
}
}
function showShutdownModal() {
closeAllDropdowns();
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
if (activeSwaps > 0) {
shutdownWarning.classList.remove('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
} else {
shutdownWarning.classList.add('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down';
}
shutdownModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideShutdownModal() {
shutdownModal.classList.add('hidden');
document.body.style.overflow = '';
}
if (shutdownButtons.length) {
shutdownButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
showShutdownModal();
});
});
}
if (closeModalButton) {
closeModalButton.addEventListener('click', hideShutdownModal);
}
if (confirmShutdownButton) {
confirmShutdownButton.addEventListener('click', function() {
const shutdownToken = document.querySelector('.shutdown-button')
.getAttribute('href').split('/').pop();
window.location.href = '/shutdown/' + shutdownToken;
});
}
if (shutdownModal) {
shutdownModal.addEventListener('click', function(e) {
if (e.target === this) {
hideShutdownModal();
}
});
}
if (shutdownButtons.length) {
updateShutdownButtons();
}
}
function setupDarkMode() {
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' ||
(!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
}
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
if (themeToggle) {
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
if (themeToggleDarkIcon && themeToggleLightIcon) {
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
}
toggleImages();
});
}
}
function toggleImages() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
}
function toggleImageDisplay(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
// Burger menus
document.addEventListener('DOMContentLoaded', function() {
// open
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
if (burger.length && menu.length) {
for (var i = 0; i < burger.length; i++) {
burger[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
// close
const close = document.querySelectorAll('.navbar-close');
const backdrop = document.querySelectorAll('.navbar-backdrop');
if (close.length) {
for (var i = 0; i < close.length; i++) {
close[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
if (backdrop.length) {
for (var i = 0; i < backdrop.length; i++) {
backdrop[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
});

View File

@@ -0,0 +1,376 @@
const ApiManager = (function() {
const state = {
isInitialized: false
};
function getConfig() {
return window.config || window.ConfigManager || {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: { requestsPerMinute: 50, minInterval: 1200 }
}
};
}
const rateLimiter = {
lastRequestTime: {},
requestQueue: {},
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.getMinInterval(apiName);
},
updateLastRequestTime: function(apiName) {
this.lastRequestTime[apiName] = Date.now();
},
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
if (!this.requestQueue[apiName]) {
this.requestQueue[apiName] = Promise.resolve();
}
try {
await this.requestQueue[apiName];
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
this.updateLastRequestTime(apiName);
return await requestFn();
} catch (error) {
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 => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
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 => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
throw error;
}
};
this.requestQueue[apiName] = executeRequest();
return await this.requestQueue[apiName];
} catch (error) {
if (error.message.includes('429') ||
error.message.includes('timeout') ||
error.name === 'NetworkError') {
const cacheKey = `coinData_${apiName}`;
try {
const cachedData = JSON.parse(localStorage.getItem(cacheKey));
if (cachedData && cachedData.value) {
return cachedData.value;
}
} catch (e) {
console.warn('Error accessing cached data:', e);
}
}
throw error;
}
}
};
const publicAPI = {
config,
rateLimiter,
initialize: function(options = {}) {
if (state.isInitialized) {
console.warn('[ApiManager] Already initialized');
return this;
}
if (options.config) {
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
}
state.isInitialized = true;
console.log('ApiManager initialized');
return this;
},
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,
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();
} catch (error) {
console.error(`Request failed for ${url}:`, error);
throw error;
}
},
makePostRequest: async function(url, headers = {}) {
return new Promise((resolve, reject) => {
fetch('/json/readurl', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: url,
headers: headers
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.Error) {
reject(new Error(data.Error));
} else {
resolve(data);
}
})
.catch(error => {
console.error(`Request failed for ${url}:`, error);
reject(error);
});
});
},
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
if (!coins) {
throw new Error('No coins specified for price lookup');
}
let coinsParam;
if (Array.isArray(coins)) {
coinsParam = coins.filter(c => c && c.trim() !== '').join(',');
} else if (typeof coins === 'object' && coins.coins) {
coinsParam = coins.coins;
} else {
coinsParam = coins;
}
if (!coinsParam || coinsParam.trim() === '') {
throw new Error('No valid coins to fetch prices for');
}
return this.makeRequest('/json/coinprices', 'POST', {}, {
coins: coinsParam,
source: source,
ttl: ttl
});
},
fetchCoinGeckoData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
const coins = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => coin.name)
.join(',') :
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
const response = await this.fetchCoinPrices(coins);
if (!response || typeof response !== 'object') {
throw new Error('Invalid response type');
}
if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
throw new Error('No valid rates found in response');
}
return response;
} catch (error) {
console.error('Error in fetchCoinGeckoData:', {
message: error.message,
stack: error.stack
});
throw error;
}
});
},
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
coins: coinSymbols.join(','),
source: 'coingecko.com',
ttl: 300
});
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
const volumeData = {};
Object.entries(response.data).forEach(([coinSymbol, data]) => {
const coinKey = coinSymbol.toLowerCase();
volumeData[coinKey] = {
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
price_change_percentage_24h: data.price_change_24h || 0
};
});
return volumeData;
} catch (error) {
console.error("Error fetching volume data:", error);
throw error;
}
});
},
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let days;
if (resolution === 'day') {
days = 1;
} else if (resolution === 'year') {
days = 365;
} else {
days = 180;
}
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;
}
});
},
dispose: function() {
rateLimiter.requestQueue = {};
rateLimiter.lastRequestTime = {};
state.isInitialized = false;
console.log('ApiManager disposed');
}
};
return publicAPI;
})();
window.Api = ApiManager;
window.ApiManager = ApiManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.apiManagerInitialized) {
ApiManager.initialize();
window.apiManagerInitialized = true;
}
});
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

@@ -0,0 +1,552 @@
const CacheManager = (function() {
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';
const CACHE_KEY_PATTERNS = [
'coinData_',
'chartData_',
'historical_',
'rates_',
'prices_',
'offers_',
'fallback_',
'volumeData'
];
const isCacheKey = (key) => {
return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
key === 'coinGeckoOneLiner' ||
key === PRICES_CACHE_KEY;
};
const isLocalStorageAvailable = () => {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
};
let storageAvailable = isLocalStorageAvailable();
const memoryCache = new Map();
if (!storageAvailable) {
console.warn('localStorage is not available. Using in-memory cache instead.');
}
const cacheAPI = {
getTTL: function(resourceType) {
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) {
try {
this.cleanup();
if (!value) {
console.warn('Attempted to cache null/undefined value for key:', key);
return false;
}
let ttl;
if (typeof resourceTypeOrCustomTtl === 'string') {
ttl = this.getTTL(resourceTypeOrCustomTtl);
} else if (typeof resourceTypeOrCustomTtl === 'number') {
ttl = resourceTypeOrCustomTtl;
} else {
ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
}
const item = {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + ttl
};
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) {
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
return false;
}
if (storageAvailable) {
try {
localStorage.setItem(key, serializedItem);
return true;
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
this.cleanup(true);
try {
localStorage.setItem(key, serializedItem);
return true;
} catch (retryError) {
console.error('Storage quota exceeded even after cleanup:', retryError);
storageAvailable = false;
console.warn('Switching to in-memory cache due to quota issues');
memoryCache.set(key, item);
return true;
}
} else {
console.error('localStorage error:', storageError);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
memoryCache.set(key, item);
return true;
}
}
} else {
memoryCache.set(key, item);
if (memoryCache.size > defaults.maxItems) {
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));
keysToDelete.forEach(k => memoryCache.delete(k));
}
return true;
}
} catch (error) {
console.error('Cache set error:', error);
try {
memoryCache.set(key, {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
});
return true;
} catch (e) {
console.error('Memory cache set error:', e);
return false;
}
}
},
get: function(key) {
try {
if (storageAvailable) {
try {
const itemStr = localStorage.getItem(key);
if (itemStr) {
let item;
try {
item = JSON.parse(itemStr);
} catch (parseError) {
console.error('Failed to parse cached item:', parseError);
localStorage.removeItem(key);
return null;
}
if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
console.warn('Invalid cache item structure for key:', key);
localStorage.removeItem(key);
return null;
}
const now = Date.now();
if (now < item.expiresAt) {
return {
value: item.value,
remainingTime: item.expiresAt - now
};
}
localStorage.removeItem(key);
return null;
}
} catch (error) {
console.error("localStorage access error:", error);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
}
}
if (memoryCache.has(key)) {
const item = memoryCache.get(key);
const now = Date.now();
if (now < item.expiresAt) {
return {
value: item.value,
remainingTime: item.expiresAt - now
};
} else {
memoryCache.delete(key);
}
}
return null;
} catch (error) {
console.error("Cache retrieval error:", error);
try {
if (storageAvailable) {
localStorage.removeItem(key);
}
memoryCache.delete(key);
} catch (removeError) {
console.error("Failed to remove invalid cache entry:", removeError);
}
return null;
}
},
isValid: function(key) {
return this.get(key) !== null;
},
cleanup: function(aggressive = false) {
const now = Date.now();
let totalSize = 0;
let itemCount = 0;
const items = [];
if (storageAvailable) {
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!isCacheKey(key)) continue;
try {
const itemStr = localStorage.getItem(key);
const size = new Blob([itemStr]).size;
const item = JSON.parse(itemStr);
if (now >= item.expiresAt) {
localStorage.removeItem(key);
continue;
}
items.push({
key,
size,
expiresAt: item.expiresAt,
timestamp: item.timestamp
});
totalSize += size;
itemCount++;
} catch (error) {
console.error("Error processing cache item:", error);
localStorage.removeItem(key);
}
}
if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
items.sort((a, b) => b.timestamp - a.timestamp);
while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
const item = items.pop();
try {
localStorage.removeItem(item.key);
totalSize -= item.size;
itemCount--;
} catch (error) {
console.error("Error removing cache item:", error);
}
}
}
} catch (error) {
console.error("Error during localStorage cleanup:", error);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
}
}
const expiredKeys = [];
memoryCache.forEach((item, key) => {
if (now >= item.expiresAt) {
expiredKeys.push(key);
}
});
expiredKeys.forEach(key => memoryCache.delete(key));
if (aggressive && memoryCache.size > defaults.maxItems / 2) {
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));
keysToDelete.forEach(key => memoryCache.delete(key));
}
return {
totalSize,
itemCount,
memoryCacheSize: memoryCache.size,
cleaned: items.length,
storageAvailable
};
},
clear: function() {
if (storageAvailable) {
try {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (isCacheKey(key)) {
keys.push(key);
}
}
keys.forEach(key => {
try {
localStorage.removeItem(key);
} catch (error) {
console.error("Error clearing cache item:", error);
}
});
} catch (error) {
console.error("Error clearing localStorage cache:", error);
storageAvailable = false;
}
}
Array.from(memoryCache.keys())
.filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key));
return true;
},
getStats: function() {
let totalSize = 0;
let itemCount = 0;
let expiredCount = 0;
const now = Date.now();
if (storageAvailable) {
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!isCacheKey(key)) continue;
try {
const itemStr = localStorage.getItem(key);
const size = new Blob([itemStr]).size;
const item = JSON.parse(itemStr);
totalSize += size;
itemCount++;
if (now >= item.expiresAt) {
expiredCount++;
}
} catch (error) {
console.error("Error getting cache stats:", error);
}
}
} catch (error) {
console.error("Error getting localStorage stats:", error);
storageAvailable = false;
}
}
let memoryCacheSize = 0;
let memoryCacheItems = 0;
let memoryCacheExpired = 0;
memoryCache.forEach((item, key) => {
if (isCacheKey(key)) {
memoryCacheItems++;
if (now >= item.expiresAt) {
memoryCacheExpired++;
}
try {
memoryCacheSize += new Blob([JSON.stringify(item)]).size;
} catch (e) {
}
}
});
return {
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
itemCount,
expiredCount,
utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
memoryCacheItems,
memoryCacheExpired,
memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
storageType: storageAvailable ? 'localStorage' : 'memory'
};
},
checkStorage: function() {
const wasAvailable = storageAvailable;
storageAvailable = isLocalStorageAvailable();
if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
console.log('localStorage is now available. Migrating memory cache...');
let migratedCount = 0;
memoryCache.forEach((item, key) => {
if (isCacheKey(key)) {
try {
localStorage.setItem(key, JSON.stringify(item));
memoryCache.delete(key);
migratedCount++;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
return false;
}
}
}
});
console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
}
return {
available: storageAvailable,
type: storageAvailable ? 'localStorage' : 'memory'
};
}
};
const publicAPI = {
...cacheAPI,
setPrices: function(priceData, customTtl = null) {
return this.set(PRICES_CACHE_KEY, priceData,
customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
},
getPrices: function() {
return this.get(PRICES_CACHE_KEY);
},
getCoinPrice: function(symbol) {
const prices = this.getPrices();
if (!prices || !prices.value) {
return null;
}
const normalizedSymbol = symbol.toLowerCase();
return prices.value[normalizedSymbol] || null;
},
getCompatiblePrices: function(format) {
const prices = this.getPrices();
if (!prices || !prices.value) {
return null;
}
switch(format) {
case 'rates':
const ratesFormat = {};
Object.entries(prices.value).forEach(([coin, data]) => {
const coinKey = coin.replace(/-/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
.toLowerCase()
.replace(' ', '-');
ratesFormat[coinKey] = {
usd: data.price || data.usd,
btc: data.price_btc || data.btc
};
});
return {
value: ratesFormat,
remainingTime: prices.remainingTime
};
case 'coinGecko':
const geckoFormat = {};
Object.entries(prices.value).forEach(([coin, data]) => {
const symbol = this.getSymbolFromCoinId(coin);
if (symbol) {
geckoFormat[symbol.toLowerCase()] = {
current_price: data.price || data.usd,
price_btc: data.price_btc || data.btc,
total_volume: data.total_volume,
price_change_percentage_24h: data.price_change_percentage_24h,
displayName: symbol
};
}
});
return {
value: geckoFormat,
remainingTime: prices.remainingTime
};
default:
return prices;
}
},
getSymbolFromCoinId: function(coinId) {
const symbolMap = {
'bitcoin': 'BTC',
'litecoin': 'LTC',
'monero': 'XMR',
'wownero': 'WOW',
'particl': 'PART',
'pivx': 'PIVX',
'firo': 'FIRO',
'zcoin': 'FIRO',
'dash': 'DASH',
'decred': 'DCR',
'namecoin': 'NMR',
'bitcoin-cash': 'BCH',
'dogecoin': 'DOGE'
};
return symbolMap[coinId] || null;
}
};
if (window.CleanupManager) {
window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
cm.clear();
});
}
return publicAPI;
})();
window.CacheManager = CacheManager;
console.log('CacheManager initialized');

View File

@@ -0,0 +1,508 @@
const CleanupManager = (function() {
const state = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
resources: new Map(),
debug: false,
memoryOptimizationInterval: null
};
function log(message, ...args) {
if (state.debug) {
console.log(`[CleanupManager] ${message}`, ...args);
}
}
const publicAPI = {
addListener: function(element, type, handler, options = false) {
if (!element) {
log('Warning: Attempted to add listener to null/undefined element');
return handler;
}
element.addEventListener(type, handler, options);
state.eventListeners.push({ element, type, handler, options });
log(`Added ${type} listener to`, element);
return handler;
},
setTimeout: function(callback, delay) {
const id = window.setTimeout(callback, delay);
state.timeouts.push(id);
log(`Created timeout ${id} with ${delay}ms delay`);
return id;
},
setInterval: function(callback, delay) {
const id = window.setInterval(callback, delay);
state.intervals.push(id);
log(`Created interval ${id} with ${delay}ms delay`);
return id;
},
requestAnimationFrame: function(callback) {
const id = window.requestAnimationFrame(callback);
state.animationFrames.push(id);
log(`Requested animation frame ${id}`);
return id;
},
registerResource: function(type, resource, cleanupFn) {
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
state.resources.set(id, { resource, cleanupFn });
log(`Registered custom resource ${id} of type ${type}`);
return id;
},
unregisterResource: function(id) {
const resourceInfo = state.resources.get(id);
if (resourceInfo) {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
state.resources.delete(id);
log(`Unregistered and cleaned up resource ${id}`);
return true;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
return false;
}
}
log(`Resource ${id} not found`);
return false;
},
clearTimeout: function(id) {
const index = state.timeouts.indexOf(id);
if (index !== -1) {
window.clearTimeout(id);
state.timeouts.splice(index, 1);
log(`Cleared timeout ${id}`);
}
},
clearInterval: function(id) {
const index = state.intervals.indexOf(id);
if (index !== -1) {
window.clearInterval(id);
state.intervals.splice(index, 1);
log(`Cleared interval ${id}`);
}
},
cancelAnimationFrame: function(id) {
const index = state.animationFrames.indexOf(id);
if (index !== -1) {
window.cancelAnimationFrame(id);
state.animationFrames.splice(index, 1);
log(`Cancelled animation frame ${id}`);
}
},
removeListener: function(element, type, handler, options = false) {
if (!element) return;
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
state.eventListeners = state.eventListeners.filter(
listener => !(listener.element === element &&
listener.type === type &&
listener.handler === handler)
);
},
removeListenersByElement: function(element) {
if (!element) return;
const listenersToRemove = state.eventListeners.filter(
listener => listener.element === element
);
listenersToRemove.forEach(({ element, type, handler, options }) => {
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
});
state.eventListeners = state.eventListeners.filter(
listener => listener.element !== element
);
},
clearAllTimeouts: function() {
state.timeouts.forEach(id => {
window.clearTimeout(id);
});
const count = state.timeouts.length;
state.timeouts = [];
log(`Cleared all timeouts (${count})`);
},
clearAllIntervals: function() {
state.intervals.forEach(id => {
window.clearInterval(id);
});
const count = state.intervals.length;
state.intervals = [];
log(`Cleared all intervals (${count})`);
},
clearAllAnimationFrames: function() {
state.animationFrames.forEach(id => {
window.cancelAnimationFrame(id);
});
const count = state.animationFrames.length;
state.animationFrames = [];
log(`Cancelled all animation frames (${count})`);
},
clearAllResources: function() {
let successCount = 0;
let errorCount = 0;
state.resources.forEach((resourceInfo, id) => {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
successCount++;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
errorCount++;
}
});
state.resources.clear();
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
},
clearAllListeners: function() {
state.eventListeners.forEach(({ element, type, handler, options }) => {
if (element) {
try {
element.removeEventListener(type, handler, options);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
}
});
const count = state.eventListeners.length;
state.eventListeners = [];
log(`Removed all event listeners (${count})`);
},
clearAll: function() {
const counts = {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size
};
this.clearAllListeners();
this.clearAllTimeouts();
this.clearAllIntervals();
this.clearAllAnimationFrames();
this.clearAllResources();
log(`All resources cleaned up:`, counts);
return counts;
},
getResourceCounts: function() {
return {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size,
total: state.eventListeners.length +
state.timeouts.length +
state.intervals.length +
state.animationFrames.length +
state.resources.size
};
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
if (state.memoryOptimizationInterval) {
this.clearInterval(state.memoryOptimizationInterval);
}
this.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
log('Tab hidden - running memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
} else if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
});
state.memoryOptimizationInterval = this.setInterval(() => {
if (document.hidden) {
log('Periodic memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
}
}, memoryCheckInterval);
log('Memory optimization setup complete');
return state.memoryOptimizationInterval;
},
optimizeMemory: function(options = {}) {
log('Running memory optimization');
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
}
this.cleanupOrphanedResources();
if (window.gc) {
try {
window.gc();
log('Forced garbage collection');
} catch (e) {
}
}
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
timestamp: Date.now(),
maxDataSize: options.maxDataSize || 1000
}
}));
log('Memory optimization complete');
},
cleanupOrphanedResources: function() {
let removedListeners = 0;
const validListeners = [];
for (let i = 0; i < state.eventListeners.length; i++) {
const listener = state.eventListeners[i];
if (!listener.element) {
removedListeners++;
continue;
}
try {
const isDetached = !(listener.element instanceof Node) ||
!document.body.contains(listener.element) ||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
(listener.element.style && listener.element.style.display === 'none');
if (isDetached) {
try {
if (listener.element instanceof Node) {
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
}
removedListeners++;
} catch (e) {
}
} else {
validListeners.push(listener);
}
} catch (e) {
log(`Error checking listener (removing): ${e.message}`);
removedListeners++;
}
}
if (removedListeners > 0) {
state.eventListeners = validListeners;
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
}
let removedResources = 0;
const resourcesForRemoval = [];
state.resources.forEach((info, id) => {
const resource = info.resource;
try {
if (resource instanceof Element && !document.body.contains(resource)) {
resourcesForRemoval.push(id);
}
if (resource && resource.element) {
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
resourcesForRemoval.push(id);
}
}
} catch (e) {
log(`Error checking resource ${id}: ${e.message}`);
}
});
resourcesForRemoval.forEach(id => {
this.unregisterResource(id);
removedResources++;
});
if (removedResources > 0) {
log(`Removed ${removedResources} orphaned resources`);
}
if (window.TooltipManager) {
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
try {
window.TooltipManager.cleanupOrphanedTooltips();
} catch (e) {
if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (err) {
log(`Error cleaning up tooltips: ${err.message}`);
}
}
}
} else if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (e) {
log(`Error cleaning up tooltips: ${e.message}`);
}
}
}
try {
this.cleanupTooltipDOM();
} catch (e) {
log(`Error in cleanupTooltipDOM: ${e.message}`);
}
},
cleanupTooltipDOM: function() {
let removedElements = 0;
try {
const tooltipSelectors = [
'[role="tooltip"]',
'[id^="tooltip-"]',
'.tippy-box',
'[data-tippy-root]'
];
tooltipSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
try {
if (!(element instanceof Element)) return;
const isDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden') ||
element.style.display === 'none' ||
element.style.visibility === 'hidden';
if (isDetached) {
try {
element.remove();
removedElements++;
} catch (e) {
}
}
} catch (err) {
}
});
} catch (err) {
log(`Error querying for ${selector}: ${err.message}`);
}
});
} catch (e) {
log(`Error in tooltip DOM cleanup: ${e.message}`);
}
if (removedElements > 0) {
log(`Removed ${removedElements} detached tooltip elements`);
}
},
setDebugMode: function(enabled) {
state.debug = Boolean(enabled);
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
return state.debug;
},
dispose: function() {
this.clearAll();
log('CleanupManager disposed');
},
initialize: function(options = {}) {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
this.addListener(window, 'beforeunload', () => {
this.clearAll();
});
}
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
this.setupMemoryOptimization(options.memoryOptions || {});
}
log('CleanupManager initialized');
return this;
}
};
return publicAPI;
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = CleanupManager;
}
if (typeof window !== 'undefined') {
window.CleanupManager = CleanupManager;
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
CleanupManager.initialize({ debug: false });
} else {
document.addEventListener('DOMContentLoaded', () => {
CleanupManager.initialize({ debug: false });
}, { once: true });
}
}

View File

@@ -0,0 +1,256 @@
const CoinManager = (function() {
const coinRegistry = [
{
symbol: 'BTC',
name: 'bitcoin',
displayName: 'Bitcoin',
aliases: ['btc', 'bitcoin'],
coingeckoId: 'bitcoin',
cryptocompareId: 'BTC',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin.png'
},
{
symbol: 'XMR',
name: 'monero',
displayName: 'Monero',
aliases: ['xmr', 'monero'],
coingeckoId: 'monero',
cryptocompareId: 'XMR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Monero.png'
},
{
symbol: 'PART',
name: 'particl',
displayName: 'Particl',
aliases: ['part', 'particl', 'particl anon', 'particl blind'],
variants: ['Particl', 'Particl Blind', 'Particl Anon'],
coingeckoId: 'particl',
cryptocompareId: 'PART',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Particl.png'
},
{
symbol: 'BCH',
name: 'bitcoin-cash',
displayName: 'Bitcoin Cash',
aliases: ['bch', 'bitcoincash', 'bitcoin cash'],
coingeckoId: 'bitcoin-cash',
cryptocompareId: 'BCH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin%20Cash.png'
},
{
symbol: 'PIVX',
name: 'pivx',
displayName: 'PIVX',
aliases: ['pivx'],
coingeckoId: 'pivx',
cryptocompareId: 'PIVX',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'PIVX.png'
},
{
symbol: 'FIRO',
name: 'firo',
displayName: 'Firo',
aliases: ['firo', 'zcoin'],
coingeckoId: 'firo',
cryptocompareId: 'FIRO',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Firo.png'
},
{
symbol: 'DASH',
name: 'dash',
displayName: 'Dash',
aliases: ['dash'],
coingeckoId: 'dash',
cryptocompareId: 'DASH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dash.png'
},
{
symbol: 'LTC',
name: 'litecoin',
displayName: 'Litecoin',
aliases: ['ltc', 'litecoin'],
variants: ['Litecoin', 'Litecoin MWEB'],
coingeckoId: 'litecoin',
cryptocompareId: 'LTC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Litecoin.png'
},
{
symbol: 'DOGE',
name: 'dogecoin',
displayName: 'Dogecoin',
aliases: ['doge', 'dogecoin'],
coingeckoId: 'dogecoin',
cryptocompareId: 'DOGE',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dogecoin.png'
},
{
symbol: 'DCR',
name: 'decred',
displayName: 'Decred',
aliases: ['dcr', 'decred'],
coingeckoId: 'decred',
cryptocompareId: 'DCR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Decred.png'
},
{
symbol: 'NMC',
name: 'namecoin',
displayName: 'Namecoin',
aliases: ['nmc', 'namecoin'],
coingeckoId: 'namecoin',
cryptocompareId: 'NMC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Namecoin.png'
},
{
symbol: 'WOW',
name: 'wownero',
displayName: 'Wownero',
aliases: ['wow', 'wownero'],
coingeckoId: 'wownero',
cryptocompareId: 'WOW',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Wownero.png'
}
];
const symbolToInfo = {};
const nameToInfo = {};
const displayNameToInfo = {};
const coinAliasesMap = {};
function buildLookupMaps() {
coinRegistry.forEach(coin => {
symbolToInfo[coin.symbol.toLowerCase()] = coin;
nameToInfo[coin.name.toLowerCase()] = coin;
displayNameToInfo[coin.displayName.toLowerCase()] = coin;
if (coin.aliases && Array.isArray(coin.aliases)) {
coin.aliases.forEach(alias => {
coinAliasesMap[alias.toLowerCase()] = coin;
});
}
coinAliasesMap[coin.symbol.toLowerCase()] = coin;
coinAliasesMap[coin.name.toLowerCase()] = coin;
coinAliasesMap[coin.displayName.toLowerCase()] = coin;
if (coin.variants && Array.isArray(coin.variants)) {
coin.variants.forEach(variant => {
coinAliasesMap[variant.toLowerCase()] = coin;
});
}
});
}
buildLookupMaps();
function getCoinByAnyIdentifier(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
return coinAliasesMap[normalizedId] || null;
}
return {
getAllCoins: function() {
return [...coinRegistry];
},
getCoinByAnyIdentifier: getCoinByAnyIdentifier,
getSymbol: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.symbol : null;
},
getDisplayName: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl Anon';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl Blind';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin MWEB';
}
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.displayName : null;
},
getCoingeckoId: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.coingeckoId : null;
},
coinMatches: function(coinId1, coinId2) {
if (!coinId1 || !coinId2) return false;
const coin1 = getCoinByAnyIdentifier(coinId1);
const coin2 = getCoinByAnyIdentifier(coinId2);
if (!coin1 || !coin2) return false;
return coin1.symbol === coin2.symbol;
},
getPriceKey: function(coinIdentifier) {
if (!coinIdentifier) return null;
const coin = getCoinByAnyIdentifier(coinIdentifier);
if (!coin) return coinIdentifier.toLowerCase();
return coin.coingeckoId;
},
getCoinIcon: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl.png';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl.png';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin.png';
}
const coin = getCoinByAnyIdentifier(identifier);
if (coin && coin.icon) {
return coin.icon;
}
const capitalizedName = identifier.toString().split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('%20');
return `${capitalizedName}.png`;
}
};
})();
window.CoinManager = CoinManager;
console.log('CoinManager initialized');

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

@@ -0,0 +1,283 @@
const ConfigManager = (function() {
const state = {
isInitialized: false
};
function determineWebSocketPort() {
const wsPort =
window.ws_port ||
(typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
'11700';
return wsPort;
}
const selectedWsPort = determineWebSocketPort();
const defaultConfig = {
cacheDuration: 10 * 60 * 1000,
requestTimeout: 60000,
wsPort: selectedWsPort,
cacheConfig: {
defaultTTL: 10 * 60 * 1000,
ttlSettings: {
prices: 5 * 60 * 1000,
chart: 5 * 60 * 1000,
historical: 60 * 60 * 1000,
volume: 30 * 60 * 1000,
offers: 2 * 60 * 1000,
identity: 15 * 60 * 1000
},
storage: {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200
},
fallbackTTL: 24 * 60 * 60 * 1000
},
itemsPerPage: 50,
apiEndpoints: {
coinGecko: 'https://api.coingecko.com/api/v3',
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
},
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
}
},
retryDelays: [5000, 15000, 30000],
get coins() {
if (window.CoinManager) {
return window.CoinManager.getAllCoins();
}
console.warn('[ConfigManager] CoinManager not available, returning empty array');
return [];
},
chartConfig: {
colors: {
default: {
lineColor: 'rgba(77, 132, 240, 1)',
backgroundColor: 'rgba(77, 132, 240, 0.1)'
}
},
showVolume: false,
specialCoins: [''],
resolutions: {
year: { days: 365, interval: 'month' },
sixMonths: { days: 180, interval: 'daily' },
day: { days: 1, interval: 'hourly' }
},
currentResolution: 'year'
}
};
const publicAPI = {
...defaultConfig,
initialize: function(options = {}) {
if (state.isInitialized) {
console.warn('[ConfigManager] Already initialized');
return this;
}
if (options) {
Object.assign(this, options);
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
}
this.utils = utils;
state.isInitialized = true;
console.log('ConfigManager initialized');
return this;
},
getAPIKeys: function() {
if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys();
return {
coinGecko: apiKeys.coinGecko || ''
};
}
return {
coinGecko: ''
};
},
getCoinBackendId: function(coinName) {
if (!coinName) return null;
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
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);
}
if (window.CoinUtils) {
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
return this;
},
get: function(path, defaultValue = null) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length; i++) {
if (current === undefined || current === null) {
return defaultValue;
}
current = current[parts[i]];
}
return current !== undefined ? current : defaultValue;
},
dispose: function() {
state.isInitialized = false;
console.log('ConfigManager disposed');
}
};
const utils = {
formatNumber: function(number, decimals = 2) {
if (typeof number !== 'number' || isNaN(number)) {
console.warn('formatNumber received a non-number value:', number);
return '0';
}
try {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
} catch (e) {
return '0';
}
},
formatDate: function(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: function(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {
const now = Math.floor(Date.now() / 1000);
if (timestamp <= now) return "Expired";
return this.formatTime(timestamp);
},
formatTime: function(timestamp, addAgoSuffix = false) {
const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - timestamp);
let timeString;
if (diff < 60) {
timeString = `${diff} seconds`;
} else if (diff < 3600) {
timeString = `${Math.floor(diff / 60)} minutes`;
} else if (diff < 86400) {
timeString = `${Math.floor(diff / 3600)} hours`;
} else if (diff < 2592000) {
timeString = `${Math.floor(diff / 86400)} days`;
} else if (diff < 31536000) {
timeString = `${Math.floor(diff / 2592000)} months`;
} else {
timeString = `${Math.floor(diff / 31536000)} years`;
}
return addAgoSuffix ? `${timeString} ago` : timeString;
},
escapeHtml: function(unsafe) {
if (typeof unsafe !== 'string') {
console.warn('escapeHtml received a non-string value:', unsafe);
return '';
}
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
formatPrice: function(coin, price) {
if (typeof price !== 'number' || isNaN(price)) {
console.warn(`Invalid price for ${coin}:`, price);
return 'N/A';
}
if (price < 0.000001) return price.toExponential(2);
if (price < 0.001) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 10) return price.toFixed(3);
if (price < 1000) return price.toFixed(2);
if (price < 100000) return price.toFixed(1);
return price.toFixed(0);
},
getEmptyPriceData: function() {
return {
'bitcoin': { usd: null, btc: null },
'bitcoin-cash': { usd: null, btc: null },
'dash': { usd: null, btc: null },
'dogecoin': { usd: null, btc: null },
'decred': { usd: null, btc: null },
'namecoin': { usd: null, btc: null },
'litecoin': { usd: null, btc: null },
'particl': { usd: null, btc: null },
'pivx': { usd: null, btc: null },
'monero': { usd: null, btc: null },
'zano': { usd: null, btc: null },
'wownero': { usd: null, btc: null },
'firo': { usd: null, btc: null }
};
},
getCoinSymbol: function(fullName) {
if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return fullName;
}
};
return publicAPI;
})();
window.logger = {
log: function(message) {
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
},
warn: function(message) {
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
},
error: function(message) {
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
}
};
window.config = ConfigManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.configManagerInitialized) {
ConfigManager.initialize();
window.configManagerInitialized = true;
}
});
if (typeof module !== 'undefined') {
module.exports = ConfigManager;
}
console.log('ConfigManager initialized');

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

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