296 Commits

Author SHA1 Message Date
Cryptoguard
631ccea626 Added NMC to README 2025-04-07 16:35:51 -04: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
tecnovert
65fbcda556 Fix lint issue. 2025-01-15 18:45:16 +02:00
tecnovert
40f334ed0e Merge branch 'dev' 2025-01-15 18:34:06 +02:00
tecnovert
77bb3e6353 guix: Update packed version. 2025-01-15 15:21:33 +02:00
tecnovert
3b60472c04 tests: Switch CI tests. 2025-01-15 15:06:34 +02:00
nahuhh
b87e034719 bch: v28.0.1 2025-01-14 12:49:31 +00:00
tecnovert
def7aae1ec Tor port fixes (#215)
* Set bind for BCH when using tor

* prepare: Set local tor control host when not in docker mode.

* Unlink tor hosts from BSX_DOCKER_MODE and add BSX_LOCAL_TOR.
2025-01-14 06:04:47 +00:00
tecnovert
294595adbd Merge pull request #214 from tecnovert/ltc_rewrite
Remove =onion from litecoin.conf
2025-01-13 23:29:28 +00:00
tecnovert
ab04f27497 Add upgradecores function to prepare script. (#213)
* Add upgradecores function to prepare script.

Differences from preparebinonly:

- If with/withoutcoins isn't set
  - Read list of coins to update from basicswap.json
    - Only where manage_daemon or manage_wallet_daemon is true
- Store core version no in basicswap.json
  - Only update if missing or differs.
- Writes core_version_no and core_version_group to basicswap.json
  - Per core updated
  - Backup old config to timestamped file

* Upgrade unmanaged coin cores by default.

Disable with BSX_UPDATE_UNMANAGED.
2025-01-13 23:28:32 +00:00
tecnovert
159974d414 tests: Improve test_02_leader_recover_a_lock_tx 2025-01-13 23:42:40 +02:00
tecnovert
110b91bb75 Remove =onion from litecoin.conf 2025-01-13 11:43:26 +02:00
tecnovert
3cea5449c9 Disable binding to the onionport for BTC. 2025-01-13 00:36:32 +02:00
tecnovert
07ed0af468 Merge pull request #142 from nahuhh/bitcoin_v28
bitcoin: update to 28.0
2025-01-12 22:35:06 +00:00
tecnovert
feabc619ae Add new dash subkey id. 2025-01-12 22:45:04 +02:00
tecnovert
e3f7b5b79b Constrain swap state when processing reversed bid accept message. 2025-01-12 22:23:20 +02:00
tecnovert
35bede48b0 Merge pull request #211 from nahuhh/dash_core
dash: v22.0.0 HF
2025-01-12 19:20:18 +00:00
tecnovert
af6154705c Merge pull request #210 from nahuhh/ltc_core
litecoin: v0.21.4
2025-01-12 19:20:04 +00:00
tecnovert
f010fc0c83 Merge pull request #209 from nahuhh/pgp
pgp: update expired keys
2025-01-12 19:19:07 +00:00
nahuhh
3da9221d43 bitcoin: update to 28.0 2025-01-12 12:50:58 +00:00
nahuhh
a7f0f257b8 dash: v22.0.0 HF 2025-01-12 12:12:56 +00:00
nahuhh
c095e22fdb litecoin: remove peerblockfilters and blockfilterindex flags. fixed upstream 2025-01-12 12:07:43 +00:00
nahuhh
0c98dff044 litecoin: v0.21.4 2025-01-12 12:05:12 +00:00
nahuhh
69cc56e4a7 pgp: update expired keys 2025-01-12 11:56:44 +00:00
tecnovert
a54e6daaa1 Merge pull request #208 from gerlofvanek/settings
ui: Settings, Add warning for debug enabled.
2025-01-11 21:55:09 +00:00
gerlofvanek
3009cacdb2 ui: Setting, Add warning for debug enabled. 2025-01-11 22:32:58 +01:00
tecnovert
b9bacb9988 Merge pull request #195 from nahuhh/doge_icon
images: cleanup
2025-01-11 20:25:27 +00:00
tecnovert
6905c6a131 Merge pull request #207 from gerlofvanek/offer-4
ui: Removed prefill of amount if variable is true on sending/receiving.
2025-01-11 20:14:34 +00:00
tecnovert
ce7b94a878 docker: Remove obsolete version attributes. 2025-01-11 22:10:45 +02:00
gerlofvanek
c49cdb2e98 ui: Removed prefill of amount if variable is true on sending/receiving. 2025-01-11 21:09:01 +01:00
tecnovert
0ae4651a78 Add dependabot config file. 2025-01-11 22:07:47 +02:00
tecnovert
12d24800b8 Merge pull request #206 from nahuhh/part-wallet
wallet: misc wallet.html
2025-01-11 20:04:27 +00:00
tecnovert
c09eab71cc Merge pull request #205 from tecnovert/templates
ui: Hide unused fee options for XMR and WOW on offer page.
2025-01-11 20:04:06 +00:00
nahuhh
5bbafbdb3c wallet: cleanup reseed section 2025-01-10 21:03:02 +00:00
nahuhh
157b63a5d0 wallet: center qr code if only 1 wallet addr 2025-01-10 20:59:36 +00:00
nahuhh
341d39a6a3 wallet: transparent address on left 2025-01-10 19:25:13 +00:00
tecnovert
bb8dad1607 ui: Hide unused fee options for XMR and WOW on offer page. 2025-01-10 20:45:26 +02:00
nahuhh
20bcef1891 images: remove unused 2025-01-10 17:28:58 +00:00
tecnovert
f9bf29e68c Merge pull request #204 from gerlofvanek/offers-9
ui/js: Optimization tweaks.
2025-01-10 16:24:29 +00:00
tecnovert
820e5af5fb Raise version to 0.14.3. 2025-01-10 17:48:23 +02:00
tecnovert
681122bcca Disable duplicate (proof of funds) balance check when sending offer.
Fix for blinded Particl offers.
Add fee to reverse offer balance check.
2025-01-10 17:47:45 +02:00
gerlofvanek
9418ea4385 ui/js: Optimization tweaks. 2025-01-10 16:41:34 +01:00
tecnovert
73ab5e7391 Test that the initial lock tx can be funded before posting an offer. 2025-01-10 01:19:58 +02:00
tecnovert
bf6d07a726 Merge pull request #203 from gerlofvanek/offers-7
JS: Fix API and new cleanup (memory) table row function and small fixes.
2025-01-09 19:09:59 +00:00
tecnovert
e4849d6dfe Merge pull request #201 from gerlofvanek/offer-3
ui: Update new bid section.
2025-01-09 19:09:50 +00:00
gerlofvanek
2002fcb31b JS: Fix API and new cleanup (memory) table row function and small fixes. 2025-01-09 17:37:01 +01:00
tecnovert
21c828051c Merge pull request #200 from gerlofvanek/version-2
GUI: v3.1.2
2025-01-09 13:00:08 +00:00
tecnovert
c7e84e2249 Merge pull request #199 from gerlofvanek/doge-fixes-4
doge: Wallet, add DOGE Core notice message.
2025-01-09 12:59:55 +00:00
gerlofvanek
a3645c286d ui: Update new bid section. 2025-01-08 21:27:01 +01:00
gerlofvanek
618df98abf GUI: v3.1.2 2025-01-08 19:56:22 +01:00
gerlofvanek
4bbf739786 doge: Wallet, Add DOGE Core notice message. 2025-01-08 19:52:11 +01:00
tecnovert
878a145420 Merge pull request #198 from gerlofvanek/doge-fixes-3
Fix Swap Type + Set adaptor_sig default if both adaptor/secret as option.
2025-01-07 19:19:42 +00:00
tecnovert
32bd44b19a tests: Move test_003_api to test_xmr and run in CI. 2025-01-07 21:03:52 +02:00
tecnovert
c5ced6994a api: identities returns a single object instead of a list if address is set. 2025-01-07 19:55:46 +02:00
tecnovert
2929e74c78 checkWalletSeed sets expected seed id if missing. 2025-01-07 19:47:28 +02:00
tecnovert
0c01dcf2f5 api: getcoinseed shows expected seed ids. 2025-01-07 18:39:13 +02:00
tecnovert
9eacd35319 api: Fix identities command not able to modify data. 2025-01-07 18:38:01 +02:00
gerlofvanek
ca6af04eba Fix Swap Type + Set adaptor_sig default if both adaptor/secret as option. 2025-01-07 11:44:30 +01:00
tecnovert
691e3f1b82 Merge pull request #196 from gerlofvanek/doge-fixes
doge: Fix images + coin tiles layout.
2025-01-06 19:58:15 +00:00
gerlofvanek
80dbbd3d12 Fix Swap Type select for Doge + UI update for Get Rate Inferred. 2025-01-06 20:37:33 +01:00
tecnovert
28d99c4c0f Fix recoverNoScriptTxnWithKey regression, add to more tests. 2025-01-06 20:17:21 +02:00
gerlofvanek
3f8012f0d0 doge: Fix images + coin tiles layout. 2025-01-06 19:06:14 +01:00
tecnovert
a53de511ce doge: Fix osx url. 2025-01-04 08:33:55 +02:00
tecnovert
34eb5900fb Merge pull request #185 from tecnovert/doge_23
Doge 23
2025-01-03 21:03:53 +00:00
tecnovert
514f7efc6e Fix test_persistent. 2025-01-03 22:48:00 +02:00
tecnovert
de81ec5d75 Use Particl release signing pubkey for Dogecoin. 2025-01-03 22:48:00 +02:00
tecnovert
4b23834af8 Rename getNewSecretKey 2025-01-03 22:48:00 +02:00
tecnovert
0e2be676db doge: Switch to custom binary. 2025-01-03 22:47:59 +02:00
tecnovert
3be72b3c71 Add to test_xmr_persistent. 2025-01-03 22:47:59 +02:00
nahuhh
889ffaaa33 doge: add patricklodder pgp 2025-01-03 22:47:59 +02:00
nahuhh
50515568d8 doge: add xanimo pgp key 2025-01-03 22:47:59 +02:00
nahuhh
56f96291e4 doge: docker/production/* 2025-01-03 22:47:58 +02:00
nahuhh
f5db8cf7ce doge: templates/wallets.html 2025-01-03 22:47:58 +02:00
nahuhh
ea91647862 doge: templates/wallet.html 2025-01-03 22:47:58 +02:00
nahuhh
d7a5467f4f doge: templates/offers.html 2025-01-03 22:47:58 +02:00
nahuhh
95db6655e7 doge: static/js/offerstable.js 2025-01-03 22:47:58 +02:00
nahuhh
36ec1e8683 doge: config.py 2025-01-03 22:47:57 +02:00
nahuhh
1797db97a0 doge: chainparams 2025-01-03 22:47:57 +02:00
nahuhh
10964f0f51 doge: interface/doge.py 2025-01-03 22:47:57 +02:00
nahuhh
d2733b704d doge: basicswap.py 2025-01-03 22:47:57 +02:00
nahuhh
b1401ee00b doge: prepare.py 2025-01-03 22:47:57 +02:00
tecnovert
e71589a292 Rename isCoinActive 2025-01-03 22:46:47 +02:00
tecnovert
54f56e0e2c Ignore unknown coin types in getCachedWalletsInfo 2024-12-27 16:31:07 +02:00
tecnovert
73543a5477 Merge pull request #191 from nahuhh/wow_v0.11.3.0-master
wownero: v0.11.3.0
2024-12-26 08:27:09 +00:00
tecnovert
7ad92b1bbd Merge pull request #193 from gerlofvanek/private-2
Private orderbook display + Identity stats + Various fixes.
2024-12-26 08:14:13 +00:00
gerlofvanek
a1e2592965 Remove debug messages. 2024-12-25 21:11:43 +01:00
gerlofvanek
ff29100fd4 Private orderbook display + Identity stats + Various fixes. 2024-12-25 12:02:57 +01:00
tecnovert
059356ccd8 docker: Add ninja-build package
Issue #183
2024-12-25 10:49:07 +02:00
tecnovert
5d0c7d28e4 dependencies: Update jinja2 to 3.1.5 2024-12-25 10:08:32 +02:00
tecnovert
75d0ca926f Merge branch 'nahuhh-wow_v0.11.3.0-master' into dev 2024-12-25 09:57:14 +02:00
tecnovert
8582dc479b Make bin/prepare.py executable. 2024-12-25 09:56:10 +02:00
tecnovert
b7383d99dc Merge pull request #189 from nahuhh/issue_87
prepare: throw error on removal of part
2024-12-25 07:43:30 +00:00
nahuhh
d88f5728a4 wownero: revert output distribution err 2024-12-25 01:14:40 +00:00
nahuhh
6d66ee8653 wownero: v0.11.3.0 2024-12-24 23:30:08 +00:00
nahuhh
ec21ea05bf prepare: throw error on removal of part 2024-12-17 23:29:19 +00:00
tecnovert
bba517c8b7 Merge pull request #186 from Vitalii-code/patch-1
Update install.md
2024-12-17 19:01:14 +00:00
Gerlof van Ek
ebcc4ccb06 Websockets for new listings (real time) on network/your offers table + Fix potential JS memory leaks. (#187)
* Websockets for new listings (real time) on network/your offers table + Fix potential JS memory leaks.

* Fix typo

* JS: Cleanup

* JS: Merge functions + Cleanup

* ui Fix price refresh

* JS: Big cleanup / various fixes

* Fix pagination

* JS: Fix pricechart JS error.
2024-12-17 18:58:41 +00:00
Vitalii
656335b541 Update install.md 2024-12-12 16:44:55 +00:00
tecnovert
e39613f49d Merge pull request #182 from Rucknium/patch-1
Add Bitcoin Cash to Available Assets on README.md
2024-12-03 07:28:33 +00:00
tecnovert
706d251ef4 Merge pull request #181 from nahuhh/dash_bin
prepare: fix dash bin folder
2024-12-03 07:28:20 +00:00
Rucknium
80e17c739e Add BCH to README.md 2024-12-02 20:29:33 +00:00
nahuhh
69ca41c68d prepare: fix dash bin folder 2024-11-30 14:42:41 +00:00
tecnovert
6f61c7d26d Merge pull request #180 from gerlofvanek/offer-2
ui: Update logic of "New Address"
2024-11-29 21:05:06 +00:00
tecnovert
b4a08ce15e Merge pull request #179 from gerlofvanek/chart-1
ui: Correct date display chart. Various small fixes.
2024-11-29 21:04:54 +00:00
tecnovert
744ad7988a Merge pull request #178 from tecnovert/wallets
Wallet blocks and depth spendable
2024-11-29 21:01:54 +00:00
tecnovert
56cd6da556 Update basicswap/basicswap.py
Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2024-11-29 18:54:09 +00:00
gerlofvanek
42955af42c ui: Update logic of "New Address" 2024-11-29 16:17:45 +01:00
gerlofvanek
f20a9fd75b ui: Correct date display chart. Various small fixes. 2024-11-29 14:36:05 +01:00
tecnovert
4942f23de6 Show depth spendable when lock tx B confirming. 2024-11-29 13:28:46 +02:00
tecnovert
037851a002 ui: Add wallet_blocks to XMR wallet page. 2024-11-29 13:28:42 +02:00
tecnovert
cf92c5635d Merge pull request #177 from gerlofvanek/version
GUI: v3.1.1
2024-11-28 21:29:07 +00:00
gerlofvanek
5f7abbb2eb GUI: v3.1.1 2024-11-28 22:25:18 +01:00
tecnovert
fdb02d10d6 Merge pull request #176 from gerlofvanek/offers-6
ui: Always select "new address" on offers/bids. Remember last used address.
2024-11-28 20:59:34 +00:00
tecnovert
0dc55fc449 ci: Fix cache key. 2024-11-28 22:57:11 +02:00
gerlofvanek
790a550e7f ui: Always select "new address" on offers/bids. Remember last used address. 2024-11-28 21:33:43 +01:00
tecnovert
d19a7538fd Merge pull request #173 from gerlofvanek/offer
ui: Add modal for confirm send bid.
2024-11-28 20:18:49 +00:00
tecnovert
913cdfa984 Merge pull request #172 from gerlofvanek/page_offers-5
Default set rate variable to false and amount variable to true.
2024-11-28 20:18:36 +00:00
tecnovert
289b2a53db Merge pull request #171 from gerlofvanek/pricechart
ui: Fix WOW chart and various fixes.
2024-11-28 20:18:22 +00:00
tecnovert
5941f2952e Merge pull request #170 from gerlofvanek/offers-4
ui: Offers table smart refresh button (JS), various fixes.
2024-11-28 20:18:05 +00:00
tecnovert
414947cbb5 Merge pull request #169 from gerlofvanek/wallets-4
ui: Fix cache wallets, Better hide/show (crypto/usd). Removed % on single val. Fixes.
2024-11-28 20:17:00 +00:00
tecnovert
435e74f83a ci: Add test to github actions. 2024-11-28 22:09:34 +02:00
gerlofvanek
0ca5aefc12 ui: Removed %, small fix. 2024-11-28 21:08:15 +01:00
tecnovert
938c641736 prepare: version respects withcoins. 2024-11-28 21:38:50 +02:00
gerlofvanek
68b066a2d1 ui: Add modal for confirm send bid. 2024-11-28 17:16:48 +01:00
gerlofvanek
c5508fe9be Default set rate variable to false and amount variable to true. 2024-11-28 12:11:53 +01:00
gerlofvanek
418e863d26 ui Fix WOW chart and various fixes. 2024-11-28 11:32:37 +01:00
gerlofvanek
1bb721edf5 fix: LINT 2024-11-28 09:58:52 +01:00
gerlofvanek
801006fa70 ui: Offers table smart refresh button (JS), various fixes. 2024-11-28 09:54:06 +01:00
gerlofvanek
1763dec981 ui: Fix cache wallets, Better hide/show (crypto/usd). Removed % on single val. Fixes. 2024-11-28 09:38:00 +01:00
tecnovert
128291a36a scripts: Remove incomplete feature. 2024-11-28 10:29:41 +02:00
tecnovert
31ead537c9 trivial: Fix lint issues. 2024-11-27 08:49:08 +02:00
tecnovert
25cfcc7cee Merge pull request #168 from nahuhh/monero_speed
interface: xmr.py allow startup with a busy daemon
2024-11-27 06:36:26 +00:00
nahuhh
e7a70f1e26 basicswap: revert removal of ensureWalletExists 2024-11-27 06:29:31 +00:00
nahuhh
ad43ce4095 interface: xmr.py allow startup with a busy daemon 2024-11-27 06:29:01 +00:00
tecnovert
b1b00b5342 Merge pull request #166 from bacoinin/fix_part_balances_checks
Fix part balances checks
2024-11-26 18:37:43 +00:00
tecnovert
a4dc9af301 Merge pull request #167 from tecnovert/auto-accept
Improve auto-accept
2024-11-26 18:37:23 +00:00
tecnovert
eefaab1752 Set state for expired sent reverse bids. 2024-11-26 19:36:58 +02:00
bacoinin
c16dd1bba3 Re-enabled the getSpendableBalance for the PART_BLIND interface 2024-11-26 12:43:07 +00:00
tecnovert
08df0ceae0 Improve auto-accept 2024-11-26 14:24:09 +02:00
tecnovert
26de907185 refactor: Add queryOne 2024-11-26 11:28:37 +02:00
tecnovert
e90800884a Fix expired offer check. 2024-11-26 10:50:21 +02:00
gerlofvanek
bebbba49ff Fix double bidding and connection reset problem.
Close #165
2024-11-26 00:38:40 +02:00
tecnovert
d417a46e67 Merge pull request #163 from gerlofvanek/offers-3
ui: Offers: Fixed cache, better filtering on tables, visually on refresh.
2024-11-25 21:46:51 +00:00
tecnovert
1b36154142 Merge pull request #74 from nahuhh/monero_speed
ux: XMR optimizations
2024-11-25 21:22:11 +00:00
tecnovert
f4f64423a4 Shutdown on SIGHUP signal (when terminal is closed) 2024-11-25 23:19:22 +02:00
tecnovert
3ba2145cc9 Change "Offer expired" from an error to a debug message. 2024-11-25 23:02:35 +02:00
bacoinin
9386544b3d getUnspentsByAddr correclty retrieves unspent blind utxos 2024-11-24 20:03:04 +00:00
bacoinin
3e3a83e6d4 Correct PART balance checking when creating offers 2024-11-24 17:01:22 +00:00
tecnovert
33105a832f api: Add set_max_concurrent_bids helper. 2024-11-23 20:52:34 +02:00
tecnovert
01f6a1d877 Fix missing wallets page entries. 2024-11-22 22:24:47 +02:00
gerlofvanek
4143f1a8ce Set offers price chart and offers table at 5 min cache. 2024-11-22 20:01:35 +01:00
gerlofvanek
ea41c4b41a ui: Offers: Fixed cache, better filtering on tables, visually on refresh. 2024-11-22 19:28:06 +01:00
tecnovert
bd571702cb Let SQLite handle all query parameters. 2024-11-20 22:26:35 +02:00
gerlofvanek
fa8764342e After successfully placing a bid, go directly to the bid page instead of the offer. 2024-11-20 06:46:17 +00:00
tecnovert
757f8f2762 Replace sqlalchemy with manbearpigSQL 2024-11-20 08:12:14 +02:00
gerlofvanek
0bd626d659 ui: Small fix. 2024-11-19 20:19:47 +00:00
gerlofvanek
5db8d6ccbe ui: Various bug Fixes / Cache fix wallets page. 2024-11-19 20:19:47 +00:00
gerlofvanek
eb30ef22fc ui: Hide also cryptocurrencies / Cache on USD prices / % change on Total Assets. 2024-11-19 19:37:33 +00:00
gerlofvanek
2e4be0274a ui: Refactor JS offers page / Added filter Expired/Revoked.. / Various Fixes. 2024-11-19 19:37:25 +00:00
nahuhh
28af80873a chainparams: increase min and max order sizes 2024-11-19 19:37:05 +00:00
nahuhh
8795ecc231 interface: xmr.py - remove extra refreshs 2024-11-19 04:55:52 +00:00
nahuhh
5bf20370eb basicswap: remove extra xmr wallet open 2024-11-19 04:55:47 +00:00
tecnovert
893fc87b28 Don't log to closed file. 2024-11-18 15:43:38 +02:00
gerlofvanek
6e0f6dabe4 ui: Wallets page JS refactor / one API call for all coin prices. 2024-11-15 21:16:34 +00:00
gerlofvanek
2983238ef5 ui: Fix BCH rate/market calculation and filter bids/offers. 2024-11-15 21:16:06 +00:00
gerlofvanek
3b86985ae3 ui: Fixed bug with Copied to clipboard. 2024-11-15 21:15:26 +00:00
tecnovert
732c87b013 Reformat with black. 2024-11-15 18:53:54 +02:00
nahuhh
6be9a14335 prepare: don't start daemon if coin config exists in basicswap.json 2024-11-15 15:23:07 +00:00
nahuhh
f93fae6696 prepare: disablecoin/addcoin for manage_wallet_daemon 2024-11-15 15:23:07 +00:00
nahuhh
373525b364 scripts: allow setting reserve below increment 2024-11-15 15:22:52 +00:00
tecnovert
7b03ce4769 Reformat tests with black. 2024-11-15 17:21:33 +02:00
nahuhh
b484827c15 util: bypass tor for .local domains 2024-11-15 10:11:34 +02:00
tecnovert
b5f6eb6526 Fix unbound error when no wallet data is cached. 2024-11-15 08:00:28 +00:00
tecnovert
e28d41ed0c Adjust wallet name for Particl anon and blind interfaces. 2024-11-15 08:00:28 +00:00
tecnovert
aefb094694 Add event when BCH mercy tx published. 2024-11-15 09:59:31 +02:00
tecnovert
cf1811cec3 api: Set correct default swap type for BCH. 2024-11-15 09:59:31 +02:00
tecnovert
273da833db Bump version. 2024-11-15 09:59:30 +02:00
tecnovert
f6916f093b Add debugind to prevent spending coin A lock tx. 2024-11-15 09:59:30 +02:00
tecnovert
51c1179326 ui: Fix error when txid is unknown. 2024-11-15 09:59:30 +02:00
tecnovert
6f0123e13e Fix BCH exchange name, pipe daemon output to file from prepare. 2024-11-15 09:59:30 +02:00
tecnovert
ca5b9e5e00 docker: Update templates for BCH. 2024-11-15 09:59:29 +02:00
tecnovert
283cfc7c59 Switch BCH from using wallet watchonly to watching scripts in BSX.
Using wallet watchonly to find the lock transactions only seems to work with rescanblockchain.
2024-11-15 09:59:29 +02:00
tecnovert
e05aaeba26 Add BTC type swipe tx mercy outputs. 2024-11-15 09:59:29 +02:00
tecnovert
8d96ea7fcf Test BCH mercy tx in reverse swaps. 2024-11-15 09:59:29 +02:00
tecnovert
b400669919 Check BCH mercy tx value. 2024-11-15 09:59:28 +02:00
tecnovert
fc17fa41ff Detect BCH mercy txn. 2024-11-15 09:59:28 +02:00
mainnet-pat
a214866b08 Add support for mercy transactions for refund-refund path 2024-11-15 09:59:28 +02:00
tecnovert
62e6978be1 api: Optionally display events with states. 2024-11-15 09:59:28 +02:00
tecnovert
00d70f8cc7 Add display_name to chainparams. 2024-11-15 09:59:27 +02:00
tecnovert
60cca03d31 Set Bitcoincash .conf file and port from basicswap.json. 2024-11-15 09:59:27 +02:00
tecnovert
52ae633c21 Add BCH to adaptor_swap_only_coins. 2024-11-15 09:59:27 +02:00
tecnovert
f02920721f tests: Add BCH to test_xmr_persistent 2024-11-15 09:59:27 +02:00
tecnovert
765fadb0ed tests: Move BCH code to BCH test file. 2024-11-15 09:59:27 +02:00
tecnovert
ccc90ccb67 Change BCH pidfile name and cli binary. 2024-11-15 09:59:26 +02:00
tecnovert
bff3c45976 tests: Merge BCH tests. 2024-11-15 09:59:26 +02:00
tecnovert
17308f9a66 Allow BCH as coin_to in ADS swaps. 2024-11-15 09:58:33 +02:00
mainnet-pat
ed80caf532 Gui fixes to bitcoincash 2024-11-15 07:50:12 +00:00
mainnet-pat
465f910812 Lint 2024-11-15 07:50:12 +00:00
mainnet-pat
a7c2fbba1f Final fix to refunds path, increase test coverage 2024-11-15 07:50:12 +00:00
mainnet-pat
b7da4f2096 All spend paths implemented, increasing test coverage, still some issues with refund spends and swipes to resolve 2024-11-15 07:50:12 +00:00
mainnet-pat
b73907bb84 Continue extending bch interface and test coverage
Support reverse swap happy path
2024-11-15 07:50:12 +00:00
mainnet-pat
499b086b57 Cleanup imports, change bch ops to CScriptOps 2024-11-15 07:50:12 +00:00
mainnet-pat
b83f289013 Continue building out the BCH interface
Adapt the swap flow to BCH specifics - txids change after funding and when signing inputs
BCH happy path (lock-spend) done
2024-11-15 07:50:12 +00:00
mainnet-pat
3ea832bd03 WIP continue implementing the BCH swap interface for XMR swap and atomic swap protocols 2024-11-15 07:50:12 +00:00
mainnet-pat
1b43806d51 Add bitcoincash support for prepare and run scripts, add bitcoincash to testing suite, groundwork for bch-xmr atomic swap protocol 2024-11-15 07:50:12 +00:00
186 changed files with 49583 additions and 22531 deletions

View File

@@ -6,7 +6,7 @@ lint_task:
- pip install flake8 codespell
script:
- flake8 --version
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503,E702,E131 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
test_task:

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"

71
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: ci
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
BIN_DIR: /tmp/cached_bin
TEST_RELOAD_PATH: /tmp/test_basicswap
jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 codespell pytest
pip install -r requirements.txt --require-hashes
- name: Install
run: |
pip install .
# Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt
- name: Running flake8
run: |
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Running 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
run: |
pytest tests/basicswap/test_other.py
- name: Cache coin cores
id: cache-cores
uses: actions/cache@v3
env:
cache-name: cache-cores
with:
path: /tmp/cached_bin
key: cores-${{ runner.os }}-${{ hashFiles('**/core_versions.txt') }}
- 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
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
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

View File

@@ -1,26 +0,0 @@
name: lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 codespell
- name: Running flake8
run: |
flake8 --ignore=E501,F841,W503 --per-file-ignores="basicswap/interface/bch.py:E131,E702" --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
- name: Running 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

View File

@@ -6,7 +6,7 @@ ENV LANG=C.UTF-8 \
RUN apt-get update; \
apt-get install -y --no-install-recommends \
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata;
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata cmake ninja-build;
# Install requirements first so as to skip in subsequent rebuilds
COPY ./requirements.txt requirements.txt

View File

@@ -64,6 +64,12 @@ BasicSwap is compatible with the following digital assets.
<td>XMR
</td>
</tr>
<tr>
<td>Bitcoin Cash
</td>
<td>BCH
</td>
</tr>
<tr>
<td>Dash
</td>
@@ -106,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.2"
__version__ = "0.14.3"

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# 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.
@@ -18,12 +19,18 @@ import subprocess
from sockshandler import SocksiPyHandler
from .db import (
DBMethods,
)
from .rpc import (
callrpc,
)
from .util import (
TemporaryError,
)
from .util.logging import (
BSXLogger,
)
from .chainparams import (
Coins,
chainparams,
@@ -34,8 +41,8 @@ def getaddrinfo_tor(*args):
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
class BaseApp:
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
class BaseApp(DBMethods):
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
self.log_name = log_name
self.fp = fp
self.fail_code = 0
@@ -47,19 +54,19 @@ class BaseApp:
self.coin_clients = {}
self.coin_interfaces = {}
self.mxDB = threading.Lock()
self.debug = self.settings.get('debug', False)
self.debug = self.settings.get("debug", False)
self.delay_event = threading.Event()
self.chainstate_delay_event = threading.Event()
self._network = None
self.prepareLogging()
self.log.info('Network: {}'.format(self.chain))
self.log.info("Network: {}".format(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')
self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050)
self.tor_control_password = self.settings.get('tor_control_password', None)
self.tor_control_port = self.settings.get('tor_control_port', 9051)
self.use_tor_proxy = self.settings.get("use_tor", False)
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
self.tor_proxy_port = self.settings.get("tor_proxy_port", 9050)
self.tor_control_password = self.settings.get("tor_control_password", None)
self.tor_control_port = self.settings.get("tor_control_port", 9051)
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
@@ -71,16 +78,24 @@ class BaseApp:
self.delay_event.set()
def prepareLogging(self):
logging.setLoggerClass(BSXLogger)
self.log = logging.getLogger(self.log_name)
self.log.propagate = False
# Remove any existing handlers
self.log.handlers = []
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S')
formatter = logging.Formatter(
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
)
stream_stdout = logging.StreamHandler()
if self.log_name != 'BasicSwap':
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S'))
if self.log_name != "BasicSwap":
stream_stdout.setFormatter(
logging.Formatter(
"%(asctime)s %(name)s %(levelname)s : %(message)s",
"%Y-%m-%d %H:%M:%S",
)
)
else:
stream_stdout.setFormatter(formatter)
stream_fp = logging.StreamHandler(self.fp)
@@ -92,68 +107,91 @@ class BaseApp:
def getChainClientSettings(self, coin):
try:
return self.settings['chainclients'][chainparams[coin]['name']]
return self.settings["chainclients"][chainparams[coin]["name"]]
except Exception:
return {}
def setDaemonPID(self, name, pid) -> None:
if isinstance(name, Coins):
self.coin_clients[name]['pid'] = pid
self.coin_clients[name]["pid"] = pid
return
for c, v in self.coin_clients.items():
if v['name'] == name:
v['pid'] = pid
if v["name"] == name:
v["pid"] = pid
def getChainDatadirPath(self, coin) -> str:
datadir = self.coin_clients[coin]['datadir']
testnet_name = '' if self.chain == 'mainnet' else chainparams[coin][self.chain].get('name', self.chain)
datadir = self.coin_clients[coin]["datadir"]
testnet_name = (
""
if self.chain == "mainnet"
else chainparams[coin][self.chain].get("name", self.chain)
)
return os.path.join(datadir, testnet_name)
def getCoinIdFromName(self, coin_name: str):
for c, params in chainparams.items():
if coin_name.lower() == params['name'].lower():
if coin_name.lower() == params["name"].lower():
return c
raise ValueError('Unknown coin: {}'.format(coin_name))
raise ValueError("Unknown coin: {}".format(coin_name))
def callrpc(self, method, params=[], wallet=None):
cc = self.coin_clients[Coins.PART]
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
return callrpc(
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
)
def callcoinrpc(self, coin, method, params=[], wallet=None):
cc = self.coin_clients[coin]
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
return callrpc(
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
)
def callcoincli(self, coin_type, params, wallet=None, timeout=None):
bindir = self.coin_clients[coin_type]['bindir']
datadir = self.coin_clients[coin_type]['datadir']
cli_bin: str = chainparams[coin_type].get('cli_binname', chainparams[coin_type]['name'] + '-cli')
command_cli = os.path.join(bindir, cli_bin + ('.exe' if os.name == 'nt' else ''))
args = [command_cli, ]
if self.chain != 'mainnet':
args.append('-' + self.chain)
args.append('-datadir=' + datadir)
bindir = self.coin_clients[coin_type]["bindir"]
datadir = self.coin_clients[coin_type]["datadir"]
cli_bin: str = chainparams[coin_type].get(
"cli_binname", chainparams[coin_type]["name"] + "-cli"
)
command_cli = os.path.join(
bindir, cli_bin + (".exe" if os.name == "nt" else "")
)
args = [
command_cli,
]
if self.chain != "mainnet":
args.append("-" + self.chain)
args.append("-datadir=" + datadir)
args += shlex.split(params)
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
out = p.communicate(timeout=timeout)
if len(out[1]) > 0:
raise ValueError('CLI error ' + str(out[1]))
return out[0].decode('utf-8').strip()
raise ValueError("CLI error " + str(out[1]))
return out[0].decode("utf-8").strip()
def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError):
return True
str_error = str(ex).lower()
return 'read timed out' in str_error or 'no connection to daemon' in str_error
return "read timed out" in str_error or "no connection to daemon" in str_error
def setConnectionParameters(self, timeout=120):
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
opener.addheaders = [("User-agent", "Mozilla/5.0")]
urllib.request.install_opener(opener)
if self.use_tor_proxy:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port, rdns=True)
socks.setdefaultproxy(
socks.PROXY_TYPE_SOCKS5,
self.tor_proxy_host,
self.tor_proxy_port,
rdns=True,
)
socket.socket = socks.socksocket
socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail
socket.getaddrinfo = (
getaddrinfo_tor # Without this accessing .onion links would fail
)
socket.setdefaulttimeout(timeout)
@@ -166,10 +204,16 @@ class BaseApp:
def readURL(self, url: str, timeout: int = 120, headers={}) -> bytes:
open_handler = None
if self.use_tor_proxy:
open_handler = SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port)
opener = urllib.request.build_opener(open_handler) if self.use_tor_proxy else urllib.request.build_opener()
open_handler = SocksiPyHandler(
socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port
)
opener = (
urllib.request.build_opener(open_handler)
if self.use_tor_proxy
else urllib.request.build_opener()
)
if headers is None:
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
opener.addheaders = [("User-agent", "Mozilla/5.0")]
request = urllib.request.Request(url, headers=headers)
return opener.open(request, timeout=timeout).read()
@@ -180,7 +224,9 @@ class BaseApp:
def torControl(self, query):
try:
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(self.tor_control_password, query).encode('utf-8')
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
self.tor_control_password, query
).encode("utf-8")
c = socket.create_connection((self.tor_proxy_host, self.tor_control_port))
c.send(command)
response = bytearray()
@@ -192,23 +238,23 @@ class BaseApp:
c.close()
return response
except Exception as e:
self.log.error(f'torControl {e}')
self.log.error(f"torControl {e}")
return
def getTime(self) -> int:
return int(time.time()) + self.mock_time_offset
def setMockTimeOffset(self, new_offset: int) -> None:
self.log.warning(f'Setting mocktime to {new_offset}')
self.log.warning(f"Setting mocktime to {new_offset}")
self.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
value: int = self.settings.get(name, default_v)
if value < min_v:
self.log.warning(f'Setting {name} to {min_v}')
self.log.warning(f"Setting {name} to {min_v}")
value = min_v
if value > max_v:
self.log.warning(f'Setting {name} to {max_v}')
self.log.warning(f"Setting {name} to {max_v}")
value = max_v
return value

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -76,13 +77,13 @@ class OfferStates(IntEnum):
class BidStates(IntEnum):
BID_SENT = 1
BID_RECEIVING = 2 # Partially received
BID_RECEIVING = 2 # Partially received
BID_RECEIVED = 3
BID_RECEIVING_ACC = 4 # Partially received accept message
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
SWAP_INITIATED = 6 # Initiate txn validated
SWAP_PARTICIPATING = 7 # Participate txn validated
SWAP_COMPLETED = 8 # All swap txns spent
BID_RECEIVING_ACC = 4 # Partially received accept message
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
SWAP_INITIATED = 6 # Initiate txn validated
SWAP_PARTICIPATING = 7 # Participate txn validated
SWAP_COMPLETED = 8 # All swap txns spent
XMR_SWAP_SCRIPT_COIN_LOCKED = 9
XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = 10
XMR_SWAP_NOSCRIPT_COIN_LOCKED = 11
@@ -96,16 +97,18 @@ class BidStates(IntEnum):
XMR_SWAP_FAILED = 19
SWAP_DELAYING = 20
SWAP_TIMEDOUT = 21
BID_ABANDONED = 22 # Bid will no longer be processed
BID_ERROR = 23 # An error occurred
BID_ABANDONED = 22 # Bid will no longer be processed
BID_ERROR = 23 # An error occurred
BID_STALLED_FOR_TEST = 24
BID_REJECTED = 25
BID_STATE_UNKNOWN = 26
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
BID_REQUEST_SENT = 29
BID_REQUEST_ACCEPTED = 30
BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
class TxStates(IntEnum):
@@ -231,12 +234,12 @@ class AutomationOverrideOptions(IntEnum):
def strAutomationOverrideOption(option):
if option == AutomationOverrideOptions.DEFAULT:
return 'Default'
return "Default"
if option == AutomationOverrideOptions.ALWAYS_ACCEPT:
return 'Always Accept'
return "Always Accept"
if option == AutomationOverrideOptions.NEVER_ACCEPT:
return 'Never Accept'
return 'Unknown'
return "Never Accept"
return "Unknown"
class VisibilityOverrideOptions(IntEnum):
@@ -247,250 +250,257 @@ class VisibilityOverrideOptions(IntEnum):
def strVisibilityOverrideOption(option):
if option == VisibilityOverrideOptions.DEFAULT:
return 'Default'
return "Default"
if option == VisibilityOverrideOptions.HIDE:
return 'Hide'
return "Hide"
if option == VisibilityOverrideOptions.BLOCK:
return 'Block'
return 'Unknown'
return "Block"
return "Unknown"
def strOfferState(state):
if state == OfferStates.OFFER_SENT:
return 'Sent'
return "Sent"
if state == OfferStates.OFFER_RECEIVED:
return 'Received'
return "Received"
if state == OfferStates.OFFER_ABANDONED:
return 'Abandoned'
return "Abandoned"
if state == OfferStates.OFFER_EXPIRED:
return 'Expired'
return 'Unknown'
return "Expired"
return "Unknown"
def strBidState(state):
if state == BidStates.BID_SENT:
return 'Sent'
return "Sent"
if state == BidStates.BID_RECEIVING:
return 'Receiving'
return "Receiving"
if state == BidStates.BID_RECEIVING_ACC:
return 'Receiving accept'
return "Receiving accept"
if state == BidStates.BID_RECEIVED:
return 'Received'
return "Received"
if state == BidStates.BID_ACCEPTED:
return 'Accepted'
return "Accepted"
if state == BidStates.SWAP_INITIATED:
return 'Initiated'
return "Initiated"
if state == BidStates.SWAP_PARTICIPATING:
return 'Participating'
return "Participating"
if state == BidStates.SWAP_COMPLETED:
return 'Completed'
return "Completed"
if state == BidStates.SWAP_TIMEDOUT:
return 'Timed-out'
return "Timed-out"
if state == BidStates.BID_ABANDONED:
return 'Abandoned'
return "Abandoned"
if state == BidStates.BID_STALLED_FOR_TEST:
return 'Stalled (debug)'
return "Stalled (debug)"
if state == BidStates.BID_ERROR:
return 'Error'
return "Error"
if state == BidStates.BID_REJECTED:
return 'Rejected'
return "Rejected"
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
return 'Script coin locked'
return "Script coin locked"
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
return 'Script coin spend tx valid'
return "Script coin spend tx valid"
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
return 'Scriptless coin locked'
return "Scriptless coin locked"
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
return 'Script coin lock released'
return "Script coin lock released"
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
return 'Script tx redeemed'
return "Script tx redeemed"
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
return 'Script pre-refund tx in chain'
return "Script pre-refund tx in chain"
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
return 'Scriptless tx redeemed'
return "Scriptless tx redeemed"
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
return 'Scriptless tx recovered'
return "Scriptless tx recovered"
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
return 'Failed, refunded'
return "Failed, refunded"
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
return 'Failed, swiped'
return "Failed, swiped"
if state == BidStates.XMR_SWAP_FAILED:
return 'Failed'
return "Failed"
if state == BidStates.SWAP_DELAYING:
return 'Delaying'
return "Delaying"
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
return 'Exchanged script lock tx sigs msg'
return "Exchanged script lock tx sigs msg"
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
return 'Exchanged script lock spend tx msg'
return "Exchanged script lock spend tx msg"
if state == BidStates.BID_REQUEST_SENT:
return 'Request sent'
return "Request sent"
if state == BidStates.BID_REQUEST_ACCEPTED:
return 'Request accepted'
return "Request accepted"
if state == BidStates.BID_STATE_UNKNOWN:
return 'Unknown bid state'
return "Unknown bid state"
if state == BidStates.BID_EXPIRED:
return 'Expired'
return 'Unknown' + ' ' + str(state)
return "Expired"
if state == BidStates.BID_AACCEPT_DELAY:
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
return "Unknown" + " " + str(state)
def strTxState(state):
if state == TxStates.TX_NONE:
return 'None'
return "None"
if state == TxStates.TX_SENT:
return 'Sent'
return "Sent"
if state == TxStates.TX_CONFIRMED:
return 'Confirmed'
return "Confirmed"
if state == TxStates.TX_REDEEMED:
return 'Redeemed'
return "Redeemed"
if state == TxStates.TX_REFUNDED:
return 'Refunded'
return "Refunded"
if state == TxStates.TX_IN_MEMPOOL:
return 'In Mempool'
return "In Mempool"
if state == TxStates.TX_IN_CHAIN:
return 'In Chain'
return 'Unknown'
return "In Chain"
return "Unknown"
def strTxType(tx_type):
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
return 'Chain A Lock Tx'
return "Chain A Lock Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
return 'Chain A Lock Spend Tx'
return "Chain A Lock Spend Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
return 'Chain A Lock Refund Tx'
return "Chain A Lock Refund Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
return 'Chain A Lock Refund Spend Tx'
return "Chain A Lock Refund Spend Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
return 'Chain A Lock Refund Swipe Tx'
return "Chain A Lock Refund Swipe Tx"
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
return 'Chain B Lock Tx'
return "Chain B Lock Tx"
if tx_type == TxTypes.ITX_PRE_FUNDED:
return 'Funded mock initiate Tx'
return "Funded mock initiate Tx"
if tx_type == TxTypes.BCH_MERCY:
return 'BCH Mercy Tx'
return 'Unknown'
return "BCH Mercy Tx"
return "Unknown"
def strAddressType(addr_type):
if addr_type == AddressTypes.OFFER:
return 'Offer'
return "Offer"
if addr_type == AddressTypes.BID:
return 'Bid'
return "Bid"
if addr_type == AddressTypes.RECV_OFFER:
return 'Offer recv'
return "Offer recv"
if addr_type == AddressTypes.SEND_OFFER:
return 'Offer send'
return 'Unknown'
return "Offer send"
return "Unknown"
def getLockName(lock_type):
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
return 'Sequence lock, blocks'
return "Sequence lock, blocks"
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
return 'Sequence lock, time'
return "Sequence lock, time"
if lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
return 'blocks'
return "blocks"
if lock_type == TxLockTypes.ABS_LOCK_TIME:
return 'time'
return "time"
def describeEventEntry(event_type, event_msg):
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
return 'Failed to publish lock tx B'
return "Failed to publish lock tx B"
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
return 'Lock tx A published'
return "Lock tx A published"
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
return 'Lock tx B published'
return "Lock tx B published"
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
return 'Failed to publish lock tx B spend: ' + event_msg
return "Failed to publish lock tx B spend: " + event_msg
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
return 'Lock tx A seen in chain'
return "Lock tx A seen in chain"
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
return 'Lock tx A confirmed in chain'
return "Lock tx A confirmed in chain"
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
return 'Lock tx B seen in chain'
return "Lock tx B seen in chain"
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
return 'Lock tx B confirmed in chain'
return "Lock tx B confirmed in chain"
if event_type == EventLogTypes.LOCK_TX_B_IN_MEMPOOL:
return 'Lock tx B seen in mempool'
return "Lock tx B seen in mempool"
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
return 'Debug tweak applied ' + event_msg
return "Debug tweak applied " + event_msg
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
return 'Failed to publish lock tx B refund'
return "Failed to publish lock tx B refund"
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
return 'Detected invalid lock Tx B'
return "Detected invalid lock Tx B"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
return 'Lock tx A refund tx published'
return "Lock tx A refund tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
return 'Lock tx A refund spend tx published'
return "Lock tx A refund spend tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
return 'Lock tx A refund swipe tx published'
return "Lock tx A refund swipe tx published"
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
return 'Lock tx B refund tx published'
return "Lock tx B refund tx published"
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED:
return 'Lock tx A spend tx published'
return "Lock tx A spend tx published"
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
return 'Lock tx B spend tx published'
return "Lock tx B spend tx published"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
return 'Lock tx A refund tx seen in chain'
return "Lock tx A refund tx seen in chain"
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
return 'Lock tx A refund spend tx seen in chain'
return "Lock tx A refund spend tx seen in chain"
if event_type == EventLogTypes.SYSTEM_WARNING:
return 'Warning: ' + event_msg
return "Warning: " + event_msg
if event_type == EventLogTypes.ERROR:
return 'Error: ' + event_msg
return "Error: " + event_msg
if event_type == EventLogTypes.AUTOMATION_CONSTRAINT:
return 'Failed auto accepting'
return "Failed auto accepting"
if event_type == EventLogTypes.AUTOMATION_ACCEPTING_BID:
return 'Auto accepting'
return "Auto accepting"
if event_type == EventLogTypes.ITX_PUBLISHED:
return 'Initiate tx published'
return "Initiate tx published"
if event_type == EventLogTypes.ITX_REDEEM_PUBLISHED:
return 'Initiate tx redeem tx published'
return "Initiate tx redeem tx published"
if event_type == EventLogTypes.ITX_REFUND_PUBLISHED:
return 'Initiate tx refund tx published'
return "Initiate tx refund tx published"
if event_type == EventLogTypes.PTX_PUBLISHED:
return 'Participate tx published'
return "Participate tx published"
if event_type == EventLogTypes.PTX_REDEEM_PUBLISHED:
return 'Participate tx redeem tx published'
return "Participate tx redeem tx published"
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
return 'Participate tx refund tx published'
return "Participate tx refund tx published"
if event_type == EventLogTypes.BCH_MERCY_TX_FOUND:
return 'BCH mercy tx found'
return "BCH mercy tx found"
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
return 'Lock tx B mercy tx published'
return "Lock tx B mercy tx published"
def getVoutByAddress(txjs, p2sh):
for o in txjs['vout']:
for o in txjs["vout"]:
try:
if 'address' in o['scriptPubKey'] and o['scriptPubKey']['address'] == p2sh:
return o['n']
if p2sh in o['scriptPubKey']['addresses']:
return o['n']
if "address" in o["scriptPubKey"] and o["scriptPubKey"]["address"] == p2sh:
return o["n"]
if p2sh in o["scriptPubKey"]["addresses"]:
return o["n"]
except Exception:
pass
raise ValueError('Address output not found in txn')
raise ValueError("Address output not found in txn")
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
for o in txjs['vout']:
for o in txjs["vout"]:
try:
if scriptPubKey_hex == o['scriptPubKey']['hex']:
return o['n']
if scriptPubKey_hex == o["scriptPubKey"]["hex"]:
return o["n"]
except Exception:
pass
raise ValueError('scriptPubKey output not found in txn')
raise ValueError("scriptPubKey output not found in txn")
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type="pubkey_address"):
return encodeAddress(
bytes((chainparams[coin_type][chain_name][addr_type],))
+ decodeAddress(addr)[1:]
)
def getOfferProofOfFundsHash(offer_msg, offer_addr):
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
h = hashlib.sha256()
h.update(offer_addr.encode('utf-8'))
h.update(offer_addr.encode("utf-8"))
offer_bytes = offer_msg.to_bytes()
h.update(offer_bytes)
return h.digest()
@@ -500,33 +510,48 @@ def getLastBidState(packed_states):
num_states = len(packed_states) // 12
if num_states < 2:
return BidStates.BID_STATE_UNKNOWN
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
try:
num_states = len(packed_states) // 12
if num_states < 2:
return BidStates.BID_STATE_UNKNOWN
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
except Exception:
return BidStates.BID_STATE_UNKNOWN
def strSwapType(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return 'seller_first'
return "seller_first"
if swap_type == SwapTypes.XMR_SWAP:
return 'xmr_swap'
return "xmr_swap"
return None
def strSwapDesc(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return 'Secret Hash'
return "Secret Hash"
if swap_type == SwapTypes.XMR_SWAP:
return 'Adaptor Sig'
return "Adaptor Sig"
return None
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED]
inactive_states = [
BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR,
BidStates.BID_REJECTED,
BidStates.SWAP_TIMEDOUT,
BidStates.BID_ABANDONED,
BidStates.BID_EXPIRED,
]
def canAcceptBidState(state):
return state in (
BidStates.BID_RECEIVED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
)
def isActiveBidState(state):

3200
basicswap/bin/prepare.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -2,37 +2,39 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# 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 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
from basicswap.chainparams import chainparams, Coins
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
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')
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
@@ -41,15 +43,16 @@ class Daemon:
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v['name']:
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():
swap_client.stopRunning()
@@ -57,35 +60,48 @@ 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)
# Rewrite litecoin.conf for 0.21.3
# Rewrite litecoin.conf
# TODO: Remove
ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf')
needs_rewrite: bool = False
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
config_to_add = ['blockfilterindex=0', 'peerblockfilters=0']
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line in config_to_add:
config_to_add.remove(line)
if line.endswith("=onion"):
needs_rewrite = True
break
if needs_rewrite:
logger.info("Rewriting litecoin.conf")
shutil.copyfile(ltc_conf_path, ltc_conf_path + ".last")
with (
open(ltc_conf_path + ".last") as fp_from,
open(ltc_conf_path, "w") as fp_to,
):
for line in fp_from:
if line.strip().endswith("=onion"):
fp_to.write(line.strip()[:-6] + "\n")
else:
fp_to.write(line)
if len(config_to_add) > 0:
logger.info('Rewriting litecoin.conf')
shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last')
with open(ltc_conf_path, 'a') as fp:
for line in config_to_add:
fp.write(line + '\n')
args = [daemon_bin, ]
add_datadir: bool = extra_config.get('add_datadir', True)
args = [
daemon_bin,
]
add_datadir: bool = extra_config.get("add_datadir", True)
if add_datadir:
args.append('-datadir=' + datadir_path)
args.append("-datadir=" + datadir_path)
args += opts
logger.info('Starting node {}'.format(daemon_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
logger.info("Starting node {}".format(daemon_bin))
logger.debug("Arguments {}".format(" ".join(args)))
opened_files = []
if extra_config.get('stdout_to_file', False):
stdout_dest = open(os.path.join(datadir_path, extra_config.get('stdout_filename', 'core_stdout.log')), 'w')
if extra_config.get("stdout_to_file", False):
stdout_dest = open(
os.path.join(
datadir_path, extra_config.get("stdout_filename", "core_stdout.log")
),
"w",
)
opened_files.append(stdout_dest)
stderr_dest = stdout_dest
else:
@@ -93,62 +109,113 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
stderr_dest = subprocess.PIPE
shell: bool = False
if extra_config.get('use_shell', False):
args = ' '.join(args)
if extra_config.get("use_shell", False):
args = " ".join(args)
shell = True
return Daemon(subprocess.Popen(args, shell=shell, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
return Daemon(
subprocess.Popen(
args,
shell=shell,
stdin=subprocess.PIPE,
stdout=stdout_dest,
stderr=stderr_dest,
cwd=datadir_path,
),
opened_files,
)
def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
config_filename = 'wownerod.conf' if daemon_bin.startswith('wow') else 'monerod.conf'
args = [daemon_path, '--non-interactive', '--config-file=' + os.path.join(datadir_path, config_filename)] + opts
logger.info('Starting node {}'.format(daemon_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
config_filename = (
"wownerod.conf" if daemon_bin.startswith("wow") else "monerod.conf"
)
args = [
daemon_path,
"--non-interactive",
"--config-file=" + os.path.join(datadir_path, config_filename),
] + opts
logger.info("Starting node {}".format(daemon_bin))
logger.debug("Arguments {}".format(" ".join(args)))
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
file_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w')
file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w')
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=file_stdout, stderr=file_stderr, cwd=datadir_path), [file_stdout, file_stderr])
file_stdout = open(os.path.join(datadir_path, "core_stdout.log"), "w")
file_stderr = open(os.path.join(datadir_path, "core_stderr.log"), "w")
return Daemon(
subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=file_stdout,
stderr=file_stderr,
cwd=datadir_path,
),
[file_stdout, file_stderr],
)
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_path, '--non-interactive']
args = [daemon_path, "--non-interactive"]
needs_rewrite: bool = False
config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy=']
config_to_remove = [
"daemon-address=",
"untrusted-daemon=",
"trusted-daemon=",
"proxy=",
]
data_dir = os.path.expanduser(node_dir)
wallet_config_filename = 'wownero-wallet-rpc.conf' if wallet_bin.startswith('wow') else 'monero_wallet.conf'
wallet_config_filename = (
"wownero-wallet-rpc.conf"
if wallet_bin.startswith("wow")
else "monero_wallet.conf"
)
config_path = os.path.join(data_dir, wallet_config_filename)
if os.path.exists(config_path):
args += ['--config-file=' + config_path]
args += ["--config-file=" + config_path]
with open(config_path) as fp:
for line in fp:
if any(line.startswith(config_line) for config_line in config_to_remove):
logger.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
if any(
line.startswith(config_line) for config_line in config_to_remove
):
logger.warning(
"Found old config in monero_wallet.conf: {}".format(
line.strip()
)
)
needs_rewrite = True
args += opts
if needs_rewrite:
logger.info('Rewriting wallet config')
shutil.copyfile(config_path, config_path + '.last')
with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to:
logger.info("Rewriting wallet config")
shutil.copyfile(config_path, config_path + ".last")
with open(config_path + ".last") as fp_from, open(config_path, "w") as fp_to:
for line in fp_from:
if not any(line.startswith(config_line) for config_line in config_to_remove):
if not any(
line.startswith(config_line) for config_line in config_to_remove
):
fp_to.write(line)
logger.info('Starting wallet daemon {}'.format(wallet_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
logger.info("Starting wallet daemon {}".format(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)
wallet_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w')
wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w')
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir), [wallet_stdout, wallet_stderr])
wallet_stdout = open(os.path.join(data_dir, "wallet_stdout.log"), "w")
wallet_stderr = open(os.path.join(data_dir, "wallet_stderr.log"), "w")
return Daemon(
subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=wallet_stdout,
stderr=wallet_stderr,
cwd=data_dir,
),
[wallet_stdout, wallet_stderr],
)
def ws_new_client(client, server):
@@ -165,25 +232,42 @@ def ws_client_left(client, server):
def ws_message_received(client, server, message):
if len(message) > 200:
message = 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)) + ('.exe' if os.name == 'nt' else '')
return coin_settings.get(
"core_binname", chainparams[coin_id].get("core_binname", default_name)
) + (".exe" if os.name == "nt" else "")
def getWalletBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get('wallet_binname', chainparams[coin_id].get('wallet_binname', default_name)) + ('.exe' if os.name == 'nt' else '')
return coin_settings.get(
"wallet_binname", chainparams[coin_id].get("wallet_binname", default_name)
) + (".exe" if os.name == "nt" else "")
def getCoreBinArgs(coin_id: int, coin_settings):
def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=False):
extra_args = []
if 'config_filename' in coin_settings:
extra_args.append('--conf=' + coin_settings['config_filename'])
if 'port' in coin_settings:
extra_args.append('--port=' + str(int(coin_settings['port'])))
if "config_filename" in coin_settings:
extra_args.append("--conf=" + coin_settings["config_filename"])
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
extra_args.append("--bind=127.0.0.1:" + str(int(coin_settings["port"])))
else:
extra_args.append("--port=" + str(int(coin_settings["port"])))
# BTC versions from v28 fail to start if the onionport is in use.
# 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:
port: int = coin_settings.get("port", 8333)
extra_args.append(f"--bind=0.0.0.0:{port}")
return extra_args
@@ -193,17 +277,21 @@ def runClient(fp, data_dir, chain, start_only_coins):
pids = []
threads = []
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
pids_path = os.path.join(data_dir, '.pids')
pids_path = os.path.join(data_dir, ".pids")
if os.getenv('WALLET_ENCRYPTION_PWD', '') != '':
if 'decred' in start_only_coins:
if os.getenv("WALLET_ENCRYPTION_PWD", "") != "":
if "decred" in start_only_coins:
# Workaround for dcrwallet requiring password for initial startup
logger.warning('Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred.')
logger.warning(
"Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred."
)
else:
raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.')
raise ValueError(
"Please unset the WALLET_ENCRYPTION_PWD environment variable."
)
if not os.path.exists(settings_path):
raise ValueError('Settings file not found: ' + str(settings_path))
raise ValueError("Settings file not found: " + str(settings_path))
with open(settings_path) as fs:
settings = json.load(fs)
@@ -215,7 +303,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
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()
@@ -224,155 +312,230 @@ def runClient(fp, data_dir, chain, start_only_coins):
settings = swap_client.settings
try:
# Try start daemons
for c, v in settings['chainclients'].items():
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
continue
try:
coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id)
except Exception as e:
logger.warning('Not starting unknown coin: {}'.format(c))
except Exception as e: # noqa: F841
logger.warning("Not starting unknown coin: {}".format(c))
continue
if c in ('monero', 'wownero'):
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
filename: str = getCoreBinName(coin_id, v, c + 'd')
if c in ("monero", "wownero"):
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
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("Started {} {}".format(filename, pid))
if v['manage_wallet_daemon'] is True:
swap_client.log.info(f'Starting {display_name} wallet daemon')
daemon_addr = '{}:{}'.format(v['rpchost'], v['rpcport'])
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(coin_id, v['rpchost'])
opts = ['--daemon-address', daemon_addr, ]
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
daemon_addr = "{}:{}".format(v["rpchost"], v["rpcport"])
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"]
)
opts = [
"--daemon-address",
daemon_addr,
]
proxy_log_str = ''
proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost'])
proxy_log_str = ""
proxy_host, proxy_port = swap_client.getXMRWalletProxy(
coin_id, v["rpchost"]
)
if proxy_host:
proxy_log_str = ' through proxy'
opts += ['--proxy', f'{proxy_host}:{proxy_port}', '--daemon-ssl-allow-any-cert', ]
proxy_log_str = " through proxy"
opts += [
"--proxy",
f"{proxy_host}:{proxy_port}",
"--daemon-ssl-allow-any-cert",
]
swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str))
swap_client.log.info(
"daemon-address: {} ({}){}".format(
daemon_addr,
"trusted" if trusted_daemon else "untrusted",
proxy_log_str,
)
)
daemon_rpcuser = v.get('rpcuser', '')
daemon_rpcpass = v.get('rpcpassword', '')
if daemon_rpcuser != '':
opts.append('--daemon-login')
opts.append(daemon_rpcuser + ':' + daemon_rpcpass)
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
opts.append("--daemon-login")
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
filename: str = getWalletBinName(coin_id, v, c + '-wallet-rpc')
opts.append(
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
)
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
)
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
swap_client.log.info("Started {} {}".format(filename, pid))
continue # /monero
if c == 'decred':
appdata = v['datadir']
extra_opts = [f'--appdata="{appdata}"', ]
use_shell: bool = True if os.name == 'nt' else False
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
filename: str = getCoreBinName(coin_id, v, 'dcrd')
if c == "decred":
appdata = v["datadir"]
extra_opts = [
f'--appdata="{appdata}"',
]
use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, "dcrd")
extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrd_stdout.log', 'use_shell': use_shell}
daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config))
extra_config = {
"add_datadir": False,
"stdout_to_file": True,
"stdout_filename": "dcrd_stdout.log",
"use_shell": use_shell,
}
daemons.append(
startDaemon(
appdata,
v["bindir"],
filename,
opts=extra_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
swap_client.log.info("Started {} {}".format(filename, pid))
if v['manage_wallet_daemon'] is True:
swap_client.log.info(f'Starting {display_name} wallet daemon')
filename: str = getWalletBinName(coin_id, v, 'dcrwallet')
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_pwd = v['wallet_pwd']
if wallet_pwd == '':
wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "":
# Only set when in startonlycoin mode
wallet_pwd = os.getenv('WALLET_ENCRYPTION_PWD', '')
if wallet_pwd != '':
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"')
extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrwallet_stdout.log', 'use_shell': use_shell}
daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config))
extra_config = {
"add_datadir": False,
"stdout_to_file": True,
"stdout_filename": "dcrwallet_stdout.log",
"use_shell": use_shell,
}
daemons.append(
startDaemon(
appdata,
v["bindir"],
filename,
opts=extra_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
swap_client.log.info("Started {} {}".format(filename, pid))
continue # /decred
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + 'd')
extra_opts = getCoreBinArgs(coin_id, v)
daemons.append(startDaemon(v['datadir'], v['bindir'], filename, opts=extra_opts))
filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
daemons.append(
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
)
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("Started {} {}".format(filename, pid))
if len(pids) > 0:
with open(pids_path, 'w') as fd:
with open(pids_path, "w") as fd:
for p in pids:
fd.write('{}:{}\n'.format(*p))
fd.write("{}:{}\n".format(*p))
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
if len(start_only_coins) > 0:
logger.info(f'Only running {start_only_coins}. Manually exit with Ctrl + c when ready.')
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
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)
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}.')
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 = 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.')
logger.info("Exit with Ctrl + c.")
while not swap_client.delay_event.wait(0.5):
swap_client.update()
except Exception as ex:
except Exception as e: # noqa: F841
traceback.print_exc()
if swap_client.ws_server:
try:
swap_client.log.info('Stopping websocket server.')
swap_client.log.info("Stopping websocket server.")
swap_client.ws_server.shutdown_gracefully()
except Exception as ex:
except Exception as e: # noqa: F841
traceback.print_exc()
swap_client.finalise()
swap_client.log.info('Stopping HTTP threads.')
swap_client.log.info("Stopping HTTP threads.")
for t in threads:
try:
t.stop()
t.join()
except Exception as ex:
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("Interrupting {}".format(d.handle.pid))
try:
d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT)
d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
except Exception as e:
swap_client.log.info('Interrupting %d, error %s', d.handle.pid, str(e))
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
for d in daemons:
try:
d.handle.wait(timeout=120)
@@ -380,97 +543,107 @@ 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}")
if os.path.exists(pids_path):
with open(pids_path) as fd:
lines = fd.read().split('\n')
still_running = ''
lines = fd.read().split("\n")
still_running = ""
for ln in lines:
try:
if not int(ln.split(':')[1]) in closed_pids:
still_running += ln + '\n'
if int(ln.split(":")[1]) not in closed_pids:
still_running += ln + "\n"
except Exception:
pass
with open(pids_path, 'w') as fd:
with open(pids_path, "w") as fd:
fd.write(still_running)
def printVersion():
logger.info('Basicswap version: %s', __version__)
logger.info("Basicswap version: %s", __version__)
def printHelp():
print('Usage: basicswap-run ')
print('\n--help, -h Print help.')
print('--version, -v Print version.')
print('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR))
print('--mainnet Run in mainnet mode.')
print('--testnet Run in testnet mode.')
print('--regtest Run in regtest mode.')
print('--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing.')
print("Usage: basicswap-run ")
print("\n--help, -h Print help.")
print("--version, -v Print version.")
print(
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
cfg.BASICSWAP_DATADIR
)
)
print("--mainnet Run in mainnet mode.")
print("--testnet Run in testnet mode.")
print("--regtest Run in regtest mode.")
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
def main():
data_dir = None
chain = 'mainnet'
chain = "mainnet"
start_only_coins = set()
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
logger.warning('Unknown argument %s', v)
if len(v) < 2 or v[0] != "-":
logger.warning("Unknown argument %s", v)
continue
s = v.split('=')
s = v.split("=")
name = s[0].strip()
for i in range(2):
if name[0] == '-':
if name[0] == "-":
name = name[1:]
if name == 'v' or name == 'version':
if name == "v" or name == "version":
printVersion()
return 0
if name == 'h' or name == 'help':
if name == "h" or name == "help":
printHelp()
return 0
if name in ('mainnet', 'testnet', 'regtest'):
if name in ("mainnet", "testnet", "regtest"):
chain = name
continue
if len(s) == 2:
if name == 'datadir':
if name == "datadir":
data_dir = os.path.expanduser(s[1])
continue
if name == 'startonlycoin':
for coin in [s.lower() for s in s[1].split(',')]:
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}')
raise ValueError(f"Unknown coin: {coin}")
start_only_coins.add(coin)
continue
logger.warning('Unknown argument %s', v)
logger.warning("Unknown argument %s", v)
if os.name == 'nt':
logger.warning('Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead.')
if os.name == "nt":
logger.warning(
"Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead."
)
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("Using datadir: %s", data_dir)
logger.info("Chain: %s", 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')
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)
logger.info('Done.')
print("Done.")
return swap_client.fail_code if swap_client is not None else 0
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# 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,8 +10,8 @@ from .util import (
COIN,
)
XMR_COIN = 10 ** 12
WOW_COIN = 10 ** 11
XMR_COIN = 10**12
WOW_COIN = 10**11
class Coins(IntEnum):
@@ -31,463 +32,521 @@ class Coins(IntEnum):
LTC_MWEB = 15
# ZANO = 16
BCH = 17
DOGE = 18
chainparams = {
Coins.PART: {
'name': 'particl',
'ticker': 'PART',
'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 2,
'decimal_places': 8,
'mainnet': {
'rpcport': 51735,
'pubkey_address': 0x38,
'script_address': 0x3c,
'key_prefix': 0x6c,
'stealth_key_prefix': 0x14,
'hrp': 'pw',
'bip44': 44,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "particl",
"ticker": "PART",
"message_magic": "Bitcoin Signed Message:\n",
"blocks_target": 60 * 2,
"decimal_places": 8,
"mainnet": {
"rpcport": 51735,
"pubkey_address": 0x38,
"script_address": 0x3C,
"key_prefix": 0x6C,
"stealth_key_prefix": 0x14,
"hrp": "pw",
"bip44": 44,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 51935,
'pubkey_address': 0x76,
'script_address': 0x7a,
'key_prefix': 0x2e,
'stealth_key_prefix': 0x15,
'hrp': 'tpw',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 51935,
"pubkey_address": 0x76,
"script_address": 0x7A,
"key_prefix": 0x2E,
"stealth_key_prefix": 0x15,
"hrp": "tpw",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
"regtest": {
"rpcport": 51936,
"pubkey_address": 0x76,
"script_address": 0x7A,
"key_prefix": 0x2E,
"stealth_key_prefix": 0x15,
"hrp": "rtpw",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 51936,
'pubkey_address': 0x76,
'script_address': 0x7a,
'key_prefix': 0x2e,
'stealth_key_prefix': 0x15,
'hrp': 'rtpw',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.BTC: {
'name': 'bitcoin',
'ticker': 'BTC',
'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 10,
'decimal_places': 8,
'mainnet': {
'rpcport': 8332,
'pubkey_address': 0,
'script_address': 5,
'key_prefix': 128,
'hrp': 'bc',
'bip44': 0,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "bitcoin",
"ticker": "BTC",
"message_magic": "Bitcoin Signed Message:\n",
"blocks_target": 60 * 10,
"decimal_places": 8,
"mainnet": {
"rpcport": 8332,
"pubkey_address": 0,
"script_address": 5,
"key_prefix": 128,
"hrp": "bc",
"bip44": 0,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x0488B21E,
"ext_secret_key_prefix": 0x0488ADE4,
},
'testnet': {
'rpcport': 18332,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'tb',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 18332,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "tb",
"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": "bcrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'bcrt',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.LTC: {
'name': 'litecoin',
'ticker': 'LTC',
'message_magic': 'Litecoin Signed Message:\n',
'blocks_target': 60 * 1,
'decimal_places': 8,
'mainnet': {
'rpcport': 9332,
'pubkey_address': 48,
'script_address': 5,
'script_address2': 50,
'key_prefix': 176,
'hrp': 'ltc',
'bip44': 2,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "litecoin",
"ticker": "LTC",
"message_magic": "Litecoin Signed Message:\n",
"blocks_target": 60 * 1,
"decimal_places": 8,
"mainnet": {
"rpcport": 9332,
"pubkey_address": 48,
"script_address": 5,
"script_address2": 50,
"key_prefix": 176,
"hrp": "ltc",
"bip44": 2,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 19332,
'pubkey_address': 111,
'script_address': 196,
'script_address2': 58,
'key_prefix': 239,
'hrp': 'tltc',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet4',
"testnet": {
"rpcport": 19332,
"pubkey_address": 111,
"script_address": 196,
"script_address2": 58,
"key_prefix": 239,
"hrp": "tltc",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet4",
},
"regtest": {
"rpcport": 19443,
"pubkey_address": 111,
"script_address": 196,
"script_address2": 58,
"key_prefix": 239,
"hrp": "rltc",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
},
Coins.DOGE: {
"name": "dogecoin",
"ticker": "DOGE",
"message_magic": "Dogecoin Signed Message:\n",
"blocks_target": 60 * 1,
"decimal_places": 8,
"mainnet": {
"rpcport": 22555,
"pubkey_address": 30,
"script_address": 22,
"key_prefix": 158,
"hrp": "doge",
"bip44": 3,
"min_amount": 100000, # TODO increase above fee
"max_amount": 10000000 * COIN,
},
"testnet": {
"rpcport": 44555,
"pubkey_address": 113,
"script_address": 196,
"key_prefix": 241,
"hrp": "tdge",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet4",
},
"regtest": {
"rpcport": 18332,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "rdge",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 19443,
'pubkey_address': 111,
'script_address': 196,
'script_address2': 58,
'key_prefix': 239,
'hrp': 'rltc',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.DCR: {
'name': 'decred',
'ticker': 'DCR',
'message_magic': 'Decred Signed Message:\n',
'blocks_target': 60 * 5,
'decimal_places': 8,
'mainnet': {
'rpcport': 9109,
'pubkey_address': 0x073f,
'script_address': 0x071a,
'key_prefix': 0x22de,
'bip44': 42,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "decred",
"ticker": "DCR",
"message_magic": "Decred Signed Message:\n",
"blocks_target": 60 * 5,
"decimal_places": 8,
"has_multiwallet": False,
"mainnet": {
"rpcport": 9109,
"pubkey_address": 0x073F,
"script_address": 0x071A,
"key_prefix": 0x22DE,
"bip44": 42,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 19109,
'pubkey_address': 0x0f21,
'script_address': 0x0efc,
'key_prefix': 0x230e,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 19109,
"pubkey_address": 0x0F21,
"script_address": 0x0EFC,
"key_prefix": 0x230E,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
},
"regtest": { # simnet
"rpcport": 18656,
"pubkey_address": 0x0E91,
"script_address": 0x0E6C,
"key_prefix": 0x2307,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': { # simnet
'rpcport': 18656,
'pubkey_address': 0x0e91,
'script_address': 0x0e6c,
'key_prefix': 0x2307,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.NMC: {
'name': 'namecoin',
'ticker': 'NMC',
'message_magic': 'Namecoin Signed Message:\n',
'blocks_target': 60 * 10,
'decimal_places': 8,
'mainnet': {
'rpcport': 8336,
'pubkey_address': 52,
'script_address': 13,
'hrp': 'nc',
'bip44': 7,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "namecoin",
"ticker": "NMC",
"message_magic": "Namecoin Signed Message:\n",
"blocks_target": 60 * 10,
"decimal_places": 8,
"mainnet": {
"rpcport": 8336,
"pubkey_address": 52,
"script_address": 13,
"hrp": "nc",
"bip44": 7,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 18336,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'tn',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 18336,
"pubkey_address": 111,
"script_address": 196,
"hrp": "tn",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"hrp": "ncrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'ncrt',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.XMR: {
'name': 'monero',
'ticker': 'XMR',
'client': 'xmr',
'decimal_places': 12,
'mainnet': {
'rpcport': 18081,
'walletrpcport': 18082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
"name": "monero",
"ticker": "XMR",
"client": "xmr",
"decimal_places": 12,
"mainnet": {
"rpcport": 18081,
"walletrpcport": 18082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
'testnet': {
'rpcport': 28081,
'walletrpcport': 28082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
"testnet": {
"rpcport": 28081,
"walletrpcport": 28082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
"regtest": {
"rpcport": 18081,
"walletrpcport": 18082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
'regtest': {
'rpcport': 18081,
'walletrpcport': 18082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
}
},
Coins.WOW: {
'name': 'wownero',
'ticker': 'WOW',
'client': 'wow',
'decimal_places': 11,
'mainnet': {
'rpcport': 34568,
'walletrpcport': 34572, # todo
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
"name": "wownero",
"ticker": "WOW",
"client": "wow",
"decimal_places": 11,
"mainnet": {
"rpcport": 34568,
"walletrpcport": 34572, # todo
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
'testnet': {
'rpcport': 44568,
'walletrpcport': 44572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
"testnet": {
"rpcport": 44568,
"walletrpcport": 44572,
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
"regtest": {
"rpcport": 54568,
"walletrpcport": 54572,
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
'regtest': {
'rpcport': 54568,
'walletrpcport': 54572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
}
},
Coins.PIVX: {
'name': 'pivx',
'ticker': 'PIVX',
'display_name': 'PIVX',
'message_magic': 'DarkNet Signed Message:\n',
'blocks_target': 60 * 1,
'decimal_places': 8,
'has_cltv': True,
'has_csv': False,
'has_segwit': False,
'mainnet': {
'rpcport': 51473,
'pubkey_address': 30,
'script_address': 13,
'key_prefix': 212,
'bip44': 119,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "pivx",
"ticker": "PIVX",
"display_name": "PIVX",
"message_magic": "DarkNet Signed Message:\n",
"blocks_target": 60 * 1,
"decimal_places": 8,
"has_cltv": True,
"has_csv": False,
"has_segwit": False,
"mainnet": {
"rpcport": 51473,
"pubkey_address": 30,
"script_address": 13,
"key_prefix": 212,
"bip44": 119,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 51475,
'pubkey_address': 139,
'script_address': 19,
'key_prefix': 239,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet4',
"testnet": {
"rpcport": 51475,
"pubkey_address": 139,
"script_address": 19,
"key_prefix": 239,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet4",
},
"regtest": {
"rpcport": 51477,
"pubkey_address": 139,
"script_address": 19,
"key_prefix": 239,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 51477,
'pubkey_address': 139,
'script_address': 19,
'key_prefix': 239,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.DASH: {
'name': 'dash',
'ticker': 'DASH',
'message_magic': 'DarkCoin Signed Message:\n',
'blocks_target': 60 * 2.5,
'decimal_places': 8,
'has_csv': True,
'has_segwit': False,
'mainnet': {
'rpcport': 9998,
'pubkey_address': 76,
'script_address': 16,
'key_prefix': 204,
'hrp': '',
'bip44': 5,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "dash",
"ticker": "DASH",
"message_magic": "DarkCoin Signed Message:\n",
"blocks_target": 60 * 2.5,
"decimal_places": 8,
"has_csv": True,
"has_segwit": False,
"mainnet": {
"rpcport": 9998,
"pubkey_address": 76,
"script_address": 16,
"key_prefix": 204,
"hrp": "",
"bip44": 5,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 19998,
'pubkey_address': 140,
'script_address': 19,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 19998,
"pubkey_address": 140,
"script_address": 19,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
"regtest": {
"rpcport": 18332,
"pubkey_address": 140,
"script_address": 19,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 18332,
'pubkey_address': 140,
'script_address': 19,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.FIRO: {
'name': 'firo',
'ticker': 'FIRO',
'message_magic': 'Zcoin Signed Message:\n',
'blocks_target': 60 * 10,
'decimal_places': 8,
'has_cltv': False,
'has_csv': False,
'has_segwit': False,
'mainnet': {
'rpcport': 8888,
'pubkey_address': 82,
'script_address': 7,
'key_prefix': 210,
'hrp': '',
'bip44': 136,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "firo",
"ticker": "FIRO",
"message_magic": "Zcoin Signed Message:\n",
"blocks_target": 60 * 10,
"decimal_places": 8,
"has_cltv": False,
"has_csv": False,
"has_segwit": False,
"has_multiwallet": False,
"mainnet": {
"rpcport": 8888,
"pubkey_address": 82,
"script_address": 7,
"key_prefix": 210,
"hrp": "",
"bip44": 136,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 18888,
'pubkey_address': 65,
'script_address': 178,
'key_prefix': 185,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 18888,
"pubkey_address": 65,
"script_address": 178,
"key_prefix": 185,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
"regtest": {
"rpcport": 28888,
"pubkey_address": 65,
"script_address": 178,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 28888,
'pubkey_address': 65,
'script_address': 178,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.NAV: {
'name': 'navcoin',
'ticker': 'NAV',
'message_magic': 'Navcoin Signed Message:\n',
'blocks_target': 30,
'decimal_places': 8,
'has_csv': True,
'has_segwit': True,
'mainnet': {
'rpcport': 44444,
'pubkey_address': 53,
'script_address': 85,
'key_prefix': 150,
'hrp': '',
'bip44': 130,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "navcoin",
"ticker": "NAV",
"message_magic": "Navcoin Signed Message:\n",
"blocks_target": 30,
"decimal_places": 8,
"has_csv": True,
"has_segwit": True,
"has_multiwallet": False,
"mainnet": {
"rpcport": 44444,
"pubkey_address": 53,
"script_address": 85,
"key_prefix": 150,
"hrp": "",
"bip44": 130,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 44445,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 44445,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
"regtest": {
"rpcport": 44446,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 44446,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.BCH: {
'name': 'bitcoincash',
'ticker': 'BCH',
'display_name': 'Bitcoin Cash',
'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 2,
'decimal_places': 8,
'has_cltv': True,
'has_csv': True,
'has_segwit': False,
'cli_binname': 'bitcoin-cli',
'core_binname': 'bitcoind',
'mainnet': {
'rpcport': 8332,
'pubkey_address': 0,
'script_address': 5,
'key_prefix': 128,
'hrp': 'bitcoincash',
'bip44': 0,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "bitcoincash",
"ticker": "BCH",
"display_name": "Bitcoin Cash",
"message_magic": "Bitcoin Signed Message:\n",
"blocks_target": 60 * 2,
"decimal_places": 8,
"has_cltv": True,
"has_csv": True,
"has_segwit": False,
"cli_binname": "bitcoin-cli",
"core_binname": "bitcoind",
"mainnet": {
"rpcport": 8332,
"pubkey_address": 0,
"script_address": 5,
"key_prefix": 128,
"hrp": "bitcoincash",
"bip44": 0,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 18332,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'bchtest',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 18332,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "bchtest",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "bchreg",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'bchreg',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
}
name_map = {}
ticker_map = {}
for c, params in chainparams.items():
ticker_map[params['ticker'].lower()] = c
name_map[params["name"].lower()] = c
ticker_map[params["ticker"].lower()] = c
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}")

View File

@@ -6,35 +6,51 @@
import os
CONFIG_FILENAME = 'basicswap.json'
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', os.path.join('~', '.basicswap'))
CONFIG_FILENAME = "basicswap.json"
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
DEFAULT_ALLOW_CORS = False
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')))
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"))
)
bin_suffix = ('.exe' if os.name == 'nt' else '')
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
PARTICLD = os.getenv('PARTICLD', 'particld' + bin_suffix)
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + bin_suffix)
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + bin_suffix)
bin_suffix = ".exe" if os.name == "nt" else ""
PARTICL_BINDIR = os.path.expanduser(
os.getenv("PARTICL_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "particl"))
)
PARTICLD = os.getenv("PARTICLD", "particld" + bin_suffix)
PARTICL_CLI = os.getenv("PARTICL_CLI", "particl-cli" + bin_suffix)
PARTICL_TX = os.getenv("PARTICL_TX", "particl-tx" + bin_suffix)
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoin')))
BITCOIND = os.getenv('BITCOIND', 'bitcoind' + bin_suffix)
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + bin_suffix)
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + bin_suffix)
BITCOIN_BINDIR = os.path.expanduser(
os.getenv("BITCOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "bitcoin"))
)
BITCOIND = os.getenv("BITCOIND", "bitcoind" + bin_suffix)
BITCOIN_CLI = os.getenv("BITCOIN_CLI", "bitcoin-cli" + bin_suffix)
BITCOIN_TX = os.getenv("BITCOIN_TX", "bitcoin-tx" + bin_suffix)
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'litecoin')))
LITECOIND = os.getenv('LITECOIND', 'litecoind' + bin_suffix)
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + bin_suffix)
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + bin_suffix)
LITECOIN_BINDIR = os.path.expanduser(
os.getenv("LITECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "litecoin"))
)
LITECOIND = os.getenv("LITECOIND", "litecoind" + bin_suffix)
LITECOIN_CLI = os.getenv("LITECOIN_CLI", "litecoin-cli" + bin_suffix)
LITECOIN_TX = os.getenv("LITECOIN_TX", "litecoin-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)
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)
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + 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"))
)
XMRD = os.getenv("XMRD", "monerod" + bin_suffix)
XMR_WALLET_RPC = os.getenv("XMR_WALLET_RPC", "monero-wallet-rpc" + bin_suffix)
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.

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)

File diff suppressed because it is too large Load Diff

View File

@@ -1,327 +1,427 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
from sqlalchemy.sql import text
from sqlalchemy.orm import scoped_session
from .db import (
AutomationStrategy,
BidState,
Concepts,
AutomationStrategy,
CURRENT_DB_DATA_VERSION,
CURRENT_DB_VERSION,
CURRENT_DB_DATA_VERSION)
)
from .basicswap_util import (
BidStates,
strBidState,
canAcceptBidState,
isActiveBidState,
isErrorBidState,
isFailingBidState,
isFinalBidState,
strBidState,
)
def addBidState(self, state, now, cursor):
self.add(
BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
in_error=isErrorBidState(state),
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
can_accept=canAcceptBidState(state),
label=strBidState(state),
created_at=now,
),
cursor,
)
def upgradeDatabaseData(self, data_version):
if data_version >= CURRENT_DB_DATA_VERSION:
return
self.log.info('Upgrading database records from version %d to %d.', data_version, CURRENT_DB_DATA_VERSION)
with self.mxDB:
try:
session = scoped_session(self.session_factory)
self.log.info(
"Upgrading database records from version %d to %d.",
data_version,
CURRENT_DB_DATA_VERSION,
)
cursor = self.openDB()
try:
now = int(time.time())
now = int(time.time())
if data_version < 1:
session.add(AutomationStrategy(
if data_version < 1:
self.add(
AutomationStrategy(
active_ind=1,
label='Accept All',
label="Accept All",
type_ind=Concepts.OFFER,
data=json.dumps({'exact_rate_only': True,
'max_concurrent_bids': 5}).encode('utf-8'),
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
).encode("utf-8"),
only_known_identities=False,
created_at=now))
session.add(AutomationStrategy(
created_at=now,
),
cursor,
)
self.add(
AutomationStrategy(
active_ind=1,
label='Accept Known',
label="Accept Known",
type_ind=Concepts.OFFER,
data=json.dumps({'exact_rate_only': True,
'max_concurrent_bids': 5}).encode('utf-8'),
data=json.dumps(
{"exact_rate_only": True, "max_concurrent_bids": 5}
).encode("utf-8"),
only_known_identities=True,
note='Accept bids from identities with previously successful swaps only',
created_at=now))
note="Accept bids from identities with previously successful swaps only",
created_at=now,
),
cursor,
)
for state in BidStates:
session.add(BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
in_error=isErrorBidState(state),
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
label=strBidState(state),
created_at=now))
for state in BidStates:
addBidState(self, state, now, cursor)
if data_version > 0 and data_version < 2:
for state in (BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX):
session.add(BidState(
if data_version > 0 and data_version < 2:
for state in (
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
):
self.add(
BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
label=strBidState(state),
created_at=now))
if data_version > 0 and data_version < 3:
for state in BidStates:
in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state)
session.execute(text('UPDATE bidstates SET 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, 'state_id': int(state)}))
if data_version > 0 and data_version < 4:
for state in (BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_ACCEPTED):
session.add(BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
in_error=isErrorBidState(state),
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
label=strBidState(state),
created_at=now))
created_at=now,
),
cursor,
)
if data_version > 0 and data_version < 3:
for state in BidStates:
in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state)
cursor.execute(
"UPDATE bidstates SET 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,
"state_id": int(state),
},
)
if data_version > 0 and data_version < 4:
for state in (
BidStates.BID_REQUEST_SENT,
BidStates.BID_REQUEST_ACCEPTED,
):
addBidState(self, state, now, cursor)
self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKV('db_data_version', self.db_data_version, session)
session.commit()
self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
finally:
session.close()
session.remove()
if data_version > 0 and data_version < 5:
for state in (
BidStates.BID_EXPIRED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
):
addBidState(self, state, now, cursor)
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)
)
finally:
self.closeDB(cursor, commit=False)
def upgradeDatabase(self, db_version):
if db_version >= CURRENT_DB_VERSION:
return
self.log.info('Upgrading database from version %d to %d.', db_version, CURRENT_DB_VERSION)
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
)
while True:
session = scoped_session(self.session_factory)
try:
cursor = self.openDB()
current_version = db_version
if current_version == 6:
session.execute(text('ALTER TABLE bids ADD COLUMN security_token BLOB'))
session.execute(text('ALTER TABLE offers ADD COLUMN security_token BLOB'))
db_version += 1
elif current_version == 7:
session.execute(text('ALTER TABLE transactions ADD COLUMN block_hash BLOB'))
session.execute(text('ALTER TABLE transactions ADD COLUMN block_height INTEGER'))
session.execute(text('ALTER TABLE transactions ADD COLUMN block_time INTEGER'))
db_version += 1
elif current_version == 8:
session.execute(text('''
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:
session.execute(text('ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR'))
db_version += 1
elif current_version == 10:
session.execute(text('ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER'))
session.execute(text('ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER'))
session.execute(text('ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR'))
session.execute(text('ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR'))
session.execute(text('UPDATE smsgaddresses SET active_ind = 1, created_at = 1'))
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")
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"
)
session.execute(text('ALTER TABLE offers ADD COLUMN addr_to VARCHAR'))
session.execute(text(f'UPDATE offers SET addr_to = "{self.network_addr}"'))
db_version += 1
elif current_version == 11:
session.execute(text('ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER'))
session.execute(text('ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER'))
session.execute(text('ALTER TABLE bids ADD COLUMN protocol_version INTEGER'))
session.execute(text('ALTER TABLE offers ADD COLUMN protocol_version INTEGER'))
session.execute(text('ALTER TABLE transactions ADD COLUMN tx_data BLOB'))
db_version += 1
elif current_version == 12:
session.execute(text('''
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))'''))
session.execute(text('ALTER TABLE bids ADD COLUMN reject_code INTEGER'))
session.execute(text('ALTER TABLE bids ADD COLUMN rate INTEGER'))
session.execute(text('ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER'))
session.execute(text('ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER'))
db_version += 1
elif current_version == 13:
db_version += 1
session.execute(text('''
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,
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))'''))
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
session.execute(text('''
CREATE TABLE automationlinks (
record_id INTEGER NOT NULL,
active_ind INTEGER,
cursor.execute(
"""
CREATE TABLE automationlinks (
record_id INTEGER NOT NULL,
active_ind INTEGER,
linked_type INTEGER,
linked_id BLOB,
strategy_id INTEGER,
linked_type INTEGER,
linked_id BLOB,
strategy_id INTEGER,
data BLOB,
repeat_limit INTEGER,
repeat_count INTEGER,
data BLOB,
repeat_limit INTEGER,
repeat_count INTEGER,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))'''))
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
session.execute(text('''
CREATE TABLE history (
record_id INTEGER NOT NULL,
concept_type INTEGER,
concept_id INTEGER,
changed_data BLOB,
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))'''))
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
session.execute(text('''
CREATE TABLE bidstates (
record_id INTEGER NOT NULL,
active_ind INTEGER,
state_id INTEGER,
label VARCHAR,
in_progress INTEGER,
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))'''))
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
session.execute(text('ALTER TABLE wallets ADD COLUMN active_ind INTEGER'))
session.execute(text('ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER'))
session.execute(text('ALTER TABLE eventqueue RENAME TO actions'))
session.execute(text('ALTER TABLE actions RENAME COLUMN event_id TO action_id'))
session.execute(text('ALTER TABLE actions RENAME COLUMN event_type TO action_type'))
session.execute(text('ALTER TABLE actions RENAME COLUMN event_data TO action_data'))
elif current_version == 14:
db_version += 1
session.execute(text('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB'))
session.execute(text('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
session.execute(text('''
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
session.execute(text('''
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
session.execute(text('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER'))
session.execute(text('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER'))
session.execute(text('ALTER TABLE knownidentities ADD COLUMN data BLOB'))
session.execute(text('UPDATE knownidentities SET active_ind = 1'))
elif current_version == 18:
db_version += 1
session.execute(text('ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING'))
session.execute(text('ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING'))
elif current_version == 19:
db_version += 1
session.execute(text('ALTER TABLE bidstates ADD COLUMN in_error INTEGER'))
session.execute(text('ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER'))
session.execute(text('ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER'))
elif current_version == 20:
db_version += 1
session.execute(text('''
CREATE TABLE message_links (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
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,
linked_type INTEGER,
linked_id BLOB,
msg_type INTEGER,
msg_sequence INTEGER,
msg_id BLOB,
PRIMARY KEY (record_id))'''))
session.execute(text('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER'))
elif current_version == 21:
db_version += 1
session.execute(text('ALTER TABLE offers ADD COLUMN proof_utxos BLOB'))
session.execute(text('ALTER TABLE bids ADD COLUMN proof_utxos BLOB'))
elif current_version == 22:
db_version += 1
session.execute(text('ALTER TABLE offers ADD COLUMN amount_to INTEGER'))
elif current_version == 23:
db_version += 1
session.execute(text('''
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))'''))
session.execute(text('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB'))
if current_version != db_version:
self.db_version = db_version
self.setIntKV('db_version', db_version, session)
session.commit()
session.close()
session.remove()
self.log.info('Upgraded database to version {}'.format(self.db_version))
continue
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))
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.')
raise ValueError("Unable to upgrade database.")

View File

@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023-2024 The BSX Developers
# Copyright (c) 2023-2024 The Basicswap Developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from sqlalchemy.sql import text
from .db import (
Concepts,
)
@@ -13,47 +12,122 @@ from .db import (
def remove_expired_data(self, time_offset: int = 0):
now: int = self.getTime()
try:
session = self.openSession()
cursor = self.openDB()
active_bids_insert = self.activeBidsQueryStr(now, '', 'b2')
query_str = f'''
active_bids_insert: str = self.activeBidsQueryStr("", "b2")
query_str = f"""
SELECT o.offer_id FROM offers o
WHERE o.expire_at <= :expired_at AND 0 = (SELECT COUNT(*) FROM bids b2 WHERE b2.offer_id = o.offer_id AND {active_bids_insert})
'''
"""
num_offers = 0
num_bids = 0
offer_rows = session.execute(text(query_str), {'expired_at': now - time_offset})
offer_rows = cursor.execute(
query_str, {"now": now, "expired_at": now - time_offset}
)
for offer_row in offer_rows:
num_offers += 1
bid_rows = session.execute(text('SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id'), {'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]},
)
for bid_row in bid_rows:
num_bids += 1
session.execute(text('DELETE FROM transactions WHERE transactions.bid_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('DELETE FROM actions WHERE actions.linked_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('DELETE FROM bids WHERE bids.bid_id = :bid_id'), {'bid_id': bid_row[0]})
session.execute(text('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 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]},
)
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('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]})
session.execute(text('DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id'), {'offer_id': offer_row[0]})
session.execute(text('DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id'), {'offer_id': offer_row[0]})
session.execute(text('DELETE FROM actions WHERE actions.linked_id = :offer_id'), {'offer_id': offer_row[0]})
session.execute(text('DELETE FROM offers WHERE offers.offer_id = :offer_id'), {'offer_id': offer_row[0]})
session.execute(text('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id'), {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
cursor.execute(
"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]},
)
if num_offers > 0 or num_bids > 0:
self.log.info('Removed data for {} expired offer{} and {} bid{}.'.format(num_offers, 's' if num_offers != 1 else '', num_bids, 's' if num_bids != 1 else ''))
self.log.info(
"Removed data for {} expired offer{} and {} bid{}.".format(
num_offers,
"s" if num_offers != 1 else "",
num_bids,
"s" if num_bids != 1 else "",
)
)
session.execute(text('DELETE FROM checkedblocks WHERE created_at <= :expired_at'), {'expired_at': now - time_offset})
cursor.execute(
"DELETE FROM checkedblocks WHERE created_at <= :expired_at",
{"expired_at": now - time_offset},
)
finally:
self.closeSession(session)
self.closeDB(cursor)

View File

@@ -13,8 +13,8 @@ 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')
y += (x & 1) << 255
return y.to_bytes(32, byteorder="little")
def hashToEd25519(bytes_in):
@@ -22,8 +22,8 @@ def hashToEd25519(bytes_in):
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')
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
@@ -33,4 +33,4 @@ def hashToEd25519(bytes_in):
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
return P
hashed = hashlib.sha256(hashed).digest()
raise ValueError('hashToEd25519 failed')
raise ValueError("hashToEd25519 failed")

View File

@@ -7,7 +7,7 @@
import json
class Explorer():
class Explorer:
def __init__(self, swapclient, coin_type, base_url):
self.swapclient = swapclient
self.coin_type = coin_type
@@ -15,82 +15,94 @@ class Explorer():
self.log = self.swapclient.log
def readURL(self, url):
self.log.debug('Explorer url: {}'.format(url))
self.log.debug("Explorer url: {}".format(url))
return self.swapclient.readURL(url)
class ExplorerInsight(Explorer):
def getChainHeight(self):
return json.loads(self.readURL(self.base_url + '/sync'))['blockChainHeight']
return json.loads(self.readURL(self.base_url + "/sync"))["blockChainHeight"]
def getBlock(self, block_hash):
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
return data
def getTransaction(self, txid):
data = json.loads(self.readURL(self.base_url + '/tx/{}'.format(txid)))
data = json.loads(self.readURL(self.base_url + "/tx/{}".format(txid)))
return data
def getBalance(self, address):
data = json.loads(self.readURL(self.base_url + '/addr/{}/balance'.format(address)))
data = json.loads(
self.readURL(self.base_url + "/addr/{}/balance".format(address))
)
return data
def lookupUnspentByAddress(self, address):
data = json.loads(self.readURL(self.base_url + '/addr/{}/utxo'.format(address)))
data = json.loads(self.readURL(self.base_url + "/addr/{}/utxo".format(address)))
rv = []
for utxo in data:
rv.append({
'txid': utxo['txid'],
'index': utxo['vout'],
'height': utxo['height'],
'n_conf': utxo['confirmations'],
'value': utxo['satoshis'],
})
rv.append(
{
"txid": utxo["txid"],
"index": utxo["vout"],
"height": utxo["height"],
"n_conf": utxo["confirmations"],
"value": utxo["satoshis"],
}
)
return rv
class ExplorerBitAps(Explorer):
def getChainHeight(self):
return json.loads(self.readURL(self.base_url + '/block/last'))['data']['block']['height']
return json.loads(self.readURL(self.base_url + "/block/last"))["data"]["block"][
"height"
]
def getBlock(self, block_hash):
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
return data
def getTransaction(self, txid):
data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(txid)))
data = json.loads(self.readURL(self.base_url + "/transaction/{}".format(txid)))
return data
def getBalance(self, address):
data = json.loads(self.readURL(self.base_url + '/address/state/' + address))
return data['data']['balance']
data = json.loads(self.readURL(self.base_url + "/address/state/" + address))
return data["data"]["balance"]
def lookupUnspentByAddress(self, address):
# Can't get unspents return only if exactly one transaction exists
data = json.loads(self.readURL(self.base_url + '/address/transactions/' + address))
data = json.loads(
self.readURL(self.base_url + "/address/transactions/" + address)
)
try:
assert data['data']['list'] == 1
assert data["data"]["list"] == 1
except Exception as ex:
self.log.debug('Explorer error: {}'.format(str(ex)))
self.log.debug("Explorer error: {}".format(str(ex)))
return None
tx = data['data']['list'][0]
tx_data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(tx['txId'])))['data']
tx = data["data"]["list"][0]
tx_data = json.loads(
self.readURL(self.base_url + "/transaction/{}".format(tx["txId"]))
)["data"]
for i, vout in tx_data['vOut'].items():
if vout['address'] == address:
return [{
'txid': tx_data['txId'],
'index': int(i),
'height': tx_data['blockHeight'],
'n_conf': tx_data['confirmations'],
'value': vout['value'],
}]
for i, vout in tx_data["vOut"].items():
if vout["address"] == address:
return [
{
"txid": tx_data["txId"],
"index": int(i),
"height": tx_data["blockHeight"],
"n_conf": tx_data["confirmations"],
"value": vout["value"],
}
]
class ExplorerChainz(Explorer):
def getChainHeight(self):
return int(self.readURL(self.base_url + '?q=getblockcount'))
return int(self.readURL(self.base_url + "?q=getblockcount"))
def lookupUnspentByAddress(self, address):
chain_height = self.getChainHeight()
self.log.debug('[rm] chain_height %d', chain_height)
self.log.debug("[rm] chain_height %d", chain_height)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -56,12 +57,12 @@ from .ui.page_identity import page_identity
from .ui.page_smsgaddresses import page_smsgaddresses
from .ui.page_debug import page_debug
env = Environment(loader=PackageLoader('basicswap', 'templates'))
env.filters['formatts'] = format_timestamp
env = Environment(loader=PackageLoader("basicswap", "templates"))
env.filters["formatts"] = format_timestamp
def extractDomain(url):
return url.split('://', 1)[1].split('/', 1)[0]
return url.split("://", 1)[1].split("/", 1)[0]
def listAvailableExplorers(swap_client):
@@ -69,40 +70,47 @@ def listAvailableExplorers(swap_client):
for c in Coins:
if c not in chainparams:
continue
for i, e in enumerate(swap_client.coin_clients[c]['explorers']):
explorers.append(('{}_{}'.format(int(c), i), getCoinName(c) + ' - ' + extractDomain(e.base_url)))
for i, e in enumerate(swap_client.coin_clients[c]["explorers"]):
explorers.append(
(
"{}_{}".format(int(c), i),
getCoinName(c) + " - " + extractDomain(e.base_url),
)
)
return explorers
def listExplorerActions(swap_client):
actions = [('height', 'Chain Height'),
('block', 'Get Block'),
('tx', 'Get Transaction'),
('balance', 'Address Balance'),
('unspent', 'List Unspent')]
actions = [
("height", "Chain Height"),
("block", "Get Block"),
("tx", "Get Transaction"),
("balance", "Address Balance"),
("unspent", "List Unspent"),
]
return actions
def parse_cmd(cmd: str, type_map: str):
params = shlex.split(cmd)
if len(params) < 1:
return '', []
return "", []
method = params[0]
typed_params = []
params = params[1:]
for i, param in enumerate(params):
if i >= len(type_map):
type_ind = 's'
type_ind = "s"
else:
type_ind = type_map[i]
if type_ind == 'i':
if type_ind == "i":
typed_params.append(int(param))
elif type_ind == 'f':
elif type_ind == "f":
typed_params.append(float(param))
elif type_ind == 'b':
elif type_ind == "b":
typed_params.append(toBool(param))
elif type_ind == 'j':
elif type_ind == "j":
typed_params.append(json.loads(param))
else:
typed_params.append(param)
@@ -122,99 +130,112 @@ class HttpHandler(BaseHTTPRequestHandler):
return os.urandom(8).hex()
def checkForm(self, post_string, name, messages):
if post_string == '':
if post_string == "":
return None
form_data = parse.parse_qs(post_string)
form_id = form_data[b'formid'][0].decode('utf-8')
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))
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__):
def render_template(
self, template, args_dict, status_code=200, version=__version__
):
swap_client = self.server.swap_client
if swap_client.ws_server:
args_dict['ws_port'] = swap_client.ws_server.client_port
args_dict["ws_port"] = swap_client.ws_server.client_port
if swap_client.debug:
args_dict['debug_mode'] = True
args_dict["debug_mode"] = True
if swap_client.debug_ui:
args_dict['debug_ui_mode'] = True
args_dict["debug_ui_mode"] = True
if swap_client.use_tor_proxy:
args_dict['use_tor_proxy'] = True
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
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception as e:
args_dict['tor_established'] = False
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()
args_dict["notifications"] = swap_client.getNotifications()
if 'messages' in args_dict:
if "messages" in args_dict:
messages_with_ids = []
for msg in args_dict['messages']:
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:
args_dict["messages"] = messages_with_ids
if "err_messages" in args_dict:
err_messages_with_ids = []
for msg in args_dict['err_messages']:
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
args_dict["err_messages"] = err_messages_with_ids
shutdown_token = os.urandom(8).hex()
self.server.session_tokens['shutdown'] = shutdown_token
args_dict['shutdown_token'] = shutdown_token
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
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
args_dict['version'] = version
args_dict["version"] = version
self.putHeaders(status_code, 'text/html')
return bytes(template.render(
title=self.server.title,
h2=self.server.title,
form_id=self.generate_form_id(),
**args_dict,
), 'UTF-8')
self.putHeaders(status_code, "text/html")
return bytes(
template.render(
title=self.server.title,
h2=self.server.title,
form_id=self.generate_form_id(),
**args_dict,
),
"UTF-8",
)
def render_simple_template(self, template, args_dict):
swap_client = self.server.swap_client
return bytes(template.render(
title=self.server.title,
**args_dict,
), 'UTF-8')
return bytes(
template.render(
title=self.server.title,
**args_dict,
),
"UTF-8",
)
def page_info(self, info_str, post_string=None):
template = env.get_template('info.html')
template = env.get_template("info.html")
swap_client = self.server.swap_client
summary = swap_client.getSummary()
return self.render_template(template, {
'title_str': 'BasicSwap Info',
'message_str': info_str,
'summary': summary,
})
return self.render_template(
template,
{
"title_str": "BasicSwap Info",
"message_str": info_str,
"summary": summary,
},
)
def page_error(self, error_str, post_string=None):
template = env.get_template('error.html')
template = env.get_template("error.html")
swap_client = self.server.swap_client
summary = swap_client.getSummary()
return self.render_template(template, {
'title_str': 'BasicSwap Error',
'message_str': error_str,
'summary': summary,
})
return self.render_template(
template,
{
"title_str": "BasicSwap Error",
"message_str": error_str,
"summary": summary,
},
)
def page_explorers(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -226,42 +247,49 @@ class HttpHandler(BaseHTTPRequestHandler):
action = -1
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'explorers', err_messages)
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 = form_data[b"explorer"][0].decode("utf-8")
action = form_data[b"action"][0].decode("utf-8")
args = '' if b'args' not in form_data else form_data[b'args'][0].decode('utf-8')
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)]
if action == 'height':
c, e = explorer.split("_")
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
if action == "height":
result = str(exp.getChainHeight())
elif action == 'block':
elif action == "block":
result = dumpj(exp.getBlock(args))
elif action == 'tx':
elif action == "tx":
result = dumpj(exp.getTransaction(args))
elif action == 'balance':
elif action == "balance":
result = dumpj(exp.getBalance(args))
elif action == 'unspent':
elif action == "unspent":
result = dumpj(exp.lookupUnspentByAddress(args))
else:
result = 'Unknown action'
result = "Unknown action"
except Exception as ex:
result = str(ex)
template = env.get_template('explorers.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'explorers': listAvailableExplorers(swap_client),
'explorer': explorer,
'actions': listExplorerActions(swap_client),
'action': action,
'result': result,
'summary': summary,
})
template = env.get_template("explorers.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"explorers": listAvailableExplorers(swap_client),
"explorer": explorer,
"actions": listExplorerActions(swap_client),
"action": action,
"result": result,
"summary": summary,
},
)
def page_rpc(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -269,34 +297,33 @@ class HttpHandler(BaseHTTPRequestHandler):
summary = swap_client.getSummary()
result = None
cmd = ''
cmd = ""
coin_type_selected = -1
coin_type = -1
coin_id = -1
call_type = 'cli'
type_map = ''
call_type = "cli"
type_map = ""
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'rpc', err_messages)
form_data = self.checkForm(post_string, "rpc", err_messages)
if form_data:
try:
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
type_map = get_data_entry_or(form_data, 'type_map', '')
call_type = get_data_entry_or(form_data, "call_type", "cli")
type_map = get_data_entry_or(form_data, "type_map", "")
try:
coin_type_selected = get_data_entry(form_data, 'coin_type')
coin_type_split = coin_type_selected.split(',')
coin_type_selected = get_data_entry(form_data, "coin_type")
coin_type_split = coin_type_selected.split(",")
coin_type = Coins(int(coin_type_split[0]))
coin_variant = int(coin_type_split[1])
except Exception:
raise ValueError('Unknown Coin Type')
raise ValueError("Unknown Coin Type")
if coin_type in (Coins.DCR,):
call_type = 'http'
call_type = "http"
try:
cmd = get_data_entry(form_data, 'cmd')
cmd = get_data_entry(form_data, "cmd")
except Exception:
raise ValueError('Invalid command')
raise ValueError("Invalid command")
if coin_type in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_type)
arr = cmd.split(None, 1)
@@ -311,10 +338,10 @@ class HttpHandler(BaseHTTPRequestHandler):
params = None
rv = ci.rpc2(method, params)
else:
raise ValueError('Unknown RPC variant')
raise ValueError("Unknown RPC variant")
result = json.dumps(rv, indent=4)
else:
if call_type == 'http':
if call_type == "http":
ci = swap_client.ci(coin_type)
method, params = parse_cmd(cmd, type_map)
if coin_variant == 1:
@@ -322,50 +349,56 @@ class HttpHandler(BaseHTTPRequestHandler):
elif coin_variant == 2:
rv = ci.rpc_wallet_mweb(method, params)
else:
if coin_type in (Coins.DCR, ):
if coin_type in (Coins.DCR,):
rv = ci.rpc(method, params)
else:
rv = ci.rpc_wallet(method, params)
if not isinstance(rv, str):
rv = json.dumps(rv, indent=4)
result = cmd + '\n' + rv
result = cmd + "\n" + rv
else:
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
result = cmd + "\n" + swap_client.callcoincli(coin_type, cmd)
except Exception as ex:
result = cmd + '\n' + str(ex)
result = cmd + "\n" + str(ex)
if self.server.swap_client.debug is True:
self.server.swap_client.log.error(traceback.format_exc())
template = env.get_template('rpc.html')
template = env.get_template("rpc.html")
coin_available = listAvailableCoins(swap_client, with_variants=False)
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, Coins.WOW)]
coins = [
(str(c[0]) + ",0", c[1])
for c in coin_available
if c[0] not in (Coins.XMR, Coins.WOW)
]
if any(c[0] == Coins.DCR for c in coin_available):
coins.append((str(int(Coins.DCR)) + ',1', 'Decred Wallet'))
coins.append((str(int(Coins.DCR)) + ",1", "Decred Wallet"))
if any(c[0] == Coins.LTC for c in coin_available):
coins.append((str(int(Coins.LTC)) + ',2', 'Litecoin MWEB Wallet'))
coins.append((str(int(Coins.LTC)) + ",2", "Litecoin MWEB Wallet"))
if with_xmr:
coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
coins.append((str(int(Coins.XMR)) + ",0", "Monero"))
coins.append((str(int(Coins.XMR)) + ",1", "Monero JSON"))
coins.append((str(int(Coins.XMR)) + ",2", "Monero Wallet"))
if with_wow:
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
coins.append((str(int(Coins.WOW)) + ",0", "Wownero"))
coins.append((str(int(Coins.WOW)) + ",1", "Wownero JSON"))
coins.append((str(int(Coins.WOW)) + ",2", "Wownero Wallet"))
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'coins': coins,
'coin_type': coin_type_selected,
'call_type': call_type,
'result': result,
'messages': messages,
'summary': summary,
})
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"coins": coins,
"coin_type": coin_type_selected,
"call_type": call_type,
"result": result,
"summary": summary,
},
)
def page_active(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -373,12 +406,24 @@ class HttpHandler(BaseHTTPRequestHandler):
active_swaps = swap_client.listSwapsInProgress()
summary = swap_client.getSummary()
template = env.get_template('active.html')
return self.render_template(template, {
'refresh': 30,
'active_swaps': [(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
'summary': summary,
})
template = env.get_template("active.html")
return self.render_template(
template,
{
"refresh": 30,
"active_swaps": [
(
s[0].hex(),
s[1],
strBidState(s[2]),
strTxState(s[3]),
strTxState(s[4]),
)
for s in active_swaps
],
"summary": summary,
},
)
def page_watched(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -386,60 +431,68 @@ class HttpHandler(BaseHTTPRequestHandler):
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
summary = swap_client.getSummary()
template = env.get_template('watched.html')
return self.render_template(template, {
'refresh': 30,
'last_scanned': [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
'watched_outputs': [(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
'summary': summary,
})
template = env.get_template("watched.html")
return self.render_template(
template,
{
"refresh": 30,
"last_scanned": [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
"watched_outputs": [
(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4]))
for wo in watched_outputs
],
"summary": summary,
},
)
def page_shutdown(self, url_split, post_string):
swap_client = self.server.swap_client
if len(url_split) > 2:
token = url_split[2]
expect_token = self.server.session_tokens.get('shutdown', None)
expect_token = self.server.session_tokens.get("shutdown", None)
if token != expect_token:
return self.page_info('Unexpected token, still running.')
return self.page_info("Unexpected token, still running.")
swap_client.stopRunning()
return self.page_info('Shutting down')
return self.page_info("Shutting down")
def page_index(self, url_split):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
self.send_response(302)
self.send_header('Location', '/offers')
self.send_header("Location", "/offers")
self.end_headers()
return b''
return b""
def page_404(self, url_split):
swap_client = self.server.swap_client
summary = swap_client.getSummary()
template = env.get_template('404.html')
return self.render_template(template, {
'summary': summary,
})
template = env.get_template("404.html")
return self.render_template(
template,
{
"summary": summary,
},
)
def putHeaders(self, status_code, content_type):
self.send_response(status_code)
if self.server.allow_cors:
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Content-Type', content_type)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Type", content_type)
self.end_headers()
def handle_http(self, status_code, path, post_string='', is_json=False):
def handle_http(self, status_code, path, post_string="", is_json=False):
swap_client = self.server.swap_client
parsed = parse.urlparse(self.path)
url_split = parsed.path.split('/')
if post_string == '' and len(parsed.query) > 0:
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':
if len(url_split) > 1 and url_split[1] == "json":
try:
self.putHeaders(status_code, 'text/plain')
self.putHeaders(status_code, "text/plain")
func = js_url_to_function(url_split)
return func(self, url_split, post_string, is_json)
except Exception as ex:
@@ -447,37 +500,42 @@ class HttpHandler(BaseHTTPRequestHandler):
swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex))
if len(url_split) > 1 and url_split[1] == 'static':
if len(url_split) > 1 and url_split[1] == "static":
try:
static_path = os.path.join(os.path.dirname(__file__), 'static')
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')
static_path = os.path.join(os.path.dirname(__file__), "static")
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()
elif len(url_split) > 3 and url_split[2] == 'images':
elif len(url_split) > 3 and url_split[2] == "images":
filename = os.path.join(*url_split[3:])
_, extension = os.path.splitext(filename)
mime_type = {
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.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:
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".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':
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')
with open(os.path.join(static_path, "css", filename), "rb") as fp:
self.putHeaders(status_code, "text/css; charset=utf-8")
return fp.read()
elif len(url_split) > 3 and url_split[2] == 'js':
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')
with open(os.path.join(static_path, "js", filename), "rb") as fp:
self.putHeaders(status_code, "application/javascript")
return fp.read()
else:
return self.page_404(url_split)
@@ -492,63 +550,61 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 1:
page = url_split[1]
if page == 'active':
if page == "active":
return self.page_active(url_split, post_string)
if page == 'wallets':
if page == "wallets":
return page_wallets(self, url_split, post_string)
if page == 'wallet':
if page == "wallet":
return page_wallet(self, url_split, post_string)
if page == 'settings':
if page == "settings":
return page_settings(self, url_split, post_string)
if page == 'error':
if page == "error":
return self.page_error(url_split, post_string)
if page == 'info':
if page == "info":
return self.page_info(url_split, post_string)
if page == 'rpc':
if page == "rpc":
return self.page_rpc(url_split, post_string)
if page == 'debug':
if page == "debug":
return page_debug(self, url_split, post_string)
if page == 'explorers':
if page == "explorers":
return self.page_explorers(url_split, post_string)
if page == 'offer':
if page == "offer":
return page_offer(self, url_split, post_string)
if page == 'offers':
if page == "offers":
return page_offers(self, url_split, post_string)
if page == 'newoffer':
if page == "newoffer":
return page_newoffer(self, url_split, post_string)
if page == 'sentoffers':
if page == "sentoffers":
return page_offers(self, url_split, post_string, sent=True)
if page == 'bid':
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 == 'availablebids':
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':
if page == "watched":
return self.page_watched(url_split, post_string)
if page == 'smsgaddresses':
if page == "smsgaddresses":
return page_smsgaddresses(self, url_split, post_string)
if page == 'identity':
if page == "identity":
return page_identity(self, url_split, post_string)
if page == 'tor':
if page == "tor":
return page_tor(self, url_split, post_string)
if page == 'automation':
if page == "automation":
return page_automation_strategies(self, url_split, post_string)
if page == 'automationstrategy':
if page == "automationstrategy":
return page_automation_strategy(self, url_split, post_string)
if page == 'newautomationstrategy':
if page == "newautomationstrategy":
return page_automation_strategy_new(self, url_split, post_string)
if page == 'shutdown':
if page == "shutdown":
return self.page_shutdown(url_split, post_string)
if page == 'changepassword':
if page == "changepassword":
return page_changepassword(self, url_split, post_string)
if page == 'unlock':
if page == "unlock":
return page_unlock(self, url_split, post_string)
if page == 'lock':
if page == "lock":
return page_lock(self, url_split, post_string)
if page != '':
if page != "":
return self.page_404(url_split)
return self.page_index(url_split)
except LockedCoinError:
@@ -563,20 +619,20 @@ class HttpHandler(BaseHTTPRequestHandler):
self.wfile.write(response)
def do_POST(self):
post_string = self.rfile.read(int(self.headers.get('Content-Length')))
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
is_json = True if 'json' in self.headers.get('Content-Type', '') else False
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)
def do_HEAD(self):
self.putHeaders(200, 'text/html')
self.putHeaders(200, "text/html")
def do_OPTIONS(self):
self.send_response(200, 'ok')
self.send_response(200, "ok")
if self.server.allow_cors:
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "*")
self.end_headers()
@@ -590,7 +646,7 @@ class HttpThread(threading.Thread, HTTPServer):
self.port_no = port_no
self.allow_cors = allow_cors
self.swap_client = swap_client
self.title = 'BasicSwap - ' + __version__
self.title = "BasicSwap - " + __version__
self.last_form_id = dict()
self.session_tokens = dict()
self.env = env
@@ -605,9 +661,9 @@ class HttpThread(threading.Thread, HTTPServer):
# Send fake request
conn = http.client.HTTPConnection(self.host_name, self.port_no)
conn.connect()
conn.request('GET', '/none')
conn.request("GET", "/none")
response = conn.getresponse()
data = response.read()
_ = response.read()
conn.close()
def serve_forever(self):

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.
@@ -14,7 +15,8 @@ from basicswap.chainparams import (
)
from basicswap.util import (
ensure,
i2b, b2i,
i2b,
b2i,
make_int,
format_amount,
TemporaryError,
@@ -26,9 +28,7 @@ from basicswap.util.ecc import (
ep,
getSecretInt,
)
from coincurve.dleag import (
verify_secp256k1_point
)
from coincurve.dleag import verify_secp256k1_point
from coincurve.keys import (
PublicKey,
)
@@ -54,6 +54,10 @@ class CoinInterface:
self._mx_wallet = threading.Lock()
self._altruistic = True
def interface_type(self) -> int:
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.
return self.coin_type()
def setDefaults(self):
self._unknown_wallet_seed = True
self._restore_height = None
@@ -67,33 +71,33 @@ class CoinInterface:
def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()]
if 'display_name' in coin_chainparams:
return coin_chainparams['display_name']
return coin_chainparams['name'].capitalize()
if "display_name" in coin_chainparams:
return coin_chainparams["display_name"]
return coin_chainparams["name"].capitalize()
def ticker(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
ticker = chainparams[self.coin_type()]["ticker"]
if self._network == "testnet":
ticker = "t" + ticker
elif self._network == "regtest":
ticker = "rt" + ticker
return ticker
def getExchangeTicker(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['ticker']
return chainparams[self.coin_type()]["ticker"]
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['name']
return chainparams[self.coin_type()]["name"]
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
ticker = chainparams[self.coin_type()]["ticker"]
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['min_amount']
return chainparams[self.coin_type()][self._network]["min_amount"]
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['max_amount']
return chainparams[self.coin_type()][self._network]["max_amount"]
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
@@ -111,7 +115,7 @@ class CoinInterface:
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
return chainparams[self.coin_type()].get("has_segwit", True)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
@@ -121,24 +125,26 @@ class CoinInterface:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if 'not enough unlocked money' in str_error:
if "not enough unlocked money" in str_error:
return True
if 'no unlocked balance' in str_error:
if "no unlocked balance" in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
if "transaction was rejected by daemon" in str_error:
return True
if 'invalid unlocked_balance' in str_error:
if "invalid unlocked_balance" in str_error:
return True
if 'daemon is busy' in str_error:
if "daemon is busy" in str_error:
return True
if 'timed out' in str_error:
if "timed out" in str_error:
return True
if 'request-sent' in str_error:
if "request-sent" in str_error:
return True
return False
def setConfTarget(self, new_conf_target: int) -> None:
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
ensure(
new_conf_target >= 1 and new_conf_target < 33, "Invalid conf_target value"
)
self._conf_target = new_conf_target
def walletRestoreHeight(self) -> int:
@@ -171,30 +177,15 @@ class CoinInterface:
return self._altruistic
class AdaptorSigInterface():
class AdaptorSigInterface:
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
return [b"", bytes(72), bytes(72), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
return [bytes(72), b"", bytes(len(script))]
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
@@ -202,7 +193,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def curve_type():
return Curves.secp256k1
def getNewSecretKey(self) -> bytes:
def getNewRandomKey(self) -> bytes:
return i2b(getSecretInt())
def getPubkey(self, privkey: bytes) -> bytes:
@@ -213,7 +204,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return (i < ep.o and i > 0)
return i < ep.o and i > 0
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,7 +12,10 @@ from basicswap.util.address import decodeAddress
from basicswap.contrib.mnemonic import Mnemonic
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
OP_CHECKSIG,
)
@@ -22,41 +26,49 @@ class DASHInterface(BTCInterface):
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
self._wallet_passphrase = ''
self._wallet_passphrase = ""
self._have_checked_seed = False
self._wallet_v20_compatible = False if not swap_client else swap_client.getChainClientSettings(self.coin_type()).get('wallet_v20_compatible', False)
self._wallet_v20_compatible = (
False
if not swap_client
else swap_client.getChainClientSettings(self.coin_type()).get(
"wallet_v20_compatible", False
)
)
def decodeAddress(self, address: str) -> bytes:
return decodeAddress(address)[1:]
def getWalletSeedID(self) -> str:
hdseed: str = self.rpc_wallet('dumphdinfo')['hdseed']
hdseed: str = self.rpc_wallet("dumphdinfo")["hdseed"]
return self.getSeedHash(bytes.fromhex(hdseed)).hex()
def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic('english').to_mnemonic(key)
return Mnemonic("english").to_mnemonic(key)
def initialiseWallet(self, key_bytes: bytes) -> None:
self._have_checked_seed = False
if self._wallet_v20_compatible:
self._log.warning('Generating wallet compatible with v20 seed.')
self._log.warning("Generating wallet compatible with v20 seed.")
words = self.entropyToMnemonic(key_bytes)
mnemonic_passphrase = ''
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
mnemonic_passphrase = ""
self.rpc_wallet(
"upgradetohd", [words, mnemonic_passphrase, self._wallet_passphrase]
)
self._have_checked_seed = False
if self._wallet_passphrase != '':
if self._wallet_passphrase != "":
self.unlockWallet(self._wallet_passphrase)
return
key_wif = self.encodeKey(key_bytes)
self.rpc_wallet('sethdseed', [True, key_wif])
self.rpc_wallet("sethdseed", [True, key_wif])
def checkExpectedSeed(self, expect_seedid: str) -> bool:
self._expect_seedid_hex = expect_seedid
rv = self.rpc_wallet('dumphdinfo')
if rv['mnemonic'] != '':
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
rv = self.rpc_wallet("dumphdinfo")
if rv["mnemonic"] != "":
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
have_expected_seed: bool = expect_seedid == entropy_hash
else:
@@ -65,11 +77,11 @@ class DASHInterface(BTCInterface):
return have_expected_seed
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
return self.rpc_wallet('sendtoaddress', params)
params = [addr_to, value, "", "", subfee, False, False, self._conf_target]
return self.rpc_wallet("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
return self.make_int(self.rpc_wallet("getwalletinfo")["balance"])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
@@ -79,19 +91,23 @@ class DASHInterface(BTCInterface):
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
)
return pay_fee
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_wallet('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
rv = self.rpc_wallet("gettransaction", [txid_hex])
except Exception as e: # noqa: F841
self._log.debug(
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
)
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
@@ -105,8 +121,8 @@ class DASHInterface(BTCInterface):
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}.')
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
def lockWallet(self):
super().lockWallet()
self._wallet_passphrase = ''
self._wallet_passphrase = ""

View File

@@ -1,4 +1,5 @@
from .dcr import DCRInterface
__all__ = ['DCRInterface',]
__all__ = [
"DCRInterface",
]

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ class SigHashType(IntEnum):
SigHashSingle = 0x3
SigHashAnyOneCanPay = 0x80
SigHashMask = 0x1f
SigHashMask = 0x1F
class SignatureType(IntEnum):
@@ -33,7 +33,7 @@ class SignatureType(IntEnum):
class COutPoint:
__slots__ = ('hash', 'n', 'tree')
__slots__ = ("hash", "n", "tree")
def __init__(self, hash=0, n=0, tree=0):
self.hash = hash
@@ -41,24 +41,30 @@ class COutPoint:
self.tree = tree
def get_hash(self) -> bytes:
return self.hash.to_bytes(32, 'big')
return self.hash.to_bytes(32, "big")
class CTxIn:
__slots__ = ('prevout', 'sequence',
'value_in', 'block_height', 'block_index', 'signature_script') # Witness
__slots__ = (
"prevout",
"sequence",
"value_in",
"block_height",
"block_index",
"signature_script",
) # Witness
def __init__(self, prevout=COutPoint(), sequence=0):
self.prevout = prevout
self.sequence = sequence
self.value_in = -1
self.block_height = 0
self.block_index = 0xffffffff
self.block_index = 0xFFFFFFFF
self.signature_script = bytes()
class CTxOut:
__slots__ = ('value', 'version', 'script_pubkey')
__slots__ = ("value", "version", "script_pubkey")
def __init__(self, value=0, script_pubkey=bytes()):
self.value = value
@@ -67,7 +73,7 @@ class CTxOut:
class CTransaction:
__slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry')
__slots__ = ("hash", "version", "vin", "vout", "locktime", "expiry")
def __init__(self, tx=None):
if tx is None:
@@ -85,8 +91,8 @@ class CTransaction:
def deserialize(self, data: bytes) -> None:
version = int.from_bytes(data[:4], 'little')
self.version = version & 0xffff
version = int.from_bytes(data[:4], "little")
self.version = version & 0xFFFF
ser_type: int = version >> 16
o = 4
@@ -97,13 +103,13 @@ class CTransaction:
for i in range(num_txin):
txi = CTxIn()
txi.prevout = COutPoint()
txi.prevout.hash = int.from_bytes(data[o:o + 32], 'little')
txi.prevout.hash = int.from_bytes(data[o : o + 32], "little")
o += 32
txi.prevout.n = int.from_bytes(data[o:o + 4], 'little')
txi.prevout.n = int.from_bytes(data[o : o + 4], "little")
o += 4
txi.prevout.tree = data[o]
o += 1
txi.sequence = int.from_bytes(data[o:o + 4], 'little')
txi.sequence = int.from_bytes(data[o : o + 4], "little")
o += 4
self.vin.append(txi)
@@ -112,19 +118,19 @@ class CTransaction:
for i in range(num_txout):
txo = CTxOut()
txo.value = int.from_bytes(data[o:o + 8], 'little')
txo.value = int.from_bytes(data[o : o + 8], "little")
o += 8
txo.version = int.from_bytes(data[o:o + 2], 'little')
txo.version = int.from_bytes(data[o : o + 2], "little")
o += 2
script_bytes, nb = decode_compactsize(data, o)
o += nb
txo.script_pubkey = data[o:o + script_bytes]
txo.script_pubkey = data[o : o + script_bytes]
o += script_bytes
self.vout.append(txo)
self.locktime = int.from_bytes(data[o:o + 4], 'little')
self.locktime = int.from_bytes(data[o : o + 4], "little")
o += 4
self.expiry = int.from_bytes(data[o:o + 4], 'little')
self.expiry = int.from_bytes(data[o : o + 4], "little")
o += 4
if ser_type == TxSerializeType.NoWitness:
@@ -137,51 +143,53 @@ class CTransaction:
self.vin = [CTxIn() for _ in range(num_wit_scripts)]
else:
if num_wit_scripts != len(self.vin):
raise ValueError('non equal witness and prefix txin quantities')
raise ValueError("non equal witness and prefix txin quantities")
for i in range(num_wit_scripts):
txi = self.vin[i]
txi.value_in = int.from_bytes(data[o:o + 8], 'little')
txi.value_in = int.from_bytes(data[o : o + 8], "little")
o += 8
txi.block_height = int.from_bytes(data[o:o + 4], 'little')
txi.block_height = int.from_bytes(data[o : o + 4], "little")
o += 4
txi.block_index = int.from_bytes(data[o:o + 4], 'little')
txi.block_index = int.from_bytes(data[o : o + 4], "little")
o += 4
script_bytes, nb = decode_compactsize(data, o)
o += nb
txi.signature_script = data[o:o + script_bytes]
txi.signature_script = data[o : o + script_bytes]
o += script_bytes
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
data = bytes()
version = (self.version & 0xffff) | (ser_type << 16)
data += version.to_bytes(4, 'little')
version = (self.version & 0xFFFF) | (ser_type << 16)
data += version.to_bytes(4, "little")
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
data += encode_compactsize(len(self.vin))
for txi in self.vin:
data += txi.prevout.hash.to_bytes(32, 'little')
data += txi.prevout.n.to_bytes(4, 'little')
data += txi.prevout.tree.to_bytes(1, 'little')
data += txi.sequence.to_bytes(4, 'little')
data += txi.prevout.hash.to_bytes(32, "little")
data += txi.prevout.n.to_bytes(4, "little")
data += txi.prevout.tree.to_bytes(1, "little")
data += txi.sequence.to_bytes(4, "little")
data += encode_compactsize(len(self.vout))
for txo in self.vout:
data += txo.value.to_bytes(8, 'little')
data += txo.version.to_bytes(2, 'little')
data += txo.value.to_bytes(8, "little")
data += txo.version.to_bytes(2, "little")
data += encode_compactsize(len(txo.script_pubkey))
data += txo.script_pubkey
data += self.locktime.to_bytes(4, 'little')
data += self.expiry.to_bytes(4, 'little')
data += self.locktime.to_bytes(4, "little")
data += self.expiry.to_bytes(4, "little")
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
data += encode_compactsize(len(self.vin))
for txi in self.vin:
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
data += tc_value_in.to_bytes(8, 'little')
data += txi.block_height.to_bytes(4, 'little')
data += txi.block_index.to_bytes(4, 'little')
tc_value_in = (
txi.value_in & 0xFFFFFFFFFFFFFFFF
) # Convert negative values
data += tc_value_in.to_bytes(8, "little")
data += txi.block_height.to_bytes(4, "little")
data += txi.block_index.to_bytes(4, "little")
data += encode_compactsize(len(txi.signature_script))
data += txi.signature_script
@@ -191,10 +199,10 @@ class CTransaction:
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
def TxHashWitness(self) -> bytes:
raise ValueError('todo')
raise ValueError("todo")
def TxHashFull(self) -> bytes:
raise ValueError('todo')
raise ValueError("todo")
def findOutput(tx, script_pk: bytes):

View File

@@ -9,34 +9,34 @@ import traceback
from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
x = Jsonrpc(url)
x.__handler = None
v = x.json_request(method, params)
x.close()
r = json.loads(v.decode('utf-8'))
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("RPC server error " + str(ex) + ", method: " + method)
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
return r['result']
return r["result"]
def openrpc(rpc_port, auth, host='127.0.0.1'):
def openrpc(rpc_port, auth, host="127.0.0.1"):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC error ' + str(ex))
raise ValueError("RPC error " + str(ex))
def make_rpc_func(port, auth, host='127.0.0.1'):
def make_rpc_func(port, auth, host="127.0.0.1"):
port = port
auth = auth
host = host
@@ -44,4 +44,5 @@ def make_rpc_func(port, auth, host='127.0.0.1'):
def rpc_func(method, params=None):
nonlocal port, auth, host
return callrpc(port, auth, method, params, host)
return rpc_func

View File

@@ -7,7 +7,7 @@
OP_0 = 0x00
OP_DATA_1 = 0x01
OP_1NEGATE = 0x4f
OP_1NEGATE = 0x4F
OP_1 = 0x51
OP_IF = 0x63
OP_ELSE = 0x67
@@ -16,13 +16,13 @@ OP_DROP = 0x75
OP_DUP = 0x76
OP_EQUAL = 0x87
OP_EQUALVERIFY = 0x88
OP_PUSHDATA1 = 0x4c
OP_PUSHDATA2 = 0x4d
OP_PUSHDATA4 = 0x4e
OP_HASH160 = 0xa9
OP_CHECKSIG = 0xac
OP_CHECKMULTISIG = 0xae
OP_CHECKSEQUENCEVERIFY = 0xb2
OP_PUSHDATA1 = 0x4C
OP_PUSHDATA2 = 0x4D
OP_PUSHDATA4 = 0x4E
OP_HASH160 = 0xA9
OP_CHECKSIG = 0xAC
OP_CHECKMULTISIG = 0xAE
OP_CHECKSEQUENCEVERIFY = 0xB2
def push_script_data(data_array: bytearray, data: bytes) -> None:
@@ -39,12 +39,12 @@ def push_script_data(data_array: bytearray, data: bytes) -> None:
return
if len_data < OP_PUSHDATA1:
data_array += len_data.to_bytes(1, 'little')
elif len_data <= 0xff:
data_array += len_data.to_bytes(1, "little")
elif len_data <= 0xFF:
data_array += bytes((OP_PUSHDATA1, len_data))
elif len_data <= 0xffff:
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
elif len_data <= 0xFFFF:
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, "little")
else:
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, "little")
data_array += data

View File

@@ -10,46 +10,48 @@ import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info('Creating DCR wallet')
logging.info("Creating DCR wallet")
(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)
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')
buf = os.read(pipe_r, 1024).decode("utf-8")
response = None
if 'Opened wallet' in buf:
if "Opened wallet" in buf:
pass
elif 'Use the existing configured private passphrase' in buf:
response = b'y\n'
elif 'Do you want to add an additional layer of encryption' in buf:
response = b'n\n'
elif 'Do you have an existing wallet seed' in buf:
response = b'y\n'
elif 'Enter existing wallet seed' in buf:
response = (hex_seed + '\n').encode('utf-8')
elif 'Seed input successful' in buf:
elif "Use the existing configured private passphrase" in buf:
response = b"y\n"
elif "Do you want to add an additional layer of encryption" in buf:
response = b"n\n"
elif "Do you have an existing wallet seed" in buf:
response = b"y\n"
elif "Enter existing wallet seed" in buf:
response = (hex_seed + "\n").encode("utf-8")
elif "Seed input successful" in buf:
pass
elif 'Upgrading database from version' in buf:
elif "Upgrading database from version" in buf:
pass
elif 'Ticket commitments db upgrade done' in buf:
elif "Ticket commitments db upgrade done" in buf:
pass
elif 'The wallet has been created successfully' in buf:
elif "The wallet has been created successfully" in buf:
pass
else:
raise ValueError(f'Unexpected output: {buf}')
raise ValueError(f"Unexpected output: {buf}")
if response is not None:
p.stdin.write(response)
p.stdin.flush()
try:
while p.poll() is None:
if os.name == 'nt':
if os.name == "nt":
readOutput()
delay_event.wait(0.1)
continue
@@ -57,7 +59,7 @@ def createDCRWallet(args, hex_seed, logging, delay_event):
readOutput()
delay_event.wait(0.1)
except Exception as e:
logging.error(f'dcrwallet --create failed: {e}')
logging.error(f"dcrwallet --create failed: {e}")
finally:
if p.poll() is None:
p.terminate()

View File

@@ -0,0 +1,62 @@
#!/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.
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.util.crypto import hash160
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP,
OP_CHECKSIG,
OP_HASH160,
OP_EQUAL,
OP_EQUALVERIFY,
)
class DOGEInterface(BTCInterface):
@staticmethod
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
def __init__(self, coin_settings, network, swap_client=None):
super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
def getScriptDest(self, script: bytearray) -> bytearray:
# P2SH
script_hash = hash160(script)
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def encodeScriptDest(self, script_dest: bytes) -> str:
# Extract hash from script
script_hash = script_dest[2:-1]
return self.sh_to_address(script_hash)
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
)
return pay_fee

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -40,10 +41,15 @@ class FIROInterface(BTCInterface):
def __init__(self, coin_settings, network, swap_client=None):
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
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'
return "zcoin"
def initialiseWallet(self, key):
# load with -hdseed= parameter
@@ -52,8 +58,8 @@ class FIROInterface(BTCInterface):
def checkWallets(self) -> int:
return 1
def getNewAddress(self, use_segwit, label='swap_receive'):
return self.rpc('getnewaddress', [label])
def getNewAddress(self, use_segwit, label="swap_receive"):
return self.rpc("getnewaddress", [label])
# addr_plain = self.rpc('getnewaddress', [label])
# return self.rpc('addwitnessaddress', [addr_plain])
@@ -61,20 +67,20 @@ class FIROInterface(BTCInterface):
return decodeAddress(address)[1:]
def encodeSegwitAddress(self, script):
raise ValueError('TODO')
raise ValueError("TODO")
def decodeSegwitAddress(self, addr):
raise ValueError('TODO')
raise ValueError("TODO")
def isWatchOnlyAddress(self, address):
addr_info = self.rpc('validateaddress', [address])
return addr_info['iswatchonly']
addr_info = self.rpc("validateaddress", [address])
return addr_info["iswatchonly"]
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc('validateaddress', [address])
addr_info = self.rpc("validateaddress", [address])
if not or_watch_only:
return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
return addr_info["ismine"]
return addr_info["ismine"] or addr_info["iswatchonly"]
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
@@ -82,58 +88,85 @@ class FIROInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc('validateaddress', [address])
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True])
return address
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
# Add watchonly address and rescan if required
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('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.importWatchOnlyAddress(dest_address, "bid")
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
)
)
self.rescanBlockchainForAddress(rescan_from, dest_address)
return_txid = True if txid is None else False
if txid is None:
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
txns = self.rpc(
"listunspent",
[
0,
9999999,
[
dest_address,
],
],
)
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
txid = bytes.fromhex(tx['txid'])
if self.make_int(tx["amount"]) == bid_amount:
txid = bytes.fromhex(tx["txid"])
break
if txid is None:
return None
try:
tx = self.rpc('gettransaction', [txid.hex()])
tx = self.rpc("gettransaction", [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
if "blockhash" in tx:
block_header = self.rpc("getblockheader", [tx["blockhash"]])
block_height = block_header["height"]
rv = {
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
'height': block_height}
"depth": 0 if "confirmations" not in tx else tx["confirmations"],
"height": block_height,
}
except Exception as e:
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
self._log.debug(
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
)
return None
if find_index:
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
rv['txid'] = txid.hex()
rv["txid"] = txid.hex()
return rv
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
def createSCLockTx(
self, value: int, script: bytearray, vkbv: bytes = None
) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
@@ -144,24 +177,36 @@ class FIROInterface(BTCInterface):
return self.fundTx(tx_bytes, feerate)
def signTxWithWallet(self, tx):
rv = self.rpc('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
rv = self.rpc("signrawtransaction", [tx.hex()])
return bytes.fromhex(rv["hex"])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
def createRawFundedTransaction(
self,
addr_to: str,
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = {
'lockUnspents': lock_unspents,
'feeRate': fee_rate,
"lockUnspents": lock_unspents,
"feeRate": fee_rate,
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc('fundrawtransaction', [txn, options])['hex']
options["subtractFeeFromOutputs"] = [
0,
]
return self.rpc("fundrawtransaction", [txn, options])["hex"]
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc('signrawtransaction', [txn_funded])['hex']
return self.rpc("signrawtransaction", [txn_funded])["hex"]
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
@@ -188,60 +233,75 @@ class FIROInterface(BTCInterface):
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee]
return self.rpc('sendtoaddress', params)
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getWalletSeedID(self):
return self.rpc('getwalletinfo')['hdmasterkeyid']
return self.rpc("getwalletinfo")["hdmasterkeyid"]
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc('getwalletinfo')['balance'])
return self.make_int(self.rpc("getwalletinfo")["balance"])
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
return bytes.fromhex(rv['hex'])
rv = self.rpc(
"signrawtransaction",
[
tx.hex(),
[],
[
key_wif,
],
],
)
return bytes.fromhex(rv["hex"])
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
rv = self.rpc("gettransaction", [txid_hex])
except Exception as e: # noqa: F841
self._log.debug(
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
)
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspents_by_addr = dict()
unspents = self.rpc('listunspent')
unspents = self.rpc("listunspent")
for u in unspents:
if u['spendable'] is not True:
if u["spendable"] is not True:
continue
if u['address'] not in unspents_by_addr:
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
utxo_amount: int = self.make_int(u['amount'], r=1)
unspents_by_addr[u['address']]['total'] += utxo_amount
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
if u["address"] not in unspents_by_addr:
unspents_by_addr[u["address"]] = {"total": 0, "utxos": []}
utxo_amount: int = self.make_int(u["amount"], r=1)
unspents_by_addr[u["address"]]["total"] += utxo_amount
unspents_by_addr[u["address"]]["utxos"].append(
(utxo_amount, u["txid"], u["vout"])
)
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data['total'] >= amount_for:
if data["total"] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
@@ -256,13 +316,17 @@ class FIROInterface(BTCInterface):
viable_addrs.append(addr)
continue
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
ensure(
len(viable_addrs) > 0, "Could not find address with enough funds for proof"
)
sign_for_addr: str = random.choice(viable_addrs)
self._log.debug('sign_for_addr %s', sign_for_addr)
self._log.debug("sign_for_addr %s", sign_for_addr)
prove_utxos = []
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
sorted_utxos = sorted(
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
)
hasher = hashlib.sha256()
@@ -272,18 +336,29 @@ class FIROInterface(BTCInterface):
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[1].to_bytes(2, "big"))
if sum_value >= amount_for:
break
utxos_hash = hasher.digest()
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
if (
self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
pkh = self.decodeAddress(sign_for_addr)
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug('sign_for_addr converted %s', sign_for_addr)
self._log.debug("sign_for_addr converted %s", sign_for_addr)
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
signature = self.rpc(
"signmessage",
[
sign_for_addr,
sign_for_addr
+ "_swap_proof_"
+ utxos_hash.hex()
+ extra_commit_bytes.hex(),
],
)
return (sign_for_addr, signature, prove_utxos)
@@ -292,19 +367,23 @@ class FIROInterface(BTCInterface):
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[1].to_bytes(2, "big"))
utxos_hash = hasher.digest()
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
passed = self.verifyMessage(
address,
address + "_swap_proof_" + utxos_hash.hex() + extra_commit_bytes.hex(),
signature,
)
ensure(passed is True, "Proof of funds signature invalid")
if self.using_segwit():
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
sum_value: int = 0
for outpoint in utxos:
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
sum_value += self.make_int(txout['value'])
txout = self.rpc("gettxout", [outpoint[0].hex(), outpoint[1]])
sum_value += self.make_int(txout["value"])
return sum_value
@@ -314,15 +393,15 @@ class FIROInterface(BTCInterface):
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
block_hash = self.rpc('getblockhash', [current_height])
block_hash = self.rpc("getblockhash", [current_height])
script_hash: bytes = self.decodeAddress(addr_find)
find_scriptPubKey = self.getDestForScriptHash(script_hash)
while current_height > height_start:
block_hash = self.rpc('getblockhash', [current_height])
block_hash = self.rpc("getblockhash", [current_height])
block = self.rpc('getblock', [block_hash, False])
block = self.rpc("getblock", [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
@@ -330,38 +409,46 @@ class FIROInterface(BTCInterface):
if txo.scriptPubKey == find_scriptPubKey:
tx.rehash()
txid = i2b(tx.sha256)
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
self.rpc('invalidateblock', [block_hash])
self.rpc('reconsiderblock', [block_hash])
self._log.info(
"Found output to addr: {} in tx {} in block {}".format(
addr_find, txid.hex(), block_hash
)
)
self._log.info(
"rescanblockchain hack invalidateblock {}".format(
block_hash
)
)
self.rpc("invalidateblock", [block_hash])
self.rpc("reconsiderblock", [block_hash])
return
current_height -= 1
def getBlockWithTxns(self, block_hash: str):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = []
for tx in decoded_block.vtx:
tx_hex = tx.serialize_with_witness().hex()
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
if 'hex' not in tx_dec:
tx_dec['hex'] = tx_hex
tx_dec = self.rpc("decoderawtransaction", [tx_hex])
if "hex" not in tx_dec:
tx_dec["hex"] = tx_hex
tx_rv.append(tx_dec)
block_rv = {
'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# 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.
@@ -17,75 +18,86 @@ 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 = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet_mweb)
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet_mweb = make_rpc_func(
self._rpcport,
self._rpcauth,
host=self._rpc_host,
wallet=self._rpc_wallet_mweb,
)
def getNewMwebAddress(self, use_segwit=False, label='swap_receive') -> str:
return self.rpc_wallet_mweb('getnewaddress', [label, 'mweb'])
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
def getNewStealthAddress(self, label=''):
def getNewStealthAddress(self, label=""):
return self.getNewMwebAddress(False, label)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
params = [addr_to, value, '', '', subfee, True, self._conf_target]
if type_from == 'mweb':
return self.rpc_wallet_mweb('sendtoaddress', params)
return self.rpc_wallet('sendtoaddress', params)
params = [addr_to, value, "", "", subfee, True, self._conf_target]
if type_from == "mweb":
return self.rpc_wallet_mweb("sendtoaddress", params)
return self.rpc_wallet("sendtoaddress", params)
def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance()
if spendable_balance < value_sats:
raise ValueError('Balance too low')
raise ValueError("Balance too low")
address = self.getNewAddress(self._use_segwit, 'create_utxo')
return self.withdrawCoin(self.format_amount(value_sats), 'plain', address, False), address
address = self.getNewAddress(self._use_segwit, "create_utxo")
return (
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
address,
)
def getWalletInfo(self):
rv = super(LTCInterface, self).getWalletInfo()
mweb_info = self.rpc_wallet_mweb('getwalletinfo')
rv['mweb_balance'] = mweb_info['balance']
rv['mweb_unconfirmed'] = mweb_info['unconfirmed_balance']
rv['mweb_immature'] = mweb_info['immature_balance']
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
rv["mweb_balance"] = mweb_info["balance"]
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
rv["mweb_immature"] = mweb_info["immature_balance"]
return rv
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet('listunspent')
unspent = self.rpc_wallet("listunspent")
for u in unspent:
if u.get('spendable', False) is False:
if u.get("spendable", False) is False:
continue
if u.get('solvable', False) is False: # Filter out mweb outputs
if u.get("solvable", False) is False: # Filter out mweb outputs
continue
if 'address' not in u:
if "address" not in u:
continue
if 'desc' in u:
desc = u['desc']
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
if self.use_p2shp2wsh():
if not desc.startswith('sh(wpkh'):
if not desc.startswith("sh(wpkh"):
continue
else:
if not desc.startswith('wpkh'):
if not desc.startswith("wpkh"):
continue
else:
if not desc.startswith('pkh'):
if not desc.startswith("pkh"):
continue
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
unspent_addr[u["address"]] = unspent_addr.get(
u["address"], 0
) + self.make_int(u["amount"], r=1)
return unspent_addr
class LTCInterfaceMWEB(LTCInterface):
@staticmethod
def coin_type():
def interface_type(self) -> int:
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
self._rpc_wallet = 'mweb'
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
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
)
def chainparams(self):
return chainparams[Coins.LTC]
@@ -95,54 +107,54 @@ class LTCInterfaceMWEB(LTCInterface):
def coin_name(self) -> str:
coin_chainparams = chainparams[Coins.LTC]
return coin_chainparams['name'].capitalize() + ' MWEB'
return coin_chainparams["name"].capitalize() + " MWEB"
def ticker(self) -> str:
ticker = chainparams[Coins.LTC]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
return ticker + '_MWEB'
ticker = chainparams[Coins.LTC]["ticker"]
if self._network == "testnet":
ticker = "t" + ticker
elif self._network == "regtest":
ticker = "rt" + ticker
return ticker + "_MWEB"
def getNewAddress(self, use_segwit=False, label='swap_receive') -> str:
def getNewAddress(self, use_segwit=False, label="swap_receive") -> str:
return self.getNewMwebAddress()
def has_mweb_wallet(self) -> bool:
return 'mweb' in self.rpc('listwallets')
return "mweb" in self.rpc("listwallets")
def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock
self._log.info('init_wallet - {}'.format(self.ticker()))
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])
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
if password is not None:
# Max timeout value, ~3 years
self.rpc_wallet('walletpassphrase', [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000])
if self.getWalletSeedID() == 'Not found':
self._sc.initialiseWallet(self.coin_type())
if self.getWalletSeedID() == "Not found":
self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc('unloadwallet', ['mweb'])
self.rpc('loadwallet', ['mweb'])
self.rpc("unloadwallet", ["mweb"])
self.rpc("loadwallet", ["mweb"])
if password is not None:
self.rpc_wallet('walletpassphrase', [password, 100000000])
self.rpc_wallet('keypoolrefill')
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str):
if password == '':
if password == "":
return
self._log.info('unlockWallet - {}'.format(self.ticker()))
self._log.info("unlockWallet - {}".format(self.ticker()))
if not self.has_mweb_wallet():
self.init_wallet(password)
else:
# Max timeout value, ~3 years
self.rpc_wallet('walletpassphrase', [password, 100000000])
self.rpc_wallet("walletpassphrase", [password, 100000000])
self._sc.checkWalletSeed(self.coin_type())

File diff suppressed because it is too large Load Diff

View File

@@ -14,26 +14,38 @@ class NMCInterface(BTCInterface):
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')
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():
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'])
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']
rv = {"depth": 0, "height": o["height"]}
if o["height"] > 0:
rv["depth"] = ro["height"] - o["height"]
if find_index:
rv['index'] = o['vout']
rv["index"] = o["vout"]
if return_txid:
rv['txid'] = o['txid']
rv["txid"] = o["txid"]
return rv

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,7 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from .btc import BTCInterface
from basicswap.contrib.test_framework.messages import (
CTxOut)
from basicswap.contrib.test_framework.messages import CTxOut
class PassthroughBTCInterface(BTCInterface):
@@ -15,5 +14,5 @@ class PassthroughBTCInterface(BTCInterface):
super().__init__(coin_settings, network)
self.txoType = CTxOut
self._network = network
self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target'])
self.blocks_confirmed = coin_settings["blocks_confirmed"]
self.setConfTarget(coin_settings["conf_target"])

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,11 +12,7 @@ from .btc import BTCInterface
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import (
CBlock,
ToHex,
FromHex,
CTransaction)
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP,
@@ -33,65 +30,79 @@ class PIVXInterface(BTCInterface):
def __init__(self, coin_settings, network, swap_client=None):
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
def checkWallets(self) -> int:
return 1
def signTxWithWallet(self, tx):
rv = self.rpc('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
rv = self.rpc("signrawtransaction", [tx.hex()])
return bytes.fromhex(rv["hex"])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
def createRawFundedTransaction(
self,
addr_to: str,
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = {
'lockUnspents': lock_unspents,
'feeRate': fee_rate,
"lockUnspents": lock_unspents,
"feeRate": fee_rate,
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc('fundrawtransaction', [txn, options])['hex']
options["subtractFeeFromOutputs"] = [
0,
]
return self.rpc("fundrawtransaction", [txn, options])["hex"]
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc('signrawtransaction', [txn_funded])['hex']
return self.rpc("signrawtransaction", [txn_funded])["hex"]
def decodeAddress(self, address):
return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = []
for tx in decoded_block.vtx:
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
tx_rv.append(tx_dec)
block_rv = {
'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee]
return self.rpc('sendtoaddress', params)
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc('getwalletinfo')['balance'])
return self.make_int(self.rpc("getwalletinfo")["balance"])
def loadTx(self, tx_bytes):
# Load tx from bytes to internal representation
@@ -107,22 +118,35 @@ class PIVXInterface(BTCInterface):
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
self._log.info(
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
return bytes.fromhex(rv['hex'])
rv = self.rpc(
"signrawtransaction",
[
tx.hex(),
[],
[
key_wif,
],
],
)
return bytes.fromhex(rv["hex"])
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
rv = self.rpc("gettransaction", [txid_hex])
except Exception as e: # noqa: F841
self._log.debug(
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
)
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None

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.

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# 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.
@@ -26,16 +27,9 @@ from coincurve.dleag import (
from basicswap.interface.base import (
Curves,
)
from basicswap.util import (
i2b, b2i, b2h,
dumpj,
ensure,
TemporaryError)
from basicswap.util.network import (
is_private_ip_address)
from basicswap.rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func)
from basicswap.util import i2b, b2i, b2h, dumpj, ensure, TemporaryError
from basicswap.util.network import is_private_ip_address
from basicswap.rpc_xmr import make_xmr_rpc_func, make_xmr_rpc2_func
from basicswap.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface
@@ -75,7 +69,12 @@ class XMRInterface(CoinInterface):
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
raise ValueError('Not possible')
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:
@@ -84,92 +83,153 @@ class XMRInterface(CoinInterface):
def is_transient_error(self, ex) -> bool:
str_error: str = str(ex).lower()
if 'failed to get output distribution' in str_error:
if "failed to get earliest fork height" in str_error:
return True
return super().is_transient_error(ex)
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
self._addr_prefix = self.chainparams_network()['address_prefix']
self._addr_prefix = self.chainparams_network()["address_prefix"]
self.blocks_confirmed = coin_settings['blocks_confirmed']
self._restore_height = coin_settings.get('restore_height', 0)
self.setFeePriority(coin_settings.get('fee_priority', 0))
self.blocks_confirmed = coin_settings["blocks_confirmed"]
self._restore_height = coin_settings.get("restore_height", 0)
self.setFeePriority(coin_settings.get("fee_priority", 0))
self._sc = swap_client
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', '') != '':
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
if coin_settings.get("rpcuser", "") != "":
daemon_login = (
coin_settings.get("rpcuser", ""),
coin_settings.get("rpcpassword", ""),
)
rpchost = coin_settings.get('rpchost', '127.0.0.1')
rpchost = coin_settings.get("rpchost", "127.0.0.1")
proxy_host = None
proxy_port = None
# Connect to the daemon over a proxy if not running locally
if swap_client:
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
manage_daemon: bool = chain_client_settings['manage_daemon']
manage_daemon: bool = chain_client_settings["manage_daemon"]
if swap_client.use_tor_proxy:
if manage_daemon is False:
log_str: str = ''
have_cc_tor_opt = 'use_tor' in chain_client_settings
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
log_str = ' bypassing proxy (use_tor false for XMR)'
log_str: str = ""
have_cc_tor_opt = "use_tor" in chain_client_settings
if have_cc_tor_opt and chain_client_settings["use_tor"] is False:
log_str = (
f" bypassing proxy (use_tor false for {self.coin_name()})"
)
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
log_str = ' bypassing proxy (private ip address)'
log_str = " bypassing proxy (private ip address)"
else:
proxy_host = swap_client.tor_proxy_host
proxy_port = swap_client.tor_proxy_port
log_str = f' through proxy at {proxy_host}'
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
log_str = f" through proxy at {proxy_host}"
self._log.info(
f"Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}."
)
else:
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
self._log.info(
f"Not connecting to local {self.coin_name()} daemon through proxy."
)
elif manage_daemon is False:
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
self._log.info(
f"Connecting to remote {self.coin_name()} daemon at {rpchost}."
)
self._rpctimeout = coin_settings.get('rpctimeout', 60)
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600)
self._rpctimeout = coin_settings.get("rpctimeout", 60)
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
# walletrpctimeoutlong likely unneeded
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
self.rpc = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ')
self.rpc2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint
self.rpc_wallet = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ')
self.rpc = make_xmr_rpc_func(
coin_settings["rpcport"],
daemon_login,
host=rpchost,
proxy_host=proxy_host,
proxy_port=proxy_port,
default_timeout=self._rpctimeout,
tag="Node(j) ",
)
self.rpc2 = make_xmr_rpc2_func(
coin_settings["rpcport"],
daemon_login,
host=rpchost,
proxy_host=proxy_host,
proxy_port=proxy_port,
default_timeout=self._rpctimeout,
tag="Node ",
) # non-json endpoint
self.rpc_wallet = make_xmr_rpc_func(
coin_settings["walletrpcport"],
coin_settings["walletrpcauth"],
host=coin_settings.get("walletrpchost", "127.0.0.1"),
default_timeout=self._walletrpctimeout,
tag="Wallet ",
)
def setFeePriority(self, new_priority):
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
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))
params["password"] = self._wallet_password
rv = self.rpc_wallet("generate_from_keys", params)
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}
params = {"filename": filename}
if self._wallet_password is not None:
params['password'] = self._wallet_password
params["password"] = self._wallet_password
try:
# Can't reopen the same wallet in windows, !is_keys_file_locked()
self.rpc_wallet('close_wallet')
except Exception:
pass
self.rpc_wallet('open_wallet', params)
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
def initialiseWallet(self, key_view: bytes, key_spend: bytes, restore_height=None) -> None:
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)
# 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")
def initialiseWallet(
self, key_view: bytes, key_spend: bytes, restore_height=None
) -> None:
with self._mx_wallet:
try:
self.openWallet(self._wallet_filename)
# TODO: Check address
return # Wallet exists
except Exception as e:
except Exception as e: # noqa: F841
pass
Kbv = self.getPubkey(key_view)
@@ -177,11 +237,11 @@ class XMRInterface(CoinInterface):
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
params = {
'filename': self._wallet_filename,
'address': address_b58,
'viewkey': b2h(key_view[::-1]),
'spendkey': b2h(key_spend[::-1]),
'restore_height': self._restore_height,
"filename": self._wallet_filename,
"address": address_b58,
"viewkey": b2h(key_view[::-1]),
"spendkey": b2h(key_spend[::-1]),
"restore_height": self._restore_height,
}
self.createWallet(params)
self.openWallet(self._wallet_filename)
@@ -191,86 +251,97 @@ class XMRInterface(CoinInterface):
self.openWallet(self._wallet_filename)
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet('get_languages')
self.rpc_wallet("get_languages")
def getDaemonVersion(self):
return self.rpc_wallet('get_version')['version']
return self.rpc_wallet("get_version")["version"]
def getBlockchainInfo(self):
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
rv = {
'blocks': get_height['height'],
'verificationprogress': 0.0,
"blocks": get_height["height"],
"verificationprogress": 0.0,
}
try:
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
# get_block_count returns "Internal error" if bootstrap-daemon is active
if get_height['untrusted'] is True:
rv['bootstrapping'] = True
get_info = self.rpc2('get_info', timeout=self._rpctimeout)
if 'height_without_bootstrap' in get_info:
rv['blocks'] = get_info['height_without_bootstrap']
if get_height["untrusted"] is True:
rv["bootstrapping"] = True
get_info = self.rpc2("get_info", timeout=self._rpctimeout)
if "height_without_bootstrap" in get_info:
rv["blocks"] = get_info["height_without_bootstrap"]
rv['known_block_count'] = get_info['height']
if rv['known_block_count'] > rv['blocks']:
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
rv["known_block_count"] = get_info["height"]
if rv["known_block_count"] > rv["blocks"]:
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
else:
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
rv["known_block_count"] = self.rpc(
"get_block_count", timeout=self._rpctimeout
)["count"]
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
except Exception as e:
self._log.warning(f'{self.ticker_str()} get_block_count failed with: {e}')
rv['verificationprogress'] = 0.0
self._log.warning(f"{self.ticker_str()} get_block_count failed with: {e}")
rv["verificationprogress"] = 0.0
return rv
def getChainHeight(self):
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
def getWalletInfo(self):
with self._mx_wallet:
try:
self.openWallet(self._wallet_filename)
except Exception as e:
if 'Failed to open wallet' in str(e):
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0}
if "Failed to open wallet" in str(e):
rv = {
"encrypted": True,
"locked": True,
"balance": 0,
"unconfirmed_balance": 0,
}
return rv
raise e
rv = {}
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
balance_info = self.rpc_wallet("get_balance")
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
rv['encrypted'] = False if self._wallet_password is None else True
rv['locked'] = False
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
rv["balance"] = self.format_amount(balance_info["unlocked_balance"])
rv["unconfirmed_balance"] = self.format_amount(
balance_info["balance"] - balance_info["unlocked_balance"]
)
rv["encrypted"] = False if self._wallet_password is None else True
rv["locked"] = False
return rv
def getMainWalletAddress(self) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
return self.rpc_wallet('get_address')['address']
return self.rpc_wallet("get_address")["address"]
def getNewAddress(self, placeholder) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
self.rpc_wallet('store')
new_address = self.rpc_wallet("create_address", {"account_index": 0})[
"address"
]
self.rpc_wallet("store")
return new_address
def get_fee_rate(self, conf_target: int = 2):
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
fee_est = self.rpc('get_fee_estimate')
fee_est = self.rpc("get_fee_estimate")
if conf_target <= 1:
conf_target = 1 # normal
else:
conf_target = 0 # slow
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000
fee_per_k_bytes = fee_est["fees"][conf_target] * 1000
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate'
return float(self.format_amount(fee_per_k_bytes)), "get_fee_estimate"
def getNewSecretKey(self) -> bytes:
def getNewRandomKey(self) -> bytes:
# Note: Returned bytes are in big endian order
return i2b(edu.get_secret())
@@ -299,7 +370,7 @@ class XMRInterface(CoinInterface):
def verifyKey(self, k: int) -> bool:
i = b2i(k)
return (i < edf.l and i > 8)
return i < edf.l and i > 8
def verifyPubkey(self, pubkey_bytes):
# Calls ed25519_decode_check_point() in secp256k1
@@ -325,45 +396,58 @@ class XMRInterface(CoinInterface):
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
def publishBLockTx(
self,
kbv: bytes,
Kbs: bytes,
output_amount: int,
feerate: int,
unlock_time: int = 0,
) -> bytes:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
params = {
"destinations": [{"amount": output_amount, "address": shared_addr}],
"unlock_time": unlock_time,
}
if self._fee_priority > 0:
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)
tx_hash = bytes.fromhex(rv['tx_hash'])
params["priority"] = self._fee_priority
rv = self.rpc_wallet("transfer", params)
self._log.info(
"publishBLockTx %s to address_b58 %s",
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):
def findTxB(
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
):
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
kbv_le = kbv[::-1]
params = {
'restore_height': restore_height,
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv_le),
"restore_height": restore_height,
"filename": address_b58,
"address": address_b58,
"viewkey": b2h(kbv_le),
}
try:
self.openWallet(address_b58)
except Exception as e:
except Exception as e: # noqa: F841
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
'''
"""
# Debug
try:
current_height = self.rpc_wallet('get_height')['height']
@@ -372,137 +456,219 @@ class XMRInterface(CoinInterface):
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
'''
params = {'transfer_type': 'available'}
transfers = self.rpc_wallet('incoming_transfers', params)
"""
params = {"transfer_type": "available"}
transfers = self.rpc_wallet("incoming_transfers", params)
rv = None
if 'transfers' in transfers:
for transfer in transfers['transfers']:
if "transfers" in transfers:
for transfer in transfers["transfers"]:
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
if not transfer['unlocked']:
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
unlock_time = full_tx['transfer']['unlock_time']
if not transfer["unlocked"]:
full_tx = self.rpc_wallet(
"get_transfer_by_txid", {"txid": transfer["tx_hash"]}
)
unlock_time = full_tx["transfer"]["unlock_time"]
if unlock_time != 0:
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
self._log.warning(
"Coin b lock txn is locked: {}, unlock_time {}".format(
transfer["tx_hash"], unlock_time
)
)
rv = -1
continue
if transfer['amount'] == cb_swap_value:
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
if transfer["amount"] == cb_swap_value:
return {
"txid": transfer["tx_hash"],
"amount": transfer["amount"],
"height": (
0
if "block_height" not in transfer
else transfer["block_height"]
),
}
else:
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
self._log.warning(
"Incorrect amount detected for coin b lock txn: {}".format(
transfer["tx_hash"]
)
)
rv = -1
return rv
def findTxnByHash(self, txid):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
try:
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
self._log.info(f'findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}')
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
"height"
]
self._log.info(
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
)
except Exception as e:
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
self._log.info("rpc failed %s", str(e))
current_height = (
None # If the transfer is available it will be deep enough
)
params = {'transfer_type': 'available'}
rv = self.rpc_wallet('incoming_transfers', params)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['tx_hash'] == txid \
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed):
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']}
params = {"transfer_type": "available"}
rv = self.rpc_wallet("incoming_transfers", params)
if "transfers" in rv:
for transfer in rv["transfers"]:
if transfer["tx_hash"] == txid and (
current_height is None
or current_height - transfer["block_height"]
> self.blocks_confirmed
):
return {
"txid": transfer["tx_hash"],
"amount": transfer["amount"],
"height": transfer["block_height"],
}
return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
'''
def spendBLockTx(
self,
chain_b_lock_txid: bytes,
address_to: str,
kbv: bytes,
kbs: bytes,
cb_swap_value: int,
b_fee_rate: int,
restore_height: int,
spend_actual_balance: bool = False,
lock_tx_vout=None,
) -> bytes:
"""
Notes:
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
'''
"""
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
wallet_filename = address_b58 + '_spend'
wallet_filename = address_b58 + "_spend"
params = {
'filename': wallet_filename,
'address': address_b58,
'viewkey': b2h(kbv[::-1]),
'spendkey': b2h(kbs[::-1]),
'restore_height': restore_height,
"filename": wallet_filename,
"address": address_b58,
"viewkey": b2h(kbv[::-1]),
"spendkey": b2h(kbs[::-1]),
"restore_height": restore_height,
}
try:
self.openWallet(wallet_filename)
except Exception as e:
except Exception as e: # noqa: F841
self.createWallet(params)
self.openWallet(wallet_filename)
self.rpc_wallet('refresh')
rv = self.rpc_wallet('get_balance')
if rv['balance'] < cb_swap_value:
self._log.warning('Balance is too low, checking for existing spend.')
txns = self.rpc_wallet('get_transfers', {'out': True})
if 'out' in txns:
txns = txns['out']
rv = self.rpc_wallet("get_balance")
if rv["balance"] < cb_swap_value:
self._log.warning("Balance is too low, checking for existing spend.")
txns = self.rpc_wallet("get_transfers", {"out": True})
if "out" in txns:
txns = txns["out"]
if len(txns) > 0:
txid = txns[0]['txid']
self._log.warning(f'spendBLockTx detected spending tx: {txid}.')
txid = txns[0]["txid"]
self._log.warning(f"spendBLockTx detected spending tx: {txid}.")
# Should check for address_to, but only the from address is found in the output
if txns[0]['address'] == address_b58:
if txns[0]["address"] == address_b58:
return bytes.fromhex(txid)
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
self._log.error(
"wallet {} balance {}, expected {}".format(
wallet_filename, rv["balance"], cb_swap_value
)
)
if not spend_actual_balance:
raise TemporaryError('Invalid balance')
raise TemporaryError("Invalid balance")
if spend_actual_balance and rv['balance'] != cb_swap_value:
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value))
cb_swap_value = rv['balance']
if rv['unlocked_balance'] < cb_swap_value:
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock']))
raise TemporaryError('Invalid unlocked_balance')
if spend_actual_balance and rv["balance"] != cb_swap_value:
self._log.warning(
"Spending actual balance {}, not swap value {}.".format(
rv["balance"], cb_swap_value
)
)
cb_swap_value = rv["balance"]
if rv["unlocked_balance"] < cb_swap_value:
self._log.error(
"wallet {} balance {}, expected {}, blocks_to_unlock {}".format(
wallet_filename,
rv["unlocked_balance"],
cb_swap_value,
rv["blocks_to_unlock"],
)
)
raise TemporaryError("Invalid unlocked_balance")
params = {'address': address_to}
params = {"address": address_to}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
params["priority"] = self._fee_priority
rv = self.rpc_wallet('sweep_all', params)
rv = self.rpc_wallet("sweep_all", params)
return bytes.fromhex(rv['tx_hash_list'][0])
return bytes.fromhex(rv["tx_hash_list"][0])
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str:
def withdrawCoin(
self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False
) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
if sweepall:
balance = self.rpc_wallet('get_balance')
if balance['balance'] != balance['unlocked_balance']:
raise ValueError('Balance must be fully confirmed to use sweep all.')
self._log.info('{} {} sweep_all.'.format(self.ticker_str(), 'estimate fee' if estimate_fee else 'withdraw'))
self._log.debug('{} balance: {}'.format(self.ticker_str(), balance['balance']))
params = {'address': addr_to, 'do_not_relay': estimate_fee, 'subaddr_indices_all': True}
balance = self.rpc_wallet("get_balance")
if balance["balance"] != balance["unlocked_balance"]:
raise ValueError(
"Balance must be fully confirmed to use sweep all."
)
self._log.info(
"{} {} sweep_all.".format(
self.ticker_str(),
"estimate fee" if estimate_fee else "withdraw",
)
)
self._log.debug(
"{} balance: {}".format(self.ticker_str(), balance["balance"])
)
params = {
"address": addr_to,
"do_not_relay": estimate_fee,
"subaddr_indices_all": True,
}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('sweep_all', params)
params["priority"] = self._fee_priority
rv = self.rpc_wallet("sweep_all", params)
if estimate_fee:
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])}
return rv['tx_hash_list'][0]
return {
"num_txns": len(rv["fee_list"]),
"sum_amount": sum(rv["amount_list"]),
"sum_fee": sum(rv["fee_list"]),
"sum_weight": sum(rv["weight_list"]),
}
return rv["tx_hash_list"][0]
value_sats: int = self.make_int(value)
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee}
params = {
"destinations": [{"amount": value_sats, "address": addr_to}],
"do_not_relay": estimate_fee,
}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('transfer', params)
params["priority"] = self._fee_priority
rv = self.rpc_wallet("transfer", params)
if estimate_fee:
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']}
return rv['tx_hash']
return {
"num_txns": 1,
"sum_amount": rv["amount"],
"sum_fee": rv["fee"],
"sum_weight": rv["weight"],
}
return rv["tx_hash"]
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
@@ -512,7 +678,7 @@ class XMRInterface(CoinInterface):
try:
Kbv = self.getPubkey(kbv)
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
wallet_file = address_b58 + '_spend'
wallet_file = address_b58 + "_spend"
try:
self.openWallet(wallet_file)
except Exception:
@@ -520,54 +686,59 @@ class XMRInterface(CoinInterface):
try:
self.openWallet(wallet_file)
except Exception:
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.')
self._log.info(
f"showLockTransfers trying to create wallet for address {address_b58}."
)
kbv_le = kbv[::-1]
params = {
'restore_height': restore_height,
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv_le),
"restore_height": restore_height,
"filename": address_b58,
"address": address_b58,
"viewkey": b2h(kbv_le),
}
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet('refresh')
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
rv['filename'] = wallet_file
rv = self.rpc_wallet(
"get_transfers",
{"in": True, "out": True, "pending": True, "failed": True},
)
rv["filename"] = wallet_file
return rv
except Exception as e:
return {'error': str(e)}
return {"error": str(e)}
def getSpendableBalance(self) -> int:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
return balance_info['unlocked_balance']
balance_info = self.rpc_wallet("get_balance")
return balance_info["unlocked_balance"]
def changeWalletPassword(self, old_password, new_password):
self._log.info('changeWalletPassword - {}'.format(self.ticker()))
self._log.info("changeWalletPassword - {}".format(self.ticker()))
orig_password = self._wallet_password
if old_password != '':
if old_password != "":
self._wallet_password = old_password
try:
self.openWallet(self._wallet_filename)
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
self.rpc_wallet(
"change_wallet_password",
{"old_password": old_password, "new_password": new_password},
)
except Exception as e:
self._wallet_password = orig_password
raise e
def unlockWallet(self, password: str) -> None:
self._log.info('unlockWallet - {}'.format(self.ticker()))
self._log.info("unlockWallet - {}".format(self.ticker()))
self._wallet_password = password
if not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None:
self._log.info('lockWallet - {}'.format(self.ticker()))
self._log.info("lockWallet - {}".format(self.ticker()))
self._wallet_password = None
def isAddressMine(self, address):
@@ -576,7 +747,14 @@ class XMRInterface(CoinInterface):
def ensureFunds(self, amount: int) -> None:
if self.getSpendableBalance() < amount:
raise ValueError('Balance too low')
raise ValueError("Balance too low")
def getTransaction(self, txid: bytes):
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})
return self.rpc2(
"get_transactions",
{
"txs_hashes": [
txid.hex(),
]
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
'''
"""
syntax = "proto3";
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
@@ -18,12 +18,12 @@ Don't encode fields of default values.
When decoding initialise all fields not set from data.
protobuf ParseFromString would reset the whole object, from_bytes won't.
'''
"""
from basicswap.util.integer import encode_varint, decode_varint
class NonProtobufClass():
class NonProtobufClass:
def __init__(self, init_all: bool = True, **kwargs):
for key, value in kwargs.items():
found_field: bool = False
@@ -34,7 +34,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()
@@ -53,7 +53,7 @@ class NonProtobufClass():
else:
setattr(self, field_name, bytes())
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
def to_bytes(self) -> bytes:
rv = bytes()
@@ -74,11 +74,11 @@ class NonProtobufClass():
continue
rv += encode_varint(tag)
if isinstance(field_value, str):
field_value = field_value.encode('utf-8')
field_value = field_value.encode("utf-8")
rv += encode_varint(len(field_value))
rv += field_value
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
return rv
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
@@ -92,7 +92,9 @@ class NonProtobufClass():
field_name, wire_type_expect, field_type = self._map[field_num]
if wire_type != wire_type_expect:
raise ValueError(f'Unexpected wire_type {wire_type} for field {field_num}')
raise ValueError(
f"Unexpected wire_type {wire_type} for field {field_num}"
)
if wire_type == 0:
field_value, lv = decode_varint(b, o)
@@ -100,12 +102,12 @@ class NonProtobufClass():
elif wire_type == 2:
field_len, lv = decode_varint(b, o)
o += lv
field_value = b[o: o + field_len]
field_value = b[o : o + field_len]
o += field_len
if field_type == 1:
field_value = field_value.decode('utf-8')
field_value = field_value.decode("utf-8")
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
setattr(self, field_name, field_value)
@@ -115,151 +117,150 @@ 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", 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),
}
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", 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),
}
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", 2, 0),
2: ("initiate_txid", 2, 0),
3: ("contract_script", 2, 0),
4: ("pkhash_seller", 2, 0),
}
class OfferRevokeMessage(NonProtobufClass):
_map = {
1: ('offer_msg_id', 2, 0),
2: ('signature', 2, 0),
1: ("offer_msg_id", 2, 0),
2: ("signature", 2, 0),
}
class BidRejectMessage(NonProtobufClass):
_map = {
1: ('bid_msg_id', 2, 0),
2: ('reject_code', 0, 0),
1: ("bid_msg_id", 2, 0),
2: ("reject_code", 0, 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", 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),
}
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", 2, 0),
2: ("msg_type", 0, 0),
3: ("sequence", 0, 0),
4: ("dleag", 2, 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", 2, 0),
2: ("pkal", 2, 0),
3: ("kbvl", 2, 0),
4: ("kbsl_dleag", 2, 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", 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),
}
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", 2, 0),
2: ("af_lock_refund_spend_tx_esig", 2, 0),
3: ("af_lock_refund_tx_sig", 2, 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", 2, 0),
2: ("a_lock_spend_tx", 2, 0),
3: ("kal_sig", 2, 0),
}
class XmrBidLockReleaseMessage(NonProtobufClass):
# MSG5F
_map = {
1: ('bid_msg_id', 2, 0),
2: ('al_lock_spend_tx_esig', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("al_lock_spend_tx_esig", 2, 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", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
}
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", 2, 0),
2: ("pkaf", 2, 0),
3: ("kbvf", 2, 0),
4: ("kbsf_dleag", 2, 0),
5: ("dest_af", 2, 0),
}

View File

@@ -5,7 +5,7 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
'''
"""
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
Handshake procedure:
@@ -17,7 +17,7 @@
Both nodes are initialised
XChaCha20_Poly1305 mac is 16bytes
'''
"""
import time
import queue
@@ -36,11 +36,12 @@ from Crypto.Cipher import ChaCha20_Poly1305 # TODO: Add to libsecp256k1/coincur
from coincurve.keys import PrivateKey, PublicKey
from basicswap.contrib.rfc6979 import (
rfc6979_hmac_sha256_initialize,
rfc6979_hmac_sha256_generate)
rfc6979_hmac_sha256_generate,
)
START_TOKEN = 0xabcd
MSG_START_TOKEN = START_TOKEN.to_bytes(2, 'big')
START_TOKEN = 0xABCD
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
MSG_MAX_SIZE = 0x200000 # 2MB
@@ -63,49 +64,71 @@ class NetMessageTypes(IntEnum):
return value in cls._value2member_map_
'''
"""
class NetMessage:
def __init__(self):
self._msg_class = None # 2 bytes
self._len = None # 4 bytes
self._msg_type = None # 2 bytes
'''
"""
# Ensure handshake keys are not reused by including the time in the msg, mac and key hash
# Verify timestamp is not too old
# Add keys to db to catch concurrent attempts, records can be cleared periodically, the timestamp should catch older replay attempts
class MsgHandshake:
__slots__ = ('_timestamp', '_ephem_pk', '_ct', '_mac')
__slots__ = ("_timestamp", "_ephem_pk", "_ct", "_mac")
def __init__(self):
pass
def encode_aad(self): # Additional Authenticated Data
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
self._timestamp.to_bytes(8, 'big') + \
self._ephem_pk
return (
int(NetMessageTypes.HANDSHAKE).to_bytes(2, "big")
+ self._timestamp.to_bytes(8, "big")
+ self._ephem_pk
)
def encode(self):
return self.encode_aad() + self._ct + self._mac
def decode(self, msg_mv):
o = 2
self._timestamp = int.from_bytes(msg_mv[o: o + 8], 'big')
self._timestamp = int.from_bytes(msg_mv[o : o + 8], "big")
o += 8
self._ephem_pk = bytes(msg_mv[o: o + 33])
self._ephem_pk = bytes(msg_mv[o : o + 33])
o += 33
self._ct = bytes(msg_mv[o: -16])
self._ct = bytes(msg_mv[o:-16])
self._mac = bytes(msg_mv[-16:])
class Peer:
__slots__ = (
'_mx', '_pubkey', '_address', '_socket', '_version', '_ready', '_incoming',
'_connected_at', '_last_received_at', '_bytes_sent', '_bytes_received',
'_receiving_length', '_receiving_buffer', '_recv_messages', '_misbehaving_score',
'_ke', '_km', '_dir', '_sent_nonce', '_recv_nonce', '_last_handshake_at',
'_ping_nonce', '_last_ping_at', '_last_ping_rtt')
"_mx",
"_pubkey",
"_address",
"_socket",
"_version",
"_ready",
"_incoming",
"_connected_at",
"_last_received_at",
"_bytes_sent",
"_bytes_received",
"_receiving_length",
"_receiving_buffer",
"_recv_messages",
"_misbehaving_score",
"_ke",
"_km",
"_dir",
"_sent_nonce",
"_recv_nonce",
"_last_handshake_at",
"_ping_nonce",
"_last_ping_at",
"_last_ping_rtt",
)
def __init__(self, address, socket, pubkey):
self._mx = threading.Lock()
@@ -141,14 +164,16 @@ def listen_thread(cls):
max_bytes = 0x10000
while cls._running:
# logging.info('[rm] network loop %d', cls._running)
readable, writable, errored = select.select(cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout)
readable, writable, errored = select.select(
cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout
)
cls._mx.acquire()
try:
disconnected_peers = []
for s in readable:
if s == cls._socket:
peer_socket, address = cls._socket.accept()
logging.info('Connection from %s', address)
logging.info("Connection from %s", address)
new_peer = Peer(address, peer_socket, None)
new_peer._incoming = True
cls._peers.append(new_peer)
@@ -160,12 +185,12 @@ def listen_thread(cls):
try:
bytes_recv = s.recv(max_bytes, socket.MSG_DONTWAIT)
except socket.error as se:
if se.args[0] not in (socket.EWOULDBLOCK, ):
logging.error('Receive error %s', str(se))
if se.args[0] not in (socket.EWOULDBLOCK,):
logging.error("Receive error %s", str(se))
disconnected_peers.append(peer)
continue
except Exception as e:
logging.error('Receive error %s', str(e))
logging.error("Receive error %s", str(e))
disconnected_peers.append(peer)
continue
@@ -175,7 +200,7 @@ def listen_thread(cls):
cls.receive_bytes(peer, bytes_recv)
for s in errored:
logging.warning('Socket error')
logging.warning("Socket error")
for peer in disconnected_peers:
cls.disconnect(peer)
@@ -193,7 +218,9 @@ def msg_thread(cls):
try:
now_us = time.time_ns() // 1000
if peer._ready is True:
if now_us - peer._last_ping_at >= 5000000: # 5 seconds TODO: Make variable
if (
now_us - peer._last_ping_at >= 5000000
): # 5 seconds TODO: Make variable
cls.send_ping(peer)
msg = peer._recv_messages.get(False)
cls.process_message(peer, msg)
@@ -201,7 +228,7 @@ def msg_thread(cls):
except queue.Empty:
pass
except Exception as e:
logging.warning('process message error %s', str(e))
logging.warning("process message error %s", str(e))
if cls._sc.debug:
logging.error(traceback.format_exc())
@@ -211,9 +238,24 @@ def msg_thread(cls):
class Network:
__slots__ = (
'_p2p_host', '_p2p_port', '_network_key', '_network_pubkey',
'_sc', '_peers', '_max_connections', '_running', '_network_thread', '_msg_thread',
'_mx', '_socket', '_read_sockets', '_write_sockets', '_error_sockets', '_csprng', '_seen_ephem_keys')
"_p2p_host",
"_p2p_port",
"_network_key",
"_network_pubkey",
"_sc",
"_peers",
"_max_connections",
"_running",
"_network_thread",
"_msg_thread",
"_mx",
"_socket",
"_read_sockets",
"_write_sockets",
"_error_sockets",
"_csprng",
"_seen_ephem_keys",
)
def __init__(self, p2p_host, p2p_port, network_key, swap_client):
self._p2p_host = p2p_host
@@ -278,7 +320,13 @@ class Network:
self._mx.release()
def add_connection(self, host, port, peer_pubkey):
self._sc.log.info('Connecting from %s to %s at %s %d', self._network_pubkey.hex(), peer_pubkey.hex(), host, port)
self._sc.log.info(
"Connecting from %s to %s at %s %d",
self._network_pubkey.hex(),
peer_pubkey.hex(),
host,
port,
)
self._mx.acquire()
try:
address = (host, port)
@@ -294,7 +342,7 @@ class Network:
self.send_handshake(peer)
def disconnect(self, peer):
self._sc.log.info('Closing peer socket %s', peer._address)
self._sc.log.info("Closing peer socket %s", peer._address)
self._read_sockets.pop(self._read_sockets.index(peer._socket))
self._error_sockets.pop(self._error_sockets.index(peer._socket))
peer.close()
@@ -305,7 +353,11 @@ class Network:
used = self._seen_ephem_keys.get(ephem_pk)
if used:
raise ValueError('Handshake ephem_pk reused %s peer %s', 'for' if direction == 1 else 'by', used[0])
raise ValueError(
"Handshake ephem_pk reused %s peer %s",
"for" if direction == 1 else "by",
used[0],
)
self._seen_ephem_keys[ephem_pk] = (peer._address, timestamp)
@@ -313,12 +365,14 @@ class Network:
self._seen_ephem_keys.popitem(last=False)
def send_handshake(self, peer):
self._sc.log.debug('send_handshake %s', peer._address)
self._sc.log.debug("send_handshake %s", peer._address)
peer._mx.acquire()
try:
# TODO: Drain peer._recv_messages
if not peer._recv_messages.empty():
self._sc.log.warning('send_handshake %s - Receive queue dumped.', peer._address)
self._sc.log.warning(
"send_handshake %s - Receive queue dumped.", peer._address
)
while not peer._recv_messages.empty():
peer._recv_messages.get(False)
@@ -332,7 +386,7 @@ class Network:
ss = k.ecdh(peer._pubkey)
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -361,11 +415,13 @@ class Network:
peer._mx.release()
def process_handshake(self, peer, msg_mv):
self._sc.log.debug('process_handshake %s', peer._address)
self._sc.log.debug("process_handshake %s", peer._address)
# TODO: Drain peer._recv_messages
if not peer._recv_messages.empty():
self._sc.log.warning('process_handshake %s - Receive queue dumped.', peer._address)
self._sc.log.warning(
"process_handshake %s - Receive queue dumped.", peer._address
)
while not peer._recv_messages.empty():
peer._recv_messages.get(False)
@@ -375,17 +431,19 @@ class Network:
try:
now = int(time.time())
if now - peer._last_handshake_at < 30:
raise ValueError('Too many handshakes from peer %s', peer._address)
raise ValueError("Too many handshakes from peer %s", peer._address)
if abs(msg._timestamp - now) > TIMESTAMP_LEEWAY:
raise ValueError('Bad handshake timestamp from peer %s', peer._address)
raise ValueError("Bad handshake timestamp from peer %s", peer._address)
self.check_handshake_ephem_key(peer, msg._timestamp, msg._ephem_pk, direction=2)
self.check_handshake_ephem_key(
peer, msg._timestamp, msg._ephem_pk, direction=2
)
nk = PrivateKey(self._network_key)
ss = nk.ecdh(msg._ephem_pk)
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -395,7 +453,9 @@ class Network:
aad += nonce
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(aad)
plaintext = cipher.decrypt_and_verify(msg._ct, msg._mac) # Will raise error if mac doesn't match
plaintext = cipher.decrypt_and_verify(
msg._ct, msg._mac
) # Will raise error if mac doesn't match
peer._version = plaintext[:6]
sig = plaintext[6:]
@@ -414,26 +474,30 @@ class Network:
except Exception as e:
# TODO: misbehaving
self._sc.log.debug('[rm] process_handshake %s', str(e))
self._sc.log.debug("[rm] process_handshake %s", str(e))
def process_ping(self, peer, msg_mv):
nonce = peer._recv_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_mv[0: 2])
cipher.update(msg_mv[0:2])
cipher.update(nonce)
mac = msg_mv[-16:]
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
ping_nonce = int.from_bytes(plaintext[:4], 'big')
ping_nonce = int.from_bytes(plaintext[:4], "big")
# Version is added to a ping following a handshake message
if len(plaintext) >= 10:
peer._ready = True
version = plaintext[4: 10]
version = plaintext[4:10]
if peer._version is None:
peer._version = version
self._sc.log.debug('Set version from ping %s, %s', peer._pubkey.hex(), peer._version.hex())
self._sc.log.debug(
"Set version from ping %s, %s",
peer._pubkey.hex(),
peer._version.hex(),
)
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
@@ -443,32 +507,32 @@ class Network:
nonce = peer._recv_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_mv[0: 2])
cipher.update(msg_mv[0:2])
cipher.update(nonce)
mac = msg_mv[-16:]
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
pong_nonce = int.from_bytes(plaintext[:4], 'big')
pong_nonce = int.from_bytes(plaintext[:4], "big")
if pong_nonce == peer._ping_nonce:
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
else:
self._sc.log.debug('Pong received out of order %s', peer._address)
self._sc.log.debug("Pong received out of order %s", peer._address)
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
def send_ping(self, peer):
ping_nonce = random.getrandbits(32)
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, 'big')
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, "big")
nonce = peer._sent_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_bytes)
cipher.update(nonce)
payload = ping_nonce.to_bytes(4, 'big')
payload = ping_nonce.to_bytes(4, "big")
if peer._last_ping_at == 0:
payload += self._sc._version
ct, mac = cipher.encrypt_and_digest(payload)
@@ -483,14 +547,14 @@ class Network:
self.send_msg(peer, msg_bytes)
def send_pong(self, peer, ping_nonce):
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, 'big')
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, "big")
nonce = peer._sent_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_bytes)
cipher.update(nonce)
payload = ping_nonce.to_bytes(4, 'big')
payload = ping_nonce.to_bytes(4, "big")
ct, mac = cipher.encrypt_and_digest(payload)
msg_bytes += ct + mac
@@ -502,19 +566,21 @@ class Network:
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
len_encoded = len(msg_encoded)
msg_packed = bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, 'big') + msg_encoded
msg_packed = (
bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, "big") + msg_encoded
)
peer._socket.sendall(msg_packed)
peer._bytes_sent += len_encoded
def process_message(self, peer, msg_bytes):
logging.info('[rm] process_message %s len %d', peer._address, len(msg_bytes))
logging.info("[rm] process_message %s len %d", peer._address, len(msg_bytes))
peer._mx.acquire()
try:
mv = memoryview(msg_bytes)
o = 0
msg_type = int.from_bytes(mv[o: o + 2], 'big')
msg_type = int.from_bytes(mv[o : o + 2], "big")
if msg_type == NetMessageTypes.HANDSHAKE:
self.process_handshake(peer, mv)
elif msg_type == NetMessageTypes.PING:
@@ -522,7 +588,7 @@ class Network:
elif msg_type == NetMessageTypes.PONG:
self.process_pong(peer, mv)
else:
self._sc.log.debug('Unknown message type %d', msg_type)
self._sc.log.debug("Unknown message type %d", msg_type)
finally:
peer._mx.release()
@@ -533,7 +599,6 @@ class Network:
peer._last_received_at = time.time()
peer._bytes_received += len_received
invalid_msg = False
mv = memoryview(bytes_recv)
o = 0
@@ -541,34 +606,34 @@ class Network:
while o < len_received:
if peer._receiving_length == 0:
if len(bytes_recv) < MSG_HEADER_LEN:
raise ValueError('Msg too short')
raise ValueError("Msg too short")
if mv[o: o + 2] != MSG_START_TOKEN:
raise ValueError('Invalid start token')
if mv[o : o + 2] != MSG_START_TOKEN:
raise ValueError("Invalid start token")
o += 2
msg_len = int.from_bytes(mv[o: o + 4], 'big')
msg_len = int.from_bytes(mv[o : o + 4], "big")
o += 4
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
raise ValueError('Invalid data length')
raise ValueError("Invalid data length")
# Precheck msg_type
msg_type = int.from_bytes(mv[o: o + 2], 'big')
msg_type = int.from_bytes(mv[o : o + 2], "big")
# o += 2 # Don't inc offset, msg includes type
if not NetMessageTypes.has_value(msg_type):
raise ValueError('Invalid msg type')
raise ValueError("Invalid msg type")
peer._receiving_length = msg_len
len_pkt = (len_received - o)
len_pkt = len_received - o
nc = msg_len if len_pkt > msg_len else len_pkt
peer._receiving_buffer = mv[o: o + nc]
peer._receiving_buffer = mv[o : o + nc]
o += nc
else:
len_to_go = peer._receiving_length - len(peer._receiving_buffer)
len_pkt = (len_received - o)
len_pkt = len_received - o
nc = len_to_go if len_pkt > len_to_go else len_pkt
peer._receiving_buffer = mv[o: o + nc]
peer._receiving_buffer = mv[o : o + nc]
o += nc
if len(peer._receiving_buffer) == peer._receiving_length:
peer._recv_messages.put(peer._receiving_buffer)
@@ -576,11 +641,13 @@ class Network:
except Exception as e:
if self._sc.debug:
self._sc.log.error('Invalid message received from %s %s', peer._address, str(e))
self._sc.log.error(
"Invalid message received from %s %s", peer._address, str(e)
)
# TODO: misbehaving
def test_onion(self, path):
self._sc.log.debug('test_onion packet')
self._sc.log.debug("test_onion packet")
def get_info(self):
rv = {}
@@ -589,14 +656,14 @@ class Network:
with self._mx:
for peer in self._peers:
peer_info = {
'pubkey': 'Unknown' if not peer._pubkey else peer._pubkey.hex(),
'address': '{}:{}'.format(peer._address[0], peer._address[1]),
'bytessent': peer._bytes_sent,
'bytesrecv': peer._bytes_received,
'ready': peer._ready,
'incoming': peer._incoming,
"pubkey": "Unknown" if not peer._pubkey else peer._pubkey.hex(),
"address": "{}:{}".format(peer._address[0], peer._address[1]),
"bytessent": peer._bytes_sent,
"bytesrecv": peer._bytes_received,
"ready": peer._ready,
"incoming": peer._incoming,
}
peers.append(peer_info)
rv['peers'] = peers
rv["peers"] = peers
return rv

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
t8FIucvlOdt4EqKaSmoBQZk0Aj/N5q4=
=vjZr
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF8V/EkBCAC8YTo6YJLNY0To+25b+dcSRcMCo/g9TJlraoJagO9Hr0Njbryg
jG5iptxi6UjDD+8xPK7YYRhaKyzJq1yTjGe5u5WEEtMfNaiVgA6dSEOXTdH4xT6q
v3VundebzZ7TFue7kj7fzEh7t9x2k5+RI2RvOs26ANEBKgJliQIZDXKOLcQuW7k9
9pWvqMWqRyn8WVGNf/UGBoFDcXQ1wo3h6m/LMJIO5L2IGlQWPmc8WT3uHJ/X/5Ln
slQ1ml7h+JjNwN0rAY/ZaJHSEi2y0RtLRzISP0EsA6EbqvJNGI8jqs5rpImgUn9U
8Q8Xz6hLPAiVTmteF63LlKo03wRcH8d/FVSvABEBAAG0N1BhdHJpY2sgTG9kZGVy
IDxwYXRyaWNrbG9kZGVyQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbT6JAVQEEwEI
AD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTcbvSov58bHk3h7lItOjRb
mNDcHwUCYtNqvwUJB3/VdgAKCRAtOjRbmNDcH+sVB/9jGPwrd1Om6L3ALzkZniR7
ODYFN4m8MRC4LPH2Ngt1Ea3/5DA68hEzQVGAFF+m7i7ZH9bmTvGB9R+qqF9WLTRc
aoO0XvYI8YrRLuhZFazafsLFRD5/c6QfpkBAjiDuxNIjEg2i+nY3avraxicKQKBY
PWWY0TFbz8K+CgIBh8Dnv7lqcxCFWHit/KHHjGAOvIPD5sLtv42dYk4TBEff4MVK
CzuCQtU8viy5doQPYHwfNADpOguskiNtFZmG2iPwgIE2tzHpLG2kidzZvJbHDcXY
XP13FnLvONf2bkS11gZSRm8pa6uay8/KfBNlCeMOYQDVoCuBbD5/2MwuV6o6OfSI
uQENBF8V/EkBCADN8eWUf0OtQdthNoWhRgotz/EzLI9r3sVv2SqbA++rHW9TC7mB
Wl/3e5emXWgKI1EK1Poz5HeKnL3SRx3xizgBTK6+RNQK6svvaLwcx06y8pZP9RqX
jLaRR67fXZCL+ulPtTcbt/JwlaTaokwWsgfy3UZRcK33llLbvWFjht2OGfx8B6Z9
UFRxW4sP0HuE3RrnMATGymWvOZlwYDr73HltksnOEFkz4lVP5VK9kdbndQjIB3Cf
zw/waTqjX+xXjJsFMYZhEDARhP5BQIoQvEv8KRtptNoLJGFZ9RGf+fIHiar2GAZL
4WZbZ0IuGLj419TkgvsUkI83Bx97DkS5Xa+jABEBAAGJATwEGAEIACYCGwwWIQTc
bvSov58bHk3h7lItOjRbmNDcHwUCYtNq0AUJB3/VhwAKCRAtOjRbmNDcH8cfB/4q
Puoir46sAGHBJt4TVe+R5ErVmGfGVUc3n6svguJnRMTAi1gpb6EapjdR9gUx+3Ja
wUE1keJuw5xeFi2JGp/XHt+8LAhsRAaLA4YViho8KL3yjzARvqrkYfl+FuO6kZIj
FEPJjRI1hOx5pWtPa3L3GZOexYDhRVdIJDci3gbFmU8HjgFx0G50zAysGR4DLVXj
FQBPvt4asUTdx30HU/pxWqFEzAeJPOVyjoxotdsMcIYXVBDhte5eADJ4OSMmc7k3
k46yHnbD4wyqqGtWqxHitTrl2U+M5MO5rlOZpGtIMtHz186OyMySZ5Gc886vPlOG
XgtNHT7E4rDrhySwy6Yk
=DQYN
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
tS35OCTSJzNMoXtTZqCkDLc9
=Z8rt
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,52 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
PSxdIAmV43ZrNIOxKzYOsA==
=w412
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
PSxdIAmV43ZrNIOxKzYOsA==
=w412
-----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)
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
=sNb2
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: https://keybase.io/nicolasdorier
Version: Keybase Go 5.0.0 (windows)
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
=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,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

@@ -16,19 +16,26 @@ class ProtocolInterface:
swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError('base class')
raise ValueError("base class")
def getMockScript(self) -> bytearray:
return bytearray([
OpCodes.OP_RETURN, OpCodes.OP_1])
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
def getMockScriptScriptPubkey(self, ci) -> bytearray:
script = self.getMockScript()
return ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
return (
ci.getScriptDest(script)
if ci._use_segwit
else ci.get_p2sh_script_pubkey(script)
)
def getMockAddrTo(self, ci):
script = self.getMockScript()
return ci.encodeScriptDest(ci.getScriptDest(script)) if ci._use_segwit else ci.encode_p2sh(script)
return (
ci.encodeScriptDest(ci.getScriptDest(script))
if ci._use_segwit
else ci.encode_p2sh(script)
)
def findMockVout(self, ci, itx_decoded):
mock_addr = self.getMockAddrTo(ci)

View File

@@ -26,73 +26,91 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
ABS_LOCK_TIME_LEEWAY = 10 * 60
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256) -> bytearray:
script = bytearray([
OpCodes.OP_IF,
OpCodes.OP_SIZE,
0x01, 0x20, # 32
OpCodes.OP_EQUALVERIFY,
op_hash,
0x20]) \
+ secret_hash \
+ bytearray([
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_DUP,
OpCodes.OP_HASH160,
0x14]) \
+ pkh_redeem \
+ bytearray([OpCodes.OP_ELSE, ]) \
+ SerialiseNum(lock_val) \
+ bytearray([
op_lock,
OpCodes.OP_DROP,
OpCodes.OP_DUP,
OpCodes.OP_HASH160,
0x14]) \
+ pkh_refund \
+ bytearray([
OpCodes.OP_ENDIF,
OpCodes.OP_EQUALVERIFY,
OpCodes.OP_CHECKSIG])
def buildContractScript(
lock_val: int,
secret_hash: bytes,
pkh_redeem: bytes,
pkh_refund: bytes,
op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY,
op_hash=OpCodes.OP_SHA256,
) -> bytearray:
script = (
bytearray(
[
OpCodes.OP_IF,
OpCodes.OP_SIZE,
0x01,
0x20, # 32
OpCodes.OP_EQUALVERIFY,
op_hash,
0x20,
]
)
+ secret_hash
+ bytearray([OpCodes.OP_EQUALVERIFY, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14])
+ pkh_redeem
+ bytearray(
[
OpCodes.OP_ELSE,
]
)
+ SerialiseNum(lock_val)
+ bytearray(
[op_lock, OpCodes.OP_DROP, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14]
)
+ pkh_refund
+ bytearray([OpCodes.OP_ENDIF, OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG])
)
return script
def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256):
if script[0] != OpCodes.OP_IF or \
script[1] != OpCodes.OP_SIZE or \
script[2] != 0x01 or script[3] != 0x20 or \
script[4] != OpCodes.OP_EQUALVERIFY or \
script[5] != op_hash or \
script[6] != 0x20:
def verifyContractScript(
script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256
):
if (
script[0] != OpCodes.OP_IF
or script[1] != OpCodes.OP_SIZE
or script[2] != 0x01
or script[3] != 0x20
or script[4] != OpCodes.OP_EQUALVERIFY
or script[5] != op_hash
or script[6] != 0x20
):
return False, None, None, None, None
o = 7
script_hash = script[o: o + 32]
script_hash = script[o : o + 32]
o += 32
if script[o] != OpCodes.OP_EQUALVERIFY or \
script[o + 1] != OpCodes.OP_DUP or \
script[o + 2] != OpCodes.OP_HASH160 or \
script[o + 3] != 0x14:
if (
script[o] != OpCodes.OP_EQUALVERIFY
or script[o + 1] != OpCodes.OP_DUP
or script[o + 2] != OpCodes.OP_HASH160
or script[o + 3] != 0x14
):
return False, script_hash, None, None, None
o += 4
pkh_redeem = script[o: o + 20]
pkh_redeem = script[o : o + 20]
o += 20
if script[o] != OpCodes.OP_ELSE:
return False, script_hash, pkh_redeem, None, None
o += 1
lock_val, nb = decodeScriptNum(script, o)
o += nb
if script[o] != op_lock or \
script[o + 1] != OpCodes.OP_DROP or \
script[o + 2] != OpCodes.OP_DUP or \
script[o + 3] != OpCodes.OP_HASH160 or \
script[o + 4] != 0x14:
if (
script[o] != op_lock
or script[o + 1] != OpCodes.OP_DROP
or script[o + 2] != OpCodes.OP_DUP
or script[o + 3] != OpCodes.OP_HASH160
or script[o + 4] != 0x14
):
return False, script_hash, pkh_redeem, lock_val, None
o += 5
pkh_refund = script[o: o + 20]
pkh_refund = script[o : o + 20]
o += 20
if script[o] != OpCodes.OP_ENDIF or \
script[o + 1] != OpCodes.OP_EQUALVERIFY or \
script[o + 2] != OpCodes.OP_CHECKSIG:
if (
script[o] != OpCodes.OP_ENDIF
or script[o + 1] != OpCodes.OP_EQUALVERIFY
or script[o + 2] != OpCodes.OP_CHECKSIG
):
return False, script_hash, pkh_redeem, lock_val, pkh_refund
return True, script_hash, pkh_redeem, lock_val, pkh_refund
@@ -101,16 +119,23 @@ def extractScriptSecretHash(script):
return script[7:39]
def redeemITx(self, bid_id: bytes, session):
bid, offer = self.getBidAndOffer(bid_id, session)
def redeemITx(self, bid_id: bytes, cursor):
bid, offer = self.getBidAndOffer(bid_id, cursor)
ci_from = self.ci(offer.coin_from)
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate', session=session)
txn = self.createRedeemTxn(
ci_from.coin_type(), bid, for_txn_type="initiate", cursor=cursor
)
txid = ci_from.publishTx(bytes.fromhex(txn))
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex())
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, '', session)
self.log.debug(
"Submitted initiate redeem txn %s to %s chain for bid %s",
txid,
ci_from.coin_name(),
bid_id.hex(),
)
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
class AtomicSwapInterface(ProtocolInterface):
@@ -118,13 +143,19 @@ class AtomicSwapInterface(ProtocolInterface):
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
)
return bytes.fromhex(funded_tx)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
real_txo_script = (
ci.getScriptDest(script)
if ci._use_segwit
else ci.get_p2sh_script_pubkey(script)
)
found: int = 0
ctx = ci.loadTx(mock_tx)
@@ -134,9 +165,9 @@ class AtomicSwapInterface(ProtocolInterface):
found += 1
if found < 1:
raise ValueError('Mocked output not found')
raise ValueError("Mocked output not found")
if found > 1:
raise ValueError('Too many mocked outputs found')
raise ValueError("Too many mocked outputs found")
ctx.nLockTime = 0
funded_tx = ctx.serialize()

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# 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.
@@ -14,23 +15,23 @@ from basicswap.chainparams import (
Coins,
)
from basicswap.basicswap_util import (
EventLogTypes,
KeyTypes,
SwapTypes,
EventLogTypes,
TxTypes,
)
from . import ProtocolInterface
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
OP_CHECKMULTISIG
)
from basicswap.contrib.test_framework.script import CScript, CScriptOp, OP_CHECKMULTISIG
def addLockRefundSigs(self, xmr_swap, ci):
self.log.debug('Setting lock refund tx sigs')
self.log.debug("Setting lock refund tx sigs")
witness_stack = []
if ci.coin_type() not in (Coins.DCR, ):
witness_stack += [b'', ]
if ci.coin_type() not in (Coins.DCR,):
witness_stack += [
b"",
]
witness_stack += [
xmr_swap.al_lock_refund_tx_sig,
xmr_swap.af_lock_refund_tx_sig,
@@ -38,37 +39,38 @@ def addLockRefundSigs(self, xmr_swap, ci):
]
signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack)
ensure(signed_tx, 'setTxSignature failed')
ensure(signed_tx, "setTxSignature failed")
xmr_swap.a_lock_refund_tx = signed_tx
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, session=None):
self.log.info(f'Manually recovering {bid_id.hex()}')
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
self.log.info(f"Manually recovering {self.log.id(bid_id)}")
# Manually recover txn if other key is known
try:
use_session = self.openSession(session)
bid, xmr_swap = self.getXmrBidFromSession(use_session, bid_id)
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
offer, xmr_offer = self.getXmrOfferFromSession(use_session, bid.offer_id, sent=False)
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
use_cursor = self.openDB(cursor)
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
ensure(bid, "Bid not found: {}.".format(bid_id.hex()))
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex()))
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex()))
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex()))
# The no-script coin is always the follower
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from)
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
ci_from = self.ci(Coins(offer.coin_from))
ci_to = self.ci(Coins(offer.coin_to))
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to
try:
decoded_key_half = ci_follower.decodeKey(encoded_key)
except Exception as e:
raise ValueError('Failed to decode provided key-half: ', str(e))
raise ValueError("Failed to decode provided key-half: ", str(e))
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
localkeyhalf = ci_follower.decodeKey(getChainBSplitKey(self, bid, xmr_swap, offer))
localkeyhalf = ci_follower.decodeKey(
getChainBSplitKey(self, bid, xmr_swap, offer)
)
if was_sent:
kbsl = decoded_key_half
kbsf = localkeyhalf
@@ -76,41 +78,64 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, session=None):
kbsl = localkeyhalf
kbsf = decoded_key_half
ensure(ci_follower.verifyKey(kbsl), 'Invalid kbsl')
ensure(ci_follower.verifyKey(kbsf), 'Invalid kbsf')
ensure(ci_follower.verifyKey(kbsl), "Invalid kbsl")
ensure(ci_follower.verifyKey(kbsf), "Invalid kbsf")
if kbsl == kbsf:
raise ValueError('Provided key matches local key')
raise ValueError("Provided key matches local key")
vkbs = ci_follower.sumKeys(kbsl, kbsf)
ensure(ci_follower.verifyPubkey(xmr_swap.pkbs), 'Invalid pkbs') # Sanity check
ensure(ci_follower.verifyPubkey(xmr_swap.pkbs), "Invalid pkbs") # Sanity check
# Ensure summed key matches the expected pubkey
summed_pkbs = ci_follower.getPubkey(vkbs)
if (summed_pkbs != xmr_swap.pkbs):
err_msg: str = 'Summed key does not match expected wallet spend pubkey'
have_pk = summed_pkbs.hex()
expect_pk = xmr_swap.pkbs.hex()
self.log.error(f'{err_msg}. Got: {have_pk}, Expect: {expect_pk}')
if summed_pkbs != xmr_swap.pkbs:
err_msg: str = "Summed key does not match expected wallet spend pubkey"
self.log.error(
f"{err_msg}. Got: {summed_pkbs.hex()}, Expect: {xmr_swap.pkbs.hex()}"
)
raise ValueError(err_msg)
if ci_follower.coin_type() in (Coins.XMR, Coins.WOW):
address_to = self.getCachedMainWalletAddress(ci_follower, use_session)
coin_to: int = ci_follower.interface_type()
base_coin_to: int = ci_follower.coin_type()
if coin_to in (Coins.XMR, Coins.WOW):
address_to = self.getCachedMainWalletAddress(ci_follower, use_cursor)
elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
address_to = self.getCachedStealthAddressForCoin(base_coin_to, use_cursor)
else:
address_to = self.getCachedStealthAddressForCoin(ci_follower.coin_type(), use_session)
address_to = self.getReceiveAddressFromPool(
base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor
)
amount = bid.amount_to
lock_tx_vout = bid.getLockTXBVout()
txid = ci_follower.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, 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())
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), use_session)
use_session.commit()
txid = ci_follower.spendBLockTx(
xmr_swap.b_lock_tx_id,
address_to,
xmr_swap.vkbv,
vkbs,
amount,
xmr_offer.b_fee_rate,
bid.chain_b_height_start,
spend_actual_balance=True,
lock_tx_vout=lock_tx_vout,
)
self.log.debug(
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
)
self.logBidEvent(
bid.bid_id,
EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED,
txid.hex(),
use_cursor,
)
self.commitDB()
return txid
except Exception as e:
self.log.error(traceback.format_exc())
raise (e)
finally:
if session is None:
self.closeSession(use_session, commit=False)
if cursor is None:
self.closeDB(use_cursor, commit=False)
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
@@ -122,7 +147,17 @@ def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
key_type = KeyTypes.KBSF if was_sent else KeyTypes.KBSL
return ci_follower.encodeKey(swap_client.getPathKey(ci_leader.coin_type(), ci_follower.coin_type(), bid.created_at, xmr_swap.contract_count, key_type, for_ed25519))
return ci_follower.encodeKey(
swap_client.getPathKey(
ci_leader.interface_type(),
ci_follower.interface_type(),
bid.created_at,
xmr_swap.contract_count,
key_type,
for_ed25519,
)
)
def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
@@ -132,13 +167,21 @@ def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
if bid.was_sent:
if xmr_swap.a_lock_refund_spend_tx:
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx)
kbsl = ci_leader.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl)
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(
xmr_swap.a_lock_refund_spend_tx
)
kbsl = ci_leader.recoverEncKey(
xmr_swap.af_lock_refund_spend_tx_esig,
af_lock_refund_spend_tx_sig,
xmr_swap.pkasl,
)
return ci_follower.encodeKey(kbsl)
else:
if xmr_swap.a_lock_spend_tx:
al_lock_spend_tx_sig = ci_leader.extractLeaderSig(xmr_swap.a_lock_spend_tx)
kbsf = ci_leader.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf)
kbsf = ci_leader.recoverEncKey(
xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf
)
return ci_follower.encodeKey(kbsf)
return None
@@ -146,18 +189,22 @@ def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
if ci_to.curve_type() == Curves.ed25519:
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0: 33]
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10):
xmr_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, 'proof kbsf owned for swap')
pk_recovered: bytes = ci_to.verifySigAndRecover(xmr_swap.kbsf_dleag, 'proof kbsf owned for swap')
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
kbsf, "proof kbsf owned for swap"
)
pk_recovered: bytes = ci_to.verifySigAndRecover(
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
)
if pk_recovered == xmr_swap.pkbsf:
break
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
assert (pk_recovered == xmr_swap.pkbsf)
assert pk_recovered == xmr_swap.pkbsf
xmr_swap.pkasf = xmr_swap.pkbsf
else:
raise ValueError('Unknown curve')
raise ValueError("Unknown curve")
class XmrSwapInterface(ProtocolInterface):
@@ -165,7 +212,7 @@ class XmrSwapInterface(ProtocolInterface):
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there
if hasattr(ci, 'genScriptLockTxScript') and callable(ci.genScriptLockTxScript):
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)
@@ -175,7 +222,9 @@ class XmrSwapInterface(ProtocolInterface):
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
)
return bytes.fromhex(funded_tx)
@@ -191,9 +240,9 @@ class XmrSwapInterface(ProtocolInterface):
found += 1
if found < 1:
raise ValueError('Mocked output not found')
raise ValueError("Mocked output not found")
if found > 1:
raise ValueError('Too many mocked outputs found')
raise ValueError("Too many mocked outputs found")
ctx.nLockTime = 0
return ctx.serialize()

View File

@@ -18,31 +18,42 @@ from xmlrpc.client import (
from .util import jsonDecimal
class Jsonrpc():
class Jsonrpc:
# __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False, use_builtin_types=False,
*, context=None):
def __init__(
self,
uri,
transport=None,
encoding=None,
verbose=False,
allow_none=False,
use_datetime=False,
use_builtin_types=False,
*,
context=None,
):
# 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')
if parsed.scheme not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
self.__host = parsed.netloc
self.__handler = parsed.path
if not self.__handler:
self.__handler = '/RPC2'
self.__handler = "/RPC2"
if transport is None:
handler = SafeTransport if parsed.scheme == 'https' else Transport
handler = SafeTransport if parsed.scheme == "https" else Transport
extra_kwargs = {}
transport = handler(use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs)
transport = handler(
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs,
)
self.__transport = transport
self.__encoding = encoding or 'utf-8'
self.__encoding = encoding or "utf-8"
self.__verbose = verbose
self.__allow_none = allow_none
@@ -57,17 +68,16 @@ class Jsonrpc():
connection = self.__transport.make_connection(self.__host)
headers = self.__transport._extra_headers[:]
request_body = {
'method': method,
'params': params,
'id': self.__request_id
}
request_body = {"method": method, "params": params, "id": self.__request_id}
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('User-Agent', 'jsonrpc'))
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", "jsonrpc"))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
self.__transport.send_content(
connection,
json.dumps(request_body, default=jsonDecimal).encode("utf-8"),
)
self.__request_id += 1
resp = connection.getresponse()
@@ -82,55 +92,59 @@ class Jsonrpc():
raise
def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
url += 'wallet/' + urllib.parse.quote(wallet)
url += "wallet/" + urllib.parse.quote(wallet)
x = Jsonrpc(url)
v = x.json_request(method, params)
x.close()
r = json.loads(v.decode('utf-8'))
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("RPC server error " + str(ex) + ", method: " + method)
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
return r['result']
return r["result"]
def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'):
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
url += 'wallet/' + urllib.parse.quote(wallet)
url += "wallet/" + urllib.parse.quote(wallet)
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC error ' + str(ex))
raise ValueError("RPC error " + str(ex))
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None):
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)
args = [
cli_bin,
]
if chain != "mainnet":
args.append("-" + chain)
args.append("-datadir=" + datadir)
if wallet is not None:
args.append('-rpcwallet=' + wallet)
args.append("-rpcwallet=" + wallet)
args += shlex.split(cmd)
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
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]))
raise ValueError("RPC error " + str(out[1]))
r = out[0].decode('utf-8').strip()
r = out[0].decode("utf-8").strip()
try:
r = json.loads(r)
except Exception:
@@ -138,7 +152,7 @@ def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None)
return r
def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
port = port
auth = auth
wallet = wallet
@@ -146,11 +160,19 @@ def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
def rpc_func(method, params=None, wallet_override=None):
nonlocal port, auth, wallet, host
return callrpc(port, auth, method, params, wallet if wallet_override is None else wallet_override, host)
return callrpc(
port,
auth,
method,
params,
wallet if wallet_override is None else wallet_override,
host,
)
return rpc_func
def escape_rpcauth(auth_str: str) -> str:
username, password = auth_str.split(':', 1)
password = urllib.parse.quote(password, safe='')
return f'{username}:{password}'
username, password = auth_str.split(":", 1)
password = urllib.parse.quote(password, safe="")
return f"{username}:{password}"

View File

@@ -33,31 +33,50 @@ class SocksTransport(Transport):
return self._connection[1]
# create a HTTP connection object from a host descriptor
chost, self._extra_headers, x509 = self.get_host_info(host)
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost)
self._connection = host, SocksiPyConnection(
self.proxy_type,
self.proxy_host,
self.proxy_port,
self.proxy_rdns,
self.proxy_username,
self.proxy_password,
chost,
)
return self._connection[1]
class JsonrpcDigest():
class JsonrpcDigest:
# __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False, use_builtin_types=False,
*, context=None):
def __init__(
self,
uri,
transport=None,
encoding=None,
verbose=False,
allow_none=False,
use_datetime=False,
use_builtin_types=False,
*,
context=None,
):
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ('http', 'https'):
raise OSError('unsupported XML-RPC protocol')
if parsed.scheme not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
self.__host = parsed.netloc
self.__handler = parsed.path
if transport is None:
handler = SafeTransport if parsed.scheme == 'https' else Transport
handler = SafeTransport if parsed.scheme == "https" else Transport
extra_kwargs = {}
transport = handler(use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs)
transport = handler(
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs,
)
self.__transport = transport
self.__encoding = encoding or 'utf-8'
self.__encoding = encoding or "utf-8"
self.__verbose = verbose
self.__allow_none = allow_none
@@ -77,11 +96,18 @@ class JsonrpcDigest():
connection.timeout = timeout
headers = self.__transport._extra_headers[:]
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('User-Agent', 'jsonrpc'))
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", "jsonrpc"))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8'))
self.__transport.send_content(
connection,
(
""
if params is None
else json.dumps(params, default=jsonDecimal).encode("utf-8")
),
)
self.__request_id += 1
resp = connection.getresponse()
@@ -93,7 +119,7 @@ class JsonrpcDigest():
self.__transport.close()
raise
def json_request(self, request_body, username='', password='', timeout=None):
def json_request(self, request_body, username="", password="", timeout=None):
try:
connection = self.__transport.make_connection(self.__host)
if timeout:
@@ -101,65 +127,82 @@ class JsonrpcDigest():
headers = self.__transport._extra_headers[:]
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('Connection', 'keep-alive'))
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("Connection", "keep-alive"))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
self.__transport.send_content(
connection,
(
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
if request_body
else ""
),
)
resp = connection.getresponse()
if resp.status == 401:
resp_headers = resp.getheaders()
v = resp.read()
_ = resp.read()
algorithm = ''
realm = ''
nonce = ''
realm = ""
nonce = ""
for h in resp_headers:
if h[0] != 'WWW-authenticate':
if h[0] != "WWW-authenticate":
continue
fields = h[1].split(',')
fields = h[1].split(",")
for f in fields:
key, value = f.split('=', 1)
if key == 'algorithm' and value != 'MD5':
key, value = f.split("=", 1)
if key == "algorithm" and value != "MD5":
break
if key == 'realm':
if key == "realm":
realm = value.strip('"')
if key == 'nonce':
if key == "nonce":
nonce = value.strip('"')
if realm != '' and nonce != '':
if realm != "" and nonce != "":
break
if realm == '' or nonce == '':
raise ValueError('Authenticate header not found.')
if realm == "" or nonce == "":
raise ValueError("Authenticate header not found.")
path = self.__handler
HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest()
HA1 = hashlib.md5(
f"{username}:{realm}:{password}".encode("utf-8")
).hexdigest()
http_method = 'POST'
HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest()
http_method = "POST"
HA2 = hashlib.md5(f"{http_method}:{path}".encode("utf-8")).hexdigest()
ncvalue = '{:08x}'.format(1)
s = ncvalue.encode('utf-8')
s += nonce.encode('utf-8')
s += time.ctime().encode('utf-8')
ncvalue = "{:08x}".format(1)
s = ncvalue.encode("utf-8")
s += nonce.encode("utf-8")
s += time.ctime().encode("utf-8")
s += os.urandom(8)
cnonce = (hashlib.sha1(s).hexdigest()[:16])
cnonce = hashlib.sha1(s).hexdigest()[:16]
# MD5-SESS
HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest()
HA1 = hashlib.md5(f"{HA1}:{nonce}:{cnonce}".encode("utf-8")).hexdigest()
respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest()
respdig = hashlib.md5(
f"{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}".encode("utf-8")
).hexdigest()
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
headers = self.__transport._extra_headers[:]
headers.append(('Authorization', header_value))
headers.append(("Authorization", header_value))
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('Connection', 'keep-alive'))
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("Connection", "keep-alive"))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
self.__transport.send_content(
connection,
(
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
if request_body
else ""
),
)
resp = connection.getresponse()
self.__request_id += 1
@@ -172,57 +215,88 @@ class JsonrpcDigest():
raise
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''):
def callrpc_xmr(
rpc_port,
method,
params=[],
rpc_host="127.0.0.1",
path="json_rpc",
auth=None,
timeout=120,
transport=None,
tag="",
):
# auth is a tuple: (username, password)
try:
if rpc_host.count('://') > 0:
url = '{}:{}/{}'.format(rpc_host, rpc_port, path)
if rpc_host.count("://") > 0:
url = "{}:{}/{}".format(rpc_host, rpc_port, path)
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
url = "http://{}:{}/{}".format(rpc_host, rpc_port, path)
x = JsonrpcDigest(url, transport=transport)
request_body = {
'method': method,
'params': params,
'jsonrpc': '2.0',
'id': x.request_id()
"method": method,
"params": params,
"jsonrpc": "2.0",
"id": x.request_id(),
}
if auth:
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
v = x.json_request(
request_body, username=auth[0], password=auth[1], timeout=timeout
)
else:
v = x.json_request(request_body, timeout=timeout)
x.close()
r = json.loads(v.decode('utf-8'))
r = json.loads(v.decode("utf-8"))
except Exception as ex:
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
if 'error' in r and r['error'] is not None:
raise ValueError(tag + 'RPC error ' + str(r['error']))
if "error" in r and r["error"] is not None:
raise ValueError(tag + "RPC error " + str(r["error"]))
return r['result']
return r["result"]
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''):
def callrpc_xmr2(
rpc_port: int,
method: str,
params=None,
auth=None,
rpc_host="127.0.0.1",
timeout=120,
transport=None,
tag="",
):
try:
if rpc_host.count('://') > 0:
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
if rpc_host.count("://") > 0:
url = "{}:{}/{}".format(rpc_host, rpc_port, method)
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
url = "http://{}:{}/{}".format(rpc_host, rpc_port, method)
x = JsonrpcDigest(url, transport=transport)
if auth:
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
v = x.json_request(
params, username=auth[0], password=auth[1], timeout=timeout
)
else:
v = x.json_request(params, timeout=timeout)
x.close()
r = json.loads(v.decode('utf-8'))
r = json.loads(v.decode("utf-8"))
except Exception as ex:
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
return r
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
def make_xmr_rpc2_func(
port,
auth,
host="127.0.0.1",
proxy_host=None,
proxy_port=None,
default_timeout=120,
tag="",
):
port = port
auth = auth
host = host
@@ -236,11 +310,29 @@ def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
return callrpc_xmr2(
port,
method,
params,
auth=auth,
rpc_host=host,
timeout=timeout,
transport=transport,
tag=tag,
)
return rpc_func
def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
def make_xmr_rpc_func(
port,
auth,
host="127.0.0.1",
proxy_host=None,
proxy_port=None,
default_timeout=120,
tag="",
):
port = port
auth = auth
host = host
@@ -254,5 +346,15 @@ def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
return callrpc_xmr(
port,
method,
params,
rpc_host=host,
auth=auth,
timeout=timeout,
transport=transport,
tag=tag,
)
return rpc_func

View File

@@ -8,23 +8,23 @@ from enum import IntEnum
class OpCodes(IntEnum):
OP_0 = 0x00,
OP_PUSHDATA1 = 0x4c,
OP_1 = 0x51,
OP_16 = 0x60,
OP_IF = 0x63,
OP_ELSE = 0x67,
OP_ENDIF = 0x68,
OP_RETURN = 0x6a,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_SIZE = 0x82,
OP_EQUAL = 0x87,
OP_EQUALVERIFY = 0x88,
OP_SHA256 = 0xa8,
OP_HASH160 = 0xa9,
OP_CHECKSIG = 0xac,
OP_CHECKLOCKTIMEVERIFY = 0xb1,
OP_CHECKSEQUENCEVERIFY = 0xb2,
OP_0 = (0x00,)
OP_PUSHDATA1 = (0x4C,)
OP_1 = (0x51,)
OP_16 = (0x60,)
OP_IF = (0x63,)
OP_ELSE = (0x67,)
OP_ENDIF = (0x68,)
OP_RETURN = (0x6A,)
OP_DROP = (0x75,)
OP_DUP = (0x76,)
OP_SIZE = (0x82,)
OP_EQUAL = (0x87,)
OP_EQUALVERIFY = (0x88,)
OP_SHA256 = (0xA8,)
OP_HASH160 = (0xA9,)
OP_CHECKSIG = (0xAC,)
OP_CHECKLOCKTIMEVERIFY = (0xB1,)
OP_CHECKSEQUENCEVERIFY = (0xB2,)
OP_SHA256_DECRED = 0xc0,
OP_SHA256_DECRED = (0xC0,)

View File

@@ -1,153 +1,157 @@
/* 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;
}
/* 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,205 +159,209 @@
.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;
}
}
/* 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;
}

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,872 @@
// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
// Global state
const state = {
identities: new Map(),
currentPage: 1,
wsConnected: false,
swapsData: [],
isLoading: false,
isRefreshing: false,
refreshPromise: null
};
// DOM
const elements = {
swapsBody: document.getElementById('active-swaps-body'),
prevPageButton: document.getElementById('prevPage'),
nextPageButton: document.getElementById('nextPage'),
currentPageSpan: document.getElementById('currentPage'),
paginationControls: document.getElementById('pagination-controls'),
activeSwapsCount: document.getElementById('activeSwapsCount'),
refreshSwapsButton: document.getElementById('refreshSwaps'),
statusDot: document.getElementById('status-dot'),
statusText: document.getElementById('status-text')
};
// Identity Manager
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes
async getIdentityData(address) {
if (!address) {
return { address: '' };
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
return { ...cachedData, address };
}
if (this.pendingRequests.has(address)) {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.cache.set(address, {
data,
timestamp: Date.now()
});
return { ...data, address };
} catch (error) {
console.warn(`Error fetching identity for ${address}:`, error);
return { address };
} finally {
this.pendingRequests.delete(address);
}
},
getCachedIdentity(address) {
const cached = this.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
async fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
...data,
address,
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
label: data.label || '',
note: data.note || '',
automation_override: safeParseInt(data.automation_override)
};
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return {
address,
num_sent_bids_successful: 0,
num_recv_bids_successful: 0,
num_sent_bids_failed: 0,
num_recv_bids_failed: 0,
num_sent_bids_rejected: 0,
num_recv_bids_rejected: 0,
label: '',
note: '',
automation_override: 0
};
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
}
};
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
};
const getStatusClass = (status, tx_a, tx_b) => {
switch (status) {
case 'Completed':
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
case 'Expired':
case 'Timed-out':
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-white';
case 'Error':
case 'Failed':
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
case 'Failed, swiped':
case 'Failed, refunded':
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-red-500';
case 'InProgress':
case 'Script coin locked':
case 'Scriptless coin locked':
case 'Script coin lock released':
case 'SendingInitialTx':
case 'SendingPaymentTx':
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
case 'Received':
case 'Exchanged script lock tx sigs msg':
case 'Exchanged script lock spend tx msg':
case 'Script tx redeemed':
case 'Scriptless tx redeemed':
case 'Scriptless tx recovered':
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
case 'Accepted':
case 'Request accepted':
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
case 'Delaying':
case 'Auto accept delay':
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
case 'Abandoned':
case 'Rejected':
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
default:
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
}
};
const getTxStatusClass = (status) => {
if (!status || status === 'None') return 'text-gray-400';
if (status.includes('Complete') || status.includes('Confirmed')) {
return 'text-green-500';
}
if (status.includes('Error') || status.includes('Failed')) {
return 'text-red-500';
}
if (status.includes('Progress') || status.includes('Sending')) {
return 'text-yellow-500';
}
return 'text-blue-500';
};
// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
if (diff < 60) return `${diff} seconds ago`;
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
return `${Math.floor(diff / 86400)} days ago`;
};
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const formatAddress = (address, displayLength = 15) => {
if (!address) return '';
if (address.length <= displayLength) return address;
return `${address.slice(0, displayLength)}...`;
};
const getStatusColor = (status) => {
const statusColors = {
'Received': 'text-blue-500',
'Accepted': 'text-green-500',
'InProgress': 'text-yellow-500',
'Complete': 'text-green-600',
'Failed': 'text-red-500',
'Expired': 'text-gray-500'
};
return statusColors[status] || 'text-gray-500';
};
const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
};
// WebSocket Manager
const WebSocketManager = {
ws: null,
processingQueue: false,
reconnectTimeout: null,
maxReconnectAttempts: 5,
reconnectAttempts: 0,
reconnectDelay: 5000,
initialize() {
this.connect();
this.startHealthCheck();
},
connect() {
if (this.ws?.readyState === WebSocket.OPEN) return;
try {
const wsPort = window.ws_port || '11700';
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection error:', error);
this.handleReconnect();
}
},
setupEventHandlers() {
this.ws.onopen = () => {
state.wsConnected = true;
this.reconnectAttempts = 0;
updateConnectionStatus('connected');
console.log('🟢 WebSocket connection established for Swaps in Progress');
updateSwapsTable({ resetPage: true, refreshData: true });
};
this.ws.onmessage = () => {
if (!this.processingQueue) {
this.processingQueue = true;
setTimeout(async () => {
try {
if (!state.isRefreshing) {
await updateSwapsTable({ resetPage: false, refreshData: true });
}
} finally {
this.processingQueue = false;
}
}, 200);
}
};
this.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus('disconnected');
this.handleReconnect();
};
this.ws.onerror = () => {
updateConnectionStatus('error');
};
},
startHealthCheck() {
setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.handleReconnect();
}
}, 30000);
},
handleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
} else {
updateConnectionStatus('error');
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
}, 60000);
}
}
};
// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
const statusConfig = {
connected: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2',
textClass: 'text-sm text-green-500',
message: 'Connected'
},
disconnected: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2',
textClass: 'text-sm text-red-500',
message: 'Disconnected - Reconnecting...'
},
error: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2',
textClass: 'text-sm text-yellow-500',
message: 'Connection Error'
},
default: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2',
textClass: 'text-sm text-gray-500',
message: 'Connecting...'
}
};
const config = statusConfig[status] || statusConfig.default;
statusDot.className = config.dotClass;
statusText.className = config.textClass;
statusText.textContent = config.message;
};
const updateLoadingState = (isLoading) => {
state.isLoading = isLoading;
if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.disabled = isLoading;
elements.refreshSwapsButton.classList.toggle('opacity-75', isLoading);
elements.refreshSwapsButton.classList.toggle('cursor-wait', isLoading);
const refreshIcon = elements.refreshSwapsButton.querySelector('svg');
const refreshText = elements.refreshSwapsButton.querySelector('#refreshText');
if (refreshIcon) {
refreshIcon.style.transition = 'transform 0.3s ease';
refreshIcon.classList.toggle('animate-spin', isLoading);
}
if (refreshText) {
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
}
}
};
const processIdentityStats = (identity) => {
if (!identity) return null;
const stats = {
sentSuccessful: safeParseInt(identity.num_sent_bids_successful),
recvSuccessful: safeParseInt(identity.num_recv_bids_successful),
sentFailed: safeParseInt(identity.num_sent_bids_failed),
recvFailed: safeParseInt(identity.num_recv_bids_failed),
sentRejected: safeParseInt(identity.num_sent_bids_rejected),
recvRejected: safeParseInt(identity.num_recv_bids_rejected)
};
stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful;
stats.totalFailed = stats.sentFailed + stats.recvFailed;
stats.totalRejected = stats.sentRejected + stats.recvRejected;
stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected;
stats.successRate = stats.totalBids > 0
? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1)
: '0.0';
return stats;
};
const createIdentityTooltip = (identity) => {
if (!identity) return '';
const stats = processIdentityStats(identity);
if (!stats) return '';
const getSuccessRateColor = (rate) => {
const numRate = parseFloat(rate);
if (numRate >= 80) return 'text-green-600';
if (numRate >= 60) return 'text-yellow-600';
return 'text-red-600';
};
return `
<div class="identity-info space-y-2">
${identity.label ? `
<div class="border-b border-gray-400 pb-2">
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
<div class="text-white">${identity.label}</div>
</div>
` : ''}
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${identity.address || ''}
</div>
</div>
${identity.note ? `
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
<div class="text-white text-sm italic">${identity.note}</div>
</div>
` : ''}
<div class="pt-2 mt-2">
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
<div class="grid grid-cols-2 gap-2">
<div class="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold ${getSuccessRateColor(stats.successRate)}">
${stats.successRate}%
</div>
<div class="text-xs text-white">Success Rate</div>
</div>
<div class="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold text-blue-500">${stats.totalBids}</div>
<div class="text-xs text-white">Total Trades</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
<div>
<div class="text-green-600 font-semibold">
${stats.totalSuccessful}
</div>
<div class="text-white">Successful</div>
</div>
<div>
<div class="text-yellow-600 font-semibold">
${stats.totalRejected}
</div>
<div class="text-white">Rejected</div>
</div>
<div>
<div class="text-red-600 font-semibold">
${stats.totalFailed}
</div>
<div class="text-white">Failed</div>
</div>
</div>
</div>
</div>
`;
};
const createSwapTableRow = async (swap) => {
if (!swap || !swap.bid_id) {
console.warn('Invalid swap data:', swap);
return '';
}
const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to;
const timeColor = getTimeStrokeColor(swap.expire_at);
const fromAmount = parseFloat(swap.amount_from) || 0;
const toAmount = parseFloat(swap.amount_to) || 0;
return `
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${swap.bid_id}">
<td class="relative w-0 p-0 m-0">
<div class="absolute top-0 bottom-0 left-0 w-1"></div>
</td>
<!-- Time Column -->
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
<div class="flex items-center">
<div class="relative" data-tooltip-target="tooltip-time-${uniqueId}">
<svg class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${timeColor}" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12"></polyline>
</g>
</svg>
</div>
<div class="flex flex-col hidden xl:block">
<div class="text-xs whitespace-nowrap">
<span class="bold">Posted:</span> ${formatTimeAgo(swap.created_at)}
</div>
</div>
</div>
</td>
<!-- Details Column -->
<td class="py-8 px-4 text-xs text-left hidden xl:block">
<div class="flex flex-col gap-2 relative">
<div class="flex items-center">
<a href="/identity/${swap.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
${identity?.label || formatAddress(swap.addr_from)}
</span>
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Bid ID:</span>
<a href="/bid/${swap.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
${formatAddress(swap.bid_id)}
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Offer ID:</span>
<a href="/offer/${swap.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
${formatAddress(swap.offer_id)}
</a>
</div>
</div>
</td>
<!-- You Send Column -->
<td class="py-0">
<div class="py-3 px-4 text-left">
<div class="items-center monospace">
<div class="pr-2">
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
</div>
</div>
</div>
</td>
<!-- Swap Column -->
<td class="py-0">
<div class="py-3 px-4 text-center">
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
alt="${swap.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
alt="${swap.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
</div>
</div>
</td>
<!-- You Receive Column -->
<td class="py-0">
<div class="py-3 px-4 text-right">
<div class="items-center monospace">
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
</div>
</div>
</td>
<!-- Status Column -->
<td class="py-3 px-4 text-center">
<div data-tooltip-target="tooltip-status-${uniqueId}" class="flex justify-center">
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
${swap.bid_state}
</span>
</div>
</td>
<!-- Actions Column -->
<td class="py-3 px-4 text-center">
<a href="/bid/${swap.bid_id}"
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
Details
</a>
</td>
<!-- Tooltips -->
<div id="tooltip-time-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="active-revoked-expired">
<span class="bold">
<div class="text-xs"><span class="bold">Posted:</span> ${formatTimeAgo(swap.created_at)}</div>
<div class="text-xs"><span class="bold">Expires in:</span> ${formatTime(swap.expire_at)}</div>
</span>
</div>
<div class="mt-5 text-xs">
<p class="font-bold mb-3">Time Indicator Colors:</p>
<p class="flex items-center">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
</g>
</svg>
Green: More than 30 minutes left
</p>
<p class="flex items-center mt-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
</g>
</svg>
Blue: Between 5 and 30 minutes left
</p>
<p class="flex items-center mt-3 mb-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
</g>
</svg>
Grey: Less than 5 minutes left or expired
</p>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
${createIdentityTooltip(identity)}
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Offer ID:</div>
<div class="monospace text-xs break-all">
${swap.offer_id}
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-bid-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid ID:</div>
<div class="monospace text-xs break-all">
${swap.bid_id}
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="text-white">
<p class="font-bold mb-2">Transaction Status</p>
<div class="grid grid-cols-2 gap-2">
<div class="bg-gray-500 p-2 rounded">
<p class="text-xs font-bold">ITX:</p>
<p>${swap.tx_state_a || 'N/A'}</p>
</div>
<div class="bg-gray-500 p-2 rounded">
<p class="text-xs font-bold">PTX:</p>
<p>${swap.tx_state_b || 'N/A'}</p>
</div>
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</tr>
`;
};
async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
if (state.refreshPromise) {
await state.refreshPromise;
return;
}
try {
updateLoadingState(true);
if (refreshData) {
state.refreshPromise = (async () => {
try {
const response = await fetch('/json/active', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sort_by: "created_at",
sort_dir: "desc"
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
state.swapsData = Array.isArray(data) ? data : [];
} catch (error) {
console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
}
})();
await state.refreshPromise;
}
if (elements.activeSwapsCount) {
elements.activeSwapsCount.textContent = state.swapsData.length;
}
const totalPages = Math.ceil(state.swapsData.length / PAGE_SIZE);
if (resetPage && state.swapsData.length > 0) {
state.currentPage = 1;
}
state.currentPage = Math.min(Math.max(1, state.currentPage), Math.max(1, totalPages));
const startIndex = (state.currentPage - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
if (elements.swapsBody) {
if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
const rows = await Promise.all(rowPromises);
elements.swapsBody.innerHTML = rows.join('');
// Initialize tooltips
if (window.TooltipManager) {
window.TooltipManager.cleanup();
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
tooltipTriggers.forEach(trigger => {
const targetId = trigger.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
}
});
}
} else {
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
No active swaps found
</td>
</tr>`;
}
}
if (elements.paginationControls) {
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
}
if (elements.currentPageSpan) {
elements.currentPageSpan.textContent = state.currentPage;
}
if (elements.prevPageButton) {
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
}
if (elements.nextPageButton) {
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
}
} catch (error) {
console.error('Error updating swaps table:', error);
if (elements.swapsBody) {
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-red-500">
Error loading active swaps. Please try again later.
</td>
</tr>`;
}
} finally {
updateLoadingState(false);
}
}
// Event
const setupEventListeners = () => {
if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.addEventListener('click', async (e) => {
e.preventDefault();
if (state.isRefreshing) return;
updateLoadingState(true);
try {
await updateSwapsTable({ resetPage: true, refreshData: true });
} finally {
updateLoadingState(false);
}
});
}
if (elements.prevPageButton) {
elements.prevPageButton.addEventListener('click', async (e) => {
e.preventDefault();
if (state.isLoading) return;
if (state.currentPage > 1) {
state.currentPage--;
await updateSwapsTable({ resetPage: false, refreshData: false });
}
});
}
if (elements.nextPageButton) {
elements.nextPageButton.addEventListener('click', async (e) => {
e.preventDefault();
if (state.isLoading) return;
const totalPages = Math.ceil(state.swapsData.length / PAGE_SIZE);
if (state.currentPage < totalPages) {
state.currentPage++;
await updateSwapsTable({ resetPage: false, refreshData: false });
}
});
}
};
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
});

View File

@@ -0,0 +1,899 @@
// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
// Global state
const state = {
dentities: new Map(),
currentPage: 1,
wsConnected: false,
jsonData: [],
isLoading: false,
isRefreshing: false,
refreshPromise: null
};
// DOM
const elements = {
bidsBody: document.getElementById('bids-body'),
prevPageButton: document.getElementById('prevPage'),
nextPageButton: document.getElementById('nextPage'),
currentPageSpan: document.getElementById('currentPage'),
paginationControls: document.getElementById('pagination-controls'),
availableBidsCount: document.getElementById('availableBidsCount'),
refreshBidsButton: document.getElementById('refreshBids'),
statusDot: document.getElementById('status-dot'),
statusText: document.getElementById('status-text')
};
// Identity Manager
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes
async getIdentityData(address) {
if (!address) {
return { address: '' };
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
return { ...cachedData, address };
}
if (this.pendingRequests.has(address)) {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.cache.set(address, {
data,
timestamp: Date.now()
});
return { ...data, address };
} catch (error) {
console.warn(`Error fetching identity for ${address}:`, error);
return { address };
} finally {
this.pendingRequests.delete(address);
}
},
getCachedIdentity(address) {
const cached = this.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
async fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
...data,
address,
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
label: data.label || '',
note: data.note || '',
automation_override: safeParseInt(data.automation_override)
};
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return {
address,
num_sent_bids_successful: 0,
num_recv_bids_successful: 0,
num_sent_bids_failed: 0,
num_recv_bids_failed: 0,
num_sent_bids_rejected: 0,
num_recv_bids_rejected: 0,
label: '',
note: '',
automation_override: 0
};
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
},
clearCache() {
this.cache.clear();
this.pendingRequests.clear();
},
removeFromCache(address) {
this.cache.delete(address);
this.pendingRequests.delete(address);
},
cleanup() {
const now = Date.now();
for (const [address, cached] of this.cache.entries()) {
if (now - cached.timestamp >= this.cacheTimeout) {
this.cache.delete(address);
}
}
}
};
// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
if (diff < 60) return `${diff} seconds ago`;
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
return `${Math.floor(diff / 86400)} days ago`;
};
const formatTime = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = timestamp - now;
if (diff <= 0) return "Expired";
if (diff < 60) return `${diff} seconds`;
if (diff < 3600) return `${Math.floor(diff / 60)} minutes`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hours`;
return `${Math.floor(diff / 86400)} days`;
};
const formatAddress = (address, displayLength = 15) => {
if (!address) return '';
if (address.length <= displayLength) return address;
return `${address.slice(0, displayLength)}...`;
};
const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
};
const createTimeTooltip = (bid) => {
const postedTime = formatTimeAgo(bid.created_at);
const expiresIn = formatTime(bid.expire_at);
return `
<div class="active-revoked-expired">
<span class="bold">
<div class="text-xs"><span class="bold">Posted:</span> ${postedTime}</div>
<div class="text-xs"><span class="bold">Expires in:</span> ${expiresIn}</div>
</span>
</div>
<div class="mt-5 text-xs">
<p class="font-bold mb-3">Time Indicator Colors:</p>
<p class="flex items-center">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
</g>
</svg>
Green: More than 30 minutes left
</p>
<p class="flex items-center mt-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
</g>
</svg>
Blue: Between 5 and 30 minutes left
</p>
<p class="flex items-center mt-3 mb-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
</g>
</svg>
Grey: Less than 5 minutes left or expired
</p>
</div>
`;
};
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
};
const processIdentityStats = (identity) => {
if (!identity) return null;
const stats = {
sentSuccessful: safeParseInt(identity.num_sent_bids_successful),
recvSuccessful: safeParseInt(identity.num_recv_bids_successful),
sentFailed: safeParseInt(identity.num_sent_bids_failed),
recvFailed: safeParseInt(identity.num_recv_bids_failed),
sentRejected: safeParseInt(identity.num_sent_bids_rejected),
recvRejected: safeParseInt(identity.num_recv_bids_rejected)
};
stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful;
stats.totalFailed = stats.sentFailed + stats.recvFailed;
stats.totalRejected = stats.sentRejected + stats.recvRejected;
stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected;
stats.successRate = stats.totalBids > 0
? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1)
: '0.0';
return stats;
};
const createIdentityTooltip = (identity) => {
if (!identity) return '';
const stats = processIdentityStats(identity);
if (!stats) return '';
const getSuccessRateColor = (rate) => {
const numRate = parseFloat(rate);
if (numRate >= 80) return 'text-green-600';
if (numRate >= 60) return 'text-yellow-600';
return 'text-red-600';
};
return `
<div class="identity-info space-y-2">
${identity.label ? `
<div class="border-b border-gray-400 pb-2">
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
<div class="text-white">${identity.label}</div>
</div>
` : ''}
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${identity.address || ''}
</div>
</div>
${identity.note ? `
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
<div class="text-white text-sm italic">${identity.note}</div>
</div>
` : ''}
<div class="pt-2 mt-2">
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
<div class="grid grid-cols-2 gap-2">
<div class="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold ${getSuccessRateColor(stats.successRate)}">
${stats.successRate}%
</div>
<div class="text-xs text-white">Success Rate</div>
</div>
<div class="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold text-blue-500">${stats.totalBids}</div>
<div class="text-xs text-white">Total Trades</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
<div>
<div class="text-green-600 font-semibold">
${stats.totalSuccessful}
</div>
<div class="text-white">Successful</div>
</div>
<div>
<div class="text-yellow-600 font-semibold">
${stats.totalRejected}
</div>
<div class="text-white">Rejected</div>
</div>
<div>
<div class="text-red-600 font-semibold">
${stats.totalFailed}
</div>
<div class="text-white">Failed</div>
</div>
</div>
</div>
</div>
`;
};
// WebSocket Manager
const WebSocketManager = {
ws: null,
processingQueue: false,
reconnectTimeout: null,
maxReconnectAttempts: 5,
reconnectAttempts: 0,
reconnectDelay: 5000,
initialize() {
this.connect();
this.startHealthCheck();
},
connect() {
if (this.ws?.readyState === WebSocket.OPEN) return;
try {
const wsPort = window.ws_port || '11700';
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection error:', error);
this.handleReconnect();
}
},
setupEventHandlers() {
this.ws.onopen = () => {
state.wsConnected = true;
this.reconnectAttempts = 0;
updateConnectionStatus('connected');
console.log('🟢 WebSocket connection established for Bid Requests');
updateBidsTable({ resetPage: true, refreshData: true });
};
this.ws.onmessage = () => {
if (!this.processingQueue) {
this.processingQueue = true;
setTimeout(async () => {
try {
if (!state.isRefreshing) {
await updateBidsTable({ resetPage: false, refreshData: true });
}
} finally {
this.processingQueue = false;
}
}, 200);
}
};
this.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus('disconnected');
this.handleReconnect();
};
this.ws.onerror = () => {
updateConnectionStatus('error');
};
},
startHealthCheck() {
setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.handleReconnect();
}
}, 30000);
},
handleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
} else {
updateConnectionStatus('error');
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
}, 60000);
}
}
};
// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
const statusConfig = {
connected: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2',
textClass: 'text-sm text-green-500',
message: 'Connected'
},
disconnected: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2',
textClass: 'text-sm text-red-500',
message: 'Disconnected - Reconnecting...'
},
error: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2',
textClass: 'text-sm text-yellow-500',
message: 'Connection Error'
},
default: {
dotClass: 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2',
textClass: 'text-sm text-gray-500',
message: 'Connecting...'
}
};
const config = statusConfig[status] || statusConfig.default;
statusDot.className = config.dotClass;
statusText.className = config.textClass;
statusText.textContent = config.message;
};
const updateLoadingState = (isLoading) => {
state.isLoading = isLoading;
if (elements.refreshBidsButton) {
elements.refreshBidsButton.disabled = isLoading;
elements.refreshBidsButton.classList.toggle('opacity-75', isLoading);
elements.refreshBidsButton.classList.toggle('cursor-wait', isLoading);
const refreshIcon = elements.refreshBidsButton.querySelector('svg');
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
if (refreshIcon) {
// Add CSS transition for smoother animation
refreshIcon.style.transition = 'transform 0.3s ease';
refreshIcon.classList.toggle('animate-spin', isLoading);
}
if (refreshText) {
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
}
}
};
const createBidTableRow = async (bid) => {
if (!bid || !bid.bid_id) {
console.error('Invalid bid data:', bid);
return '';
}
const identity = await IdentityManager.getIdentityData(bid.addr_from);
const fromAmount = parseFloat(bid.amount_from) || 0;
const toAmount = parseFloat(bid.amount_to) || 0;
const rate = toAmount > 0 ? toAmount / fromAmount : 0;
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to;
const timeColor = getTimeStrokeColor(bid.expire_at);
const uniqueId = `${bid.bid_id}_${bid.created_at}`;
return `
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${bid.bid_id}">
<td class="relative w-0 p-0 m-0">
<div class="absolute top-0 bottom-0 left-0 w-1"></div>
</td>
<!-- Time Column -->
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
<div class="flex items-center">
<div class="relative" data-tooltip-target="tooltip-time-${uniqueId}">
<svg class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${timeColor}" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12"></polyline>
</g>
</svg>
</div>
<div class="flex flex-col hidden xl:block">
<div class="text-xs whitespace-nowrap">
<span class="bold">Posted:</span> ${formatTimeAgo(bid.created_at)}
</div>
<div class="text-xs whitespace-nowrap">
<span class="bold">Expires in:</span> ${formatTime(bid.expire_at)}
</div>
</div>
</div>
</td>
<!-- Details Column -->
<td class="py-8 px-4 text-xs text-left hidden xl:block">
<div class="flex flex-col gap-2 relative">
<div class="flex items-center">
<a href="/identity/${bid.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
${identity?.label || formatAddress(bid.addr_from)}
</span>
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Offer ID:</span>
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
${formatAddress(bid.offer_id)}
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Bid ID:</span>
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
${formatAddress(bid.bid_id)}
</a>
</div>
</div>
</td>
<!-- You Send Column -->
<td class="py-0">
<div class="py-3 px-4 text-left">
<div class="items-center monospace">
<div class="pr-2">
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${bid.coin_from}</div>
</div>
</div>
</div>
</td>
<!-- Swap Column -->
<td class="py-0">
<div class="py-3 px-4 text-center">
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
alt="${bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
alt="${bid.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
</div>
</div>
</td>
<!-- You Get Column -->
<td class="py-0">
<div class="py-3 px-4 text-right">
<div class="items-center monospace">
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${bid.coin_to}</div>
</div>
</div>
</td>
<!-- Rate Column -->
<td class="py-3 px-4 text-right semibold monospace item-center text-xs">
<div class="relative">
<div class="flex flex-col items-end">
<span class="bold text-gray-700 dark:text-white">
${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
</span>
<span class="semibold text-gray-400 dark:text-gray-300">
${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol}
</span>
</div>
</div>
</td>
<!-- Actions Column -->
<td class="py-3 px-4 text-center">
<a href="/bid/${bid.bid_id}/accept"
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
Accept
</a>
</td>
</tr>
<!-- Tooltips -->
<div id="tooltip-time-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="active-revoked-expired">
<span class="bold">
<div class="text-xs"><span class="bold">Posted:</span> ${formatTimeAgo(bid.created_at)}</div>
<div class="text-xs"><span class="bold">Expires in:</span> ${formatTime(bid.expire_at)}</div>
</span>
</div>
<div class="mt-5 text-xs">
<p class="font-bold mb-3">Time Indicator Colors:</p>
<p class="flex items-center">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
</g>
</svg>
Green: More than 30 minutes left
</p>
<p class="flex items-center mt-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
</g>
</svg>
Blue: Between 5 and 30 minutes left
</p>
<p class="flex items-center mt-3 mb-3">
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
</g>
</svg>
Grey: Less than 5 minutes left or expired
</p>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
${createIdentityTooltip(identity)}
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Offer ID:</div>
<div class="monospace text-xs break-all">
${bid.offer_id}
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-bid-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid ID:</div>
<div class="monospace text-xs break-all">
${bid.bid_id}
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
`;
};
const getDisplayText = (identity, address) => {
if (identity?.label) {
return identity.label;
}
return formatAddress(address);
};
const createDetailsColumn = (bid, identity, uniqueId) => `
<td class="py-8 px-4 text-xs text-left hidden xl:block">
<div class="flex flex-col gap-2 relative">
<div class="flex items-center">
<a href="/identity/${bid.addr_from}" data-tooltip-target="tooltip-identity-${uniqueId}" class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
<span class="monospace ${identity?.label ? 'dark:text-white' : 'dark:text-white'}">
${getDisplayText(identity, bid.addr_from)}
</span>
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Offer ID:</span>
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
${formatAddress(bid.offer_id)}
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Bid ID:</span>
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
${formatAddress(bid.bid_id)}
</a>
</div>
</div>
</td>
`;
async function updateBidsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
if (state.refreshPromise) {
await state.refreshPromise;
return;
}
try {
updateLoadingState(true);
if (refreshData) {
state.refreshPromise = (async () => {
try {
const response = await fetch('/json/bids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sort_by: "created_at",
sort_dir: "desc",
with_available_or_active: true
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const allBids = await response.json();
if (!Array.isArray(allBids)) {
throw new Error('Invalid response format');
}
state.jsonData = allBids.filter(bid => bid.bid_state === "Received");
state.originalJsonData = [...state.jsonData];
} finally {
state.refreshPromise = null;
}
})();
await state.refreshPromise;
}
if (elements.availableBidsCount) {
elements.availableBidsCount.textContent = state.jsonData.length;
}
const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE);
if (resetPage && state.jsonData.length > 0) {
state.currentPage = 1;
}
state.currentPage = Math.min(Math.max(1, state.currentPage), Math.max(1, totalPages));
const startIndex = (state.currentPage - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
const currentPageBids = state.jsonData.slice(startIndex, endIndex);
if (elements.bidsBody) {
if (currentPageBids.length > 0) {
const rowPromises = currentPageBids.map(bid => createBidTableRow(bid));
const rows = await Promise.all(rowPromises);
elements.bidsBody.innerHTML = rows.join('');
if (window.TooltipManager) {
window.TooltipManager.cleanup();
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
tooltipTriggers.forEach(trigger => {
const targetId = trigger.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
}
});
}
} else {
elements.bidsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
No available bids requests found
</td>
</tr>`;
}
}
if (elements.paginationControls) {
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
}
if (elements.currentPageSpan) {
elements.currentPageSpan.textContent = state.currentPage;
}
if (elements.prevPageButton) {
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
}
if (elements.nextPageButton) {
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
}
} catch (error) {
console.error('Error updating bids table:', error);
if (elements.bidsBody) {
elements.bidsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-red-500">
Error loading bids. Please try again later.
</td>
</tr>`;
}
} finally {
updateLoadingState(false);
}
}
// Event
const setupEventListeners = () => {
if (elements.refreshBidsButton) {
elements.refreshBidsButton.addEventListener('click', async () => {
if (state.isRefreshing) return;
updateLoadingState(true);
await new Promise(resolve => setTimeout(resolve, 500));
try {
await updateBidsTable({ resetPage: true, refreshData: true });
} finally {
updateLoadingState(false);
}
});
}
if (elements.prevPageButton) {
elements.prevPageButton.addEventListener('click', async () => {
if (state.isLoading) return;
if (state.currentPage > 1) {
state.currentPage--;
await updateBidsTable({ resetPage: false, refreshData: false });
}
});
}
if (elements.nextPageButton) {
elements.nextPageButton.addEventListener('click', async () => {
if (state.isLoading) return;
const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE);
if (state.currentPage < totalPages) {
state.currentPage++;
await updateBidsTable({ resetPage: false, refreshData: false });
}
});
}
};
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
});

View File

@@ -0,0 +1,141 @@
const BidExporter = {
toCSV(bids, type) {
if (!bids || !bids.length) {
return 'No data to export';
}
const isSent = type === 'sent';
const headers = [
'Date/Time',
'Bid ID',
'Offer ID',
'From Address',
isSent ? 'You Send Amount' : 'You Receive Amount',
isSent ? 'You Send Coin' : 'You Receive Coin',
isSent ? 'You Receive Amount' : 'You Send Amount',
isSent ? 'You Receive Coin' : 'You Send Coin',
'Status',
'Created At',
'Expires At'
];
let csvContent = headers.join(',') + '\n';
bids.forEach(bid => {
const row = [
`"${formatTime(bid.created_at)}"`,
`"${bid.bid_id}"`,
`"${bid.offer_id}"`,
`"${bid.addr_from}"`,
isSent ? bid.amount_from : bid.amount_to,
`"${isSent ? bid.coin_from : bid.coin_to}"`,
isSent ? bid.amount_to : bid.amount_from,
`"${isSent ? bid.coin_to : bid.coin_from}"`,
`"${bid.bid_state}"`,
bid.created_at,
bid.expire_at
];
csvContent += row.join(',') + '\n';
});
return csvContent;
},
download(content, filename) {
try {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename);
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error('Error downloading CSV:', error);
const csvData = 'data:text/csv;charset=utf-8,' + encodeURIComponent(content);
const link = document.createElement('a');
link.setAttribute('href', csvData);
link.setAttribute('download', filename);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
},
exportCurrentView() {
const type = state.currentTab;
const data = state.data[type];
if (!data || !data.length) {
alert('No data to export');
return;
}
const csvContent = this.toCSV(data, type);
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const filename = `bsx_${type}_bids_${dateStr}.csv`;
this.download(csvContent, filename);
}
};
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
const exportSentButton = document.getElementById('exportSentBids');
if (exportSentButton) {
EventManager.add(exportSentButton, 'click', (e) => {
e.preventDefault();
state.currentTab = 'sent';
BidExporter.exportCurrentView();
});
}
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportReceivedButton) {
EventManager.add(exportReceivedButton, 'click', (e) => {
e.preventDefault();
state.currentTab = 'received';
BidExporter.exportCurrentView();
});
}
}
}, 500);
});
const originalCleanup = window.cleanup || function(){};
window.cleanup = function() {
originalCleanup();
const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click');
}
if (exportReceivedButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportReceivedButton, 'click');
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
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;

View File

@@ -0,0 +1,190 @@
(function(window) {
'use strict';
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
targetEl.style.visibility = 'hidden';
targetEl.style.display = 'block';
const triggerRect = triggerEl.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let top, left;
top = triggerRect.bottom + offsetDistance;
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
switch (placement) {
case 'bottom-start':
left = triggerRect.left;
break;
case 'bottom-end':
left = triggerRect.right - targetRect.width;
break;
}
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
if (left < 10) left = 10;
if (left + targetRect.width > viewport.width - 10) {
left = viewport.width - targetRect.width - 10;
}
targetEl.style.position = 'fixed';
targetEl.style.top = `${Math.round(top)}px`;
targetEl.style.left = `${Math.round(left)}px`;
targetEl.style.margin = '0';
targetEl.style.maxHeight = `${viewport.height - top - 10}px`;
targetEl.style.overflow = 'auto';
targetEl.style.visibility = 'visible';
}
class Dropdown {
constructor(targetEl, triggerEl, options = {}) {
this._targetEl = targetEl;
this._triggerEl = triggerEl;
this._options = {
placement: options.placement || 'bottom',
offset: options.offset || 5,
onShow: options.onShow || function() {},
onHide: options.onHide || function() {}
};
this._visible = false;
this._initialized = false;
this._handleScroll = this._handleScroll.bind(this);
this._handleResize = this._handleResize.bind(this);
this._handleOutsideClick = this._handleOutsideClick.bind(this);
this.init();
}
init() {
if (!this._initialized) {
this._targetEl.style.margin = '0';
this._targetEl.style.display = 'none';
this._targetEl.style.position = 'fixed';
this._targetEl.style.zIndex = '50';
this._setupEventListeners();
this._initialized = true;
}
}
_setupEventListeners() {
this._triggerEl.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
document.addEventListener('click', this._handleOutsideClick);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
});
window.addEventListener('scroll', this._handleScroll, true);
window.addEventListener('resize', this._handleResize);
}
_handleScroll() {
if (this._visible) {
requestAnimationFrame(() => {
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
);
});
}
}
_handleResize() {
if (this._visible) {
requestAnimationFrame(() => {
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
);
});
}
}
_handleOutsideClick(e) {
if (this._visible &&
!this._targetEl.contains(e.target) &&
!this._triggerEl.contains(e.target)) {
this.hide();
}
}
show() {
if (!this._visible) {
this._targetEl.style.display = 'block';
this._targetEl.style.visibility = 'hidden';
requestAnimationFrame(() => {
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
);
this._visible = true;
this._options.onShow();
});
}
}
hide() {
if (this._visible) {
this._targetEl.style.display = 'none';
this._visible = false;
this._options.onHide();
}
}
toggle() {
if (this._visible) {
this.hide();
} else {
this.show();
}
}
destroy() {
document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('resize', this._handleResize);
this._initialized = false;
}
}
function initDropdowns() {
document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
const targetId = triggerEl.getAttribute('data-dropdown-toggle');
const targetEl = document.getElementById(targetId);
if (targetEl) {
const placement = triggerEl.getAttribute('data-dropdown-placement');
new Dropdown(targetEl, triggerEl, {
placement: placement || 'bottom-start'
});
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDropdowns);
} else {
initDropdowns();
}
window.Dropdown = Dropdown;
window.initDropdowns = initDropdowns;
})(window);

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

@@ -19,8 +19,8 @@ document.addEventListener('DOMContentLoaded', function() {
const backdrop = document.querySelectorAll('.navbar-backdrop');
if (close.length) {
for (var i = 0; i < close.length; i++) {
close[i].addEventListener('click', function() {
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');
}
@@ -29,12 +29,12 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (backdrop.length) {
for (var i = 0; i < backdrop.length; i++) {
backdrop[i].addEventListener('click', function() {
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');
}
});
}
}
});
});

View File

@@ -1,5 +1,5 @@
window.addEventListener('DOMContentLoaded', (event) => {
let err_msgs = document.querySelectorAll('p.error_msg');
window.addEventListener('DOMContentLoaded', () => {
const err_msgs = document.querySelectorAll('p.error_msg');
for (let i = 0; i < err_msgs.length; i++) {
err_msg = err_msgs[i].innerText;
if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
@@ -29,9 +29,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
// remove error class on input or select focus
let inputs = document.querySelectorAll('input.error');
let selects = document.querySelectorAll('select.error');
let elements = [...inputs, ...selects];
const inputs = document.querySelectorAll('input.error');
const selects = document.querySelectorAll('select.error');
const elements = [...inputs, ...selects];
elements.forEach((element) => {
element.addEventListener('focus', (event) => {
event.target.classList.remove('error');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

115
basicswap/static/js/tabs.js Normal file
View File

@@ -0,0 +1,115 @@
(function(window) {
'use strict';
class Tabs {
constructor(tabsEl, items = [], options = {}) {
this._tabsEl = tabsEl;
this._items = items;
this._activeTab = options.defaultTabId ? this.getTab(options.defaultTabId) : null;
this._options = {
defaultTabId: options.defaultTabId || null,
activeClasses: options.activeClasses || 'text-blue-600 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500',
inactiveClasses: options.inactiveClasses || 'dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300',
onShow: options.onShow || function() {}
};
this._initialized = false;
this.init();
}
init() {
if (this._items.length && !this._initialized) {
if (!this._activeTab) {
this.setActiveTab(this._items[0]);
}
this.show(this._activeTab.id, true);
this._items.forEach(tab => {
tab.triggerEl.addEventListener('click', () => {
this.show(tab.id);
});
});
this._initialized = true;
}
}
show(tabId, force = false) {
const tab = this.getTab(tabId);
if ((tab !== this._activeTab) || force) {
this._items.forEach(t => {
if (t !== tab) {
t.triggerEl.classList.remove(...this._options.activeClasses.split(' '));
t.triggerEl.classList.add(...this._options.inactiveClasses.split(' '));
t.targetEl.classList.add('hidden');
t.triggerEl.setAttribute('aria-selected', false);
}
});
tab.triggerEl.classList.add(...this._options.activeClasses.split(' '));
tab.triggerEl.classList.remove(...this._options.inactiveClasses.split(' '));
tab.triggerEl.setAttribute('aria-selected', true);
tab.targetEl.classList.remove('hidden');
this.setActiveTab(tab);
this._options.onShow(this, tab);
}
}
getTab(id) {
return this._items.find(t => t.id === id);
}
getActiveTab() {
return this._activeTab;
}
setActiveTab(tab) {
this._activeTab = tab;
}
}
function initTabs() {
document.querySelectorAll('[data-tabs-toggle]').forEach(tabsEl => {
const items = [];
let defaultTabId = null;
tabsEl.querySelectorAll('[role="tab"]').forEach(triggerEl => {
const isActive = triggerEl.getAttribute('aria-selected') === 'true';
const tab = {
id: triggerEl.getAttribute('data-tabs-target'),
triggerEl: triggerEl,
targetEl: document.querySelector(triggerEl.getAttribute('data-tabs-target'))
};
items.push(tab);
if (isActive) {
defaultTabId = tab.id;
}
});
new Tabs(tabsEl, items, {
defaultTabId: defaultTabId
});
});
}
const style = document.createElement('style');
style.textContent = `
[data-tabs-toggle] [role="tab"] {
cursor: pointer;
}
`;
document.head.appendChild(style);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTabs);
} else {
initTabs();
}
window.Tabs = Tabs;
window.initTabs = initTabs;
})(window);

View File

@@ -0,0 +1,306 @@
class TooltipManager {
constructor() {
this.activeTooltips = new Map();
this.sizeCheckIntervals = new Map();
this.setupStyles();
this.setupCleanupEvents();
}
static initialize() {
if (!window.TooltipManager) {
window.TooltipManager = new TooltipManager();
}
return window.TooltipManager;
}
create(element, content, options = {}) {
if (!element) return null;
this.destroy(element);
const checkSize = () => {
const rect = element.getBoundingClientRect();
if (rect.width && rect.height) {
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
this.createTooltip(element, content, options, rect);
}
};
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50));
checkSize();
return null;
}
createTooltip(element, content, options, rect) {
const targetId = element.getAttribute('data-tooltip-target');
let bgClass = 'bg-gray-400';
let arrowColor = 'rgb(156 163 175)';
if (targetId?.includes('tooltip-offer-')) {
const offerId = targetId.split('tooltip-offer-')[1];
const [actualOfferId] = offerId.split('_');
if (window.jsonData) {
const offer = window.jsonData.find(o =>
o.unique_id === offerId ||
o.offer_id === actualOfferId
);
if (offer) {
if (offer.is_revoked) {
bgClass = 'bg-red-500';
arrowColor = 'rgb(239 68 68)';
} else if (offer.is_own_offer) {
bgClass = 'bg-gray-300';
arrowColor = 'rgb(209 213 219)';
} else {
bgClass = 'bg-green-700';
arrowColor = 'rgb(21 128 61)';
}
}
}
}
const instance = tippy(element, {
content,
allowHTML: true,
placement: options.placement || 'top',
appendTo: document.body,
animation: false,
duration: 0,
delay: 0,
interactive: true,
arrow: false,
theme: '',
moveTransition: 'none',
offset: [0, 10],
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 10
}
},
{
name: 'flip',
options: {
padding: 10,
fallbackPlacements: ['top', 'bottom', 'right', 'left']
}
}
]
},
onCreate(instance) {
instance._originalPlacement = instance.props.placement;
},
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
const rect = element.getBoundingClientRect();
if (!rect.width || !rect.height) {
return false;
}
instance.setProps({
placement: instance._originalPlacement
});
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
return true;
},
onMount(instance) {
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
}
});
const id = element.getAttribute('data-tooltip-trigger-id') ||
`tooltip-${Math.random().toString(36).substring(7)}`;
element.setAttribute('data-tooltip-trigger-id', id);
this.activeTooltips.set(id, instance);
return instance;
}
destroy(element) {
if (!element) return;
if (this.sizeCheckIntervals.has(element)) {
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
}
const id = element.getAttribute('data-tooltip-trigger-id');
if (!id) return;
const instance = this.activeTooltips.get(id);
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error destroying tooltip:', e);
}
}
this.activeTooltips.delete(id);
element.removeAttribute('data-tooltip-trigger-id');
}
cleanup() {
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval));
this.sizeCheckIntervals.clear();
this.activeTooltips.forEach((instance, id) => {
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error cleaning up tooltip:', e);
}
}
});
this.activeTooltips.clear();
document.querySelectorAll('[data-tippy-root]').forEach(element => {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
});
}
setupStyles() {
if (document.getElementById('tooltip-styles')) return;
document.head.insertAdjacentHTML('beforeend', `
<style id="tooltip-styles">
[data-tippy-root] {
position: fixed !important;
z-index: 9999 !important;
pointer-events: none !important;
}
.tippy-box {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
border-radius: 0.5rem;
color: white;
position: relative !important;
pointer-events: auto !important;
}
.tippy-content {
padding: 0.5rem 0.75rem !important;
}
.tippy-box .bg-gray-400 {
background-color: rgb(156 163 175);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-400) .tippy-arrow {
color: rgb(156 163 175);
}
.tippy-box .bg-red-500 {
background-color: rgb(239 68 68);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-red-500) .tippy-arrow {
color: rgb(239 68 68);
}
.tippy-box .bg-gray-300 {
background-color: rgb(209 213 219);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-300) .tippy-arrow {
color: rgb(209 213 219);
}
.tippy-box .bg-green-700 {
background-color: rgb(21 128 61);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-green-700) .tippy-arrow {
color: rgb(21 128 61);
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: currentColor;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: currentColor;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: currentColor;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: currentColor;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
</style>
`);
}
setupCleanupEvents() {
window.addEventListener('beforeunload', () => this.cleanup());
window.addEventListener('unload', () => this.cleanup());
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.cleanup();
}
});
}
initializeTooltips(selector = '[data-tooltip-target]') {
document.querySelectorAll(selector).forEach(element => {
const targetId = element.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
});
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = TooltipManager;
}
document.addEventListener('DOMContentLoaded', () => {
TooltipManager.initialize();
});

View File

@@ -1,115 +1,118 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/active">Swaps In Progress</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</div>
</div>
</section>
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Swaps in Progress</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Your swaps that are currently in progress.</p>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
{% if refresh %}
<a id="refresh" href="/active" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ circular_arrows_svg | safe }}
<span>Refresh 30 seconds</span>
</a>
{% else %}
<a id="refresh" href="/active" class="flex flex-wrap justify-center px-5 py-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white borderdark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ circular_arrows_svg | safe }}
<span>Refresh</span>
</a>
{% endif %}
</div>
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Swaps in Progress</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Monitor your currently active swap transactions.</p>
</div>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
</div>
</th>
</tr>
</thead>
{% for s in active_swaps %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 monospace">
<a href=/bid/{{ s[0] }}>{{ s[0]|truncate(50,true,'...',0) }}</a>
</td>
<td class="py-3 px-6 monospace">
<a href=/offer/{{ s[1] }}>{{ s[1]|truncate(50,true,'...',0) }}</a>
</td>
<td class="py-3 px-6 w-52 whitespace-normal break-words">{{ s[2] }}</td>
<td class="py-3 px-6">{{ s[3] }}</td>
<td class="py-3 px-6">{{ s[4] }}</td>
</tr>
{% endfor %}
</table>
</div>
</section>
{% include 'inc_messages.html' %}
<section>
<div class="mt-5 lg:container mx-auto lg:px-0 px-6">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto mt-6 overflow-auto lg:overflow-hidden">
<table class="w-full min-w-max">
<thead class="uppercase">
<tr>
<th class="p-0" data-sortable="true" data-column-index="0">
<div class="py-3 pl-4 justify-center rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold"></span>
</div>
</div>
</th>
<th class="p-0">
<div class="py-3 pl-6 pr-3 justify-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold">Time</span>
</div>
</th>
<th class="p-0 hidden xl:block">
<div class="py-3 px-4 text-left bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Swap</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 rounded-tr-xl">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody id="active-swaps-body"></tbody>
</table>
</div>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400 mr-4">Active Swaps: <span id="activeSwapsCount">0</span></p>
{% if debug_ui_mode == true %}
<button type="button" id="refreshSwaps" class="inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshText">Refresh</span>
</button>
{% endif %}
<div id="pagination-controls" class="flex items-center space-x-2" style="display: none;">
<button id="prevPage" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPage">1</span></p>
<button id="nextPage" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</section>
<script src="/static/js/active.js"></script>
{% include 'footer.html' %}
</body>
</html>

View File

@@ -1,220 +1,368 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">{{ page_type_available }} {{ page_type_received }} {{ page_type_sent }}</a> </li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul>
</div>
</div>
</section>
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt=""> <img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">{{ page_type_available }} {{ page_type_received }} {{ page_type_sent }}</h2>
<p class="font-normal text-coolGray-200 dark:text-white">{{ page_type_available_description }} {{ page_type_received_description }} {{ page_type_sent_description }}</p>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
{% if refresh %}
<a id="refresh" href="/bid/{{ bid_id }}" class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ circular_arrows_svg | safe }}
<span>Refresh {{ refresh }} seconds</span>
</a>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full mx-auto pt-2">
<form method="post">
<div class="flex items-center justify-center pb-4 dark:text-white">
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-center -m-1.5">
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sort_by" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="created_at" {% if filters.sort_by=='created_at' %} selected{% endif %}>Time At</option>
</select>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sort_dir" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="asc" {% if filters.sort_dir=='asc' %} selected{% endif %}>Ascending</option>
<option value="desc" {% if filters.sort_dir=='desc' %} selected{% endif %}>Descending</option>
</select>
</div>
</div>
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading bold">State:</p>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="state" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="-1" {% if filters.bid_state_ind==-1 %} selected{% endif %}>Any</option>
{% for s in data.bid_states %}
<option value="{{ s[0] }}" {% if filters.bid_state_ind==s[0] %} selected{% endif %}>{{ s[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading bold">Include Expired:</p>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="with_expired" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="true" {% if filters.with_expired==true %} selected{% endif %}>Include</option>
<option value="false" {% if filters.with_expired==false %} selected{% endif %}>Exclude</option>
</select> </div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
<button type="submit" name='clearfilters' value="Clear Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none">
<span>Clear Filters</span>
</button>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative"> <button type="submit" name='applyfilters' value="Apply Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ filter_apply_svg | safe }}
<span>Apply Filters</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Date/Time at</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid From</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
</tr>
</thead>
<tbody>
{% for b in bids %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<th scope="row" class="flex items-center py-7 px-46 text-gray-900 whitespace-nowrap"> <svg class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="pl-3">
<div class="font-semibold text-xs dark:text-white">{{ b[0] }}</div>
</div>
</th>
<td class="py-3 px-6 text-xs monospace"> <a href=/bid/{{ b[1] }}>{{ b[1]|truncate(20, True) }}</a> </td>
<td class="py-3 px-6 text-xs monospace"> <a href=/offer/{{ b[2] }}>{{ b[2]|truncate(20, True) }}</a> </td>
<td class="py-3 px-6 text-xs monospace"> <a href=/identity/{{ b[6] }}>{{ b[6] }}</a> </td>
<td class="py-3 px-6 text-xs">{{ b[3] }}</td>
<td class="py-3 px-6 text-xs">{{ b[4] }}</td>
<td class="py-3 px-6 text-xs">{{ b[5] }}</td>
{% if page_type_received or page_type_sent %}
<td class="py-3 px-6 text-xs"> <a class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200 bg-blue-500 text-white hover:bg-blue-600 transition duration-200" href="/bid/{{ b[1] }}">Details</a>
</td> {% elif page_type_available %}
<td class="py-3 px-6 text-xs"> <a class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200 bg-blue-500 text-white hover:bg-blue-600 transition duration-200" href="/bid/{{ b[1] }}">Accept</a>
</td>
{% endif %}
</tr>
</tbody>
{% endfor %}
</table> <input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="pageno" value="{{ filters.page_no }}">
</div>
</div>
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
{% if filters.page_no > 1 %} <div class="w-full md:w-auto p-1.5">
<button type="submit" name='pageback' value="Previous" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
{{ page_back_svg | safe }}
<span>Previous</span>
</button>
</div>
{% endif %}
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading dark:text-white">Page: {{ filters.page_no }}</p>
</div>
</div>
{% if bids_count > 20 %}
<div class="w-full md:w-auto p-1.5"> <button type="submit" name='pageforwards' value="Next" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<span>Next</span>
{{ page_forwards_svg | safe }}
</button>
</div>
{% endif %}
</div>
</div>
</div>
</form>
</div>
</div>
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
</div>
</div>
</div>
</div>
</section>
</section>
{% include 'inc_messages.html' %}
<div class="xl:container mx-auto">
<section>
<div class="pl-6 pr-6 pt-0 mt-5 h-full overflow-hidden">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true">
Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span>
</button>
</li>
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="received-tab" data-tabs-target="#received" type="button" role="tab" aria-controls="received" aria-selected="false">
Received Bids <span class="text-gray-500 dark:text-gray-400">({{ received_bids_count }})</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6 mt-6 border-coolGray-100">
<div class="flex flex-wrap justify-center -m-1.5">
<div class="w-full md:w-auto p-1.5">
<div class="relative">
<input type="text"
id="searchInput"
name="search" autocomplete="off" placeholder="Search bid ID, offer ID, address or label..."
class="w-full md:w-96 hover:border-blue-500 dark:hover:bg-gray-50 text-gray-900 pl-4 pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none block p-2.5 focus:ring-blue-500 focus:border-blue-500 focus:ring-0 dark:focus:bg-gray-500 dark:focus:text-white">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg class="w-5 h-5 text-gray-500 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
</div>
<div class="p-1.5 md:w-auto hover-container">
<div class="flex">
<button id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="coin_from" id="coin_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-r-lg outline-none block w-full p-2.5 focus:ring-0 border-l-0">
<option value="any" {% if filters.coin_from==-1 %} selected{% endif %}>You Send</option>
{% for c in coins_from %}
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_from==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading text-gray-500 dark:text-white">{{ arrow_right_svg | safe }}</p>
</div>
</div>
<button id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="coin_to" id="coin_to" class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-r-lg outline-none block w-full p-2.5 focus:ring-0 border-l-0">
<option value="any" {% if filters.coin_to==-1 %} selected{% endif %}>You Receive</option>
{% for c in coins %}
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_to==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="state" id="state" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="-1" selected="">Any State</option>
<optgroup label="Active States">
<option value="1">Sent</option>
<option value="2">Receiving</option>
<option value="3">Received</option>
<option value="4">Receiving accept</option>
<option value="5">Accepted</option>
<option value="6">Initiated</option>
<option value="7">Participating</option>
</optgroup>
<optgroup label="Completed States">
<option value="8">Completed</option>
<option value="15">Scriptless tx redeemed</option>
<option value="13">Script tx redeemed</option>
</optgroup>
<optgroup label="Failed States">
<option value="17">Failed, refunded</option>
<option value="18">Failed, swiped</option>
<option value="19">Failed</option>
<option value="22">Abandoned</option>
<option value="23">Error</option>
<option value="31">Expired</option>
</optgroup>
<optgroup label="Other States">
<option value="9">Script coin locked</option>
<option value="10">Script coin spend tx valid</option>
<option value="11">Scriptless coin locked</option>
<option value="12">Script coin lock released</option>
<option value="14">Script pre-refund tx in chain</option>
<option value="16">Scriptless tx recovered</option>
<option value="20">Delaying</option>
<option value="21">Timed-out</option>
<option value="24">Stalled (debug)</option>
<option value="25">Rejected</option>
<option value="26">Unknown bid state</option>
<option value="27">Exchanged script lock tx sigs msg</option>
<option value="28">Exchanged script lock spend tx msg</option>
<option value="29">Request sent</option>
<option value="30">Request accepted</option>
<option value="32">Auto accept delay</option>
<option value="33">Auto accept failed</option>
</optgroup>
</select>
</div>
</div>
<!-- todo
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="with_expired" id="with_expired" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="true">Include Expired</option>
<option value="false">Exclude Expired</option>
</select>
</div>
</div>-->
<div class="w-full md:w-auto p-1.5">
<div class="relative">
<button type="button" id="clearFilters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none">
<span>Clear Filters</span>
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<div id="bidstab">
<div class="rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div id="sent-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto overflow-auto lg:overflow-hidden">
<table class="w-full lg:min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0 hidden lg:block">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot-sent" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text-sent" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400">
Sent Bids: <span id="sentBidsCount">0</span>
</p>
{% if debug_ui_mode == true %}
<button id="refreshSentBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshSentText">Refresh</span>
</button>
{% endif %}
<button id="exportSentBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>Export CSV</span>
</button>
</div>
<div id="pagination-controls-sent" class="flex items-center space-x-2" style="display: none;">
<button id="prevPageSent" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageSent">1</span></p>
<button id="nextPageSent" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab">
<div id="received-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto overflow-auto lg:overflow-hidden">
<table class="w-full lg:min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="p-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot-received" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text-received" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400">
Received Bids: <span id="receivedBidsCount">0</span>
</p>
{% if debug_ui_mode == true %}
<button id="refreshReceivedBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshReceivedText">Refresh</span>
</button>
{% endif %}
<button id="exportReceivedBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>Export CSV</span>
</button>
</div>
<div id="pagination-controls-received" class="flex items-center space-x-2" style="display: none;">
<button id="prevPageReceived" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageReceived">1</span></p>
<button id="nextPageReceived" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/bids_sentreceived.js"></script>
<script src="/static/js/bids_export.js"></script>
{% include 'footer.html' %}
</body>
</html>

View File

@@ -0,0 +1,118 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Bid Requests</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Review and accept bids from other users.</p>
</div>
</div>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<section>
<div class="mt-5 lg:container mx-auto lg:px-0 px-6">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto mt-6 overflow-auto lg:overflow-hidden">
<table class="w-full min-w-max">
<thead class="uppercase">
<tr>
<th class="p-0" data-sortable="true" data-column-index="0">
<div class="py-3 pl-4 justify-center rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold"></span>
</div>
</th>
<th class="p-0">
<div class="py-3 pl-4 justify-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold">Time</span>
</div>
</th>
<th class="p-0 hidden xl:block">
<div class="py-3 px-4 text-left bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Swap</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Get</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Rate</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 rounded-tr-xl">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody id="bids-body"></tbody>
</table>
</div>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400 mr-4">Available Bids: <span id="availableBidsCount">0</span></p>
{% if debug_ui_mode == true %}
<button id="refreshBids" class="inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshText">Refresh</span>
</button>
{% endif %}
<div id="pagination-controls" class="flex items-center space-x-2" style="display: none;">
<button id="prevPage" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPage">1</span></p>
<button id="nextPage" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script src="/static/js/bids_available.js"></script>
{% include 'footer.html' %}

View File

@@ -23,9 +23,9 @@
<div class="w-full md:w-1/2 mb-6 md:mb-0">
<div class="flex items-center">
<div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2024~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.1.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.2.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +1,4 @@
{% include 'header.html' %}
{% from 'style.html' import index_wallet_svg, index_trading_svg, index_support_svg %}
<div class="container mx-auto">
<section class="relative py-24 overflow-hidden">
<div class="container px-4 mx-auto mb-16 md:mb-0">
<div class="md:w-1/2 pl-4">
<span class="inline-block py-1 px-3 mb-4 text-xs leading-5 bg-blue-500 text-white font-medium rounded-full shadow-sm">(BSX) BasicSwap v{{ version }} - (GUI) v.3.0.0</span>
<h3 class="mb-6 text-4xl md:text-5xl leading-tight text-coolGray-900 font-bold tracking-tighter dark:text-white">Welcome to BasicSwap DEX</h3>
<p class="mb-12 text-lg md:text-xl text-coolGray-500 dark:text-gray-300 font-medium">The World's Most Secure and Decentralized DEX, Safely swap cryptocurrencies without central points of failure.
Its free, completely trustless, and highly secure.</p>
<div class="flex flex-wrap mb-10 text-center md:text-left">
<div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
<a href="/wallets">
<div class="inline-flex h-14 w-14 mx-auto items-center justify-center text-white bg-blue-500 rounded-lg">
{{ index_wallet_svg | safe }}
</div>
</div>
<div class="w-full md:flex-1 md:pt-3">
<div class="md:max-w-sm">
<h3 class="mb-4 text-xl md:text-2xl leading-tight text-coolGray-900 dark:text-white font-bold">Your Wallet</h3>
<p class="text-coolGray-500 dark:text-gray-300 font-medium">Manage your cryptocurrency wallets.</p>
</a>
</div>
</div>
</div>
<div class="flex flex-wrap mb-10 text-center md:text-left">
<div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
<a href="/offers">
<div class="inline-flex h-14 w-14 mx-auto items-center justify-center text-white bg-blue-500 rounded-lg">
{{ index_trading_svg | safe }}
</div>
</div>
<div class="w-full md:flex-1 md:pt-3">
<div class="md:max-w-sm">
<h3 class="mb-4 text-xl md:text-2xl leading-tight text-coolGray-900 dark:text-white font-bold">Start Trading</h3>
<p class="text-coolGray-500 dark:text-gray-300 font-medium">Browse available swap offers placed by others.</p>
</a>
</div>
</div>
</div>
<div class="flex flex-wrap text-center md:text-left">
<div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
<a href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">
<div class="inline-flex h-14 w-14 mx-auto items-center justify-center text-white bg-blue-500 rounded-lg">
{{ index_support_svg | safe }}
</div>
</div>
<div class="w-full md:flex-1 md:pt-3">
<div class="md:max-w-sm">
<h3 class="mb-4 text-xl md:text-2xl leading-tight text-coolGray-900 dark:text-white font-bold">Help / Tutorials</h3>
<p class="text-coolGray-500 dark:text-gray-300 font-medium">Learn how to use BasicSwap with the Particl Academy.</p>
</div>
</div>
</a>
</div>
</div>
</div>
<div class="md:absolute md:top-28 lg:top-1/2 md:-right-96 xl:-right-80 md:-mr-56 lg:-mr-20 xl:-mr-0 md:transform lg:-translate-y-1/2 px-4 mb-16 md:mb-0">
<div class="relative max-w-max">
<img class="absolute p-7 -mt-1 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-10/12 z-20 imageshow light-image" src="/static/images/gfx/dashboard.jpg" alt="">
<img class="absolute p-7 -mt-1 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-10/12 z-20 imageshow dark-image" src="/static/images/gfx/dashboard2.jpg" alt="">
<img class="relative z-10 imageshow light-image" src="/static/images/gfx/macbook.png" alt="">
<img class="relative z-10 imageshow dark-image" src="/static/images/gfx/macbook2.png" alt="">
<img class="absolute -top-24 right-0 md:mt-px md:right-96 md:mr-52 lg:mr-16 xl:-mr-20 w-28 md:w-auto text-blue-500" src="/static/images/elements/dots2-red.svg">
<img class="absolute -bottom-24 left-0 md:left-auto md:mt-px md:right-96 md:mr-52 lg:mr-16 xl:-mr-20 w-28 md:w-auto text-red-500" src="/static/images/elements/dots2-red.svg">
<img class="absolute left-0 top-1/2 transform -translate-y-1/2 w-28 md:w-auto text-yellow-400" src="/static/images/elements/circle2-violet.svg">
</div>
</div>
</section>
</div>
{% include 'footer.html' %}
</body>
</html>

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