630 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
tecnovert
745d1460e5 script: Parse more than one page limit of offers. 2024-10-30 09:18:47 +02:00
tecnovert
15e7a6efda ui: Allow js_offers limit to be set above PAGE_LIMIT, add is_revoked 2024-10-29 22:48:45 +02:00
tecnovert
602682a2f4 Add sanity check. 2024-10-29 19:59:12 +02:00
tecnovert
2296198b44 tests: Catch when local key is provided to recoverNoScriptTxnWithKey. 2024-10-29 07:49:52 +02:00
tecnovert
cc3ef1c065 tests: Add Selenium webdriver options. 2024-10-28 22:05:45 +02:00
tecnovert
1d5d6004bc Fix recoverNoScriptTxnWithKey for reverse bids. 2024-10-28 10:15:30 +02:00
tecnovert
3345d56f5b Set default swap type to ADS if coin_from or to is scriptless. 2024-10-27 23:38:36 +02:00
tecnovert
e23216df07 Fix manual spendchainblocktx through GUI 2024-10-25 21:28:20 +02:00
tecnovert
3cab753398 Add 'failed to get output distribution' as a transient error. 2024-10-25 20:39:31 +02:00
tecnovert
5e71367c21 Fix websocket url in docker container. 2024-10-18 15:09:51 +02:00
tecnovert
1eca1b60ab doc: Fix markdown 2024-10-17 20:41:24 +02:00
tecnovert
01c8130535 guix: Update packed version 2024-10-16 21:35:21 +02:00
tecnovert
062cc6dbdc Set version to 0.14.1 2024-10-16 21:15:16 +02:00
tecnovert
72bfcd3521 Split requirements.txt file. 2024-10-16 21:14:09 +02:00
tecnovert
33cf81a76d Avoid installing recommended packages. 2024-10-16 21:14:09 +02:00
tecnovert
445aa116ad Update sqlalchemy from v1.4 to 2.0. 2024-10-16 21:14:09 +02:00
tecnovert
bdc173187d Remove dependencies from pyproject.toml, ensuring requirements.txt will be used. 2024-10-16 21:14:09 +02:00
tecnovert
87ad321987 doc: Update install notes to ensure dependency hashes are checked. 2024-10-16 21:14:09 +02:00
tecnovert
19c6cff7d3 Freeze dependencies. 2024-10-16 21:14:08 +02:00
tecnovert
996c67beea Convert from setup.py to pyproject.toml 2024-10-16 21:14:08 +02:00
nahuhh
e2fe0697ee ui: remove dup USD from market rate tooltip 2024-10-16 18:50:21 +00:00
nahuhh
0a5680da13 ui: market rate tooltip fix 2024-10-16 17:59:01 +00:00
nahuhh
264f4d209f ui: 100 offers per page 2024-10-16 17:58:22 +00:00
nahuhh
3ee69ea11a ui: adjust spacing of filter buttons 2024-10-16 17:57:33 +00:00
nahuhh
c523754516 ui: hide duplicated tor from chart 2024-10-16 17:57:33 +00:00
nahuhh
13015e3da9 ui: show chart on smaller screens 2024-10-16 17:57:33 +00:00
nahuhh
f7141dd0c9 script: template 'amount_step' 2024-10-16 17:27:38 +00:00
Gerlof van Ek
3430776ffc Merge pull request #139 from gerlofvanek/Fixes-7
ui: Fixes
2024-10-16 01:55:19 +02:00
Gerlof van Ek
48a46aea47 ui: Fixes 2024-10-16 01:52:12 +02:00
Gerlof van Ek
eb7f3b54ec Merge pull request #135 from gerlofvanek/tooltips
ui: Duplicate tooltip fix.
2024-10-15 19:57:53 +02:00
Gerlof van Ek
c43d46c7e8 ui: Duplicate tooltip fix. 2024-10-15 19:55:55 +02:00
Gerlof van Ek
7d77d46fa2 Merge pull request #134 from gerlofvanek/fixes-4
ui: Chart update/fixes + wallets (prices) use TOR <> API.
2024-10-15 19:16:53 +02:00
Gerlof van Ek
934e809ac3 ui: Chart update/fixes + wallets (prices) use TOR <> API. 2024-10-15 19:13:28 +02:00
tecnovert
8081f22e92 Fix intermittent DASH addcoin issue. 2024-10-14 19:08:08 +02:00
Gerlof van Ek
c9b99dd67a Merge pull request #133 from gerlofvanek/fixes-3
ui: Added custom order price tiles + Fixes
2024-10-14 18:12:51 +02:00
Gerlof van Ek
fe83736ec7 ui: Added custom order price tiles + Fixes 2024-10-14 18:11:03 +02:00
Gerlof van Ek
817d2c1e9c Merge pull request #132 from gerlofvanek/sort
ui: Sort on Time and Trade (table).
2024-10-14 16:39:15 +02:00
Gerlof van Ek
c0d9b7c161 ui: Sort on Time and Trade (table). 2024-10-14 16:37:42 +02:00
Gerlof van Ek
014ee22b79 Merge pull request #131 from gerlofvanek/BCH
ui: Activate BCH for chart/prices.
2024-10-14 16:18:21 +02:00
Gerlof van Ek
cbd0898eb1 ui: Activate BCH for chart/prices. 2024-10-14 16:01:37 +02:00
Gerlof van Ek
63d27b4a6f Merge pull request #130 from gerlofvanek/fixes-2
ui: Fix rate/percentage + Fix own offers on network offers page + Var…
2024-10-13 20:46:07 +02:00
gerlofvanek
fdfa03eaaf ui: Fix rate/percentage + Fix own offers on network offers page + Various fixes. 2024-10-13 20:40:54 +02:00
Gerlof van Ek
c9d1129e93 Merge pull request #129 from gerlofvanek/fixes
ui: Fixes
2024-10-13 19:46:44 +02:00
gerlofvanek
376b485261 ui: Fixes 2024-10-12 21:37:51 +02:00
Cryptoguard
75fa008f0a Merge pull request #128 from gerlofvanek/chart
ui: Main price chart fixes/updates.
2024-10-11 17:45:15 -04:00
gerlofvanek
54983913e1 Fix: Chart JS 2024-10-11 22:35:12 +02:00
gerlofvanek
c6f3c684a8 ui: Chart and dynamic table(s) fixes 2024-10-11 22:03:14 +02:00
gerlofvanek
39aad231cd ui: Main price chart fixes/updates. 2024-10-11 18:15:06 +02:00
tecnovert
bd06c435e9 Merge pull request #126 from gerlofvanek/dynamic
ui: Dynamic offers tables + various updates.
2024-10-11 15:56:08 +00:00
gerlofvanek
fb1caea4de fix: Update offers/sentoffers table 2024-10-11 17:41:18 +02:00
gerlofvanek
d8430f4ca9 ui: CSS update 2024-10-09 00:39:33 +02:00
gerlofvanek
f7315d405d ui: Fix 2024-10-09 00:01:11 +02:00
gerlofvanek
bcd251c4df ui: Dynamic offers tables + various updates. 2024-10-08 23:15:40 +02:00
tecnovert
3f963f3329 scripts: Rename 'min_amount' to 'amount_step'. 2024-10-08 21:06:43 +00:00
tecnovert
4117c461bb Update scripts/createoffers.py
Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2024-10-08 21:06:43 +00:00
tecnovert
8c6ea947ba script: Add min_amount offer setting.
If min_amount is set offers will be created for amounts between "min_coin_from_amt" and "amount" in increments of "min_amount".
2024-10-08 21:06:43 +00:00
tecnovert
f2a3fc1da1 Fix bug when manually setting bid state. 2024-10-08 22:31:17 +02:00
tecnovert
771ad2586a tests: Add BSX_TEST_MODE env var to prepare script to manage all daemons by default. 2024-10-07 20:44:09 +02:00
tecnovert
60a3956c07 prepare: Add backwards compatibility mode for DASH wallets.
testing notes:
gist.github.com/tecnovert/627f67b7d04746f79c3e9e975458139e
2024-10-07 20:21:25 +02:00
tecnovert
d097846756 cores: Update DASH version to 21.1 2024-10-03 14:28:24 +02:00
tecnovert
1209d1b269 doc: Reword shouldManageDaemon comment. 2024-10-02 09:56:57 +02:00
tecnovert
209dea52b3 refactor prepare script, set manage_daemon to false if a custom host or port is set.
If the user sets the -COIN-_RPC_HOST or PORT variables manage_daemon will be set to false.
The -COIN-_MANAGE_DAEMON variable can override this and set manage_daemon directly.
if BSX_DOCKER_MODE is active -COIN-_MANAGE_DAEMON will default to false.
2024-10-01 23:52:00 +02:00
tecnovert
f954822d74 Remove spurious error in debug ui mode. 2024-10-01 19:37:28 +02:00
tecnovert
dbdb89cd10 Rename BASE_XMR_RPC_PORT 2024-10-01 00:41:40 +02:00
tecnovert
c53e426989 Attempt to resume core release downloads.
New options:
    noreleasesizecheck     If unset the size of existing core release files will be compared to their size at their download url.
    redownloadreleases     If set core release files will be redownloaded.
2024-10-01 00:20:34 +02:00
tecnovert
484ad0ca38 lint: Fix issues. 2024-10-01 00:20:30 +02:00
tecnovert
ac7f24daff tests: Add Github Actions lint checks. 2024-10-01 00:18:57 +02:00
tecnovert
c1f724ac5e Merge pull request #121 from nahuhh/docs
docs: update install docs
2024-09-27 16:42:51 +00:00
nahuhh
d5f643aab9 docs: update install docs 2024-09-26 22:36:52 +00:00
tecnovert
72fc065928 guix: Update packed version. 2024-09-19 12:21:57 +02:00
tecnovert
25b479fdb6 Update coincurve version. 2024-09-19 12:16:18 +02:00
tecnovert
4b18ddfe79 Merge pull request #119 from gerlofvanek/shutdown
ui: Style info/error templates + disable shutdown button when swap is in progress.
2024-09-13 19:53:04 +00:00
gerlofvanek
bdea7de27e ui: Cannot shutdown while swaps are in progress. 2024-09-13 21:45:56 +02:00
gerlofvanek
bcd9d5c9af ui: Style info/error templates + disable shutdown on swap in progress. 2024-09-13 17:15:20 +02:00
tecnovert
fe976810e3 Merge pull request #118 from gerlofvanek/offers
ui: Refactor offers page / Added TOR <> API / Cache
2024-09-13 06:12:26 +00:00
gerlofvanek
033167a451 Deleted unused imports and is_tor_available. 2024-09-12 20:25:46 +02:00
gerlofvanek
cdfb9132ad use readURL / fix LINTING errors. 2024-09-11 12:55:20 +02:00
gerlofvanek
b6d29a33d2 ui: Refactor offers page / Added TOR <> API / Cache
Restructure / Refactor JS for the Offers page.
Added TOR for chart/rates/coin prices/ API requests.
New Chart with option of show volume(coin) / refresh chart (will clear cache).
Added cache (10min) for all the API's or manual refresh.
Disabled notifications show new offers (was spam). New bids and Bid Accepted still active.
Various small fixes.
2024-09-10 19:51:27 +02:00
tecnovert
003d7b85ab Update BTC fastsync file. 2024-09-06 20:44:02 +02:00
tecnovert
1b585ea5c9 Fix "Language not detected" error when initialising Dash. 2024-09-05 22:56:21 +02:00
tecnovert
47a80dc603 Merge pull request #115 from nahuhh/firo_hardfork
firo: v0.14.14.0 hardfork 2024-09-16 (mandatory)
2024-09-05 18:24:45 +00:00
nahuhh
b29d37a8be firo: v0.14.14.0 hardfork 2024-09-16 (mandatory) 2024-09-04 11:41:46 +00:00
tecnovert
60369fc2a4 Merge pull request #113 from nahuhh/misc
misc: fix typos
2024-08-26 14:58:00 +00:00
tecnovert
3927d823c0 Merge pull request #112 from nahuhh/monero-v0.18.3.4
monero: v0.18.3.4
2024-08-26 14:57:00 +00:00
tecnovert
1d58dfdc94 Merge pull request #110 from nahuhh/wowconf
wownero: only 3 confirmations required
2024-08-26 14:56:29 +00:00
nahuhh
7ac75f7344 misc: fix typos 2024-08-24 13:17:08 +00:00
nahuhh
80c43056cc monero: v0.18.3.4 2024-08-24 13:07:54 +00:00
gerlofvanek
1564655777 ui: Fix QRCODE visibility 2024-08-23 22:25:25 +02:00
nahuhh
2d243fc310 wownero: only 3 conf required 2024-08-14 15:00:38 +00:00
tecnovert
75ad5a5b4d guix: Update packed version. 2024-06-20 09:32:12 +02:00
tecnovert
5e69bf172c Raise version to 0.13.4 2024-06-20 09:27:59 +02:00
tecnovert
f307332409 Use subaddr_indices_all with sweep_all. 2024-06-19 23:11:41 +02:00
tecnovert
56378d168b Merge branch 'gerlofvanek-dev' into dev 2024-06-19 21:04:57 +02:00
nahuhh
274be9d716 firo: v0.14.13.3 (mandatory) 2024-06-19 10:23:39 -05:00
gerlofvanek
eec0760e44 ui: Various Fixes
- Fix warning/alert messages on unlock / offer templates.
- Add change/set your password in header nav.
- Various small tweaks / updates.
- Console debug for sweep_all
2024-06-19 13:35:43 +02:00
tecnovert
6ed108e741 tests: Remove unused travis-ci config. 2024-06-18 20:56:33 +02:00
nahuhh
7429dc5b2d ui: coingecko change pct
- different decimals per coin
- zano rate via cryptocompare
- uniform widget content
2024-06-17 23:17:33 -05:00
nahuhh
9a900a5bac ui: daily pct change fix 2024-06-14 19:44:30 -05:00
nahuhh
ba4796c763 ui: fix spacing of coin widgets & filters 2024-06-14 12:18:02 -05:00
tecnovert
57f238d48e Add local smsg addresses if missing. 2024-06-14 12:27:45 +02:00
gerlofvanek
d93a73c29e Fix wallet page
- Fixed "sweep all" checkbox on XMR and WOW if 100% is selected.
- If 100% selected if will select Substract Fee checkbox from 100% amount.
- Fixed if selected part/blind/anon and ltc/mweb with 10%/25%/100%.
2024-06-14 10:35:37 +02:00
gerlofvanek
94303cff93 Fixes and Tweaks
- Fix the 25%/50%/100% buttons on XMR and WOW.
2024-06-14 10:34:48 +02:00
tecnovert
a977cfe857 Fix start height not being set. 2024-06-14 02:35:18 +02:00
tecnovert
00912b277a Check for shutdown in block scanning loop. 2024-06-14 00:41:51 +02:00
tecnovert
40eff0ce0f decred: Add shortcut to genesis in getWalletRestoreHeight. 2024-06-13 22:55:31 +02:00
tecnovert
9835d33d12 ui: Fix setAmount. 2024-06-13 21:49:32 +02:00
gerlofvanek
64165e387e Fix wownero rate + bug fixes.
- Fix wownero rates on offers page.
- Fix wownero USD price on wallet/wallets page.
- Set dark mode as default.
- Fix tooltips.
- Fix JS errors.
2024-06-12 08:10:09 +02:00
tecnovert
34d94760a3 guix: Update packed version. 2024-06-11 10:36:15 +02:00
tecnovert
713990f57b Update github urls. 2024-06-11 09:56:09 +02:00
tecnovert
58544d141d wow: Fix config filename on windows. 2024-06-06 22:04:27 +02:00
tecnovert
e125aa33d2 wow: Add windows and osx links. 2024-06-06 22:04:27 +02:00
tecnovert
fd7977b35a wownero: Add test. 2024-06-06 22:04:26 +02:00
tecnovert
dc4f0ac2d3 wownero: deduplicate 2024-06-06 22:04:26 +02:00
nahuhh
ee2f462ee9 wownero: integration 2024-06-06 22:04:26 +02:00
tecnovert
c3cd1871ef Add disableall option to smsgaddresses command. Ensure used addresses are active. 2024-06-06 22:03:41 +02:00
tecnovert
3e4c3f10cf scripts: Set offer min_bid_amount from offer template min_swap_amount value. 2024-06-06 22:03:41 +02:00
tecnovert
80852fd0ea Fix lockup when bids timeout. 2024-06-06 22:03:40 +02:00
tecnovert
ad7d23a8de Fix reloading Decred bids. 2024-06-06 22:03:40 +02:00
tecnovert
166b035983 ui: Fix more info view for Decred HTLC bids. 2024-06-06 22:03:40 +02:00
tecnovert
c27ac833d1 ui: Fix revoke button showing on revoked offers. 2024-06-06 22:03:40 +02:00
tecnovert
e62e9eb0bf Reduce Decred log level and consider wallet blocks for verificationprogress. 2024-06-06 22:03:40 +02:00
tecnovert
b07bc3c456 Don't start cores for unknown coins. 2024-06-06 22:03:39 +02:00
tecnovert
ebdbe115dd Fix addcoin decred. 2024-06-06 22:03:39 +02:00
tecnovert
5f6819afcb refactor: Make db mutex non-recursive. 2024-06-06 22:03:39 +02:00
tecnovert
ae1df0b556 Include mnemonic dependency directly. 2024-06-06 22:03:38 +02:00
tecnovert
d3e3c3c95b Remove protobuf dependency. 2024-06-06 22:03:38 +02:00
tecnovert
adc80eabb0 Add simple protobuf encoder and decoder. 2024-06-06 22:03:38 +02:00
tecnovert
42fa4d49d4 Workarounds to run Decred in windows.
Running BSX in windows is highly discouraged.
If you have no choice at least use the WSL docker setup instead.
2024-06-06 22:03:37 +02:00
tecnovert
b077561a6f Allow Decred wallets derived from the legacy extkey.
When importing from seed dcrwallet creates two accounts, one each on coin_id 20 and 42.

Can't see how to force slip44 upgrade to run in dcrwallet, checking for either key instead.
2024-06-06 22:03:37 +02:00
tecnovert
57bc1d5ccf ui: Improve rpc page. 2024-06-06 22:03:37 +02:00
tecnovert
62aa1fa5d7 Decred: Add proxy config when using tor. 2024-06-06 22:03:36 +02:00
tecnovert
73b4b2a46b Increase the value by which the amount can be adjusted to match rate. 2024-06-06 22:03:36 +02:00
tecnovert
76445146fb Integrate Decred with wallet encryption.
dcrwallet requires the password to be entered at the first startup when encrypted.
basicswap-run with --startonlycoin=decred and the WALLET_ENCRYPTION_PWD environment var set can be used for the initial sync.
2024-06-06 22:03:36 +02:00
tecnovert
fcf234ef34 ui: Improve rpc page usability for Decred. 2024-06-06 22:03:36 +02:00
tecnovert
aa1e1fd79c Decred: test_010_txn_size 2024-06-06 22:03:35 +02:00
tecnovert
446d6fe357 Decred: Add to test_xmr_persistent 2024-06-06 22:03:35 +02:00
tecnovert
2a8c04b285 Decred xmr swap tests. 2024-06-06 22:03:35 +02:00
tecnovert
76879a2ff5 Decred: Secret hash swap tests. 2024-06-06 22:03:34 +02:00
tecnovert
d527ec4974 Decred test_008_gettxout 2024-06-06 22:03:33 +02:00
tecnovert
74c7072926 Decred CSV test. 2024-06-06 22:03:33 +02:00
tecnovert
ab472c04be Decred sighash and signing. 2024-06-06 22:03:33 +02:00
tecnovert
150caeec40 Add Decred transaction to and from bytes. 2024-06-06 22:03:32 +02:00
tecnovert
761d0ca505 Get Decred account key from seed. 2024-06-06 22:03:32 +02:00
tecnovert
9160bfe452 Add Decred rpc 2024-06-06 22:03:32 +02:00
tecnovert
942b436974 tests: Start dcrd 2024-06-06 22:03:31 +02:00
tecnovert
6ac9bbb19c prepare: Download, verify and extract Decred binaries 2024-06-06 22:03:31 +02:00
tecnovert
047fe7ba27 Encode and decode Decred addresses. 2024-06-06 22:03:31 +02:00
tecnovert
74ce19052d Add Decred chainparams. 2024-06-06 22:03:30 +02:00
tecnovert
5e8547063e Merge pull request #89 from cryptoguard/master
Update README.md
2024-06-06 20:00:59 +00:00
Cryptoguard
80a8f8967f Update README.md 2024-06-06 15:41:07 -04:00
Cryptoguard
902d9ff13b Update README.md 2024-06-06 15:38:42 -04:00
Cryptoguard
96363136d2 Update README.md 2024-06-06 15:36:25 -04:00
tecnovert
e63014026d Prefer to set bid amounts from offer amount-to instead of rate. 2024-05-09 01:05:19 +02:00
tecnovert
46d0bdde4b Fix rate lookups. 2024-05-07 20:35:22 +02:00
tecnovert
47f7b4545e ui: auto set blank amount from using amount to and rate.
Fix btc get unspents, 'solvable'.
2024-05-05 21:54:22 +02:00
tecnovert
f64b2c1030 Rewrite litecoin.conf from basicswap-run to add config for 0.21.3. 2024-05-05 14:37:54 +02:00
tecnovert
8d8743074e Remove litecoind.pid workaround. 2024-05-04 20:48:32 +02:00
tecnovert
a08bdfbdb8 Fix LTC getUnspentsByAddr() returning MWEB UTXOs. 2024-05-04 19:11:50 +02:00
tecnovert
6df09a973e coins: Update Litecoion version to 0.21.3
Removed .pid file name and assert url workarounds.

Requires additions to litecoin.conf:
blockfilterindex=0
peerblockfilters=0
2024-05-04 18:43:18 +02:00
gerlofvanek
450b562db9 ui: Small fixes 2024-05-03 13:09:59 +02:00
tecnovert
af26b5d2fe Merge pull request #83 from nahuhh/monero_v0.18.3.3
monero: v0.18.3.3 [dev branch]
2024-04-28 22:45:44 +00:00
tecnovert
48f406a338 doc: Use docker-compose run to read $COINDATA_PATH from .env 2024-04-29 00:43:00 +02:00
nahuhh
e87d54a259 monero: v0.18.3.3 2024-04-27 13:12:24 -05:00
Gerlof van Ek
ab80b9b7f5 Merge pull request #81 from gerlofvanek/dev
ui: Wallet alert updates
2024-04-26 16:33:59 +02:00
gerlofvanek
842c4f54ac ui: Wallet alert updates 2024-04-26 16:32:31 +02:00
tecnovert
a01202dcb2 Fix lint error. 2024-04-26 13:12:01 +02:00
gerlofvanek
e6c79a6743 ui: Newoffer page update / clean-up / JS fixes 2024-04-26 12:53:51 +02:00
tecnovert
926f6e1a76 Show bids only for active coins. 2024-04-15 12:10:40 +02:00
tecnovert
343fd6efbc Constrain protocol versions, fix error showing old offers. 2024-04-15 00:16:46 +02:00
tecnovert
e20516ef71 ui: Allow users to select which coins to show prices for on offers page. 2024-04-14 23:23:22 +02:00
tecnovert
8a279dc71f Raise version, transmit amount to instead of rate. 2024-04-14 14:45:13 +02:00
tecnovert
463c51c822 Merge branch 'HardenedSteel-patch-1' into dev 2024-04-13 19:07:16 +02:00
gerlofvanek
360d29128c GUI v.3.0.0 2024-04-12 14:46:47 +02:00
HardenedSteel
2cf279f27f Replace Element links with Matrix page
Not everyone using Element for Matrix, with matrix.to links people can choose a client.
2024-04-02 17:30:16 +00:00
tecnovert
1cbc2f44b0 Allow multiple base58 prefix bytes. 2024-03-25 13:52:55 +02:00
Gerlof van Ek
2a28f336e2 Merge pull request #75 from nahuhh/fix_rebase
ui: Fix rebase
2024-03-21 16:31:22 +01:00
nahuhh
10a416aa04 ui: fix rebase 2024-03-21 11:22:32 -04:00
Gerlof van Ek
e6d4ab500b Merge pull request #73 from nahuhh/gui_tweak3
ui: orderbook update & fixes
2024-03-21 15:55:16 +01:00
nahuhh
adcf875db6 ui: orderbook update & fixes 2024-03-21 10:52:02 -04:00
Gerlof van Ek
a85fbce4ae Merge pull request #72 from gerlofvanek/dev
ui: Fixes
2024-03-21 15:26:15 +01:00
gerlofvanek
109a4383ea Lint 2024-03-21 11:08:04 +01:00
gerlofvanek
1dc3c1c7ae ui: Fixes 2024-03-21 11:03:55 +01:00
gerlofvanek
e90292fb2f ui: Added wownero (chart/price/images), WIP on coingecko fallback system for chart/coin prices.
- Added wownero .png coin logos.
- (WIP) Added coingecko api / key as fallback system for the chart/coin prices.
- Change the api / keys for coingecko at settings.
- Added special function for getting wownero chart/coin data/price from CoinGecko and not CryptocCmpare.
2024-03-18 14:19:55 +02:00
Gerlof van Ek
d83555c25f Merge pull request #70 from gerlofvanek/dev
ui: Offers page update
2024-03-16 22:55:09 +01:00
gerlofvanek
00c8af6853 ui: Offers page update 2024-03-16 22:54:26 +01:00
Gerlof van Ek
3b73894ce8 Merge pull request #68 from gerlofvanek/dev
ui: Offers page / Clean-up / sortTable / Fixes
2024-03-12 17:07:50 +01:00
gerlofvanek
e49ffbfdf7 ui: Offers page / Clean-up / sortTable / Fixes
- Added extra details on the Your Offers page.
- Added function sortTable for time/rate/market.
- Various fixes.
2024-03-12 17:06:44 +01:00
Gerlof van Ek
594845e312 Merge pull request #66 from gerlofvanek/dev
GUI: Update
2024-03-09 20:13:02 +01:00
Gerlof van Ek
7ccc84f265 Merge branch 'tecnovert:dev' into dev 2024-03-09 20:11:10 +01:00
gerlofvanek
f903038964 ui: Bump GUI v2.0.4 2024-03-09 20:10:11 +01:00
Gerlof van Ek
31a1224311 Merge pull request #65 from gerlofvanek/dev
ui: JS fix
2024-03-09 19:50:39 +01:00
gerlofvanek
4711d72bf5 ui: JS fix 2024-03-09 19:48:52 +01:00
Gerlof van Ek
44f16bd28e Merge pull request #64 from nahuhh/fix_my_mistake
fix a misalignment introduced in my lastest commit
2024-03-09 19:07:49 +01:00
nahuhh
a486930cd5 fix a misalignment introduced in my lastest commit 2024-03-09 12:40:14 -05:00
tecnovert
00b8443aff Merge branch 'nahuhh-monero-fee-fix' into dev 2024-03-09 14:26:42 +02:00
nahuhh
6739e2bc5f ui: small tweaks 2024-03-08 18:39:45 -05:00
nahuhh
d71fa04781 monero: temporary spam mitigation 2024-03-08 15:15:14 -05:00
tecnovert
f4c88b5356 ui: Hide 'Accept Bid' btn when not able to accept. 2024-03-07 15:38:53 +02:00
tecnovert
d6535dbc1d Set bid and offer states when expired. 2024-03-07 15:38:49 +02:00
Gerlof van Ek
18d8dfd3fc Merge pull request #62 from gerlofvanek/dev
ui: Fix styling
2024-03-03 23:12:50 +01:00
Gerlof van Ek
8f1c3e648c ui: Fix styling 2024-03-03 23:12:07 +01:00
Gerlof van Ek
650aa9d72f Merge pull request #61 from nahuhh/gui_tweak_2
ui: offers: rate fix, color change, text size
2024-03-03 16:22:56 +01:00
nahuhh
c4ae489c45 ui: offers: rate fix, color change, text size 2024-03-03 10:03:18 -05:00
gerlofvanek
b38a7828ff ui: Update offers layout 2024-03-03 15:06:39 +02:00
tecnovert
2c8e8f3e30 Merge branch 'gerlofvanek-dev' into dev 2024-03-01 20:41:01 +02:00
Gerlof van Ek
18d4a9105d Merge branch 'dev' into dev 2024-02-29 13:43:28 +01:00
gerlofvanek
e464599cf7 ui: Bug fixes 2024-02-29 13:32:13 +01:00
gerlofvanek
69b195e317 ui: Styling / Bug fixes 2024-02-29 10:28:37 +01:00
tecnovert
13ae72b38f Merge branch 'gerlofvanek-dev' into dev 2024-02-26 22:46:31 +02:00
gerlofvanek
acc135d22e Update page_bids.py 2024-02-26 21:36:30 +01:00
tecnovert
c5cf6fb132 Merge branch 'gerlofvanek-dev' into dev 2024-02-26 21:26:08 +02:00
gerlofvanek
49e4072f2a ui: Style.css
- Fixes
2024-02-26 20:19:48 +01:00
gerlofvanek
4f114ba9ae ui: Clean-up / Fixes / Updates
- Moved parts of the svg code/images in style.html and update the templates.
- Restyled and updated the JS for the cryptocurrency price boxes on the offers page.
- Started on restyling the offers page.
- Offers page, Posted timestamps are displayed as (X min ago) and Expires in -h min.
- Fixed broken breadscrumb links.
2024-02-26 20:07:33 +01:00
tecnovert
37651598bf coins: Update PIVX version to 5.6.1 2024-02-26 20:49:06 +02:00
tecnovert
a78880bc98 Ensure remote_daemon_urls appears in settings if automatically_select_daemon is present. 2024-02-22 01:44:25 +02:00
tecnovert
b0d169421f coins: Update Firo version to 14.13.2 2024-02-20 21:57:12 +02:00
tecnovert
a1bcf8d4b9 api: Add validateamount command 2024-02-17 00:03:37 +02:00
tecnovert
42421321f6 Remove publishBLockTx debug delay. 2024-02-16 23:05:20 +02:00
tecnovert
483d77a0c6 Fix revoke ttl too low. Remove XMR fee warnings. 2024-02-15 00:00:54 +02:00
tecnovert
0d344a907c tests: Fix tests. 2024-02-14 13:04:32 +02:00
tecnovert
091e8da5e2 prepare: Add message when wallet initialisation fails. 2024-02-14 11:06:40 +02:00
tecnovert
f091d17560 prepare: Remove prune from pivx config. 2024-02-14 09:23:18 +02:00
tecnovert
73d4ade5f6 Merge pull request #55 from tecnovert/rename_env
Rename docker/.env
2024-02-12 20:01:42 +00:00
tecnovert
2793ee00dd doc: Rename docker/.env to example.env 2024-02-12 21:55:27 +02:00
Cryptoguard
b96a823536 Update .env
Update install.md

Update install.md

Update install.md

Update upgrade.md

Update upgrade.md
2024-02-12 21:39:42 +02:00
tecnovert
7d850406ca ui: Add fix from Crz. 2024-02-12 21:18:13 +02:00
tecnovert
4e7a6e994d Add debug info when failed to expire msg. 2024-02-12 11:36:14 +02:00
tecnovert
b55042bf07 ui: Added An Estimate Fee button on XMR wallet page. 2024-02-11 17:55:06 +02:00
tecnovert
9be4bd28fd ui: Change Subtract Fee to Sweep All for XMR. 2024-02-10 19:05:48 +02:00
tecnovert
e9986148d7 Automatically remove untrusted-daemon from monero_wallet.conf 2024-02-10 10:25:49 +02:00
tecnovert
0aaf3f8bcc Fix LTC create UTXO. 2024-02-09 23:57:25 +02:00
tecnovert
6e4feb33d7 ui: Rename unconfirmed balance to pending, include immature balance. 2024-02-09 23:48:53 +02:00
tecnovert
1f810fab6b Raise version to 0.12.7 2024-02-09 11:36:50 +02:00
tecnovert
7bf5f7ddfa prepare: Avoid setting tor mode for commands that only modify config. 2024-02-09 11:17:30 +02:00
tecnovert
d7da532111 prepare: Deduplicate Monero tor config 2024-02-09 11:17:29 +02:00
tecnovert
fa35102794 prepare: Select aarch64 releases 2024-02-09 11:17:29 +02:00
tecnovert
da8c3e0237 prepare: Make default wshost match htmlhost 2024-02-09 11:17:29 +02:00
tecnovert
ec29c9889c Set monero-wallet-rpc proxy with parameter instead of config. 2024-02-09 11:17:29 +02:00
tecnovert
14298d022a Don't connect to XMR nodes at private ips over tor by default. 2024-02-09 11:17:28 +02:00
tecnovert
5ceaab57d1 Set trusted-daemon for XMR node in basicswap.json
basicswap-prepare will set trusted_daemon true if the daemon host address is a local ip address else false.
Override with --trustremotenode
--trustremotenode can be used with --enabletor
2024-02-09 11:17:28 +02:00
tecnovert
a5c3534c19 Better error message when trying to swap Firo <> XMR. 2024-02-09 11:17:28 +02:00
tecnovert
3b6f72c084 Note source of XMR rpc error messages. 2024-02-09 11:17:28 +02:00
tecnovert
7c0ea36e37 Add request-sent to transient errors list. 2024-02-09 11:17:28 +02:00
tecnovert
14577f7741 tests: Fix test_wallet_restore for LTC multiwallet 2024-02-09 11:17:27 +02:00
tecnovert
dbf3f8f034 prepare: Don't use bind and nolisten together. 2024-02-09 11:17:27 +02:00
tecnovert
b85d234a0b prepare: Switch Litecoin download url to github. 2024-02-09 11:17:27 +02:00
tecnovert
1bfb271b87 Add settings for Monero rpc timeouts. 2024-02-09 11:17:27 +02:00
tecnovert
f9bc5d46af prepare: Automatically set --usetorproxy if use_tor is set in basicswap.json 2024-02-09 11:17:27 +02:00
tecnovert
fab89a42f3 doc: Simplify tor install notes. 2024-02-09 11:17:26 +02:00
tecnovert
3e14a784f3 Connect to remote XMR daemons over tor when enabled. 2024-02-09 11:17:26 +02:00
nahuhh
e4f196411a fix for remote monero daemons on tor enabled installs 2024-02-09 11:17:26 +02:00
tecnovert
8318961f0b refactor: Replace struct.pack/unpack. 2024-02-02 18:53:44 +02:00
tecnovert
7c9504e0cd Merge branch 'nahuhh-monero' 2024-02-02 12:08:46 +02:00
nahuhh
98f3a52daa monero: blocks_confirmed (release funds after) change 7 to 3 2024-02-01 18:06:58 -05:00
tecnovert
3276f9abd4 Merge pull request #45 from nahuhh/litecoin
add ltc min_fee_relay
2024-02-01 15:55:01 +00:00
tecnovert
041ab18288 Fix min_relay_fee override. 2024-02-01 11:28:21 +02:00
tecnovert
d57366c0b2 Prevent multiple LOCK_TX_B_SEEN events, use rpcwallet for lockunspent. 2024-02-01 00:58:14 +02:00
tecnovert
1ec1764012 ui: Don't list LTC MWEB yet on offers page. 2024-01-31 21:16:42 +02:00
tecnovert
28fc4817c0 debug: Add ui option to schedule bid actions. 2024-01-31 20:40:22 +02:00
nahuhh
2d1bd87b41 add ltc min_fee_relay 2024-01-31 10:25:22 -05:00
tecnovert
9ee6669179 ui: Display count of locked UTXOs on wallet page. 2024-01-28 20:16:30 +02:00
tecnovert
30a5ea1652 tests: Run more tests in ci. 2024-01-28 00:07:25 +02:00
tecnovert
53ceae718b doc: Add Cirrus CI notes. 2024-01-27 14:22:40 +02:00
tecnovert
1af9f64020 Merge pull request #43 from nahuhh/update_monero
Update monero 0.18.2.2 to 0.18.3.1
2024-01-26 21:31:01 +00:00
nahuhh
cfd2151c1a Update monero 0.18.2.2 to 0.18.3.1 2024-01-26 07:05:36 -05:00
tecnovert
237d12afa0 Add min_relay_fee for LTC 2024-01-25 23:32:44 +02:00
gerlofvanek
a0cdd8cec9 ui: Ethereum Icons 2024-01-25 23:15:39 +02:00
tecnovert
1754650e75 Merge remote-tracking branch 'crz/dev' into dev 2024-01-25 21:10:20 +02:00
gerlofvanek
3b8b512003 ui: Update 2024-01-25 17:50:06 +01:00
tecnovert
81649dcf9b Set reverse bid state to error when bidaccept fails. 2024-01-25 17:04:49 +02:00
tecnovert
f5d4b8dc0d Show error when auto-accepting a bid fails. 2024-01-24 23:12:18 +02:00
tecnovert
bcfd63037a Add limits to time delay settings. 2024-01-22 09:22:09 +02:00
tecnovert
ddf3734f9d doc: Add missing dependencies. 2024-01-19 20:33:11 +02:00
tecnovert
8c07ee5108 ui: Fix obscured svg text. 2024-01-17 20:42:58 +02:00
tecnovert
6ad5880ba4 Remove bittrex.com rate source. 2024-01-17 20:25:28 +02:00
tecnovert
0ff0a13a67 coins: Fix LTC windows assert URL 2024-01-12 18:13:14 +02:00
tecnovert
ff7e8fe0aa Add coin name to getWalletInfo failed warning. 2024-01-07 09:12:59 +02:00
tecnovert
1068694990 Fix errors when one coin wallet fails to unlock. 2024-01-04 10:41:56 +02:00
tecnovert
66d1abd888 Fix settings apply. 2024-01-02 00:33:27 +02:00
gerlofvanek
671e626551 ui: Updated LTC coin icons, JS fixes.
- Fixed display of LTC MWEB coin icons.
- Fixes JS errors with rates table.
- Fix LTC and LTC-MWEB error with lookup rates (JSON)
2023-12-31 13:14:38 +01:00
tecnovert
192aff221e coins: Update Bitcoin version. 2023-12-31 02:27:05 +02:00
tecnovert
03fdf44220 Fix unlock. 2023-12-30 10:31:41 +02:00
tecnovert
38fa498b0b coins: Add LTC MWEB wallet 2023-12-29 15:36:00 +02:00
tecnovert
7547587d4e coins: Update Firo version to 0.14.13.1 2023-12-24 21:12:53 +02:00
tecnovert
fd0772f893 prepare: Fix Dash download url for osx64. 2023-12-22 17:31:05 +02:00
tecnovert
0a12625290 ui: Define default chart api key in one place. 2023-12-22 15:23:36 +02:00
gerlofvanek
d6ed5ba24c ui: Chart API update
- Updated with a new default Chart API Key
- Implemented Chart error handling
2023-12-22 13:26:29 +01:00
tecnovert
fb48797298 tests: Fix lint issues. 2023-12-19 13:21:15 +02:00
tecnovert
5bec1c31da Add 'timed out' to list of transient errors. 2023-12-19 12:58:24 +02:00
tecnovert
3d3fcbde0b doc: Make a local xmr node the default option. 2023-12-19 11:13:41 +02:00
tecnovert
61845a7a84 tests: Prune Firo tests. 2023-12-19 01:30:52 +02:00
tecnovert
65c93eaee6 coins: Update valid Firo swap types. 2023-12-18 23:45:55 +02:00
tecnovert
6a26f72bca coins: Update Dash to v20.0.2 2023-12-18 15:15:16 +02:00
tecnovert
0a9db22828 Replace all hashlib ripemd160 functions. 2023-12-16 09:38:34 +02:00
tecnovert
08f0156b75 guix: Raise bsx version 2023-12-14 21:40:31 +02:00
tecnovert
15bf9b2187 Raise coincurve version. 2023-12-14 20:44:29 +02:00
tecnovert
258b730c41 scripts: Print errors if offer/bid creation fails. 2023-12-09 13:25:03 +02:00
tecnovert
b409fe9f0e prepare: Fix Firo osx download url. 2023-12-08 15:06:33 +02:00
tecnovert
01bb3870b6 tests: Add gettxout and scantxoutset tests to test_btc_xmr.py 2023-12-02 19:32:39 +02:00
tecnovert
9efb244952 Shorten time format in ui and log. 2023-12-01 22:40:35 +02:00
tecnovert
0be5a4fca7 Add min_relay_fee option. 2023-12-01 19:16:28 +02:00
tecnovert
a4c79fb7aa aOnly set reversed flag for ads type swaps. 2023-12-01 14:46:57 +02:00
tecnovert
5b6f447692 tests: Update swap type validation. 2023-12-01 09:23:10 +02:00
tecnovert
28baa597cc coins: Read Firo ProgPow blocks 2023-12-01 00:19:54 +02:00
tecnovert
d4a6ad7d6f Enable reverse adaptor sig swaps for segwit-less coins. 2023-11-30 21:46:03 +02:00
tecnovert
ce578f8025 Always use csv with adaptor sig swaps. 2023-11-30 18:16:24 +02:00
tecnovert
c387bfec71 coins: Remove asyncore code. 2023-11-28 20:05:31 +02:00
tecnovert
d668a2f342 prepare: Set core_version_group correctly for Firo. 2023-11-28 17:38:30 +02:00
tecnovert
8e17ee5939 coins: Raise Particl Version 2023-11-28 02:23:56 +02:00
tecnovert
3b55d17a26 Use special Firo release: 0.14.13.0-firod-only, new env var: SKIP_GPG_VALIDATION 2023-11-27 19:09:20 +02:00
tecnovert
fd0bf9ed73 Raise version to 0.12.1 2023-11-25 11:08:00 +02:00
tecnovert
e6c7c4d9bb Revert to Firo binary.
The .dmg file only contains Firo-qt
2023-11-25 00:55:41 +02:00
tecnovert
7053d7ee4b Use gettxout where scantxoutset is not available. 2023-11-25 00:40:52 +02:00
tecnovert
22cd3cf9f1 test: Fix selenium tests. 2023-11-09 22:34:50 +02:00
tecnovert
05e6edd5df ui: Fix applying XMR settings with an empty remote urls list. 2023-11-09 21:24:57 +02:00
tecnovert
9a1b7db2dc Print python version. 2023-10-17 14:42:52 +02:00
tecnovert
2a2f1ca3b6 tests: Print expected and actual states on failure. 2023-10-16 21:02:49 +02:00
tecnovert
db0e85d37c tests: Ensure XMR new subaddress is unique.
Fix xmr test 9.1 wait for event type.
Fix node shutdown issue.
2023-10-12 16:02:44 +02:00
tecnovert
ebcd7738e5 Fix XMR not returning new subaddress. 2023-10-11 17:46:52 +02:00
tecnovert
4d8d421de6 Merge pull request #38 from tecnovert/nav
Merge Navcoin branch
2023-10-09 14:47:41 +00:00
tecnovert
23330c20bc Disable Navcoin. 2023-10-09 16:43:09 +02:00
tecnovert
22e005728a Ignore the pivx pid file on windows. 2023-09-29 00:59:32 +02:00
tecnovert
f2b69e5498 preparescript: Ignore usebtcfastsync option if Bitcoin isn't selected. 2023-09-29 00:49:19 +02:00
tecnovert
d617ab1d6b Avoid division by zero. 2023-09-28 01:39:27 +02:00
tecnovert
e7ae290eb5 Use threading event in main loop. 2023-09-17 22:34:48 +02:00
tecnovert
20b405a944 Encode forward slashes in rpcauth passwords. 2023-09-11 18:37:22 +02:00
tecnovert
c01b4a3d70 coins: Update DASH version.
See https://github.com/dashpay/dash/releases/tag/v19.3.0
    When upgrading from a version < 19.2, a migration process will occur.
2023-09-05 21:48:22 +02:00
tecnovert
7caac8a8eb Escape rpcauth password. 2023-09-02 14:28:08 +02:00
tecnovert
a17129999c Raise version 2023-08-29 23:46:19 +02:00
tecnovert
5775ac5931 Add navcoin binary 2023-08-29 23:46:14 +02:00
tecnovert
0b963bffde Add p2sh-p2wsh support, add Navcoin tests. 2023-08-29 22:06:16 +02:00
tecnovert
45e49848b1 Fix rpc console in http mode. 2023-08-12 00:01:04 +02:00
tecnovert
5c23983c8e Fix tx depth displayed for reverse adaptor sig swaps. 2023-08-11 23:36:47 +02:00
tecnovert
68ff57ebdc refactor: Lazy load interfaces. 2023-08-06 13:37:49 +02:00
308 changed files with 99925 additions and 28116 deletions

View File

@@ -3,11 +3,10 @@ container:
lint_task:
setup_script:
- pip install flake8
- pip install codespell
- pip install flake8 codespell
script:
- flake8 --version
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --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:
@@ -17,25 +16,21 @@ test_task:
- BIN_DIR: /tmp/cached_bin
- PARTICL_BINDIR: ${BIN_DIR}/particl
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
- XMR_BINDIR: ${BIN_DIR}/monero
setup_script:
- apt-get update
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
- apt-get install -y python3-pip pkg-config
- pip install tox pytest
- python3 setup.py install
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
- unzip -d coincurve-anonswap coincurve-anonswap.zip
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
- cd coincurve-anonswap
- python3 setup.py install --force
- pip install .
bins_cache:
folder: /tmp/cached_bin
reupload_on_changes: false
fingerprint_script:
- basicswap-prepare -v
populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
script:
- cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}"
@@ -47,3 +42,4 @@ test_task:
- pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'

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

7
.gitignore vendored
View File

@@ -9,3 +9,10 @@ __pycache__
.tox
.eggs
*~
# geckodriver.log
*.log
docker/.env
# vscode dev container settings
compose-dev.yaml

View File

@@ -1,60 +0,0 @@
dist: bionic
os: linux
language: python
python: '3.7'
stages:
- lint
- test
env:
global:
- TEST_DIR=${HOME}/test_basicswap2
- TEST_RELOAD_PATH=~/test_basicswap1
- BIN_DIR=~/cached_bin
- PARTICL_BINDIR=${BIN_DIR}/particl
- BITCOIN_BINDIR=${BIN_DIR}/bitcoin
- LITECOIN_BINDIR=${BIN_DIR}/litecoin
- XMR_BINDIR=${BIN_DIR}/monero
cache:
directories:
- "$BIN_DIR"
before_install:
- sudo apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
install:
- travis_retry pip install tox pytest
before_script:
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
- unzip -d coincurve-anonswap coincurve-anonswap.zip
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
- cd coincurve-anonswap
- python3 setup.py install --force
script:
- cd $TRAVIS_BUILD_DIR
- python3 setup.py install
- basicswap-prepare --bindir=${BIN_DIR} --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
- export DATADIRS="${TEST_DIR}"
- mkdir -p "${DATADIRS}/bin"
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
- mkdir -p "${TEST_RELOAD_PATH}/bin"
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
- # tox
- pytest tests/basicswap/test_xmr.py
- pytest tests/basicswap/test_xmr_reload.py
- pytest tests/basicswap/test_xmr_bids_offline.py
after_success:
- echo "End test"
jobs:
include:
- stage: lint
env:
cache: false
install:
- travis_retry pip install flake8==3.7.0
- travis_retry pip install codespell==1.15.0
before_script:
script:
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.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
after_success:
- echo "End lint"
- stage: test
env:

View File

@@ -5,30 +5,15 @@ ENV LANG=C.UTF-8 \
DATADIRS="/coindata"
RUN apt-get update; \
apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata;
# Must install protoc directly as latest package is only on 3.12
RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/releases/download/v21.1/protobuf-python-4.21.1.tar.gz && \
tar xvf protobuf_src.tar.gz && \
cd protobuf-3.21.1 && \
./configure --prefix=/usr && \
make -j$(nproc) install && \
ldconfig
ARG COINCURVE_VERSION=v0.1
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
unzip coincurve-anonswap.zip && \
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
cd coincurve-anonswap && \
python3 setup.py install --force
apt-get install -y --no-install-recommends \
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
RUN pip3 install -r requirements.txt
RUN pip3 install -r requirements.txt --require-hashes
COPY . basicswap-master
RUN cd basicswap-master; \
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
pip3 install .;
RUN useradd -ms /bin/bash swap_user && \

View File

@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright (c) 2019 tecnovert
Copyright (c) 2019-2024 tecnovert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +0,0 @@
include *.md LICENSE
recursive-include doc *
recursive-include basicswap/templates *
recursive-include basicswap/static *

View File

@@ -15,27 +15,31 @@ Table of Contents
## About
The BasicSwap DEX is a privacy-first and decentralized exchange which features cross-chain atomic swaps and a distributed order book.
**BasicSwap** is the worlds most secure and decentralized DEX. It facilitates cross-chain atomic swaps by enabling peers to interact directly with each other within a free and open environment without central points of failure.
[BasicSwap](https://academy.particl.io/en/latest/glossary.html#term-BasicSwap) is a cross-chain and privacy-centric DEX (decentralized exchange) that lets you trade cryptocurrencies with no third party involvement. Its distributed order book lets you make or take orders at no cost and trade within a free and open environment without central points of failure.
This DEX is fully non-custodial and features a decentralized order book, letting you create or accept swap offers without any fees, counterparties, or the need for accounts.
This DEX protocol was built in direct response to the increasingly invasive demands and data mining practices of todays cryptocurrency exchanges. It strives to bring more decentralized and more private cryptocurrency trading conditions for all.
Built as a low-friction, highly secure solution to the frequent losses of funds on centralized exchanges (e.g., FTX, BitFinex, MtGox), **BasicSwap** aims to provide more reliable and secure cryptocurrency trading conditions for everyone.
BasicSwap is still in beta. This means that, while it already offers most of the vital trading features youd expect to see on centralized exchanges, it is still in heavy development, and many more features will come about in the near future.
Check out our [roadmap](https://basicswapdex.com/roadmap) to get a better idea of what we've got planned for it!
**BasicSwap** is currently in active development by the community. While it already offers some of the essential trading features you'd expect from an exchange, more features and quality-of-life improvements are being worked on with the goal to provide a smoother user experience.
## Features
* **True cross-chain support** — Swap cryptocurrencies that live on entirely different blockchain environments, like Bitcoin and Monero.
* **Distributed order book** — Make or take limit orders on a completely distributed order book system.
* **No third-party or middleman** — Trade crypto with no intermediaries whatsoever.
* **Decentralized order book** — Make or take swap offers on a completely decentralized order book system.
* **No third-party or middleman** — Trade crypto with no intermediaries, completely eliminating central points of failure.
* **No trading fees** — Only pay the typical cryptocurrency network fee.
* **Privacy from the ground up** — Every component of BasicSwap is built with a privacy-first commitment.
* **Full Monero support** — Swap Monero with a variety of other cryptocurrencies like Bitcoin or Particl. No wrapped assets or trickery involved.
* **Superior financial privacy** — Protect your financial information from unauthorized access with BasicSwaps privacy-conscious technology.
* **Full Monero support** — Swap Monero with a variety of other cryptocurrencies like Bitcoin or Particl. No wrapped assets or layer-2 involved.
* **User-friendly interface** — Enjoy all these features within a user-friendly and intuitive interface that handles all the complicated parts for you.
BasicSwap is still in beta. This means that, while it already offers most of the vital trading features youd expect to see on centralized exchanges, it is still in heavy development, and many more features will come about in the near future.
## Under the Hood
**BasicSwap** can be best understood as the decentralized version of the SWIFT messaging network; providing a decentralized messaging protocol that allows for peers to connect directly with each other with the purpose of executing atomic swaps without central points of failure and using official core wallets (Bitcoin Core, Litecoin Core, etc).
**BasicSwap** does not process, initiate, or execute swaps; it merely enables peers to communicate with each other and exchange the required information to simplify the process of using atomic swaps on the respective blockchains of the coins being swapped.
In essence, **BasicSwap** operates merely as a decentralized messaging protocol supplemented by a user-friendly interface.
## Available Assets
@@ -60,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>
@@ -84,47 +94,63 @@ BasicSwap is compatible with the following digital assets.
<td>PIVX
</td>
</tr>
<tr>
<td>Decred
</td>
<td>DCR
</td>
</tr>
<tr>
<td>Wownero
</td>
<td>WOW
</td>
</tr>
<tr>
<td>Particl
</td>
<td>PART
</td>
</tr>
<tr>
<td>Dogecoin
</td>
<td>DOGE
</td>
</tr>
<tr>
<td>Namecoin
</td>
<td>NMC
</td>
</tr>
</table>
We plan on adding many other cryptocurrencies moving forward, including ETH and its ERC-20 tokens. However, due to the true cross-chain nature of the BasicSwap DEX protocol, each integration has to be done on a case-by-case basis.
If youd like to add a cryptocurrency to BasicSwap, either [apply for a listing using our listing application form](https://forms.gle/9DsHoHTJVqSiMNHW9), or try coding the integration yourself by referencing how other cryptocurrencies have been added. Follow [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html) for more information on how to integrate a coin yourself.
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).
# Participate
### Chats
* **For developers** The chat [#particl-dev:matrix.org](https://app.element.io/#/room/#particl-dev:matrix.org) using [Element](https://element.io) (formerly Riot).
* **For community** The community chat [https://discord.me/particl](https://discord.me/particl) [![Discord](https://img.shields.io/discord/391967609660112925)](https://discord.me/particl).
* **For support** Join the community on [#basicswap:matrix.org](https://matrix.to/#/#basicswap:matrix.org) using a Matrix client.
[![Twitter Follow](https://img.shields.io/twitter/follow/BasicSwapDEX?label=follow%20us&style=social)](http://twitter.com/BasicSwapDEX)
[![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/particl?style=social)](http://reddit.com/r/particl)
### Documentation, installation
For non-developers curious to explore a new world of commerce, binaries can be downloaded and installed. It is the easiest way to get started. Following the guides on [Particl Academy](https://academy.particl.io), a reference book in straightforward language, is recommended.
Follow the guides on [Particl Academy](https://academy.particl.io) for tutorials and guides on how BasicSwap works.
* [Download BasicSwapDEX](https://github.com/tecnovert/basicswap/tree/master/doc)
* [Download BasicSwapDEX](https://github.com/basicswap/basicswap/tree/master/doc)
#### Community chat support
* [Discord](https://discord.me/particl) navigate to the #support channel
* [Telegram](https://t.me/particlhelp)
* [Element](https://app.element.io/#/room/#particlhelp:matrix.org)
* [Matrix](https://matrix.to/#/#basicswap:matrix.org)
# Tutorials
You can find a wide variety of tutorials and step-by-step guides about BasicSwap on the [Particl Academy](https://academy.particl.io) or on Particls Youtube channel.
If you encounter an issue or try to accomplish something not mentioned in any of the tutorials included in the links above, please join the community chat support channels; youll be sure to find help and support from our awesome community and open-source team there!
If you encounter an issue or try to accomplish something not mentioned in any of the tutorials included in the links above, please join the community chat support channel; youll be sure to find help and support from current contributors there!
# License

View File

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

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# 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.
@@ -8,6 +9,7 @@ import os
import time
import shlex
import socks
import random
import socket
import urllib
import logging
@@ -17,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,
@@ -33,11 +41,10 @@ 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.is_running = True
self.fail_code = 0
self.mock_time_offset = 0
@@ -46,18 +53,20 @@ class BaseApp:
self.settings = settings
self.coin_clients = {}
self.coin_interfaces = {}
self.mxDB = threading.RLock()
self.debug = self.settings.get('debug', False)
self.mxDB = threading.Lock()
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
@@ -65,20 +74,28 @@ class BaseApp:
def stopRunning(self, with_code=0):
self.fail_code = with_code
with self.mxDB:
self.is_running = False
self.chainstate_delay_event.set()
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')
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'))
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)
@@ -90,67 +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']
command_cli = os.path.join(bindir, chainparams[coin_type]['name'] + '-cli' + ('.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)
@@ -160,12 +201,19 @@ class BaseApp:
socket.getaddrinfo = self.default_socket_getaddrinfo
socket.setdefaulttimeout(self.default_socket_timeout)
def readURL(self, url: str, timeout: int = 120, headers=None) -> bytes:
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()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
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")]
request = urllib.request.Request(url, headers=headers)
return opener.open(request, timeout=timeout).read()
@@ -176,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()
@@ -188,12 +238,37 @@ 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}")
value = min_v
if value > max_v:
self.log.warning(f"Setting {name} to {max_v}")
value = max_v
return value
def get_delay_event_seconds(self):
if self.min_delay_event == self.max_delay_event:
return self.min_delay_event
return random.randrange(self.min_delay_event, self.max_delay_event)
def get_short_delay_event_seconds(self):
if self.min_delay_event_short == self.max_delay_event_short:
return self.min_delay_event_short
return random.randrange(self.min_delay_event_short, self.max_delay_event_short)
def get_delay_retry_seconds(self):
if self.min_delay_retry == self.max_delay_retry:
return self.min_delay_retry
return random.randrange(self.min_delay_retry, self.max_delay_retry)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2023 tecnovert
# 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.
@@ -64,23 +65,25 @@ class SwapTypes(IntEnum):
SELLER_FIRST_2MSG = auto()
BUYER_FIRST_2MSG = auto()
XMR_SWAP = auto()
XMR_BCH_SWAP = auto()
class OfferStates(IntEnum):
OFFER_SENT = 1
OFFER_RECEIVED = 2
OFFER_ABANDONED = 3
OFFER_EXPIRED = 4
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
@@ -94,15 +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):
@@ -134,6 +140,8 @@ class TxTypes(IntEnum):
ITX_PRE_FUNDED = auto()
BCH_MERCY = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
@@ -180,6 +188,9 @@ class EventLogTypes(IntEnum):
PTX_PUBLISHED = auto()
PTX_REDEEM_PUBLISHED = auto()
PTX_REFUND_PUBLISHED = auto()
LOCK_TX_B_IN_MEMPOOL = auto()
BCH_MERCY_TX_PUBLISHED = auto()
BCH_MERCY_TX_FOUND = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -191,6 +202,7 @@ class DebugTypes(IntEnum):
NONE = 0
BID_STOP_AFTER_COIN_A_LOCK = auto()
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
BID_DONT_SPEND_COIN_A_LOCK_REFUND2 = auto() # continues
CREATE_INVALID_COIN_B_LOCK = auto()
BUYER_STOP_AFTER_ITX = auto()
MAKE_INVALID_PTX = auto()
@@ -198,6 +210,13 @@ class DebugTypes(IntEnum):
SKIP_LOCK_TX_REFUND = auto()
SEND_LOCKED_XMR = auto()
B_LOCK_TX_MISSED_SEND = auto()
DUPLICATE_ACTIONS = auto()
DONT_CONFIRM_PTX = auto()
OFFER_LOCK_2_VALUE_INC = auto()
BID_STOP_AFTER_COIN_B_LOCK = auto()
BID_DONT_SPEND_COIN_B_LOCK = auto()
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
BID_DONT_SPEND_COIN_A_LOCK = auto()
class NotificationTypes(IntEnum):
@@ -215,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):
@@ -231,240 +250,258 @@ 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 'Unknown'
return "Abandoned"
if state == OfferStates.OFFER_EXPIRED:
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' + ' ' + str(state)
return "Unknown bid state"
if state == BidStates.BID_EXPIRED:
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 'Unknown'
return "Funded mock initiate Tx"
if tx_type == TxTypes.BCH_MERCY:
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"
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"
if event_type == EventLogTypes.BCH_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 getVoutByP2WSH(txjs, p2wsh_hex):
for o in txjs['vout']:
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
for o in txjs["vout"]:
try:
if p2wsh_hex == o['scriptPubKey']['hex']:
return o['n']
if scriptPubKey_hex == o["scriptPubKey"]["hex"]:
return o["n"]
except Exception:
pass
raise ValueError('P2WSH 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'))
offer_bytes = offer_msg.SerializeToString()
h.update(offer_addr.encode("utf-8"))
offer_bytes = offer_msg.to_bytes()
h.update(offer_bytes)
return h.digest()
@@ -473,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]
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):

View File

@@ -0,0 +1 @@
name = "bin"

2952
basicswap/bin/prepare.py Executable file

File diff suppressed because it is too large Load Diff

649
basicswap/bin/run.py Executable file
View File

@@ -0,0 +1,649 @@
#!/usr/bin/env python3
# -*- 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 json
import logging
import os
import shutil
import signal
import subprocess
import sys
import traceback
import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
if not len(initial_logger.handlers):
initial_logger.addHandler(initial_logger.StreamHandler(sys.stdout))
logger = initial_logger
swap_client = None
class Daemon:
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v["name"]:
return True
return False
def signal_handler(sig, frame):
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()
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
# TODO: Remove
needs_rewrite: bool = False
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
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)
args = [
daemon_bin,
]
add_datadir: bool = extra_config.get("add_datadir", True)
if add_datadir:
args.append("-datadir=" + datadir_path)
args += opts
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",
)
opened_files.append(stdout_dest)
stderr_dest = stdout_dest
else:
stdout_dest = subprocess.PIPE
stderr_dest = subprocess.PIPE
shell: bool = False
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,
)
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)))
# 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],
)
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"]
needs_rewrite: bool = False
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"
)
config_path = os.path.join(data_dir, wallet_config_filename)
if os.path.exists(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()
)
)
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:
for line in fp_from:
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)))
# 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],
)
def ws_new_client(client, server):
if swap_client:
swap_client.log.debug(f'ws_new_client {client["id"]}')
def ws_client_left(client, server):
if client is None:
return
if swap_client:
swap_client.log.debug(f'ws_client_left {client["id"]}')
def ws_message_received(client, server, message):
if len(message) > 200:
message = message[:200] + ".."
if swap_client:
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get(
"core_binname", chainparams[coin_id].get("core_binname", default_name)
) + (".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 "")
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 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
def runClient(fp, data_dir, chain, start_only_coins):
global swap_client, logger
daemons = []
pids = []
threads = []
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
pids_path = os.path.join(data_dir, ".pids")
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."
)
else:
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))
with open(settings_path) as fs:
settings = json.load(fs)
swap_client = BasicSwap(fp, data_dir, settings, chain)
logger = swap_client.log
if os.path.exists(pids_path):
with open(pids_path) as fd:
for ln in fd:
# TODO: try close
logger.warning("Found pid for daemon {} ".format(ln.strip()))
# Ensure daemons are stopped
swap_client.stopDaemons()
# Settings may have been modified
settings = swap_client.settings
try:
# Try start daemons
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: # 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")
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
pid = daemons[-1].handle.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,
]
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",
]
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)
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)
)
pid = daemons[-1].handle.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")
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))
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 == "":
# Only set when in startonlycoin mode
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,
)
)
pid = daemons[-1].handle.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")
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))
if len(pids) > 0:
with open(pids_path, "w") as fd:
for p in pids:
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."
)
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,
)
threads.append(thread_http)
thread_http.start()
if "wshost" in settings:
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
swap_client.log.info(f"Starting ws server at {ws_url}.")
swap_client.ws_server = WebsocketServer(
host=settings["wshost"], port=settings["wsport"]
)
swap_client.ws_server.client_port = settings.get(
"wsclientport", settings["wsport"]
)
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
logger.info("Exit with Ctrl + c.")
while not swap_client.delay_event.wait(0.5):
swap_client.update()
except Exception as e: # noqa: F841
traceback.print_exc()
if swap_client.ws_server:
try:
swap_client.log.info("Stopping websocket server.")
swap_client.ws_server.shutdown_gracefully()
except Exception as e: # noqa: F841
traceback.print_exc()
swap_client.finalise()
swap_client.log.info("Stopping HTTP threads.")
for t in threads:
try:
t.stop()
t.join()
except Exception as e: # noqa: F841
traceback.print_exc()
closed_pids = []
for d in daemons:
swap_client.log.info("Interrupting {}".format(d.handle.pid))
try:
d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
except Exception as e:
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
for d in daemons:
try:
d.handle.wait(timeout=120)
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp:
fp.close()
closed_pids.append(d.handle.pid)
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 = ""
for ln in lines:
try:
if int(ln.split(":")[1]) not in closed_pids:
still_running += ln + "\n"
except Exception:
pass
with open(pids_path, "w") as fd:
fd.write(still_running)
def printVersion():
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."
)
def main():
data_dir = None
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)
continue
s = v.split("=")
name = s[0].strip()
for i in range(2):
if name[0] == "-":
name = name[1:]
if name == "v" or name == "version":
printVersion()
return 0
if name == "h" or name == "help":
printHelp()
return 0
if name in ("mainnet", "testnet", "regtest"):
chain = name
continue
if len(s) == 2:
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 is_known_coin(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
start_only_coins.add(coin)
continue
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 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)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
logger.info(
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
)
runClient(fp, data_dir, chain, start_only_coins)
print("Done.")
return swap_client.fail_code if swap_client is not None else 0
if __name__ == "__main__":
main()

View File

@@ -1,465 +1,552 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# 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 threading
from enum import IntEnum
from .util import (
COIN,
make_int,
format_amount,
TemporaryError,
)
XMR_COIN = 10 ** 12
XMR_COIN = 10**12
WOW_COIN = 10**11
class Coins(IntEnum):
PART = 1
BTC = 2
LTC = 3
# DCR = 4
DCR = 4
NMC = 5
XMR = 6
PART_BLIND = 7
PART_ANON = 8
# ZANO = 9
WOW = 9
# NDAU = 10
PIVX = 11
DASH = 12
FIRO = 13
VEIL = 14
NAV = 14
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,
},
},
Coins.DCR: {
"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": 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': {
'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.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,
"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,
"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,
},
},
Coins.WOW: {
"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": 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': 18081,
'walletrpcport': 18082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
}
},
Coins.PIVX: {
'name': 'pivx',
'ticker': 'PIVX',
'message_magic': 'DarkNet Signed Message:\n',
'blocks_target': 60 * 1,
'decimal_places': 8,
'has_csv': False,
'has_segwit': False,
'use_ticker_as_name': True,
'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_csv': True,
'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.VEIL: {
'name': 'veil',
'ticker': 'VEIL',
'message_magic': 'Veil Signed Message:\n',
'blocks_target': 120,
'decimal_places': 8,
'has_csv': True,
'has_segwit': False,
'mainnet': {
'rpcport': 58812,
'pubkey_address': 70,
'script_address': 5,
'key_prefix': 128,
'hrp': 'bv',
'bip44': 698,
'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,
"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': 58813,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'tv',
'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,
},
},
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": 100000,
"max_amount": 10000000 * COIN,
},
"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': 58823,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'tv',
'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):
def getCoinIdFromTicker(ticker: str) -> str:
try:
return ticker_map[ticker.lower()]
except Exception:
raise ValueError('Unknown coin')
raise ValueError(f"Unknown coin {ticker}")
class CoinInterface:
def __init__(self, network):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
def setDefaults(self):
self._unknown_wallet_seed = True
self._restore_height = None
def make_int(self, amount_in: int, r: int = 0) -> int:
return make_int(amount_in, self.exp(), r=r)
def format_amount(self, amount_in, conv_int=False, r=0):
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
return format_amount(amount_int, self.exp())
def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
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
return ticker
def getExchangeTicker(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['ticker']
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['name']
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['min_amount']
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['max_amount']
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
def setWalletRestoreHeight(self, value: int) -> None:
self._restore_height = value
def knownWalletSeed(self) -> bool:
return not self._unknown_wallet_seed
def chainparams(self):
return chainparams[self.coin_type()]
def chainparams_network(self):
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if 'not enough unlocked money' in str_error:
return True
if 'no unlocked balance' in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
return True
if 'invalid unlocked_balance' in str_error:
return True
if 'daemon is busy' in str_error:
return True
return False
def getCoinIdFromName(name: str) -> str:
try:
return name_map[name.lower()]
except Exception:
raise ValueError(f"Unknown coin {name}")

View File

@@ -1,53 +1,56 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2022 tecnovert
# Copyright (c) 2019-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 os
CONFIG_FILENAME = 'basicswap.json'
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', '~/.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', '~/.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)
PIVX_BINDIR = os.path.expanduser(os.getenv('PIVX_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'pivx')))
PIVXD = os.getenv('PIVXD', 'pivxd' + bin_suffix)
PIVX_CLI = os.getenv('PIVX_CLI', 'pivx-cli' + bin_suffix)
PIVX_TX = os.getenv('PIVX_TX', 'pivx-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)
DASH_BINDIR = os.path.expanduser(os.getenv('DASH_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'dash')))
DASHD = os.getenv('DASHD', 'dashd' + bin_suffix)
DASH_CLI = os.getenv('DASH_CLI', 'dash-cli' + bin_suffix)
DASH_TX = os.getenv('DASH_TX', 'dash-tx' + bin_suffix)
FIRO_BINDIR = os.path.expanduser(os.getenv('FIRO_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'firo')))
FIROD = os.getenv('FIROD', 'firod' + bin_suffix)
FIRO_CLI = os.getenv('FIRO_CLI', 'firo-cli' + bin_suffix)
FIRO_TX = os.getenv('FIRO_TX', 'firo-tx' + bin_suffix)
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,533 @@
intro = """
blake.py
version 5, 2-Apr-2014
BLAKE is a SHA3 round-3 finalist designed and submitted by
Jean-Philippe Aumasson et al.
At the core of BLAKE is a ChaCha-like mixer, very similar
to that found in the stream cipher, ChaCha8. Besides being
a very good mixer, ChaCha is fast.
References:
http://www.131002.net/blake/
http://csrc.nist.gov/groups/ST/hash/sha-3/index.html
http://en.wikipedia.org/wiki/BLAKE_(hash_function)
This implementation assumes all data is in increments of
whole bytes. (The formal definition of BLAKE allows for
hashing individual bits.) Note too that this implementation
does include the round-3 tweaks where the number of rounds
was increased to 14/16 from 10/14.
This version can be imported into both Python2 (2.6 and 2.7)
and Python3 programs. Python 2.5 requires an older version
of blake.py (version 4).
Here are some comparative times for different versions of
Python:
64-bit:
2.6 6.284s
2.7 6.343s
3.2 7.620s
pypy (2.7) 2.080s
32-bit:
2.5 (32) 15.389s (with psyco)
2.7-32 13.645s
3.2-32 12.574s
One test on a 2.0GHz Core 2 Duo of 10,000 iterations of
BLAKE-256 on a short message produced a time of 5.7 seconds.
Not bad, but if raw speed is what you want, look to the
the C version. It is 40x faster and did the same thing
in 0.13 seconds.
Copyright (c) 2009-2012 by Larry Bugbee, Kent, WA
ALL RIGHTS RESERVED.
blake.py IS EXPERIMENTAL SOFTWARE FOR EDUCATIONAL
PURPOSES ONLY. IT IS MADE AVAILABLE "AS-IS" WITHOUT
WARRANTY OR GUARANTEE OF ANY KIND. USE SIGNIFIES
ACCEPTANCE OF ALL RISK.
To make your learning and experimentation less cumbersome,
blake.py is free for any use.
Enjoy,
Larry Bugbee
March 2011
rev May 2011 - fixed Python version check (tx JP)
rev Apr 2012 - fixed an out-of-order bit set in final()
- moved self-test to a separate test pgm
- this now works with Python2 and Python3
rev Apr 2014 - added test and conversion of string input
to byte string in update() (tx Soham)
- added hexdigest() method.
- now support state 3 so only one call to
final() per instantiation is allowed. all
subsequent calls to final(), digest() or
hexdigest() simply return the stored value.
"""
import struct
from binascii import hexlify, unhexlify
#---------------------------------------------------------------
class BLAKE(object):
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
# initial values, constants and padding
# IVx for BLAKE-x
IV64 = [
0x6A09E667F3BCC908, 0xBB67AE8584CAA73B,
0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1,
0x510E527FADE682D1, 0x9B05688C2B3E6C1F,
0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179,
]
IV48 = [
0xCBBB9D5DC1059ED8, 0x629A292A367CD507,
0x9159015A3070DD17, 0x152FECD8F70E5939,
0x67332667FFC00B31, 0x8EB44A8768581511,
0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4,
]
# note: the values here are the same as the high-order
# half-words of IV64
IV32 = [
0x6A09E667, 0xBB67AE85,
0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C,
0x1F83D9AB, 0x5BE0CD19,
]
# note: the values here are the same as the low-order
# half-words of IV48
IV28 = [
0xC1059ED8, 0x367CD507,
0x3070DD17, 0xF70E5939,
0xFFC00B31, 0x68581511,
0x64F98FA7, 0xBEFA4FA4,
]
# constants for BLAKE-64 and BLAKE-48
C64 = [
0x243F6A8885A308D3, 0x13198A2E03707344,
0xA4093822299F31D0, 0x082EFA98EC4E6C89,
0x452821E638D01377, 0xBE5466CF34E90C6C,
0xC0AC29B7C97C50DD, 0x3F84D5B5B5470917,
0x9216D5D98979FB1B, 0xD1310BA698DFB5AC,
0x2FFD72DBD01ADFB7, 0xB8E1AFED6A267E96,
0xBA7C9045F12C7F99, 0x24A19947B3916CF7,
0x0801F2E2858EFC16, 0x636920D871574E69,
]
# constants for BLAKE-32 and BLAKE-28
# note: concatenate and the values are the same as the values
# for the 1st half of C64
C32 = [
0x243F6A88, 0x85A308D3,
0x13198A2E, 0x03707344,
0xA4093822, 0x299F31D0,
0x082EFA98, 0xEC4E6C89,
0x452821E6, 0x38D01377,
0xBE5466CF, 0x34E90C6C,
0xC0AC29B7, 0xC97C50DD,
0x3F84D5B5, 0xB5470917,
]
# the 10 permutations of:0,...15}
SIGMA = [
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15],
[14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3],
[11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4],
[ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8],
[ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13],
[ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9],
[12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11],
[13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10],
[ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5],
[10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0],
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15],
[14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3],
[11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4],
[ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8],
[ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13],
[ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9],
[12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11],
[13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10],
[ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5],
[10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0],
]
MASK32BITS = 0xFFFFFFFF
MASK64BITS = 0xFFFFFFFFFFFFFFFF
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def __init__(self, hashbitlen):
"""
load the hashSate structure (copy hashbitlen...)
hashbitlen: length of the hash output
"""
if hashbitlen not in [224, 256, 384, 512]:
raise Exception('hash length not 224, 256, 384 or 512')
self.hashbitlen = hashbitlen
self.h = [0]*8 # current chain value (initialized to the IV)
self.t = 0 # number of *BITS* hashed so far
self.cache = b'' # cached leftover data not yet compressed
self.salt = [0]*4 # salt (null by default)
self.state = 1 # set to 2 by update and 3 by final
self.nullt = 0 # Boolean value for special case \ell_i=0
# The algorithm is the same for both the 32- and 64- versions
# of BLAKE. The difference is in word size (4 vs 8 bytes),
# blocksize (64 vs 128 bytes), number of rounds (14 vs 16)
# and a few very specific constants.
if (hashbitlen == 224) or (hashbitlen == 256):
# setup for 32-bit words and 64-bit block
self.byte2int = self._fourByte2int
self.int2byte = self._int2fourByte
self.MASK = self.MASK32BITS
self.WORDBYTES = 4
self.WORDBITS = 32
self.BLKBYTES = 64
self.BLKBITS = 512
self.ROUNDS = 14 # was 10 before round 3
self.cxx = self.C32
self.rot1 = 16 # num bits to shift in G
self.rot2 = 12 # num bits to shift in G
self.rot3 = 8 # num bits to shift in G
self.rot4 = 7 # num bits to shift in G
self.mul = 0 # for 32-bit words, 32<<self.mul where self.mul = 0
# 224- and 256-bit versions (32-bit words)
if hashbitlen == 224:
self.h = self.IV28[:]
else:
self.h = self.IV32[:]
elif (hashbitlen == 384) or (hashbitlen == 512):
# setup for 64-bit words and 128-bit block
self.byte2int = self._eightByte2int
self.int2byte = self._int2eightByte
self.MASK = self.MASK64BITS
self.WORDBYTES = 8
self.WORDBITS = 64
self.BLKBYTES = 128
self.BLKBITS = 1024
self.ROUNDS = 16 # was 14 before round 3
self.cxx = self.C64
self.rot1 = 32 # num bits to shift in G
self.rot2 = 25 # num bits to shift in G
self.rot3 = 16 # num bits to shift in G
self.rot4 = 11 # num bits to shift in G
self.mul = 1 # for 64-bit words, 32<<self.mul where self.mul = 1
# 384- and 512-bit versions (64-bit words)
if hashbitlen == 384:
self.h = self.IV48[:]
else:
self.h = self.IV64[:]
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _compress(self, block):
byte2int = self.byte2int
mul = self.mul # de-reference these for ...speed? ;-)
cxx = self.cxx
rot1 = self.rot1
rot2 = self.rot2
rot3 = self.rot3
rot4 = self.rot4
MASK = self.MASK
WORDBITS = self.WORDBITS
SIGMA = self.SIGMA
# get message (<<2 is the same as *4 but faster)
m = [byte2int(block[i<<2<<mul:(i<<2<<mul)+(4<<mul)]) for i in range(16)]
# initialization
v = [0]*16
v[ 0: 8] = [self.h[i] for i in range(8)]
v[ 8:16] = [self.cxx[i] for i in range(8)]
v[ 8:12] = [v[8+i] ^ self.salt[i] for i in range(4)]
if self.nullt == 0: # (i>>1 is the same as i/2 but faster)
v[12] = v[12] ^ (self.t & MASK)
v[13] = v[13] ^ (self.t & MASK)
v[14] = v[14] ^ (self.t >> self.WORDBITS)
v[15] = v[15] ^ (self.t >> self.WORDBITS)
# - - - - - - - - - - - - - - - - -
# ready? let's ChaCha!!!
def G(a, b, c, d, i):
va = v[a] # it's faster to deref and reref later
vb = v[b]
vc = v[c]
vd = v[d]
sri = SIGMA[round][i]
sri1 = SIGMA[round][i+1]
va = ((va + vb) + (m[sri] ^ cxx[sri1]) ) & MASK
x = vd ^ va
vd = (x >> rot1) | ((x << (WORDBITS-rot1)) & MASK)
vc = (vc + vd) & MASK
x = vb ^ vc
vb = (x >> rot2) | ((x << (WORDBITS-rot2)) & MASK)
va = ((va + vb) + (m[sri1] ^ cxx[sri]) ) & MASK
x = vd ^ va
vd = (x >> rot3) | ((x << (WORDBITS-rot3)) & MASK)
vc = (vc + vd) & MASK
x = vb ^ vc
vb = (x >> rot4) | ((x << (WORDBITS-rot4)) & MASK)
v[a] = va
v[b] = vb
v[c] = vc
v[d] = vd
for round in range(self.ROUNDS):
# column step
G( 0, 4, 8,12, 0)
G( 1, 5, 9,13, 2)
G( 2, 6,10,14, 4)
G( 3, 7,11,15, 6)
# diagonal step
G( 0, 5,10,15, 8)
G( 1, 6,11,12,10)
G( 2, 7, 8,13,12)
G( 3, 4, 9,14,14)
# - - - - - - - - - - - - - - - - -
# save current hash value (use i&0x3 to get 0,1,2,3,0,1,2,3)
self.h = [self.h[i]^v[i]^v[i+8]^self.salt[i&0x3]
for i in range(8)]
# print 'self.h', [num2hex(h) for h in self.h]
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def addsalt(self, salt):
""" adds a salt to the hash function (OPTIONAL)
should be called AFTER Init, and BEFORE update
salt: a bytestring, length determined by hashbitlen.
if not of sufficient length, the bytestring
will be assumed to be a big endian number and
prefixed with an appropriate number of null
bytes, and if too large, only the low order
bytes will be used.
if hashbitlen=224 or 256, then salt will be 16 bytes
if hashbitlen=384 or 512, then salt will be 32 bytes
"""
# fail if addsalt() was not called at the right time
if self.state != 1:
raise Exception('addsalt() not called after init() and before update()')
# salt size is to be 4x word size
saltsize = self.WORDBYTES * 4
# if too short, prefix with null bytes. if too long,
# truncate high order bytes
if len(salt) < saltsize:
salt = (chr(0)*(saltsize-len(salt)) + salt)
else:
salt = salt[-saltsize:]
# prep the salt array
self.salt[0] = self.byte2int(salt[ : 4<<self.mul])
self.salt[1] = self.byte2int(salt[ 4<<self.mul: 8<<self.mul])
self.salt[2] = self.byte2int(salt[ 8<<self.mul:12<<self.mul])
self.salt[3] = self.byte2int(salt[12<<self.mul: ])
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def update(self, data):
""" update the state with new data, storing excess data
as necessary. may be called multiple times and if a
call sends less than a full block in size, the leftover
is cached and will be consumed in the next call
data: data to be hashed (bytestring)
"""
self.state = 2
BLKBYTES = self.BLKBYTES # de-referenced for improved readability
BLKBITS = self.BLKBITS
datalen = len(data)
if not datalen: return
if type(data) == type(u''):
# use either of the next two lines for a proper
# response under both Python2 and Python3
data = data.encode('UTF-8') # converts to byte string
#data = bytearray(data, 'utf-8') # use if want mutable
# This next line works for Py3 but fails under
# Py2 because the Py2 version of bytes() will
# accept only *one* argument. Arrrrgh!!!
#data = bytes(data, 'utf-8') # converts to immutable byte
# string but... under p7
# bytes() wants only 1 arg
# ...a dummy, 2nd argument like encoding=None
# that does nothing would at least allow
# compatibility between Python2 and Python3.
left = len(self.cache)
fill = BLKBYTES - left
# if any cached data and any added new data will fill a
# full block, fill and compress
if left and datalen >= fill:
self.cache = self.cache + data[:fill]
self.t += BLKBITS # update counter
self._compress(self.cache)
self.cache = b''
data = data[fill:]
datalen -= fill
# compress new data until not enough for a full block
while datalen >= BLKBYTES:
self.t += BLKBITS # update counter
self._compress(data[:BLKBYTES])
data = data[BLKBYTES:]
datalen -= BLKBYTES
# cache all leftover bytes until next call to update()
if datalen > 0:
self.cache = self.cache + data[:datalen]
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def final(self, data=''):
""" finalize the hash -- pad and hash remaining data
returns hashval, the digest
"""
if self.state == 3:
# we have already finalized so simply return the
# previously calculated/stored hash value
return self.hash
if data:
self.update(data)
ZZ = b'\x00'
ZO = b'\x01'
OZ = b'\x80'
OO = b'\x81'
PADDING = OZ + ZZ*128 # pre-formatted padding data
# copy nb. bits hash in total as a 64-bit BE word
# copy nb. bits hash in total as a 128-bit BE word
tt = self.t + (len(self.cache) << 3)
if self.BLKBYTES == 64:
msglen = self._int2eightByte(tt)
else:
low = tt & self.MASK
high = tt >> self.WORDBITS
msglen = self._int2eightByte(high) + self._int2eightByte(low)
# size of block without the words at the end that count
# the number of bits, 55 or 111.
# Note: (((self.WORDBITS/8)*2)+1) equals ((self.WORDBITS>>2)+1)
sizewithout = self.BLKBYTES - ((self.WORDBITS>>2)+1)
if len(self.cache) == sizewithout:
# special case of one padding byte
self.t -= 8
if self.hashbitlen in [224, 384]:
self.update(OZ)
else:
self.update(OO)
else:
if len(self.cache) < sizewithout:
# enough space to fill the block
# use t=0 if no remaining data
if len(self.cache) == 0:
self.nullt=1
self.t -= (sizewithout - len(self.cache)) << 3
self.update(PADDING[:sizewithout - len(self.cache)])
else:
# NOT enough space, need 2 compressions
# ...add marker, pad with nulls and compress
self.t -= (self.BLKBYTES - len(self.cache)) << 3
self.update(PADDING[:self.BLKBYTES - len(self.cache)])
# ...now pad w/nulls leaving space for marker & bit count
self.t -= (sizewithout+1) << 3
self.update(PADDING[1:sizewithout+1]) # pad with zeroes
self.nullt = 1 # raise flag to set t=0 at the next _compress
# append a marker byte
if self.hashbitlen in [224, 384]:
self.update(ZZ)
else:
self.update(ZO)
self.t -= 8
# append the number of bits (long long)
self.t -= self.BLKBYTES
self.update(msglen)
hashval = []
if self.BLKBYTES == 64:
for h in self.h:
hashval.append(self._int2fourByte(h))
else:
for h in self.h:
hashval.append(self._int2eightByte(h))
self.hash = b''.join(hashval)[:self.hashbitlen >> 3]
self.state = 3
return self.hash
digest = final # may use digest() as a synonym for final()
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
def hexdigest(self, data=''):
return hexlify(self.final(data)).decode('UTF-8')
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# utility functions
def _fourByte2int(self, bytestr): # see also long2byt() below
""" convert a 4-byte string to an int (long) """
return struct.unpack('!L', bytestr)[0]
def _eightByte2int(self, bytestr):
""" convert a 8-byte string to an int (long long) """
return struct.unpack('!Q', bytestr)[0]
def _int2fourByte(self, x): # see also long2byt() below
""" convert a number to a 4-byte string, high order
truncation possible (in Python x could be a BIGNUM)
"""
return struct.pack('!L', x)
def _int2eightByte(self, x):
""" convert a number to a 8-byte string, high order
truncation possible (in Python x could be a BIGNUM)
"""
return struct.pack('!Q', x)
#---------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
def blake_hash(data):
return BLAKE(256).digest(data)

View File

@@ -0,0 +1,37 @@
from blake256 import blake_hash
testVectors = [
["716f6e863f744b9ac22c97ec7b76ea5f5908bc5b2f67c61510bfc4751384ea7a", ""],
["43234ff894a9c0590d0246cfc574eb781a80958b01d7a2fa1ac73c673ba5e311", "a"],
["658c6d9019a1deddbcb3640a066dfd23471553a307ab941fd3e677ba887be329", "ab"],
["1833a9fa7cf4086bd5fda73da32e5a1d75b4c3f89d5c436369f9d78bb2da5c28", "abc"],
["35282468f3b93c5aaca6408582fced36e578f67671ed0741c332d68ac72d7aa2", "abcd"],
["9278d633efce801c6aa62987d7483d50e3c918caed7d46679551eed91fba8904", "abcde"],
["7a17ee5e289845adcafaf6ca1b05c4a281b232a71c7083f66c19ba1d1169a8d4", "abcdef"],
["ee8c7f94ff805cb2e644643010ea43b0222056420917ec70c3da764175193f8f", "abcdefg"],
["7b37c0876d29c5add7800a1823795a82b809fc12f799ff6a4b5e58d52c42b17e", "abcdefgh"],
["bdc514bea74ffbb9c3aa6470b08ceb80a88e313ad65e4a01457bbffd0acc86de", "abcdefghi"],
["12e3afb9739df8d727e93d853faeafc374cc55aedc937e5a1e66f5843b1d4c2e", "abcdefghij"],
["22297d373b751f581944bb26315133f6fda2f0bf60f65db773900f61f81b7e79", "Discard medicine more than two years old."],
["4d48d137bc9cf6d21415b805bf33f59320337d85c673998260e03a02a0d760cd", "He who has a shady past knows that nice guys finish last."],
["beba299e10f93e17d45663a6dc4b8c9349e4f5b9bac0d7832389c40a1b401e5c", "I wouldn't marry him with a ten foot pole."],
["42e082ae7f967781c6cd4e0ceeaeeb19fb2955adbdbaf8c7ec4613ac130071b3", "Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave"],
["207d06b205bfb359df91b48b6fd8aa6e4798b712d1cc5e91a254da9cef8684a3", "The days of the digital watch are numbered. -Tom Stoppard"],
["d56eab6927e371e2148b0788779aaf565d30567af2af822b6be3b90db9767a70", "Nepal premier won't resign."],
["01020709ca7fd10dc7756ce767d508d7206167d300b7a7ed76838a8547a7898c", "For every action there is an equal and opposite government program."],
["5569a6cc6535a66da221d8f6ad25008f28752d0343f3f1d757f1ecc9b1c61536", "His money is twice tainted: 'taint yours and 'taint mine."],
["8ff699b5ac7687c82600e89d0ff6cfa87e7179759184386971feb76fbae9975f", "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"],
["f4b3a7c85a418b15ce330fd41ae0254b036ad48dd98aa37f0506a995ba9c6029", "It's a tiny change to the code and not completely disgusting. - Bob Manchek"],
["1ed94bab64fe560ef0983165fcb067e9a8a971c1db8e6fb151ff9a7c7fe877e3", "size: a.out: bad magic"],
["ff15b54992eedf9889f7b4bbb16692881aa01ed10dfc860fdb04785d8185cd3c", "The major problem is with sendmail. -Mark Horton"],
["8a0a7c417a47deec0b6474d8c247da142d2e315113a2817af3de8f45690d8652", "Give me a rock, paper and scissors and I will move the world. CCFestoon"],
["310d263fdab056a930324cdea5f46f9ea70219c1a74b01009994484113222a62", "If the enemy is within range, then so are you."],
["1aaa0903aa4cf872fe494c322a6e535698ea2140e15f26fb6088287aedceb6ba", "It's well we cannot hear the screams/That we create in others' dreams."],
["2eb81bcaa9e9185a7587a1b26299dcfb30f2a58a7f29adb584b969725457ad4f", "You remind me of a TV show, but that's all right: I watch it anyway."],
["c27b1683ef76e274680ab5492e592997b0d9d5ac5a5f4651b6036f64215256af", "C is as portable as Stonehedge!!"],
["3995cce8f32b174c22ffac916124bd095c80205d9d5f1bb08a155ac24b40d6cb", "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley"],
["496f7063f8bd479bf54e9d87e9ba53e277839ac7fdaecc5105f2879b58ee562f", "The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule"],
["2e0eff918940b01eea9539a02212f33ee84f77fab201f4287aa6167e4a1ed043", "How can you write a big system without C++? -Paul Glick"]]
for vectorSet in testVectors:
assert vectorSet[0] == blake_hash(vectorSet[1]).encode('hex')

View File

@@ -0,0 +1,3 @@
from .mnemonic import Mnemonic
__all__ = ["Mnemonic"]

View File

@@ -0,0 +1,298 @@
#
# Copyright (c) 2013 Pavol Rusnak
# Copyright (c) 2017 mruddy
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
from __future__ import annotations
import hashlib
import hmac
import itertools
import os
import secrets
import typing as t
import unicodedata
PBKDF2_ROUNDS = 2048
class ConfigurationError(Exception):
pass
# Refactored code segments from <https://github.com/keis/base58>
def b58encode(v: bytes) -> str:
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
p, acc = 1, 0
for c in reversed(v):
acc += p * c
p = p << 8
string = ""
while acc:
acc, idx = divmod(acc, 58)
string = alphabet[idx : idx + 1] + string
return string
class Mnemonic(object):
def __init__(self, language: str = "english", wordlist: list[str] | None = None):
self.radix = 2048
self.language = language
if wordlist is None:
d = os.path.join(os.path.dirname(__file__), f"wordlist/{language}.txt")
if os.path.exists(d) and os.path.isfile(d):
with open(d, "r", encoding="utf-8") as f:
wordlist = [w.strip() for w in f.readlines()]
else:
raise ConfigurationError("Language not detected")
if len(wordlist) != self.radix:
raise ConfigurationError(f"Wordlist must contain {self.radix} words.")
self.wordlist = wordlist
# Japanese must be joined by ideographic space
self.delimiter = "\u3000" if language == "japanese" else " "
@classmethod
def list_languages(cls) -> list[str]:
return [
f.split(".")[0]
for f in os.listdir(os.path.join(os.path.dirname(__file__), "wordlist"))
if f.endswith(".txt")
]
@staticmethod
def normalize_string(txt: t.AnyStr) -> str:
if isinstance(txt, bytes):
utxt = txt.decode("utf8")
elif isinstance(txt, str):
utxt = txt
else:
raise TypeError("String value expected")
return unicodedata.normalize("NFKD", utxt)
@classmethod
def detect_language(cls, code: str) -> str:
"""Scan the Mnemonic until the language becomes unambiguous, including as abbreviation prefixes.
Unfortunately, there are valid words that are ambiguous between languages, which are complete words
in one language and are prefixes in another:
english: abandon ... about
french: abandon ... aboutir
If prefixes remain ambiguous, require exactly one language where word(s) match exactly.
"""
code = cls.normalize_string(code)
possible = set(cls(lang) for lang in cls.list_languages())
words = set(code.split())
for word in words:
# possible languages have candidate(s) starting with the word/prefix
possible = set(
p for p in possible if any(c.startswith(word) for c in p.wordlist)
)
if not possible:
raise ConfigurationError(f"Language unrecognized for {word!r}")
if len(possible) == 1:
return possible.pop().language
# Multiple languages match: A prefix in many, but an exact match in one determines language.
complete = set()
for word in words:
exact = set(p for p in possible if word in p.wordlist)
if len(exact) == 1:
complete.update(exact)
if len(complete) == 1:
return complete.pop().language
raise ConfigurationError(
f"Language ambiguous between {', '.join(p.language for p in possible)}"
)
def generate(self, strength: int = 128) -> str:
"""
Create a new mnemonic using a random generated number as entropy.
As defined in BIP39, the entropy must be a multiple of 32 bits, and its size must be between 128 and 256 bits.
Therefore the possible values for `strength` are 128, 160, 192, 224 and 256.
If not provided, the default entropy length will be set to 128 bits.
The return is a list of words that encodes the generated entropy.
:param strength: Number of bytes used as entropy
:type strength: int
:return: A randomly generated mnemonic
:rtype: str
"""
if strength not in [128, 160, 192, 224, 256]:
raise ValueError(
"Invalid strength value. Allowed values are [128, 160, 192, 224, 256]."
)
return self.to_mnemonic(secrets.token_bytes(strength // 8))
# Adapted from <http://tinyurl.com/oxmn476>
def to_entropy(self, words: list[str] | str) -> bytearray:
if not isinstance(words, list):
words = words.split(" ")
if len(words) not in [12, 15, 18, 21, 24]:
raise ValueError(
"Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)."
% len(words)
)
# Look up all the words in the list and construct the
# concatenation of the original entropy and the checksum.
concatLenBits = len(words) * 11
concatBits = [False] * concatLenBits
wordindex = 0
for word in words:
# Find the words index in the wordlist
ndx = self.wordlist.index(self.normalize_string(word))
if ndx < 0:
raise LookupError('Unable to find "%s" in word list.' % word)
# Set the next 11 bits to the value of the index.
for ii in range(11):
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0
wordindex += 1
checksumLengthBits = concatLenBits // 33
entropyLengthBits = concatLenBits - checksumLengthBits
# Extract original entropy as bytes.
entropy = bytearray(entropyLengthBits // 8)
for ii in range(len(entropy)):
for jj in range(8):
if concatBits[(ii * 8) + jj]:
entropy[ii] |= 1 << (7 - jj)
# Take the digest of the entropy.
hashBytes = hashlib.sha256(entropy).digest()
hashBits = list(
itertools.chain.from_iterable(
[c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes
)
)
# Check all the checksum bits.
for i in range(checksumLengthBits):
if concatBits[entropyLengthBits + i] != hashBits[i]:
raise ValueError("Failed checksum.")
return entropy
def to_mnemonic(self, data: bytes) -> str:
if len(data) not in [16, 20, 24, 28, 32]:
raise ValueError(
f"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not {len(data)}."
)
h = hashlib.sha256(data).hexdigest()
b = (
bin(int.from_bytes(data, byteorder="big"))[2:].zfill(len(data) * 8)
+ bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
)
result = []
for i in range(len(b) // 11):
idx = int(b[i * 11 : (i + 1) * 11], 2)
result.append(self.wordlist[idx])
return self.delimiter.join(result)
def check(self, mnemonic: str) -> bool:
mnemonic_list = self.normalize_string(mnemonic).split(" ")
# list of valid mnemonic lengths
if len(mnemonic_list) not in [12, 15, 18, 21, 24]:
return False
try:
idx = map(
lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic_list
)
b = "".join(idx)
except ValueError:
return False
l = len(b) # noqa: E741
d = b[: l // 33 * 32]
h = b[-l // 33 :]
nd = int(d, 2).to_bytes(l // 33 * 4, byteorder="big")
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
return h == nh
def expand_word(self, prefix: str) -> str:
if prefix in self.wordlist:
return prefix
else:
matches = [word for word in self.wordlist if word.startswith(prefix)]
if len(matches) == 1: # matched exactly one word in the wordlist
return matches[0]
else:
# exact match not found.
# this is not a validation routine, just return the input
return prefix
def expand(self, mnemonic: str) -> str:
return " ".join(map(self.expand_word, mnemonic.split(" ")))
@classmethod
def to_seed(cls, mnemonic: str, passphrase: str = "") -> bytes:
mnemonic = cls.normalize_string(mnemonic)
passphrase = cls.normalize_string(passphrase)
passphrase = "mnemonic" + passphrase
mnemonic_bytes = mnemonic.encode("utf-8")
passphrase_bytes = passphrase.encode("utf-8")
stretched = hashlib.pbkdf2_hmac(
"sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
)
return stretched[:64]
@staticmethod
def to_hd_master_key(seed: bytes, testnet: bool = False) -> str:
if len(seed) != 64:
raise ValueError("Provided seed should have length of 64")
# Compute HMAC-SHA512 of seed
seed = hmac.new(b"Bitcoin seed", seed, digestmod=hashlib.sha512).digest()
# Serialization format can be found at: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
xprv = b"\x04\x88\xad\xe4" # Version for private mainnet
if testnet:
xprv = b"\x04\x35\x83\x94" # Version for private testnet
xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number
xprv += seed[32:] # Chain code
xprv += b"\x00" + seed[:32] # Master key
# Double hash using SHA256
hashed_xprv = hashlib.sha256(xprv).digest()
hashed_xprv = hashlib.sha256(hashed_xprv).digest()
# Append 4 bytes of checksum
xprv += hashed_xprv[:4]
# Return base58
return b58encode(xprv)
def main() -> None:
import sys
if len(sys.argv) > 1:
hex_data = sys.argv[1]
else:
hex_data = sys.stdin.readline().strip()
data = bytes.fromhex(hex_data)
m = Mnemonic("english")
print(m.to_mnemonic(data))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
# Marker file for PEP 561.

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

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

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

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

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,308 +1,427 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# 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.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('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.setIntKVInSession('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('ALTER TABLE bids ADD COLUMN security_token BLOB')
session.execute('ALTER TABLE offers ADD COLUMN security_token BLOB')
db_version += 1
elif current_version == 7:
session.execute('ALTER TABLE transactions ADD COLUMN block_hash BLOB')
session.execute('ALTER TABLE transactions ADD COLUMN block_height INTEGER')
session.execute('ALTER TABLE transactions ADD COLUMN block_time INTEGER')
db_version += 1
elif current_version == 8:
session.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:
session.execute('ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR')
db_version += 1
elif current_version == 10:
session.execute('ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER')
session.execute('ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER')
session.execute('ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR')
session.execute('ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR')
session.execute('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('ALTER TABLE offers ADD COLUMN addr_to VARCHAR')
session.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
db_version += 1
elif current_version == 11:
session.execute('ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER')
session.execute('ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER')
session.execute('ALTER TABLE bids ADD COLUMN protocol_version INTEGER')
session.execute('ALTER TABLE offers ADD COLUMN protocol_version INTEGER')
session.execute('ALTER TABLE transactions ADD COLUMN tx_data BLOB')
db_version += 1
elif current_version == 12:
session.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))''')
session.execute('ALTER TABLE bids ADD COLUMN reject_code INTEGER')
session.execute('ALTER TABLE bids ADD COLUMN rate INTEGER')
session.execute('ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER')
session.execute('ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER')
db_version += 1
elif current_version == 13:
db_version += 1
session.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,
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('''
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('''
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('''
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('ALTER TABLE wallets ADD COLUMN active_ind INTEGER')
session.execute('ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER')
session.execute('ALTER TABLE eventqueue RENAME TO actions')
session.execute('ALTER TABLE actions RENAME COLUMN event_id TO action_id')
session.execute('ALTER TABLE actions RENAME COLUMN event_type TO action_type')
session.execute('ALTER TABLE actions RENAME COLUMN event_data TO action_data')
elif current_version == 14:
db_version += 1
session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB')
session.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
session.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
session.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
session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER')
session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER')
session.execute('ALTER TABLE knownidentities ADD COLUMN data BLOB')
session.execute('UPDATE knownidentities SET active_ind = 1')
elif current_version == 18:
db_version += 1
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING')
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING')
elif current_version == 19:
db_version += 1
session.execute('ALTER TABLE bidstates ADD COLUMN in_error INTEGER')
session.execute('ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER')
session.execute('ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER')
elif current_version == 20:
db_version += 1
session.execute('''
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('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER')
if current_version != db_version:
self.db_version = db_version
self.setIntKVInSession('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,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2023 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.
@@ -12,45 +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(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('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('DELETE FROM transactions WHERE transactions.bid_id = :bid_id', {'bid_id': bid_row[0]})
session.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]})
session.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]})
session.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]})
session.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]})
session.execute('DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id', {'bid_id': bid_row[0]})
session.execute('DELETE FROM actions WHERE actions.linked_id = :bid_id', {'bid_id': bid_row[0]})
session.execute('DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id', {'bid_id': bid_row[0]})
session.execute('DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id', {'bid_id': bid_row[0]})
session.execute('DELETE FROM bids WHERE bids.bid_id = :bid_id', {'bid_id': bid_row[0]})
session.execute('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id', {'type_ind': int(Concepts.BID), 'linked_id': bid_row[0]})
cursor.execute(
"DELETE FROM 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('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('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('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('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('DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
session.execute('DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id', {'offer_id': offer_row[0]})
session.execute('DELETE FROM actions WHERE actions.linked_id = :offer_id', {'offer_id': offer_row[0]})
session.execute('DELETE FROM offers WHERE offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
session.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]})
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 "",
)
)
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-2023 tecnovert
# 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,38 +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 == 'b':
elif type_ind == "f":
typed_params.append(float(param))
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)
@@ -109,7 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
class HttpHandler(BaseHTTPRequestHandler):
def log_error(self, format, *args):
super().log_message(format, *args)
@@ -121,96 +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_url'] = swap_client.ws_server.url
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:
args_dict['tor_established'] = True if get_tor_established_state(swap_client) == '1' else False
except Exception:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception as e:
args_dict["tor_established"] = False
if swap_client.debug:
swap_client.log.error(f"Error getting Tor state: {str(e)}")
swap_client.log.error(traceback.format_exc())
if swap_client._show_notifications:
args_dict['notifications'] = swap_client.getNotifications()
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
@@ -222,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
@@ -265,75 +297,108 @@ class HttpHandler(BaseHTTPRequestHandler):
summary = swap_client.getSummary()
result = None
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_id = int(get_data_entry(form_data, 'coin_type'))
if coin_id in (-2, -3, -4):
coin_type = Coins(Coins.XMR)
else:
coin_type = Coins(coin_id)
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"
try:
cmd = get_data_entry(form_data, 'cmd')
cmd = get_data_entry(form_data, "cmd")
except Exception:
raise ValueError('Invalid command')
if coin_type == Coins.XMR:
raise ValueError("Invalid command")
if coin_type in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_type)
arr = cmd.split(None, 1)
method = arr[0]
params = json.loads(arr[1]) if len(arr) > 1 else []
if coin_id == -4:
rv = ci.rpc_wallet_cb(method, params)
elif coin_id == -3:
rv = ci.rpc_cb(method, params)
elif coin_id == -2:
if coin_variant == 2:
rv = ci.rpc_wallet(method, params)
elif coin_variant == 0:
rv = ci.rpc(method, params)
elif coin_variant == 1:
if params == []:
params = None
rv = ci.rpc_cb2(method, params)
rv = ci.rpc2(method, params)
else:
raise ValueError('Unknown XMR 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)
result = cmd + '\n' + swap_client.ci(coin_type).rpc_callback(method, params)
if coin_variant == 1:
rv = ci.rpc_wallet(method, params)
elif coin_variant == 2:
rv = ci.rpc_wallet_mweb(method, params)
else:
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
else:
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
result = cmd + "\n" + swap_client.callcoincli(coin_type, cmd)
except Exception as ex:
result = 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")
coins = listAvailableCoins(swap_client, with_variants=False)
coins = [c for c in coins if c[0] != Coins.XMR]
coins.append((-2, 'Monero'))
coins.append((-3, 'Monero JSON'))
coins.append((-4, 'Monero Wallet'))
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)
]
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'coins': coins,
'coin_type': coin_id,
'call_type': call_type,
'result': result,
'messages': messages,
'summary': summary,
})
if any(c[0] == Coins.DCR for c in coin_available):
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"))
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"))
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"))
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
@@ -341,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
@@ -354,62 +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()
template = env.get_template('index.html')
return self.render_template(template, {
'refresh': 30,
'summary': summary,
'use_tor_proxy': swap_client.use_tor_proxy
})
self.send_response(302)
self.send_header("Location", "/offers")
self.end_headers()
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:
@@ -417,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)
@@ -462,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:
@@ -533,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()
@@ -560,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
@@ -575,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

@@ -1,13 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from enum import IntEnum
class Curves(IntEnum):
secp256k1 = 1
ed25519 = 2

233
basicswap/interface/base.py Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python
# -*- 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.
import threading
from enum import IntEnum
from basicswap.chainparams import (
chainparams,
)
from basicswap.util import (
ensure,
i2b,
b2i,
make_int,
format_amount,
TemporaryError,
)
from basicswap.util.crypto import (
hash160,
)
from basicswap.util.ecc import (
ep,
getSecretInt,
)
from coincurve.dleag import verify_secp256k1_point
from coincurve.keys import (
PublicKey,
)
class Curves(IntEnum):
secp256k1 = 1
ed25519 = 2
class CoinInterface:
@staticmethod
def watch_blocks_for_scripts() -> bool:
return False
@staticmethod
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
self.setDefaults()
self._network = network
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
def make_int(self, amount_in: int, r: int = 0) -> int:
return make_int(amount_in, self.exp(), r=r)
def format_amount(self, amount_in, conv_int=False, r=0):
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
return format_amount(amount_int, self.exp())
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()
def ticker(self) -> str:
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"]
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]["name"]
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]["ticker"]
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]["min_amount"]
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]["max_amount"]
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
def setWalletRestoreHeight(self, value: int) -> None:
self._restore_height = value
def knownWalletSeed(self) -> bool:
return not self._unknown_wallet_seed
def chainparams(self):
return chainparams[self.coin_type()]
def chainparams_network(self):
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get("has_segwit", True)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return False
def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if "not enough unlocked money" in str_error:
return True
if "no unlocked balance" in str_error:
return True
if "transaction was rejected by daemon" in str_error:
return True
if "invalid unlocked_balance" in str_error:
return True
if "daemon is busy" in str_error:
return True
if "timed out" in str_error:
return True
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"
)
self._conf_target = new_conf_target
def walletRestoreHeight(self) -> int:
return self._restore_height
def get_connection_type(self):
return self._connection_type
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
def use_tx_vsize(self) -> bool:
return self._use_segwit
def getLockTxSwapOutputValue(self, bid, xmr_swap) -> int:
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap) -> int:
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap) -> int:
# Only one prevout exists
return 0
def checkWallets(self) -> int:
return 1
def altruistic(self) -> bool:
return self._altruistic
class AdaptorSigInterface:
def getScriptLockTxDummyWitness(self, script: bytes):
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))]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [bytes(72), b"", bytes(len(script))]
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
@staticmethod
def curve_type():
return Curves.secp256k1
def getNewRandomKey(self) -> bytes:
return i2b(getSecretInt())
def getPubkey(self, privkey: bytes) -> bytes:
return PublicKey.from_secret(privkey).format()
def pkh(self, pubkey: bytes) -> bytes:
return hash160(pubkey)
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return i < ep.o and i > 0
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)
def isValidAddressHash(self, address_hash: bytes) -> bool:
hash_len = len(address_hash)
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()

1136
basicswap/interface/bch.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,247 @@
import unittest
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def polymod(values):
chk = 1
generator = [
(0x01, 0x98F2BC8E61),
(0x02, 0x79B76D99E2),
(0x04, 0xF33E5FB3C4),
(0x08, 0xAE2EABE2A8),
(0x10, 0x1E4F43E470),
]
for value in values:
top = chk >> 35
chk = ((chk & 0x07FFFFFFFF) << 5) ^ value
for i in generator:
if top & i[0] != 0:
chk ^= i[1]
return chk ^ 1
def calculate_checksum(prefix, payload):
poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
out = list()
for i in range(8):
out.append((poly >> 5 * (7 - i)) & 0x1F)
return out
def verify_checksum(prefix, payload):
return polymod(prefix_expand(prefix) + payload) == 0
def b32decode(inputs):
out = list()
for letter in inputs:
out.append(CHARSET.find(letter))
return out
def b32encode(inputs):
out = ""
for char_code in inputs:
out += CHARSET[char_code]
return out
def convertbits(data, frombits, tobits, pad=True):
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def prefix_expand(prefix):
return [ord(x) & 0x1F for x in prefix] + [0]
class Address:
"""
Class to handle CashAddr.
:param version: Version of CashAddr
:type version: ``str``
:param payload: Payload of CashAddr as int list of the bytearray
:type payload: ``list`` of ``int``
"""
VERSIONS = {
"P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"},
"P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"},
"P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"},
"P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"},
"P2SH32-TESTNET": {
"prefix": "bchtest",
"version_bit": 11,
"network": "testnet",
},
"P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"},
"P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"},
"P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"},
"P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"},
"P2SH20-CATKN": {
"prefix": "bitcoincash",
"version_bit": 24,
"network": "mainnet",
},
"P2SH32-CATKN": {
"prefix": "bitcoincash",
"version_bit": 27,
"network": "mainnet",
},
"P2PKH-CATKN": {
"prefix": "bitcoincash",
"version_bit": 16,
"network": "mainnet",
},
"P2SH20-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 24,
"network": "testnet",
},
"P2SH32-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 27,
"network": "testnet",
},
"P2PKH-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 16,
"network": "testnet",
},
"P2SH20-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 24,
"network": "regtest",
},
"P2SH32-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 27,
"network": "regtest",
},
"P2PKH-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 16,
"network": "regtest",
},
}
VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"}
ADDRESS_TYPES = {
0: "P2PKH",
8: "P2SH20",
11: "P2SH32",
16: "P2PKH-CATKN",
24: "P2SH20-CATKN",
27: "P2SH32-CATKN",
}
def __init__(self, version, payload):
if version not in Address.VERSIONS:
raise ValueError("Invalid address version provided")
self.version = version
self.payload = payload
self.prefix = Address.VERSIONS[self.version]["prefix"]
def __str__(self):
return (
f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}"
)
def __repr__(self):
return f"Address('{self.cash_address()}')"
def __eq__(self, other):
if isinstance(other, str):
return self.cash_address() == other
elif isinstance(other, Address):
return self.cash_address() == other.cash_address()
else:
raise ValueError(
"Address can be compared to a string address"
" or an instance of Address"
)
def cash_address(self):
"""
Generate CashAddr of the Address
:rtype: ``str``
"""
version_bit = Address.VERSIONS[self.version]["version_bit"]
payload = [version_bit] + list(self.payload)
payload = convertbits(payload, 8, 5)
checksum = calculate_checksum(self.prefix, payload)
return self.prefix + ":" + b32encode(payload + checksum)
@staticmethod
def from_string(address):
"""
Generate Address from a cashadress string
:param scriptcode: The cashaddress string
:type scriptcode: ``str``
:returns: Instance of :class:~bitcash.cashaddress.Address
"""
try:
address = str(address)
except Exception:
raise ValueError("Expected string as input")
if address.upper() != address and address.lower() != address:
raise ValueError(
"Cash address contains uppercase and lowercase characters: " + address
)
address = address.lower()
colon_count = address.count(":")
if colon_count == 0:
raise ValueError("Cash address is missing prefix")
if colon_count > 1:
raise ValueError("Cash address contains more than one colon character")
prefix, base32string = address.split(":")
decoded = b32decode(base32string)
if not verify_checksum(prefix, decoded):
raise ValueError(
"Bad cash address checksum for address {}".format(address)
)
converted = convertbits(decoded, 5, 8)
try:
version = Address.ADDRESS_TYPES[converted[0]]
except Exception:
raise ValueError("Could not determine address version")
version += Address.VERSION_SUFFIXES[prefix]
payload = converted[1:-6]
return Address(version, payload)
class TestFrameworkScript(unittest.TestCase):
def test_base58encodedecode(self):
def check_cashaddress(address: str):
self.assertEqual(Address.from_string(address).cash_address(), address)
check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6")

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from basicswap.contrib.test_framework.script import CScriptOp
OP_TXINPUTCOUNT = CScriptOp(0xc3)
OP_1 = CScriptOp(0x51)
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
OP_TXOUTPUTCOUNT = CScriptOp(0xc4)
OP_0 = CScriptOp(0x00)
OP_UTXOVALUE = CScriptOp(0xc6)
OP_OUTPUTVALUE = CScriptOp(0xcc)
OP_SUB = CScriptOp(0x94)
OP_UTXOTOKENCATEGORY = CScriptOp(0xce)
OP_OUTPUTTOKENCATEGORY = CScriptOp(0xd1)
OP_EQUALVERIFY = CScriptOp(0x88)
OP_UTXOTOKENCOMMITMENT = CScriptOp(0xcf)
OP_OUTPUTTOKENCOMMITMENT = CScriptOp(0xd2)
OP_UTXOTOKENAMOUNT = CScriptOp(0xd0)
OP_OUTPUTTOKENAMOUNT = CScriptOp(0xd3)
OP_INPUTSEQUENCENUMBER = CScriptOp(0xcb)
OP_NOTIF = CScriptOp(0x64)
OP_OUTPUTBYTECODE = CScriptOp(0xcd)
OP_OVER = CScriptOp(0x78)
OP_CHECKDATASIG = CScriptOp(0xba)
OP_CHECKDATASIGVERIFY = CScriptOp(0xbb)
OP_ELSE = CScriptOp(0x67)
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
OP_DROP = CScriptOp(0x75)
OP_EQUAL = CScriptOp(0x87)
OP_ENDIF = CScriptOp(0x68)
OP_HASH256 = CScriptOp(0xaa)
OP_PUSHBYTES_32 = CScriptOp(0x20)
OP_DUP = CScriptOp(0x76)
OP_HASH160 = CScriptOp(0xa9)
OP_CHECKSIG = CScriptOp(0xac)
OP_SHA256 = CScriptOp(0xa8)
OP_VERIFY = CScriptOp(0x69)

View File

@@ -0,0 +1,191 @@
"""
Copyright (c) 2011 Jeff Garzik
AuthServiceProxy has the following improvements over python-jsonrpc's
ServiceProxy class:
- HTTP connections persist for the life of the AuthServiceProxy object
(if server supports HTTP/1.1)
- sends protocol 'version', per JSON-RPC 1.1
- sends proper, incrementing 'id'
- sends Basic HTTP authentication headers
- parses all JSON numbers that look like floats as Decimal
- uses standard Python json lib
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
Copyright (c) 2007 Jan-Klaas Kollhof
This file is part of jsonrpc.
jsonrpc is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this software; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
try:
import http.client as httplib
except ImportError:
import httplib
import base64
import decimal
import json
import logging
import socket
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
USER_AGENT = "AuthServiceProxy/0.1"
HTTP_TIMEOUT = 30
log = logging.getLogger("BitcoinRPC")
class JSONRPCException(Exception):
def __init__(self, rpc_error):
try:
errmsg = '%(message)s (%(code)i)' % rpc_error
except (KeyError, TypeError):
errmsg = ''
Exception.__init__(self, errmsg)
self.error = rpc_error
def EncodeDecimal(o):
if isinstance(o, decimal.Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
class AuthServiceProxy(object):
__id_count = 0
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
self.__service_url = service_url
self._service_name = service_name
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
self.__url = urlparse.urlparse(service_url)
if self.__url.port is None:
port = 80
else:
port = self.__url.port
(user, passwd) = (self.__url.username, self.__url.password)
try:
user = user.encode('utf8')
except AttributeError:
pass
try:
passwd = passwd.encode('utf8')
except AttributeError:
pass
authpair = user + b':' + passwd
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
if connection:
# Callables re-use the connection of the original proxy
self.__conn = connection
elif self.__url.scheme == 'https':
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
timeout=timeout)
else:
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
timeout=timeout)
def __getattr__(self, name):
if name.startswith('__') and name.endswith('__'):
# Python internal stuff
raise AttributeError
if self._service_name is not None:
name = "%s.%s" % (self._service_name, name)
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
def _request(self, method, path, postdata):
'''
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
'''
headers = {'Host': self.__url.hostname,
'User-Agent': USER_AGENT,
'Authorization': self.__auth_header,
'Content-type': 'application/json'}
try:
self.__conn.request(method, path, postdata, headers)
return self._get_response()
except httplib.BadStatusLine as e:
if e.line == "''": # if connection was closed, try again
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
else:
raise
except (BrokenPipeError,ConnectionResetError):
# Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
# ConnectionResetError happens on FreeBSD with Python 3.4
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
def __call__(self, *args, **argsn):
AuthServiceProxy.__id_count += 1
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
if args and argsn:
raise ValueError('Cannot handle both named and positional arguments')
postdata = json.dumps({'version': '1.1',
'method': self._service_name,
'params': args or argsn,
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if response['error'] is not None:
raise JSONRPCException(response['error'])
elif 'result' not in response:
raise JSONRPCException({
'code': -343, 'message': 'missing JSON-RPC result'})
else:
return response['result']
def _batch(self, rpc_call_list):
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
log.debug("--> "+postdata)
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
def _get_response(self):
try:
http_response = self.__conn.getresponse()
except socket.timeout as e:
raise JSONRPCException({
'code': -344,
'message': '%r RPC took longer than %f seconds. Consider '
'using larger timeout for calls that take '
'longer to return.' % (self._service_name,
self.__conn.timeout)})
if http_response is None:
raise JSONRPCException({
'code': -342, 'message': 'missing HTTP response from server'})
content_type = http_response.getheader('Content-Type')
if content_type != 'application/json':
raise JSONRPCException({
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
responsedata = http_response.read().decode('utf8')
response = json.loads(responsedata, parse_float=decimal.Decimal)
if "error" in response and response["error"] is None:
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
else:
log.debug("<-- "+responsedata)
return response

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
#
# bignum.py
#
# This file is copied from python-bitcoinlib.
#
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
"""Bignum routines"""
import struct
# generic big endian MPI format
def bn_bytes(v, have_ext=False):
ext = 0
if have_ext:
ext = 1
return ((v.bit_length()+7)//8) + ext
def bn2bin(v):
s = bytearray()
i = bn_bytes(v)
while i > 0:
s.append((v >> ((i-1) * 8)) & 0xff)
i -= 1
return s
def bin2bn(s):
l = 0
for ch in s:
l = (l << 8) | ch
return l
def bn2mpi(v):
have_ext = False
if v.bit_length() > 0:
have_ext = (v.bit_length() & 0x07) == 0
neg = False
if v < 0:
neg = True
v = -v
s = struct.pack(b">I", bn_bytes(v, have_ext))
ext = bytearray()
if have_ext:
ext.append(0)
v_bin = bn2bin(v)
if neg:
if have_ext:
ext[0] |= 0x80
else:
v_bin[0] |= 0x80
return s + ext + v_bin
def mpi2bn(s):
if len(s) < 4:
return None
s_size = bytes(s[:4])
v_len = struct.unpack(b">I", s_size)[0]
if len(s) != (v_len + 4):
return None
if v_len == 0:
return 0
v_str = bytearray(s[4:])
neg = False
i = v_str[0]
if i & 0x80:
neg = True
i &= ~0x80
v_str[0] = i
v = bin2bn(v_str)
if neg:
return -v
return v
# bitcoin-specific little endian format, with implicit size
def mpi2vch(s):
r = s[4:] # strip size
r = r[::-1] # reverse string, converting BE->LE
return r
def bn2vch(v):
return bytes(mpi2vch(bn2mpi(v)))
def vch2mpi(s):
r = struct.pack(b">I", len(s)) # size
r += s[::-1] # reverse string, converting LE->BE
return r
def vch2bn(s):
return mpi2bn(vch2mpi(s))

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
This module contains utilities for doing coverage analysis on the RPC
interface.
It provides a way to track which RPC commands are exercised during
testing.
"""
import os
REFERENCE_FILENAME = 'rpc_interface.txt'
class AuthServiceProxyWrapper(object):
"""
An object that wraps AuthServiceProxy to record specific RPC calls.
"""
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
"""
Kwargs:
auth_service_proxy_instance (AuthServiceProxy): the instance
being wrapped.
coverage_logfile (str): if specified, write each service_name
out to a file when called.
"""
self.auth_service_proxy_instance = auth_service_proxy_instance
self.coverage_logfile = coverage_logfile
def __getattr__(self, *args, **kwargs):
return_val = self.auth_service_proxy_instance.__getattr__(
*args, **kwargs)
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
def __call__(self, *args, **kwargs):
"""
Delegates to AuthServiceProxy, then writes the particular RPC method
called to a file.
"""
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
rpc_method = self.auth_service_proxy_instance._service_name
if self.coverage_logfile:
with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
f.write("%s\n" % rpc_method)
return return_val
@property
def url(self):
return self.auth_service_proxy_instance.url
def get_filename(dirname, n_node):
"""
Get a filename unique to the test process ID and node.
This file will contain a list of RPC commands covered.
"""
pid = str(os.getpid())
return os.path.join(
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
def write_all_rpc_commands(dirname, node):
"""
Write out a list of all RPC functions available in `bitcoin-cli` for
coverage comparison. This will only happen once per coverage
directory.
Args:
dirname (str): temporary test dir
node (AuthServiceProxy): client
Returns:
bool. if the RPC interface file was written.
"""
filename = os.path.join(dirname, REFERENCE_FILENAME)
if os.path.isfile(filename):
return False
help_output = node.help().split('\n')
commands = set()
for line in help_output:
line = line.strip()
# Ignore blanks and headers
if line and not line.startswith('='):
commands.add("%s\n" % line.split()[0])
with open(filename, 'w', encoding='utf8') as f:
f.writelines(list(commands))
return True

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,943 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# script.py
#
# This file is modified from python-bitcoinlib.
#
"""Scripts
Functionality to build scripts, as well as SignatureHash().
"""
from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string
from binascii import hexlify
import hashlib
import sys
bchr = chr
bord = ord
if sys.version > '3':
long = int
bchr = lambda x: bytes([x])
bord = lambda x: x
import struct
from .bignum import bn2vch
MAX_SCRIPT_SIZE = 10000
MAX_SCRIPT_ELEMENT_SIZE = 520
MAX_SCRIPT_OPCODES = 201
OPCODE_NAMES = {}
_opcode_instances = []
class CScriptOp(int):
"""A single script opcode"""
__slots__ = []
@staticmethod
def encode_op_pushdata(d):
"""Encode a PUSHDATA op, returning bytes"""
if len(d) < 0x4c:
return b'' + bchr(len(d)) + d # OP_PUSHDATA
elif len(d) <= 0xff:
return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1
elif len(d) <= 0xffff:
return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
elif len(d) <= 0xffffffff:
return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
else:
raise ValueError("Data too long to encode in a PUSHDATA op")
@staticmethod
def encode_op_n(n):
"""Encode a small integer op, returning an opcode"""
if not (0 <= n <= 16):
raise ValueError('Integer must be in range 0 <= n <= 16, got %d' % n)
if n == 0:
return OP_0
else:
return CScriptOp(OP_1 + n-1)
def decode_op_n(self):
"""Decode a small integer opcode, returning an integer"""
if self == OP_0:
return 0
if not (self == OP_0 or OP_1 <= self <= OP_16):
raise ValueError('op %r is not an OP_N' % self)
return int(self - OP_1+1)
def is_small_int(self):
"""Return true if the op pushes a small integer to the stack"""
if 0x51 <= self <= 0x60 or self == 0:
return True
else:
return False
def __str__(self):
return repr(self)
def __repr__(self):
if self in OPCODE_NAMES:
return OPCODE_NAMES[self]
else:
return 'CScriptOp(0x%x)' % self
def __new__(cls, n):
try:
return _opcode_instances[n]
except IndexError:
assert len(_opcode_instances) == n
_opcode_instances.append(super(CScriptOp, cls).__new__(cls, n))
return _opcode_instances[n]
# Populate opcode instance table
for n in range(0xff+1):
CScriptOp(n)
# push value
OP_0 = CScriptOp(0x00)
OP_FALSE = OP_0
OP_PUSHDATA1 = CScriptOp(0x4c)
OP_PUSHDATA2 = CScriptOp(0x4d)
OP_PUSHDATA4 = CScriptOp(0x4e)
OP_1NEGATE = CScriptOp(0x4f)
OP_RESERVED = CScriptOp(0x50)
OP_1 = CScriptOp(0x51)
OP_TRUE=OP_1
OP_2 = CScriptOp(0x52)
OP_3 = CScriptOp(0x53)
OP_4 = CScriptOp(0x54)
OP_5 = CScriptOp(0x55)
OP_6 = CScriptOp(0x56)
OP_7 = CScriptOp(0x57)
OP_8 = CScriptOp(0x58)
OP_9 = CScriptOp(0x59)
OP_10 = CScriptOp(0x5a)
OP_11 = CScriptOp(0x5b)
OP_12 = CScriptOp(0x5c)
OP_13 = CScriptOp(0x5d)
OP_14 = CScriptOp(0x5e)
OP_15 = CScriptOp(0x5f)
OP_16 = CScriptOp(0x60)
# control
OP_NOP = CScriptOp(0x61)
OP_VER = CScriptOp(0x62)
OP_IF = CScriptOp(0x63)
OP_NOTIF = CScriptOp(0x64)
OP_VERIF = CScriptOp(0x65)
OP_VERNOTIF = CScriptOp(0x66)
OP_ELSE = CScriptOp(0x67)
OP_ENDIF = CScriptOp(0x68)
OP_VERIFY = CScriptOp(0x69)
OP_RETURN = CScriptOp(0x6a)
# stack ops
OP_TOALTSTACK = CScriptOp(0x6b)
OP_FROMALTSTACK = CScriptOp(0x6c)
OP_2DROP = CScriptOp(0x6d)
OP_2DUP = CScriptOp(0x6e)
OP_3DUP = CScriptOp(0x6f)
OP_2OVER = CScriptOp(0x70)
OP_2ROT = CScriptOp(0x71)
OP_2SWAP = CScriptOp(0x72)
OP_IFDUP = CScriptOp(0x73)
OP_DEPTH = CScriptOp(0x74)
OP_DROP = CScriptOp(0x75)
OP_DUP = CScriptOp(0x76)
OP_NIP = CScriptOp(0x77)
OP_OVER = CScriptOp(0x78)
OP_PICK = CScriptOp(0x79)
OP_ROLL = CScriptOp(0x7a)
OP_ROT = CScriptOp(0x7b)
OP_SWAP = CScriptOp(0x7c)
OP_TUCK = CScriptOp(0x7d)
# splice ops
OP_CAT = CScriptOp(0x7e)
OP_SUBSTR = CScriptOp(0x7f)
OP_LEFT = CScriptOp(0x80)
OP_RIGHT = CScriptOp(0x81)
OP_SIZE = CScriptOp(0x82)
# bit logic
OP_INVERT = CScriptOp(0x83)
OP_AND = CScriptOp(0x84)
OP_OR = CScriptOp(0x85)
OP_XOR = CScriptOp(0x86)
OP_EQUAL = CScriptOp(0x87)
OP_EQUALVERIFY = CScriptOp(0x88)
OP_RESERVED1 = CScriptOp(0x89)
OP_RESERVED2 = CScriptOp(0x8a)
# numeric
OP_1ADD = CScriptOp(0x8b)
OP_1SUB = CScriptOp(0x8c)
OP_2MUL = CScriptOp(0x8d)
OP_2DIV = CScriptOp(0x8e)
OP_NEGATE = CScriptOp(0x8f)
OP_ABS = CScriptOp(0x90)
OP_NOT = CScriptOp(0x91)
OP_0NOTEQUAL = CScriptOp(0x92)
OP_ADD = CScriptOp(0x93)
OP_SUB = CScriptOp(0x94)
OP_MUL = CScriptOp(0x95)
OP_DIV = CScriptOp(0x96)
OP_MOD = CScriptOp(0x97)
OP_LSHIFT = CScriptOp(0x98)
OP_RSHIFT = CScriptOp(0x99)
OP_BOOLAND = CScriptOp(0x9a)
OP_BOOLOR = CScriptOp(0x9b)
OP_NUMEQUAL = CScriptOp(0x9c)
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
OP_NUMNOTEQUAL = CScriptOp(0x9e)
OP_LESSTHAN = CScriptOp(0x9f)
OP_GREATERTHAN = CScriptOp(0xa0)
OP_LESSTHANOREQUAL = CScriptOp(0xa1)
OP_GREATERTHANOREQUAL = CScriptOp(0xa2)
OP_MIN = CScriptOp(0xa3)
OP_MAX = CScriptOp(0xa4)
OP_WITHIN = CScriptOp(0xa5)
# crypto
OP_RIPEMD160 = CScriptOp(0xa6)
OP_SHA1 = CScriptOp(0xa7)
OP_SHA256 = CScriptOp(0xa8)
OP_HASH160 = CScriptOp(0xa9)
OP_HASH256 = CScriptOp(0xaa)
OP_CODESEPARATOR = CScriptOp(0xab)
OP_CHECKSIG = CScriptOp(0xac)
OP_CHECKSIGVERIFY = CScriptOp(0xad)
OP_CHECKMULTISIG = CScriptOp(0xae)
OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
# expansion
OP_NOP1 = CScriptOp(0xb0)
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
OP_NOP4 = CScriptOp(0xb3)
OP_NOP5 = CScriptOp(0xb4)
OP_NOP6 = CScriptOp(0xb5)
OP_NOP7 = CScriptOp(0xb6)
OP_NOP8 = CScriptOp(0xb7)
OP_NOP9 = CScriptOp(0xb8)
OP_NOP10 = CScriptOp(0xb9)
# template matching params
OP_SMALLINTEGER = CScriptOp(0xfa)
OP_PUBKEYS = CScriptOp(0xfb)
OP_PUBKEYHASH = CScriptOp(0xfd)
OP_PUBKEY = CScriptOp(0xfe)
OP_INVALIDOPCODE = CScriptOp(0xff)
VALID_OPCODES = {
OP_1NEGATE,
OP_RESERVED,
OP_1,
OP_2,
OP_3,
OP_4,
OP_5,
OP_6,
OP_7,
OP_8,
OP_9,
OP_10,
OP_11,
OP_12,
OP_13,
OP_14,
OP_15,
OP_16,
OP_NOP,
OP_VER,
OP_IF,
OP_NOTIF,
OP_VERIF,
OP_VERNOTIF,
OP_ELSE,
OP_ENDIF,
OP_VERIFY,
OP_RETURN,
OP_TOALTSTACK,
OP_FROMALTSTACK,
OP_2DROP,
OP_2DUP,
OP_3DUP,
OP_2OVER,
OP_2ROT,
OP_2SWAP,
OP_IFDUP,
OP_DEPTH,
OP_DROP,
OP_DUP,
OP_NIP,
OP_OVER,
OP_PICK,
OP_ROLL,
OP_ROT,
OP_SWAP,
OP_TUCK,
OP_CAT,
OP_SUBSTR,
OP_LEFT,
OP_RIGHT,
OP_SIZE,
OP_INVERT,
OP_AND,
OP_OR,
OP_XOR,
OP_EQUAL,
OP_EQUALVERIFY,
OP_RESERVED1,
OP_RESERVED2,
OP_1ADD,
OP_1SUB,
OP_2MUL,
OP_2DIV,
OP_NEGATE,
OP_ABS,
OP_NOT,
OP_0NOTEQUAL,
OP_ADD,
OP_SUB,
OP_MUL,
OP_DIV,
OP_MOD,
OP_LSHIFT,
OP_RSHIFT,
OP_BOOLAND,
OP_BOOLOR,
OP_NUMEQUAL,
OP_NUMEQUALVERIFY,
OP_NUMNOTEQUAL,
OP_LESSTHAN,
OP_GREATERTHAN,
OP_LESSTHANOREQUAL,
OP_GREATERTHANOREQUAL,
OP_MIN,
OP_MAX,
OP_WITHIN,
OP_RIPEMD160,
OP_SHA1,
OP_SHA256,
OP_HASH160,
OP_HASH256,
OP_CODESEPARATOR,
OP_CHECKSIG,
OP_CHECKSIGVERIFY,
OP_CHECKMULTISIG,
OP_CHECKMULTISIGVERIFY,
OP_NOP1,
OP_CHECKLOCKTIMEVERIFY,
OP_CHECKSEQUENCEVERIFY,
OP_NOP4,
OP_NOP5,
OP_NOP6,
OP_NOP7,
OP_NOP8,
OP_NOP9,
OP_NOP10,
OP_SMALLINTEGER,
OP_PUBKEYS,
OP_PUBKEYHASH,
OP_PUBKEY,
}
OPCODE_NAMES.update({
OP_0 : 'OP_0',
OP_PUSHDATA1 : 'OP_PUSHDATA1',
OP_PUSHDATA2 : 'OP_PUSHDATA2',
OP_PUSHDATA4 : 'OP_PUSHDATA4',
OP_1NEGATE : 'OP_1NEGATE',
OP_RESERVED : 'OP_RESERVED',
OP_1 : 'OP_1',
OP_2 : 'OP_2',
OP_3 : 'OP_3',
OP_4 : 'OP_4',
OP_5 : 'OP_5',
OP_6 : 'OP_6',
OP_7 : 'OP_7',
OP_8 : 'OP_8',
OP_9 : 'OP_9',
OP_10 : 'OP_10',
OP_11 : 'OP_11',
OP_12 : 'OP_12',
OP_13 : 'OP_13',
OP_14 : 'OP_14',
OP_15 : 'OP_15',
OP_16 : 'OP_16',
OP_NOP : 'OP_NOP',
OP_VER : 'OP_VER',
OP_IF : 'OP_IF',
OP_NOTIF : 'OP_NOTIF',
OP_VERIF : 'OP_VERIF',
OP_VERNOTIF : 'OP_VERNOTIF',
OP_ELSE : 'OP_ELSE',
OP_ENDIF : 'OP_ENDIF',
OP_VERIFY : 'OP_VERIFY',
OP_RETURN : 'OP_RETURN',
OP_TOALTSTACK : 'OP_TOALTSTACK',
OP_FROMALTSTACK : 'OP_FROMALTSTACK',
OP_2DROP : 'OP_2DROP',
OP_2DUP : 'OP_2DUP',
OP_3DUP : 'OP_3DUP',
OP_2OVER : 'OP_2OVER',
OP_2ROT : 'OP_2ROT',
OP_2SWAP : 'OP_2SWAP',
OP_IFDUP : 'OP_IFDUP',
OP_DEPTH : 'OP_DEPTH',
OP_DROP : 'OP_DROP',
OP_DUP : 'OP_DUP',
OP_NIP : 'OP_NIP',
OP_OVER : 'OP_OVER',
OP_PICK : 'OP_PICK',
OP_ROLL : 'OP_ROLL',
OP_ROT : 'OP_ROT',
OP_SWAP : 'OP_SWAP',
OP_TUCK : 'OP_TUCK',
OP_CAT : 'OP_CAT',
OP_SUBSTR : 'OP_SUBSTR',
OP_LEFT : 'OP_LEFT',
OP_RIGHT : 'OP_RIGHT',
OP_SIZE : 'OP_SIZE',
OP_INVERT : 'OP_INVERT',
OP_AND : 'OP_AND',
OP_OR : 'OP_OR',
OP_XOR : 'OP_XOR',
OP_EQUAL : 'OP_EQUAL',
OP_EQUALVERIFY : 'OP_EQUALVERIFY',
OP_RESERVED1 : 'OP_RESERVED1',
OP_RESERVED2 : 'OP_RESERVED2',
OP_1ADD : 'OP_1ADD',
OP_1SUB : 'OP_1SUB',
OP_2MUL : 'OP_2MUL',
OP_2DIV : 'OP_2DIV',
OP_NEGATE : 'OP_NEGATE',
OP_ABS : 'OP_ABS',
OP_NOT : 'OP_NOT',
OP_0NOTEQUAL : 'OP_0NOTEQUAL',
OP_ADD : 'OP_ADD',
OP_SUB : 'OP_SUB',
OP_MUL : 'OP_MUL',
OP_DIV : 'OP_DIV',
OP_MOD : 'OP_MOD',
OP_LSHIFT : 'OP_LSHIFT',
OP_RSHIFT : 'OP_RSHIFT',
OP_BOOLAND : 'OP_BOOLAND',
OP_BOOLOR : 'OP_BOOLOR',
OP_NUMEQUAL : 'OP_NUMEQUAL',
OP_NUMEQUALVERIFY : 'OP_NUMEQUALVERIFY',
OP_NUMNOTEQUAL : 'OP_NUMNOTEQUAL',
OP_LESSTHAN : 'OP_LESSTHAN',
OP_GREATERTHAN : 'OP_GREATERTHAN',
OP_LESSTHANOREQUAL : 'OP_LESSTHANOREQUAL',
OP_GREATERTHANOREQUAL : 'OP_GREATERTHANOREQUAL',
OP_MIN : 'OP_MIN',
OP_MAX : 'OP_MAX',
OP_WITHIN : 'OP_WITHIN',
OP_RIPEMD160 : 'OP_RIPEMD160',
OP_SHA1 : 'OP_SHA1',
OP_SHA256 : 'OP_SHA256',
OP_HASH160 : 'OP_HASH160',
OP_HASH256 : 'OP_HASH256',
OP_CODESEPARATOR : 'OP_CODESEPARATOR',
OP_CHECKSIG : 'OP_CHECKSIG',
OP_CHECKSIGVERIFY : 'OP_CHECKSIGVERIFY',
OP_CHECKMULTISIG : 'OP_CHECKMULTISIG',
OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY',
OP_NOP1 : 'OP_NOP1',
OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY',
OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY',
OP_NOP4 : 'OP_NOP4',
OP_NOP5 : 'OP_NOP5',
OP_NOP6 : 'OP_NOP6',
OP_NOP7 : 'OP_NOP7',
OP_NOP8 : 'OP_NOP8',
OP_NOP9 : 'OP_NOP9',
OP_NOP10 : 'OP_NOP10',
OP_SMALLINTEGER : 'OP_SMALLINTEGER',
OP_PUBKEYS : 'OP_PUBKEYS',
OP_PUBKEYHASH : 'OP_PUBKEYHASH',
OP_PUBKEY : 'OP_PUBKEY',
OP_INVALIDOPCODE : 'OP_INVALIDOPCODE',
})
OPCODES_BY_NAME = {
'OP_0' : OP_0,
'OP_PUSHDATA1' : OP_PUSHDATA1,
'OP_PUSHDATA2' : OP_PUSHDATA2,
'OP_PUSHDATA4' : OP_PUSHDATA4,
'OP_1NEGATE' : OP_1NEGATE,
'OP_RESERVED' : OP_RESERVED,
'OP_1' : OP_1,
'OP_2' : OP_2,
'OP_3' : OP_3,
'OP_4' : OP_4,
'OP_5' : OP_5,
'OP_6' : OP_6,
'OP_7' : OP_7,
'OP_8' : OP_8,
'OP_9' : OP_9,
'OP_10' : OP_10,
'OP_11' : OP_11,
'OP_12' : OP_12,
'OP_13' : OP_13,
'OP_14' : OP_14,
'OP_15' : OP_15,
'OP_16' : OP_16,
'OP_NOP' : OP_NOP,
'OP_VER' : OP_VER,
'OP_IF' : OP_IF,
'OP_NOTIF' : OP_NOTIF,
'OP_VERIF' : OP_VERIF,
'OP_VERNOTIF' : OP_VERNOTIF,
'OP_ELSE' : OP_ELSE,
'OP_ENDIF' : OP_ENDIF,
'OP_VERIFY' : OP_VERIFY,
'OP_RETURN' : OP_RETURN,
'OP_TOALTSTACK' : OP_TOALTSTACK,
'OP_FROMALTSTACK' : OP_FROMALTSTACK,
'OP_2DROP' : OP_2DROP,
'OP_2DUP' : OP_2DUP,
'OP_3DUP' : OP_3DUP,
'OP_2OVER' : OP_2OVER,
'OP_2ROT' : OP_2ROT,
'OP_2SWAP' : OP_2SWAP,
'OP_IFDUP' : OP_IFDUP,
'OP_DEPTH' : OP_DEPTH,
'OP_DROP' : OP_DROP,
'OP_DUP' : OP_DUP,
'OP_NIP' : OP_NIP,
'OP_OVER' : OP_OVER,
'OP_PICK' : OP_PICK,
'OP_ROLL' : OP_ROLL,
'OP_ROT' : OP_ROT,
'OP_SWAP' : OP_SWAP,
'OP_TUCK' : OP_TUCK,
'OP_CAT' : OP_CAT,
'OP_SUBSTR' : OP_SUBSTR,
'OP_LEFT' : OP_LEFT,
'OP_RIGHT' : OP_RIGHT,
'OP_SIZE' : OP_SIZE,
'OP_INVERT' : OP_INVERT,
'OP_AND' : OP_AND,
'OP_OR' : OP_OR,
'OP_XOR' : OP_XOR,
'OP_EQUAL' : OP_EQUAL,
'OP_EQUALVERIFY' : OP_EQUALVERIFY,
'OP_RESERVED1' : OP_RESERVED1,
'OP_RESERVED2' : OP_RESERVED2,
'OP_1ADD' : OP_1ADD,
'OP_1SUB' : OP_1SUB,
'OP_2MUL' : OP_2MUL,
'OP_2DIV' : OP_2DIV,
'OP_NEGATE' : OP_NEGATE,
'OP_ABS' : OP_ABS,
'OP_NOT' : OP_NOT,
'OP_0NOTEQUAL' : OP_0NOTEQUAL,
'OP_ADD' : OP_ADD,
'OP_SUB' : OP_SUB,
'OP_MUL' : OP_MUL,
'OP_DIV' : OP_DIV,
'OP_MOD' : OP_MOD,
'OP_LSHIFT' : OP_LSHIFT,
'OP_RSHIFT' : OP_RSHIFT,
'OP_BOOLAND' : OP_BOOLAND,
'OP_BOOLOR' : OP_BOOLOR,
'OP_NUMEQUAL' : OP_NUMEQUAL,
'OP_NUMEQUALVERIFY' : OP_NUMEQUALVERIFY,
'OP_NUMNOTEQUAL' : OP_NUMNOTEQUAL,
'OP_LESSTHAN' : OP_LESSTHAN,
'OP_GREATERTHAN' : OP_GREATERTHAN,
'OP_LESSTHANOREQUAL' : OP_LESSTHANOREQUAL,
'OP_GREATERTHANOREQUAL' : OP_GREATERTHANOREQUAL,
'OP_MIN' : OP_MIN,
'OP_MAX' : OP_MAX,
'OP_WITHIN' : OP_WITHIN,
'OP_RIPEMD160' : OP_RIPEMD160,
'OP_SHA1' : OP_SHA1,
'OP_SHA256' : OP_SHA256,
'OP_HASH160' : OP_HASH160,
'OP_HASH256' : OP_HASH256,
'OP_CODESEPARATOR' : OP_CODESEPARATOR,
'OP_CHECKSIG' : OP_CHECKSIG,
'OP_CHECKSIGVERIFY' : OP_CHECKSIGVERIFY,
'OP_CHECKMULTISIG' : OP_CHECKMULTISIG,
'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY,
'OP_NOP1' : OP_NOP1,
'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY,
'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY,
'OP_NOP4' : OP_NOP4,
'OP_NOP5' : OP_NOP5,
'OP_NOP6' : OP_NOP6,
'OP_NOP7' : OP_NOP7,
'OP_NOP8' : OP_NOP8,
'OP_NOP9' : OP_NOP9,
'OP_NOP10' : OP_NOP10,
'OP_SMALLINTEGER' : OP_SMALLINTEGER,
'OP_PUBKEYS' : OP_PUBKEYS,
'OP_PUBKEYHASH' : OP_PUBKEYHASH,
'OP_PUBKEY' : OP_PUBKEY,
}
class CScriptInvalidError(Exception):
"""Base class for CScript exceptions"""
pass
class CScriptTruncatedPushDataError(CScriptInvalidError):
"""Invalid pushdata due to truncation"""
def __init__(self, msg, data):
self.data = data
super(CScriptTruncatedPushDataError, self).__init__(msg)
# This is used, eg, for blockchain heights in coinbase scripts (bip34)
class CScriptNum(object):
def __init__(self, d=0):
self.value = d
@staticmethod
def encode(obj):
r = bytearray(0)
if obj.value == 0:
return bytes(r)
neg = obj.value < 0
absvalue = -obj.value if neg else obj.value
while (absvalue):
r.append(absvalue & 0xff)
absvalue >>= 8
if r[-1] & 0x80:
r.append(0x80 if neg else 0)
elif neg:
r[-1] |= 0x80
return bytes(bchr(len(r)) + r)
class CScript(bytes):
"""Serialized script
A bytes subclass, so you can use this directly whenever bytes are accepted.
Note that this means that indexing does *not* work - you'll get an index by
byte rather than opcode. This format was chosen for efficiency so that the
general case would not require creating a lot of little CScriptOP objects.
iter(script) however does iterate by opcode.
"""
@classmethod
def __coerce_instance(cls, other):
# Coerce other into bytes
if isinstance(other, CScriptOp):
other = bchr(other)
elif isinstance(other, CScriptNum):
if (other.value == 0):
other = bchr(CScriptOp(OP_0))
else:
other = CScriptNum.encode(other)
elif isinstance(other, int):
if 0 <= other <= 16:
other = bytes(bchr(CScriptOp.encode_op_n(other)))
elif other == -1:
other = bytes(bchr(OP_1NEGATE))
else:
other = CScriptOp.encode_op_pushdata(bn2vch(other))
elif isinstance(other, (bytes, bytearray)):
other = CScriptOp.encode_op_pushdata(other)
return other
def __add__(self, other):
# Do the coercion outside of the try block so that errors in it are
# noticed.
other = self.__coerce_instance(other)
try:
# bytes.__add__ always returns bytes instances unfortunately
return CScript(super(CScript, self).__add__(other))
except TypeError:
raise TypeError('Can not add a %r instance to a CScript' % other.__class__)
def join(self, iterable):
# join makes no sense for a CScript()
raise NotImplementedError
def __new__(cls, value=b''):
if isinstance(value, bytes) or isinstance(value, bytearray):
return super(CScript, cls).__new__(cls, value)
else:
def coerce_iterable(iterable):
for instance in iterable:
yield cls.__coerce_instance(instance)
# Annoyingly on both python2 and python3 bytes.join() always
# returns a bytes instance even when subclassed.
return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value)))
def raw_iter(self):
"""Raw iteration
Yields tuples of (opcode, data, sop_idx) so that the different possible
PUSHDATA encodings can be accurately distinguished, as well as
determining the exact opcode byte indexes. (sop_idx)
"""
i = 0
while i < len(self):
sop_idx = i
opcode = bord(self[i])
i += 1
if opcode > OP_PUSHDATA4:
yield (opcode, None, sop_idx)
else:
datasize = None
pushdata_type = None
if opcode < OP_PUSHDATA1:
pushdata_type = 'PUSHDATA(%d)' % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = 'PUSHDATA1'
if i >= len(self):
raise CScriptInvalidError('PUSHDATA1: missing data length')
datasize = bord(self[i])
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = 'PUSHDATA2'
if i + 1 >= len(self):
raise CScriptInvalidError('PUSHDATA2: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = 'PUSHDATA4'
if i + 3 >= len(self):
raise CScriptInvalidError('PUSHDATA4: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24)
i += 4
else:
assert False # shouldn't happen
data = bytes(self[i:i+datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
i += datasize
yield (opcode, data, sop_idx)
def __iter__(self):
"""'Cooked' iteration
Returns either a CScriptOP instance, an integer, or bytes, as
appropriate.
See raw_iter() if you need to distinguish the different possible
PUSHDATA encodings.
"""
for (opcode, data, sop_idx) in self.raw_iter():
if data is not None:
yield data
else:
opcode = CScriptOp(opcode)
if opcode.is_small_int():
yield opcode.decode_op_n()
else:
yield CScriptOp(opcode)
def __repr__(self):
# For Python3 compatibility add b before strings so testcases don't
# need to change
def _repr(o):
if isinstance(o, bytes):
return b"x('%s')" % hexlify(o).decode('ascii')
else:
return repr(o)
ops = []
i = iter(self)
while True:
op = None
try:
op = _repr(next(i))
except CScriptTruncatedPushDataError as err:
op = '%s...<ERROR: %s>' % (_repr(err.data), err)
break
except CScriptInvalidError as err:
op = '<ERROR: %s>' % err
break
except StopIteration:
break
finally:
if op is not None:
ops.append(op)
return "CScript([%s])" % ', '.join(ops)
def GetSigOpCount(self, fAccurate):
"""Get the SigOp count.
fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details.
Note that this is consensus-critical.
"""
n = 0
lastOpcode = OP_INVALIDOPCODE
for (opcode, data, sop_idx) in self.raw_iter():
if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY):
n += 1
elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY):
if fAccurate and (OP_1 <= lastOpcode <= OP_16):
n += opcode.decode_op_n()
else:
n += 20
lastOpcode = opcode
return n
SIGHASH_ALL = 1
SIGHASH_NONE = 2
SIGHASH_SINGLE = 3
SIGHASH_ANYONECANPAY = 0x80
def FindAndDelete(script, sig):
"""Consensus critical, see FindAndDelete() in Satoshi codebase"""
r = b''
last_sop_idx = sop_idx = 0
skip = True
for (opcode, data, sop_idx) in script.raw_iter():
if not skip:
r += script[last_sop_idx:sop_idx]
last_sop_idx = sop_idx
if script[sop_idx:sop_idx + len(sig)] == sig:
skip = True
else:
skip = False
if not skip:
r += script[last_sop_idx:]
return CScript(r)
def SignatureHash(script, txTo, inIdx, hashtype):
"""Consensus-correct SignatureHash
Returns (hash, err) to precisely match the consensus-critical behavior of
the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity)
"""
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
if inIdx >= len(txTo.vin):
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
txtmp = CTransaction(txTo)
for txin in txtmp.vin:
txin.scriptSig = b''
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
if (hashtype & 0x1f) == SIGHASH_NONE:
txtmp.vout = []
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
outIdx = inIdx
if outIdx >= len(txtmp.vout):
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
tmp = txtmp.vout[outIdx]
txtmp.vout = []
for i in range(outIdx):
txtmp.vout.append(CTxOut(-1))
txtmp.vout.append(tmp)
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
if hashtype & SIGHASH_ANYONECANPAY:
tmp = txtmp.vin[inIdx]
txtmp.vin = []
txtmp.vin.append(tmp)
s = txtmp.serialize()
s += struct.pack(b"<I", hashtype)
hash = hash256(s)
return (hash, None)
# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided.
# Performance optimization probably not necessary for python tests, however.
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
# for version 0 witnesses.
def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount):
hashPrevouts = 0
hashSequence = 0
hashOutputs = 0
if not (hashtype & SIGHASH_ANYONECANPAY):
serialize_prevouts = bytes()
for i in txTo.vin:
serialize_prevouts += i.prevout.serialize()
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_sequence = bytes()
for i in txTo.vin:
serialize_sequence += struct.pack("<I", i.nSequence)
hashSequence = uint256_from_str(hash256(serialize_sequence))
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_outputs = bytes()
for o in txTo.vout:
serialize_outputs += o.serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
serialize_outputs = txTo.vout[inIdx].serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
ss = bytes()
ss += struct.pack("<i", txTo.nVersion)
ss += ser_uint256(hashPrevouts)
ss += ser_uint256(hashSequence)
ss += txTo.vin[inIdx].prevout.serialize()
ss += ser_string(script)
ss += struct.pack("<q", amount)
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
ss += ser_uint256(hashOutputs)
ss += struct.pack("<i", txTo.nLockTime)
ss += struct.pack("<I", hashtype)
return hash256(ss)

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# Copyright (c) 2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# siphash.py - Specialized SipHash-2-4 implementations
#
# This implements SipHash-2-4 for 256-bit integers.
def rotl64(n, b):
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
def siphash_round(v0, v1, v2, v3):
v0 = (v0 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 13)
v1 ^= v0
v0 = rotl64(v0, 32)
v2 = (v2 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 16)
v3 ^= v2
v0 = (v0 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 21)
v3 ^= v0
v2 = (v2 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 17)
v1 ^= v2
v2 = rotl64(v2, 32)
return (v0, v1, v2, v3)
def siphash256(k0, k1, h):
n0 = h & ((1 << 64) - 1)
n1 = (h >> 64) & ((1 << 64) - 1)
n2 = (h >> 128) & ((1 << 64) - 1)
n3 = (h >> 192) & ((1 << 64) - 1)
v0 = 0x736f6d6570736575 ^ k0
v1 = 0x646f72616e646f6d ^ k1
v2 = 0x6c7967656e657261 ^ k0
v3 = 0x7465646279746573 ^ k1 ^ n0
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n0
v3 ^= n1
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n1
v3 ^= n2
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n2
v3 ^= n3
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n3
v3 ^= 0x2000000000000000
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= 0x2000000000000000
v2 ^= 0xFF
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
return v0 ^ v1 ^ v2 ^ v3

View File

@@ -0,0 +1,841 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2016 The Bitcoin Core developers
# Copyright (c) 2014-2017 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Helpful routines for regression testing
#
import os
import sys
from binascii import hexlify, unhexlify
from base64 import b64encode
from decimal import Decimal, ROUND_DOWN
import json
import http.client
import random
import shutil
import subprocess
import tempfile
import time
import re
import errno
import logging
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
COVERAGE_DIR = None
logger = logging.getLogger("TestFramework.utils")
# The maximum number of nodes a single test can spawn
MAX_NODES = 15
# Don't assign rpc or p2p ports lower than this
PORT_MIN = 11000
# The number of ports to "reserve" for p2p and rpc, each
PORT_RANGE = 5000
BITCOIND_PROC_WAIT_TIMEOUT = 60
class PortSeed:
# Must be initialized with a unique integer for each process
n = None
#Set Mocktime default to OFF.
#MOCKTIME is only needed for scripts that use the
#cached version of the blockchain. If the cached
#version of the blockchain is used without MOCKTIME
#then the mempools will not sync due to IBD.
MOCKTIME = 0
def enable_mocktime():
#For backwared compatibility of the python scripts
#with previous versions of the cache, set MOCKTIME
#to Jan 1, 2014 + (201 * 10 * 60)
global MOCKTIME
MOCKTIME = 1414776313 + (201 * 10 * 60)
def set_mocktime(t):
global MOCKTIME
MOCKTIME = t
def disable_mocktime():
global MOCKTIME
MOCKTIME = 0
def get_mocktime():
return MOCKTIME
def enable_coverage(dirname):
"""Maintain a log of which RPC calls are made during testing."""
global COVERAGE_DIR
COVERAGE_DIR = dirname
def get_rpc_proxy(url, node_number, timeout=None):
"""
Args:
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
Kwargs:
timeout (int): HTTP timeout in seconds
Returns:
AuthServiceProxy. convenience object for making RPC calls.
"""
proxy_kwargs = {}
if timeout is not None:
proxy_kwargs['timeout'] = timeout
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(
COVERAGE_DIR, node_number) if COVERAGE_DIR else None
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
def get_evoznsync_status(node):
result = node.evoznsync("status")
return result['IsSynced']
def wait_to_sync(node, fast_znsync=False):
tm = 0
synced = False
while tm < 30:
synced = get_evoznsync_status(node)
if synced:
return
time.sleep(0.2)
if fast_znsync:
# skip mnsync states
node.evoznsync("next")
tm += 0.2
assert(synced)
def p2p_port(n):
assert(n <= MAX_NODES)
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def rpc_port(n):
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def check_json_precision():
"""Make sure json library being used does not lose precision converting BTC values"""
n = Decimal("20000000.00000003")
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
if satoshis != 2000000000000003:
raise RuntimeError("JSON encode/decode loses precision")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def bytes_to_hex_str(byte_str):
return hexlify(byte_str).decode('ascii')
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def sync_blocks(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same tip.
sync_blocks needs to be called with an rpc_connections set that has least
one node already synced to the latest, stable tip, otherwise there's a
chance it might return before all nodes are stably synced.
"""
# Use getblockcount() instead of waitforblockheight() to determine the
# initial max height because the two RPCs look at different internal global
# variables (chainActive vs latestBlock) and the former gets updated
# earlier.
maxheight = max(x.getblockcount() for x in rpc_connections)
start_time = cur_time = time.time()
while cur_time <= start_time + timeout:
tips = [r.waitforblockheight(maxheight, int(wait * 1000)) for r in rpc_connections]
if all(t["height"] == maxheight for t in tips):
if all(t["hash"] == tips[0]["hash"] for t in tips):
return
raise AssertionError("Block sync failed, mismatched block hashes:{}".format(
"".join("\n {!r}".format(tip) for tip in tips)))
time.sleep(wait)
cur_time = time.time()
raise AssertionError("Block sync to height {} timed out:{}".format(
maxheight, "".join("\n {!r}".format(tip) for tip in tips)))
def sync_znodes(rpc_connections, *, timeout=60):
"""
Waits until every node has their znsync status is synced.
"""
start_time = cur_time = time.time()
while cur_time <= start_time + timeout:
statuses = [r.znsync("status") for r in rpc_connections]
if all(stat["IsSynced"] == True for stat in statuses):
return
cur_time = time.time()
raise AssertionError("Znode sync failed.")
def sync_chain(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same best block
"""
while timeout > 0:
best_hash = [x.getbestblockhash() for x in rpc_connections]
if best_hash == [best_hash[0]]*len(best_hash):
return
time.sleep(wait)
timeout -= wait
raise AssertionError("Chain sync failed: Best block hashes don't match")
def sync_mempools(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same transactions in their memory
pools
"""
while timeout > 0:
pool = set(rpc_connections[0].getrawmempool())
num_match = 1
for i in range(1, len(rpc_connections)):
if set(rpc_connections[i].getrawmempool()) == pool:
num_match = num_match+1
if num_match == len(rpc_connections):
return
time.sleep(wait)
timeout -= wait
raise AssertionError("Mempool sync failed")
def sync_znodes(rpc_connections, fast_mnsync=False):
for node in rpc_connections:
wait_to_sync(node, fast_mnsync)
bitcoind_processes = {}
def initialize_datadir(dirname, n):
datadir = os.path.join(dirname, "node"+str(n))
if not os.path.isdir(datadir):
os.makedirs(datadir)
rpc_u, rpc_p = rpc_auth_pair(n)
with open(os.path.join(datadir, "firo.conf"), 'w', encoding='utf8') as f:
f.write("regtest=1\n")
f.write("rpcuser=" + rpc_u + "\n")
f.write("rpcpassword=" + rpc_p + "\n")
f.write("port="+str(p2p_port(n))+"\n")
f.write("rpcport="+str(rpc_port(n))+"\n")
f.write("listenonion=0\n")
return datadir
def rpc_auth_pair(n):
return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n)
def rpc_url(i, rpchost=None):
rpc_u, rpc_p = rpc_auth_pair(i)
host = '127.0.0.1'
port = rpc_port(i)
if rpchost:
parts = rpchost.split(':')
if len(parts) == 2:
host, port = parts
else:
host = rpchost
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port))
def wait_for_bitcoind_start(process, url, i):
'''
Wait for firod to start. This means that RPC is accessible and fully initialized.
Raise an exception if firod exits during initialization.
'''
while True:
if process.poll() is not None:
raise Exception('firod exited with status %i during initialization' % process.returncode)
try:
rpc = get_rpc_proxy(url, i)
blocks = rpc.getblockcount()
break # break out of loop on success
except IOError as e:
if e.errno != errno.ECONNREFUSED: # Port not yet open?
raise # unknown IO error
except JSONRPCException as e: # Initialization phase
if e.error['code'] != -28: # RPC in warmup?
raise # unknown JSON RPC exception
time.sleep(0.25)
def initialize_chain(test_dir, num_nodes, cachedir):
"""
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
Afterward, create num_nodes copies from the cache
"""
assert num_nodes <= MAX_NODES
create_cache = False
for i in range(MAX_NODES):
if not os.path.isdir(os.path.join(cachedir, 'node'+str(i))):
create_cache = True
break
if create_cache:
#find and delete old cache directories if any exist
for i in range(MAX_NODES):
if os.path.isdir(os.path.join(cachedir,"node"+str(i))):
shutil.rmtree(os.path.join(cachedir,"node"+str(i)))
# Create cache directories, run bitcoinds:
for i in range(MAX_NODES):
datadir=initialize_datadir(cachedir, i)
args = [ os.getenv("FIROD", "firod"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ]
if i > 0:
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
bitcoind_processes[i] = subprocess.Popen(args)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: bitcoind started, waiting for RPC to come up")
wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: RPC successfully started")
rpcs = []
for i in range(MAX_NODES):
try:
rpcs.append(get_rpc_proxy(rpc_url(i), i))
except:
sys.stderr.write("Error connecting to "+url+"\n")
sys.exit(1)
# Create a 200-block-long chain; each of the 4 first nodes
# gets 25 mature blocks and 25 immature.
# Note: To preserve compatibility with older versions of
# initialize_chain, only 4 nodes will generate coins.
#
# blocks are created with timestamps 10 minutes apart
# starting from 2010 minutes in the past
enable_mocktime()
block_time = get_mocktime() - (201 * 10 * 60)
for i in range(2):
for peer in range(4):
for j in range(25):
set_node_times(rpcs, block_time)
rpcs[peer].generate(1)
block_time += 10*60
# Must sync before next peer starts generating blocks
sync_blocks(rpcs)
# Shut them down, and clean up cache directories:
stop_nodes(rpcs)
disable_mocktime()
for i in range(MAX_NODES):
try:
os.remove(log_filename(cachedir, i, "debug.log"))
os.remove(log_filename(cachedir, i, "db.log"))
os.remove(log_filename(cachedir, i, "peers.dat"))
os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
except OSError:
pass
for i in range(num_nodes):
from_dir = os.path.join(cachedir, "node"+str(i))
to_dir = os.path.join(test_dir, "node"+str(i))
if from_dir != to_dir:
shutil.copytree(from_dir, to_dir)
initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf
def initialize_chain_clean(test_dir, num_nodes):
"""
Create an empty blockchain and num_nodes wallets.
Useful if a test case wants complete control over initialization.
"""
for i in range(num_nodes):
datadir=initialize_datadir(test_dir, i)
def _rpchost_to_args(rpchost):
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
if rpchost is None:
return []
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
if not match:
raise ValueError('Invalid RPC host spec ' + rpchost)
rpcconnect = match.group(1)
rpcport = match.group(2)
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
rpcconnect = rpcconnect[1:-1]
rv = ['-rpcconnect=' + rpcconnect]
if rpcport:
rv += ['-rpcport=' + rpcport]
return rv
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, redirect_stderr=False, stderr=None):
"""
Start a bitcoind and return RPC connection to it
"""
datadir = os.path.join(dirname, "node"+str(i))
if binary is None:
binary = os.getenv("FIROD", "firod")
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-dandelion=0", "-usemnemonic=0", "-mocktime="+str(get_mocktime()) ]
#Useful args for debugging
# "screen", "--",
# "gdb", "-x", "/tmp/gdb_run", "--args",
# Don't try auto backups (they fail a lot when running tests)
args += [ "-createwalletbackups=0" ]
if extra_args is not None: args.extend(extra_args)
# Allow to redirect stderr to stdout in case we expect some non-critical warnings/errors printed to stderr
# Otherwise the whole test would be considered to be failed in such cases
if redirect_stderr:
stderr = sys.stdout
bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr)
logger.debug("start_node: firod started, waiting for RPC to come up")
url = rpc_url(i, rpchost)
wait_for_bitcoind_start(bitcoind_processes[i], url, i)
logger.debug("start_node: RPC successfully started")
proxy = get_rpc_proxy(url, i, timeout=timewait)
if COVERAGE_DIR:
coverage.write_all_rpc_commands(COVERAGE_DIR, proxy)
return proxy
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
"""
Start multiple bitcoinds, return RPC connections to them
"""
if extra_args is None: extra_args = [ None for _ in range(num_nodes) ]
if binary is None: binary = [ None for _ in range(num_nodes) ]
rpcs = []
try:
for i in range(num_nodes):
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
except: # If one node failed to start, stop the others
stop_nodes(rpcs)
raise
return rpcs
def copy_datadir(from_node, to_node, dirname):
from_datadir = os.path.join(dirname, "node"+str(from_node), "regtest")
to_datadir = os.path.join(dirname, "node"+str(to_node), "regtest")
dirs = ["blocks", "chainstate", "evodb", "llmq"]
for d in dirs:
try:
src = os.path.join(from_datadir, d)
dst = os.path.join(to_datadir, d)
shutil.copytree(src, dst)
except:
pass
def log_filename(dirname, n_node, logname):
return os.path.join(dirname, "node"+str(n_node), "regtest", logname)
def wait_node(i):
return_code = bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
assert_equal(return_code, 0)
del bitcoind_processes[i]
def stop_node(node, i, wait=True):
logger.debug("Stopping node %d" % i)
try:
node.stop()
except http.client.CannotSendRequest as e:
logger.exception("Unable to stop node")
if wait:
wait_node(i)
def stop_nodes(nodes, fast=True):
for i, node in enumerate(nodes):
stop_node(node, i, not fast)
if fast:
for i, node in enumerate(nodes):
wait_node(i)
assert not bitcoind_processes.values() # All connections must be gone now
def set_node_times(nodes, t):
for node in nodes:
node.setmocktime(t)
def connect_nodes(from_connection, node_num):
# NOTE: In next line p2p_port(0) was replaced by rpc_port(0).
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()):
time.sleep(0.1)
def connect_nodes_bi(nodes, a, b):
connect_nodes(nodes[a], b)
connect_nodes(nodes[b], a)
def isolate_node(node, timeout=5):
node.setnetworkactive(False)
st = time.time()
while time.time() < st + timeout:
if node.getconnectioncount() == 0:
return
time.sleep(0.5)
raise AssertionError("disconnect_node timed out")
def reconnect_isolated_node(node, node_num):
node.setnetworkactive(True)
connect_nodes(node, node_num)
def find_output(node, txid, amount):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
def gather_inputs(from_node, amount_needed, confirmations_required=1):
"""
Return a random set of unspent txouts that are enough to pay amount_needed
"""
assert(confirmations_required >=0)
utxo = from_node.listunspent(confirmations_required)
random.shuffle(utxo)
inputs = []
total_in = Decimal("0.00000000")
while total_in < amount_needed and len(utxo) > 0:
t = utxo.pop()
total_in += t["amount"]
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
if total_in < amount_needed:
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in))
return (total_in, inputs)
def make_change(from_node, amount_in, amount_out, fee):
"""
Create change output(s), return them
"""
outputs = {}
amount = amount_out+fee
change = amount_in - amount
if change > amount*2:
# Create an extra change output to break up big inputs
change_address = from_node.getnewaddress()
# Split change in two, being careful of rounding:
outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
change = amount_in - amount - outputs[change_address]
if change > 0:
outputs[from_node.getnewaddress()] = change
return outputs
def send_zeropri_transaction(from_node, to_node, amount, fee):
"""
Create&broadcast a zero-priority transaction.
Returns (txid, hex-encoded-txdata)
Ensures transaction is zero-priority by first creating a send-to-self,
then using its output
"""
# Create a send-to-self with confirmed inputs:
self_address = from_node.getnewaddress()
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
outputs = make_change(from_node, total_in, amount+fee, fee)
outputs[self_address] = float(amount+fee)
self_rawtx = from_node.createrawtransaction(inputs, outputs)
self_signresult = from_node.signrawtransaction(self_rawtx)
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
vout = find_output(from_node, self_txid, amount+fee)
# Now immediately spend the output to create a 1-input, 1-output
# zero-priority transaction:
inputs = [ { "txid" : self_txid, "vout" : vout } ]
outputs = { to_node.getnewaddress() : float(amount) }
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"])
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random zero-priority transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
return (txid, txhex, fee)
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(total_in, inputs) = gather_inputs(from_node, amount+fee)
outputs = make_change(from_node, total_in, amount, fee)
outputs[to_node.getnewaddress()] = float(amount)
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"], fee)
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = tx_size * fee_per_kB / 1000
if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)"%(str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
if fee > (tx_size + 2) * fee_per_kB / 1000:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee)))
def assert_equal(thing1, thing2, *args):
if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
def assert_greater_than(thing1, thing2):
if thing1 <= thing2:
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
def assert_greater_than_or_equal(thing1, thing2):
if thing1 < thing2:
raise AssertionError("%s < %s"%(str(thing1),str(thing2)))
def assert_raises(exc, fun, *args, **kwds):
assert_raises_message(exc, None, fun, *args, **kwds)
def assert_raises_message(exc, message, fun, *args, **kwds):
try:
fun(*args, **kwds)
except exc as e:
if message is not None and message not in e.error['message']:
raise AssertionError("Expected substring not found:"+e.error['message'])
except Exception as e:
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
else:
raise AssertionError("No exception raised")
def assert_raises_jsonrpc(code, message, fun, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
and verifies that the error code and message are as expected. Throws AssertionError if
no JSONRPCException was returned or if the error code/message are not as expected.
Args:
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required
fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
try:
fun(*args, **kwds)
except JSONRPCException as e:
# JSONRPCException was thrown as expected. Check the code and message values are correct.
if (code is not None) and (code != e.error["code"]):
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
if (message is not None) and (message not in e.error['message']):
raise AssertionError("Expected substring not found:"+e.error['message'])
except Exception as e:
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
else:
raise AssertionError("No exception raised")
def assert_is_hex_string(string):
try:
int(string, 16)
except Exception as e:
raise AssertionError(
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
def assert_is_hash_string(string, length=64):
if not isinstance(string, str):
raise AssertionError("Expected a string, got type %r" % type(string))
elif length and len(string) != length:
raise AssertionError(
"String of length %d expected; got %d" % (length, len(string)))
elif not re.match('[abcdef0-9]+$', string):
raise AssertionError(
"String %r contains invalid characters for a hash." % string)
def assert_array_result(object_array, to_match, expected, should_not_find = False):
"""
Pass in array of JSON objects, a dictionary with key/value pairs
to match against, and another dictionary with expected key/value
pairs.
If the should_not_find flag is true, to_match should not be found
in object_array
"""
if should_not_find == True:
assert_equal(expected, { })
num_matched = 0
for item in object_array:
all_match = True
for key,value in to_match.items():
if item[key] != value:
all_match = False
if not all_match:
continue
elif should_not_find == True:
num_matched = num_matched+1
for key,value in expected.items():
if item[key] != value:
raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value)))
num_matched = num_matched+1
if num_matched == 0 and should_not_find != True:
raise AssertionError("No objects matched %s"%(str(to_match)))
if num_matched > 0 and should_not_find == True:
raise AssertionError("Objects were found %s"%(str(to_match)))
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
# Helper to create at least "count" utxos
# Pass in a fee that is sufficient for relay and mining new transactions.
def create_confirmed_utxos(fee, node, count):
node.generate(int(0.5*count)+101)
utxos = node.listunspent()
iterations = count - len(utxos)
addr1 = node.getnewaddress()
addr2 = node.getnewaddress()
if iterations <= 0:
return utxos
for i in range(iterations):
t = utxos.pop()
inputs = []
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr1] = satoshi_round(send_value/2)
outputs[addr2] = satoshi_round(send_value/2)
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransaction(raw_tx)["hex"]
txid = node.sendrawtransaction(signed_tx)
while (node.getmempoolinfo()['size'] > 0):
node.generate(1)
utxos = node.listunspent()
assert(len(utxos) >= count)
return utxos
# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions).
def gen_return_txouts():
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
# So we have big transactions (and therefore can't fit very many into each block)
# create one script_pubkey
script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes
for i in range (512):
script_pubkey = script_pubkey + "01"
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
txouts = "81"
for k in range(128):
# add txout value
txouts = txouts + "0000000000000000"
# add length of script_pubkey
txouts = txouts + "fd0402"
# add script_pubkey
txouts = txouts + script_pubkey
return txouts
def create_tx(node, coinbase, to_address, amount):
inputs = [{ "txid" : coinbase, "vout" : 0}]
outputs = { to_address : amount }
rawtx = node.createrawtransaction(inputs, outputs)
signresult = node.signrawtransaction(rawtx)
assert_equal(signresult["complete"], True)
return signresult["hex"]
def create_tx_multi_input(node, inputs, outputs):
rawtx = node.createrawtransaction(inputs, outputs)
signresult = node.signrawtransaction(rawtx)
assert_equal(signresult["complete"], True)
return signresult["hex"]
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above.
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
addr = node.getnewaddress()
txids = []
for _ in range(num):
t = utxos.pop()
inputs=[{ "txid" : t["txid"], "vout" : t["vout"]}]
outputs = {}
change = t['amount'] - fee
outputs[addr] = satoshi_round(change)
rawtx = node.createrawtransaction(inputs, outputs)
newtx = rawtx[0:92]
newtx = newtx + txouts
newtx = newtx + rawtx[94:]
signresult = node.signrawtransaction(newtx, None, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], True)
txids.append(txid)
return txids
def mine_large_block(node, utxos=None):
# generate a 66k transaction,
# and 14 of them is close to the 1MB block limit
num = 14
txouts = gen_return_txouts()
utxos = utxos if utxos is not None else []
if len(utxos) < num:
utxos.clear()
utxos.extend(node.listunspent())
fee = 100 * node.getnetworkinfo()["relayfee"]
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
node.generate(1)
def get_bip9_status(node, key):
info = node.getblockchaininfo()
return info['bip9_softforks'][key]
def dumpprivkey_otac(node, address):
import re
error_text = ''
try:
return node.dumpprivkey(address)
except JSONRPCException as e:
error_text = e.error
else:
raise
otac_match = re.search("Your one time authorization code is: ([a-zA-Z0-9]+)", error_text['message'])
if not otac_match:
raise JSONRPCException(error_text)
return node.dumpprivkey(address, otac_match.groups()[0])
def get_znsync_status(node):
result = node.znsync("status")
return result['IsSynced']
def wait_to_sync_znodes(node, fast_znsync=False):
while True:
synced = get_znsync_status(node)
if synced:
break
time.sleep(0.2)
if fast_znsync:
# skip mnsync states
node.znsync("next")
def get_full_balance(node):
wallet_info = node.getwalletinfo()
return wallet_info["balance"] + wallet_info["immature_balance"] + wallet_info["unconfirmed_balance"]

View File

@@ -0,0 +1,175 @@
"""
Copyright 2011 Jeff Garzik
AuthServiceProxy has the following improvements over python-jsonrpc's
ServiceProxy class:
- HTTP connections persist for the life of the AuthServiceProxy object
(if server supports HTTP/1.1)
- sends protocol 'version', per JSON-RPC 1.1
- sends proper, incrementing 'id'
- sends Basic HTTP authentication headers
- parses all JSON numbers that look like floats as Decimal
- uses standard Python json lib
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
Copyright (c) 2007 Jan-Klaas Kollhof
This file is part of jsonrpc.
jsonrpc is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this software; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
try:
import http.client as httplib
except ImportError:
import httplib
import base64
import decimal
import json
import logging
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
USER_AGENT = "AuthServiceProxy/0.1"
HTTP_TIMEOUT = 30
log = logging.getLogger("NavcoinRPC")
class JSONRPCException(Exception):
def __init__(self, rpc_error):
Exception.__init__(self)
self.error = rpc_error
def EncodeDecimal(o):
if isinstance(o, decimal.Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
class AuthServiceProxy(object):
__id_count = 0
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
self.__service_url = service_url
self._service_name = service_name
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
self.__url = urlparse.urlparse(service_url)
if self.__url.port is None:
port = 80
else:
port = self.__url.port
(user, passwd) = (self.__url.username, self.__url.password)
try:
user = user.encode('utf8')
except AttributeError:
pass
try:
passwd = passwd.encode('utf8')
except AttributeError:
pass
authpair = user + b':' + passwd
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
if connection:
# Callables re-use the connection of the original proxy
self.__conn = connection
elif self.__url.scheme == 'https':
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
timeout=timeout)
else:
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
timeout=timeout)
def __getattr__(self, name):
if name.startswith('__') and name.endswith('__'):
# Python internal stuff
raise AttributeError
if self._service_name is not None:
name = "%s.%s" % (self._service_name, name)
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
def _request(self, method, path, postdata):
'''
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
'''
headers = {'Host': self.__url.hostname,
'User-Agent': USER_AGENT,
'Authorization': self.__auth_header,
'Content-type': 'application/json'}
try:
self.__conn.request(method, path, postdata, headers)
return self._get_response()
except httplib.BadStatusLine as e:
if e.line == "''": # if connection was closed, try again
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
else:
raise
except BrokenPipeError:
# Python 3.5+ raises this instead of BadStatusLine when the connection was reset
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
def __call__(self, *args):
AuthServiceProxy.__id_count += 1
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
postdata = json.dumps({'version': '1.1',
'method': self._service_name,
'params': args,
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if response['error'] is not None:
raise JSONRPCException(response['error'])
elif 'result' not in response:
raise JSONRPCException({
'code': -343, 'message': 'missing JSON-RPC result'})
else:
return response['result']
def _batch(self, rpc_call_list):
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
log.debug("--> "+postdata)
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
def _get_response(self):
http_response = self.__conn.getresponse()
if http_response is None:
raise JSONRPCException({
'code': -342, 'message': 'missing HTTP response from server'})
content_type = http_response.getheader('Content-Type')
if content_type != 'application/json':
raise JSONRPCException({
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
responsedata = http_response.read().decode('utf8')
response = json.loads(responsedata, parse_float=decimal.Decimal)
if "error" in response and response["error"] is None:
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
else:
log.debug("<-- "+responsedata)
return response

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
#
# bignum.py
#
# This file is copied from python-navcoinlib.
#
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
"""Bignum routines"""
import struct
# generic big endian MPI format
def bn_bytes(v, have_ext=False):
ext = 0
if have_ext:
ext = 1
return ((v.bit_length()+7)//8) + ext
def bn2bin(v):
s = bytearray()
i = bn_bytes(v)
while i > 0:
s.append((v >> ((i-1) * 8)) & 0xff)
i -= 1
return s
def bin2bn(s):
l = 0
for ch in s:
l = (l << 8) | ch
return l
def bn2mpi(v):
have_ext = False
if v.bit_length() > 0:
have_ext = (v.bit_length() & 0x07) == 0
neg = False
if v < 0:
neg = True
v = -v
s = struct.pack(b">I", bn_bytes(v, have_ext))
ext = bytearray()
if have_ext:
ext.append(0)
v_bin = bn2bin(v)
if neg:
if have_ext:
ext[0] |= 0x80
else:
v_bin[0] |= 0x80
return s + ext + v_bin
def mpi2bn(s):
if len(s) < 4:
return None
s_size = bytes(s[:4])
v_len = struct.unpack(b">I", s_size)[0]
if len(s) != (v_len + 4):
return None
if v_len == 0:
return 0
v_str = bytearray(s[4:])
neg = False
i = v_str[0]
if i & 0x80:
neg = True
i &= ~0x80
v_str[0] = i
v = bin2bn(v_str)
if neg:
return -v
return v
# navcoin-specific little endian format, with implicit size
def mpi2vch(s):
r = s[4:] # strip size
r = r[::-1] # reverse string, converting BE->LE
return r
def bn2vch(v):
return bytes(mpi2vch(bn2mpi(v)))
def vch2mpi(s):
r = struct.pack(b">I", len(s)) # size
r += s[::-1] # reverse string, converting LE->BE
return r
def vch2bn(s):
return mpi2bn(vch2mpi(s))

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
This module contains utilities for doing coverage analysis on the RPC
interface.
It provides a way to track which RPC commands are exercised during
testing.
"""
import os
REFERENCE_FILENAME = 'rpc_interface.txt'
class AuthServiceProxyWrapper(object):
"""
An object that wraps AuthServiceProxy to record specific RPC calls.
"""
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
"""
Kwargs:
auth_service_proxy_instance (AuthServiceProxy): the instance
being wrapped.
coverage_logfile (str): if specified, write each service_name
out to a file when called.
"""
self.auth_service_proxy_instance = auth_service_proxy_instance
self.coverage_logfile = coverage_logfile
def __getattr__(self, *args, **kwargs):
return_val = self.auth_service_proxy_instance.__getattr__(
*args, **kwargs)
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
def __call__(self, *args, **kwargs):
"""
Delegates to AuthServiceProxy, then writes the particular RPC method
called to a file.
"""
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
rpc_method = self.auth_service_proxy_instance._service_name
if self.coverage_logfile:
with open(self.coverage_logfile, 'a+') as f:
f.write("%s\n" % rpc_method)
return return_val
@property
def url(self):
return self.auth_service_proxy_instance.url
def get_filename(dirname, n_node):
"""
Get a filename unique to the test process ID and node.
This file will contain a list of RPC commands covered.
"""
pid = str(os.getpid())
return os.path.join(
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
def write_all_rpc_commands(dirname, node):
"""
Write out a list of all RPC functions available in `navcoin-cli` for
coverage comparison. This will only happen once per coverage
directory.
Args:
dirname (str): temporary test dir
node (AuthServiceProxy): client
Returns:
bool. if the RPC interface file was written.
"""
filename = os.path.join(dirname, REFERENCE_FILENAME)
if os.path.isfile(filename):
return False
help_output = node.help().split('\n')
commands = set()
for line in help_output:
line = line.strip()
# Ignore blanks and headers
if line and not line.startswith('='):
commands.add("%s\n" % line.split()[0])
with open(filename, 'w') as f:
f.writelines(list(commands))
return True

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,943 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# script.py
#
# This file is modified from python-navcoinlib.
#
"""Scripts
Functionality to build scripts, as well as SignatureHash().
"""
from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string
from binascii import hexlify
import hashlib
import sys
bchr = chr
bord = ord
if sys.version > '3':
long = int
bchr = lambda x: bytes([x])
bord = lambda x: x
import struct
from .bignum import bn2vch
MAX_SCRIPT_SIZE = 10000
MAX_SCRIPT_ELEMENT_SIZE = 520
MAX_SCRIPT_OPCODES = 201
OPCODE_NAMES = {}
_opcode_instances = []
class CScriptOp(int):
"""A single script opcode"""
__slots__ = []
@staticmethod
def encode_op_pushdata(d):
"""Encode a PUSHDATA op, returning bytes"""
if len(d) < 0x4c:
return b'' + bchr(len(d)) + d # OP_PUSHDATA
elif len(d) <= 0xff:
return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1
elif len(d) <= 0xffff:
return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
elif len(d) <= 0xffffffff:
return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
else:
raise ValueError("Data too long to encode in a PUSHDATA op")
@staticmethod
def encode_op_n(n):
"""Encode a small integer op, returning an opcode"""
if not (0 <= n <= 16):
raise ValueError('Integer must be in range 0 <= n <= 16, got %d' % n)
if n == 0:
return OP_0
else:
return CScriptOp(OP_1 + n-1)
def decode_op_n(self):
"""Decode a small integer opcode, returning an integer"""
if self == OP_0:
return 0
if not (self == OP_0 or OP_1 <= self <= OP_16):
raise ValueError('op %r is not an OP_N' % self)
return int(self - OP_1+1)
def is_small_int(self):
"""Return true if the op pushes a small integer to the stack"""
if 0x51 <= self <= 0x60 or self == 0:
return True
else:
return False
def __str__(self):
return repr(self)
def __repr__(self):
if self in OPCODE_NAMES:
return OPCODE_NAMES[self]
else:
return 'CScriptOp(0x%x)' % self
def __new__(cls, n):
try:
return _opcode_instances[n]
except IndexError:
assert len(_opcode_instances) == n
_opcode_instances.append(super(CScriptOp, cls).__new__(cls, n))
return _opcode_instances[n]
# Populate opcode instance table
for n in range(0xff+1):
CScriptOp(n)
# push value
OP_0 = CScriptOp(0x00)
OP_FALSE = OP_0
OP_PUSHDATA1 = CScriptOp(0x4c)
OP_PUSHDATA2 = CScriptOp(0x4d)
OP_PUSHDATA4 = CScriptOp(0x4e)
OP_1NEGATE = CScriptOp(0x4f)
OP_RESERVED = CScriptOp(0x50)
OP_1 = CScriptOp(0x51)
OP_TRUE=OP_1
OP_2 = CScriptOp(0x52)
OP_3 = CScriptOp(0x53)
OP_4 = CScriptOp(0x54)
OP_5 = CScriptOp(0x55)
OP_6 = CScriptOp(0x56)
OP_7 = CScriptOp(0x57)
OP_8 = CScriptOp(0x58)
OP_9 = CScriptOp(0x59)
OP_10 = CScriptOp(0x5a)
OP_11 = CScriptOp(0x5b)
OP_12 = CScriptOp(0x5c)
OP_13 = CScriptOp(0x5d)
OP_14 = CScriptOp(0x5e)
OP_15 = CScriptOp(0x5f)
OP_16 = CScriptOp(0x60)
# control
OP_NOP = CScriptOp(0x61)
OP_VER = CScriptOp(0x62)
OP_IF = CScriptOp(0x63)
OP_NOTIF = CScriptOp(0x64)
OP_VERIF = CScriptOp(0x65)
OP_VERNOTIF = CScriptOp(0x66)
OP_ELSE = CScriptOp(0x67)
OP_ENDIF = CScriptOp(0x68)
OP_VERIFY = CScriptOp(0x69)
OP_RETURN = CScriptOp(0x6a)
# stack ops
OP_TOALTSTACK = CScriptOp(0x6b)
OP_FROMALTSTACK = CScriptOp(0x6c)
OP_2DROP = CScriptOp(0x6d)
OP_2DUP = CScriptOp(0x6e)
OP_3DUP = CScriptOp(0x6f)
OP_2OVER = CScriptOp(0x70)
OP_2ROT = CScriptOp(0x71)
OP_2SWAP = CScriptOp(0x72)
OP_IFDUP = CScriptOp(0x73)
OP_DEPTH = CScriptOp(0x74)
OP_DROP = CScriptOp(0x75)
OP_DUP = CScriptOp(0x76)
OP_NIP = CScriptOp(0x77)
OP_OVER = CScriptOp(0x78)
OP_PICK = CScriptOp(0x79)
OP_ROLL = CScriptOp(0x7a)
OP_ROT = CScriptOp(0x7b)
OP_SWAP = CScriptOp(0x7c)
OP_TUCK = CScriptOp(0x7d)
# splice ops
OP_CAT = CScriptOp(0x7e)
OP_SUBSTR = CScriptOp(0x7f)
OP_LEFT = CScriptOp(0x80)
OP_RIGHT = CScriptOp(0x81)
OP_SIZE = CScriptOp(0x82)
# bit logic
OP_INVERT = CScriptOp(0x83)
OP_AND = CScriptOp(0x84)
OP_OR = CScriptOp(0x85)
OP_XOR = CScriptOp(0x86)
OP_EQUAL = CScriptOp(0x87)
OP_EQUALVERIFY = CScriptOp(0x88)
OP_RESERVED1 = CScriptOp(0x89)
OP_RESERVED2 = CScriptOp(0x8a)
# numeric
OP_1ADD = CScriptOp(0x8b)
OP_1SUB = CScriptOp(0x8c)
OP_2MUL = CScriptOp(0x8d)
OP_2DIV = CScriptOp(0x8e)
OP_NEGATE = CScriptOp(0x8f)
OP_ABS = CScriptOp(0x90)
OP_NOT = CScriptOp(0x91)
OP_0NOTEQUAL = CScriptOp(0x92)
OP_ADD = CScriptOp(0x93)
OP_SUB = CScriptOp(0x94)
OP_MUL = CScriptOp(0x95)
OP_DIV = CScriptOp(0x96)
OP_MOD = CScriptOp(0x97)
OP_LSHIFT = CScriptOp(0x98)
OP_RSHIFT = CScriptOp(0x99)
OP_BOOLAND = CScriptOp(0x9a)
OP_BOOLOR = CScriptOp(0x9b)
OP_NUMEQUAL = CScriptOp(0x9c)
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
OP_NUMNOTEQUAL = CScriptOp(0x9e)
OP_LESSTHAN = CScriptOp(0x9f)
OP_GREATERTHAN = CScriptOp(0xa0)
OP_LESSTHANOREQUAL = CScriptOp(0xa1)
OP_GREATERTHANOREQUAL = CScriptOp(0xa2)
OP_MIN = CScriptOp(0xa3)
OP_MAX = CScriptOp(0xa4)
OP_WITHIN = CScriptOp(0xa5)
# crypto
OP_RIPEMD160 = CScriptOp(0xa6)
OP_SHA1 = CScriptOp(0xa7)
OP_SHA256 = CScriptOp(0xa8)
OP_HASH160 = CScriptOp(0xa9)
OP_HASH256 = CScriptOp(0xaa)
OP_CODESEPARATOR = CScriptOp(0xab)
OP_CHECKSIG = CScriptOp(0xac)
OP_CHECKSIGVERIFY = CScriptOp(0xad)
OP_CHECKMULTISIG = CScriptOp(0xae)
OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
# expansion
OP_NOP1 = CScriptOp(0xb0)
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
OP_NOP4 = CScriptOp(0xb3)
OP_NOP5 = CScriptOp(0xb4)
OP_NOP6 = CScriptOp(0xb5)
OP_NOP7 = CScriptOp(0xb6)
OP_NOP8 = CScriptOp(0xb7)
OP_NOP9 = CScriptOp(0xb8)
OP_NOP10 = CScriptOp(0xb9)
# template matching params
OP_SMALLINTEGER = CScriptOp(0xfa)
OP_PUBKEYS = CScriptOp(0xfb)
OP_PUBKEYHASH = CScriptOp(0xfd)
OP_PUBKEY = CScriptOp(0xfe)
OP_INVALIDOPCODE = CScriptOp(0xff)
VALID_OPCODES = {
OP_1NEGATE,
OP_RESERVED,
OP_1,
OP_2,
OP_3,
OP_4,
OP_5,
OP_6,
OP_7,
OP_8,
OP_9,
OP_10,
OP_11,
OP_12,
OP_13,
OP_14,
OP_15,
OP_16,
OP_NOP,
OP_VER,
OP_IF,
OP_NOTIF,
OP_VERIF,
OP_VERNOTIF,
OP_ELSE,
OP_ENDIF,
OP_VERIFY,
OP_RETURN,
OP_TOALTSTACK,
OP_FROMALTSTACK,
OP_2DROP,
OP_2DUP,
OP_3DUP,
OP_2OVER,
OP_2ROT,
OP_2SWAP,
OP_IFDUP,
OP_DEPTH,
OP_DROP,
OP_DUP,
OP_NIP,
OP_OVER,
OP_PICK,
OP_ROLL,
OP_ROT,
OP_SWAP,
OP_TUCK,
OP_CAT,
OP_SUBSTR,
OP_LEFT,
OP_RIGHT,
OP_SIZE,
OP_INVERT,
OP_AND,
OP_OR,
OP_XOR,
OP_EQUAL,
OP_EQUALVERIFY,
OP_RESERVED1,
OP_RESERVED2,
OP_1ADD,
OP_1SUB,
OP_2MUL,
OP_2DIV,
OP_NEGATE,
OP_ABS,
OP_NOT,
OP_0NOTEQUAL,
OP_ADD,
OP_SUB,
OP_MUL,
OP_DIV,
OP_MOD,
OP_LSHIFT,
OP_RSHIFT,
OP_BOOLAND,
OP_BOOLOR,
OP_NUMEQUAL,
OP_NUMEQUALVERIFY,
OP_NUMNOTEQUAL,
OP_LESSTHAN,
OP_GREATERTHAN,
OP_LESSTHANOREQUAL,
OP_GREATERTHANOREQUAL,
OP_MIN,
OP_MAX,
OP_WITHIN,
OP_RIPEMD160,
OP_SHA1,
OP_SHA256,
OP_HASH160,
OP_HASH256,
OP_CODESEPARATOR,
OP_CHECKSIG,
OP_CHECKSIGVERIFY,
OP_CHECKMULTISIG,
OP_CHECKMULTISIGVERIFY,
OP_NOP1,
OP_CHECKLOCKTIMEVERIFY,
OP_CHECKSEQUENCEVERIFY,
OP_NOP4,
OP_NOP5,
OP_NOP6,
OP_NOP7,
OP_NOP8,
OP_NOP9,
OP_NOP10,
OP_SMALLINTEGER,
OP_PUBKEYS,
OP_PUBKEYHASH,
OP_PUBKEY,
}
OPCODE_NAMES.update({
OP_0 : 'OP_0',
OP_PUSHDATA1 : 'OP_PUSHDATA1',
OP_PUSHDATA2 : 'OP_PUSHDATA2',
OP_PUSHDATA4 : 'OP_PUSHDATA4',
OP_1NEGATE : 'OP_1NEGATE',
OP_RESERVED : 'OP_RESERVED',
OP_1 : 'OP_1',
OP_2 : 'OP_2',
OP_3 : 'OP_3',
OP_4 : 'OP_4',
OP_5 : 'OP_5',
OP_6 : 'OP_6',
OP_7 : 'OP_7',
OP_8 : 'OP_8',
OP_9 : 'OP_9',
OP_10 : 'OP_10',
OP_11 : 'OP_11',
OP_12 : 'OP_12',
OP_13 : 'OP_13',
OP_14 : 'OP_14',
OP_15 : 'OP_15',
OP_16 : 'OP_16',
OP_NOP : 'OP_NOP',
OP_VER : 'OP_VER',
OP_IF : 'OP_IF',
OP_NOTIF : 'OP_NOTIF',
OP_VERIF : 'OP_VERIF',
OP_VERNOTIF : 'OP_VERNOTIF',
OP_ELSE : 'OP_ELSE',
OP_ENDIF : 'OP_ENDIF',
OP_VERIFY : 'OP_VERIFY',
OP_RETURN : 'OP_RETURN',
OP_TOALTSTACK : 'OP_TOALTSTACK',
OP_FROMALTSTACK : 'OP_FROMALTSTACK',
OP_2DROP : 'OP_2DROP',
OP_2DUP : 'OP_2DUP',
OP_3DUP : 'OP_3DUP',
OP_2OVER : 'OP_2OVER',
OP_2ROT : 'OP_2ROT',
OP_2SWAP : 'OP_2SWAP',
OP_IFDUP : 'OP_IFDUP',
OP_DEPTH : 'OP_DEPTH',
OP_DROP : 'OP_DROP',
OP_DUP : 'OP_DUP',
OP_NIP : 'OP_NIP',
OP_OVER : 'OP_OVER',
OP_PICK : 'OP_PICK',
OP_ROLL : 'OP_ROLL',
OP_ROT : 'OP_ROT',
OP_SWAP : 'OP_SWAP',
OP_TUCK : 'OP_TUCK',
OP_CAT : 'OP_CAT',
OP_SUBSTR : 'OP_SUBSTR',
OP_LEFT : 'OP_LEFT',
OP_RIGHT : 'OP_RIGHT',
OP_SIZE : 'OP_SIZE',
OP_INVERT : 'OP_INVERT',
OP_AND : 'OP_AND',
OP_OR : 'OP_OR',
OP_XOR : 'OP_XOR',
OP_EQUAL : 'OP_EQUAL',
OP_EQUALVERIFY : 'OP_EQUALVERIFY',
OP_RESERVED1 : 'OP_RESERVED1',
OP_RESERVED2 : 'OP_RESERVED2',
OP_1ADD : 'OP_1ADD',
OP_1SUB : 'OP_1SUB',
OP_2MUL : 'OP_2MUL',
OP_2DIV : 'OP_2DIV',
OP_NEGATE : 'OP_NEGATE',
OP_ABS : 'OP_ABS',
OP_NOT : 'OP_NOT',
OP_0NOTEQUAL : 'OP_0NOTEQUAL',
OP_ADD : 'OP_ADD',
OP_SUB : 'OP_SUB',
OP_MUL : 'OP_MUL',
OP_DIV : 'OP_DIV',
OP_MOD : 'OP_MOD',
OP_LSHIFT : 'OP_LSHIFT',
OP_RSHIFT : 'OP_RSHIFT',
OP_BOOLAND : 'OP_BOOLAND',
OP_BOOLOR : 'OP_BOOLOR',
OP_NUMEQUAL : 'OP_NUMEQUAL',
OP_NUMEQUALVERIFY : 'OP_NUMEQUALVERIFY',
OP_NUMNOTEQUAL : 'OP_NUMNOTEQUAL',
OP_LESSTHAN : 'OP_LESSTHAN',
OP_GREATERTHAN : 'OP_GREATERTHAN',
OP_LESSTHANOREQUAL : 'OP_LESSTHANOREQUAL',
OP_GREATERTHANOREQUAL : 'OP_GREATERTHANOREQUAL',
OP_MIN : 'OP_MIN',
OP_MAX : 'OP_MAX',
OP_WITHIN : 'OP_WITHIN',
OP_RIPEMD160 : 'OP_RIPEMD160',
OP_SHA1 : 'OP_SHA1',
OP_SHA256 : 'OP_SHA256',
OP_HASH160 : 'OP_HASH160',
OP_HASH256 : 'OP_HASH256',
OP_CODESEPARATOR : 'OP_CODESEPARATOR',
OP_CHECKSIG : 'OP_CHECKSIG',
OP_CHECKSIGVERIFY : 'OP_CHECKSIGVERIFY',
OP_CHECKMULTISIG : 'OP_CHECKMULTISIG',
OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY',
OP_NOP1 : 'OP_NOP1',
OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY',
OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY',
OP_NOP4 : 'OP_NOP4',
OP_NOP5 : 'OP_NOP5',
OP_NOP6 : 'OP_NOP6',
OP_NOP7 : 'OP_NOP7',
OP_NOP8 : 'OP_NOP8',
OP_NOP9 : 'OP_NOP9',
OP_NOP10 : 'OP_NOP10',
OP_SMALLINTEGER : 'OP_SMALLINTEGER',
OP_PUBKEYS : 'OP_PUBKEYS',
OP_PUBKEYHASH : 'OP_PUBKEYHASH',
OP_PUBKEY : 'OP_PUBKEY',
OP_INVALIDOPCODE : 'OP_INVALIDOPCODE',
})
OPCODES_BY_NAME = {
'OP_0' : OP_0,
'OP_PUSHDATA1' : OP_PUSHDATA1,
'OP_PUSHDATA2' : OP_PUSHDATA2,
'OP_PUSHDATA4' : OP_PUSHDATA4,
'OP_1NEGATE' : OP_1NEGATE,
'OP_RESERVED' : OP_RESERVED,
'OP_1' : OP_1,
'OP_2' : OP_2,
'OP_3' : OP_3,
'OP_4' : OP_4,
'OP_5' : OP_5,
'OP_6' : OP_6,
'OP_7' : OP_7,
'OP_8' : OP_8,
'OP_9' : OP_9,
'OP_10' : OP_10,
'OP_11' : OP_11,
'OP_12' : OP_12,
'OP_13' : OP_13,
'OP_14' : OP_14,
'OP_15' : OP_15,
'OP_16' : OP_16,
'OP_NOP' : OP_NOP,
'OP_VER' : OP_VER,
'OP_IF' : OP_IF,
'OP_NOTIF' : OP_NOTIF,
'OP_VERIF' : OP_VERIF,
'OP_VERNOTIF' : OP_VERNOTIF,
'OP_ELSE' : OP_ELSE,
'OP_ENDIF' : OP_ENDIF,
'OP_VERIFY' : OP_VERIFY,
'OP_RETURN' : OP_RETURN,
'OP_TOALTSTACK' : OP_TOALTSTACK,
'OP_FROMALTSTACK' : OP_FROMALTSTACK,
'OP_2DROP' : OP_2DROP,
'OP_2DUP' : OP_2DUP,
'OP_3DUP' : OP_3DUP,
'OP_2OVER' : OP_2OVER,
'OP_2ROT' : OP_2ROT,
'OP_2SWAP' : OP_2SWAP,
'OP_IFDUP' : OP_IFDUP,
'OP_DEPTH' : OP_DEPTH,
'OP_DROP' : OP_DROP,
'OP_DUP' : OP_DUP,
'OP_NIP' : OP_NIP,
'OP_OVER' : OP_OVER,
'OP_PICK' : OP_PICK,
'OP_ROLL' : OP_ROLL,
'OP_ROT' : OP_ROT,
'OP_SWAP' : OP_SWAP,
'OP_TUCK' : OP_TUCK,
'OP_CAT' : OP_CAT,
'OP_SUBSTR' : OP_SUBSTR,
'OP_LEFT' : OP_LEFT,
'OP_RIGHT' : OP_RIGHT,
'OP_SIZE' : OP_SIZE,
'OP_INVERT' : OP_INVERT,
'OP_AND' : OP_AND,
'OP_OR' : OP_OR,
'OP_XOR' : OP_XOR,
'OP_EQUAL' : OP_EQUAL,
'OP_EQUALVERIFY' : OP_EQUALVERIFY,
'OP_RESERVED1' : OP_RESERVED1,
'OP_RESERVED2' : OP_RESERVED2,
'OP_1ADD' : OP_1ADD,
'OP_1SUB' : OP_1SUB,
'OP_2MUL' : OP_2MUL,
'OP_2DIV' : OP_2DIV,
'OP_NEGATE' : OP_NEGATE,
'OP_ABS' : OP_ABS,
'OP_NOT' : OP_NOT,
'OP_0NOTEQUAL' : OP_0NOTEQUAL,
'OP_ADD' : OP_ADD,
'OP_SUB' : OP_SUB,
'OP_MUL' : OP_MUL,
'OP_DIV' : OP_DIV,
'OP_MOD' : OP_MOD,
'OP_LSHIFT' : OP_LSHIFT,
'OP_RSHIFT' : OP_RSHIFT,
'OP_BOOLAND' : OP_BOOLAND,
'OP_BOOLOR' : OP_BOOLOR,
'OP_NUMEQUAL' : OP_NUMEQUAL,
'OP_NUMEQUALVERIFY' : OP_NUMEQUALVERIFY,
'OP_NUMNOTEQUAL' : OP_NUMNOTEQUAL,
'OP_LESSTHAN' : OP_LESSTHAN,
'OP_GREATERTHAN' : OP_GREATERTHAN,
'OP_LESSTHANOREQUAL' : OP_LESSTHANOREQUAL,
'OP_GREATERTHANOREQUAL' : OP_GREATERTHANOREQUAL,
'OP_MIN' : OP_MIN,
'OP_MAX' : OP_MAX,
'OP_WITHIN' : OP_WITHIN,
'OP_RIPEMD160' : OP_RIPEMD160,
'OP_SHA1' : OP_SHA1,
'OP_SHA256' : OP_SHA256,
'OP_HASH160' : OP_HASH160,
'OP_HASH256' : OP_HASH256,
'OP_CODESEPARATOR' : OP_CODESEPARATOR,
'OP_CHECKSIG' : OP_CHECKSIG,
'OP_CHECKSIGVERIFY' : OP_CHECKSIGVERIFY,
'OP_CHECKMULTISIG' : OP_CHECKMULTISIG,
'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY,
'OP_NOP1' : OP_NOP1,
'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY,
'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY,
'OP_NOP4' : OP_NOP4,
'OP_NOP5' : OP_NOP5,
'OP_NOP6' : OP_NOP6,
'OP_NOP7' : OP_NOP7,
'OP_NOP8' : OP_NOP8,
'OP_NOP9' : OP_NOP9,
'OP_NOP10' : OP_NOP10,
'OP_SMALLINTEGER' : OP_SMALLINTEGER,
'OP_PUBKEYS' : OP_PUBKEYS,
'OP_PUBKEYHASH' : OP_PUBKEYHASH,
'OP_PUBKEY' : OP_PUBKEY,
}
class CScriptInvalidError(Exception):
"""Base class for CScript exceptions"""
pass
class CScriptTruncatedPushDataError(CScriptInvalidError):
"""Invalid pushdata due to truncation"""
def __init__(self, msg, data):
self.data = data
super(CScriptTruncatedPushDataError, self).__init__(msg)
# This is used, eg, for blockchain heights in coinbase scripts (bip34)
class CScriptNum(object):
def __init__(self, d=0):
self.value = d
@staticmethod
def encode(obj):
r = bytearray(0)
if obj.value == 0:
return bytes(r)
neg = obj.value < 0
absvalue = -obj.value if neg else obj.value
while (absvalue):
r.append(absvalue & 0xff)
absvalue >>= 8
if r[-1] & 0x80:
r.append(0x80 if neg else 0)
elif neg:
r[-1] |= 0x80
return bytes(bchr(len(r)) + r)
class CScript(bytes):
"""Serialized script
A bytes subclass, so you can use this directly whenever bytes are accepted.
Note that this means that indexing does *not* work - you'll get an index by
byte rather than opcode. This format was chosen for efficiency so that the
general case would not require creating a lot of little CScriptOP objects.
iter(script) however does iterate by opcode.
"""
@classmethod
def __coerce_instance(cls, other):
# Coerce other into bytes
if isinstance(other, CScriptOp):
other = bchr(other)
elif isinstance(other, CScriptNum):
if (other.value == 0):
other = bchr(CScriptOp(OP_0))
else:
other = CScriptNum.encode(other)
elif isinstance(other, int):
if 0 <= other <= 16:
other = bytes(bchr(CScriptOp.encode_op_n(other)))
elif other == -1:
other = bytes(bchr(OP_1NEGATE))
else:
other = CScriptOp.encode_op_pushdata(bn2vch(other))
elif isinstance(other, (bytes, bytearray)):
other = CScriptOp.encode_op_pushdata(other)
return other
def __add__(self, other):
# Do the coercion outside of the try block so that errors in it are
# noticed.
other = self.__coerce_instance(other)
try:
# bytes.__add__ always returns bytes instances unfortunately
return CScript(super(CScript, self).__add__(other))
except TypeError:
raise TypeError('Can not add a %r instance to a CScript' % other.__class__)
def join(self, iterable):
# join makes no sense for a CScript()
raise NotImplementedError
def __new__(cls, value=b''):
if isinstance(value, bytes) or isinstance(value, bytearray):
return super(CScript, cls).__new__(cls, value)
else:
def coerce_iterable(iterable):
for instance in iterable:
yield cls.__coerce_instance(instance)
# Annoyingly on both python2 and python3 bytes.join() always
# returns a bytes instance even when subclassed.
return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value)))
def raw_iter(self):
"""Raw iteration
Yields tuples of (opcode, data, sop_idx) so that the different possible
PUSHDATA encodings can be accurately distinguished, as well as
determining the exact opcode byte indexes. (sop_idx)
"""
i = 0
while i < len(self):
sop_idx = i
opcode = bord(self[i])
i += 1
if opcode > OP_PUSHDATA4:
yield (opcode, None, sop_idx)
else:
datasize = None
pushdata_type = None
if opcode < OP_PUSHDATA1:
pushdata_type = 'PUSHDATA(%d)' % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = 'PUSHDATA1'
if i >= len(self):
raise CScriptInvalidError('PUSHDATA1: missing data length')
datasize = bord(self[i])
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = 'PUSHDATA2'
if i + 1 >= len(self):
raise CScriptInvalidError('PUSHDATA2: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = 'PUSHDATA4'
if i + 3 >= len(self):
raise CScriptInvalidError('PUSHDATA4: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24)
i += 4
else:
assert False # shouldn't happen
data = bytes(self[i:i+datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
i += datasize
yield (opcode, data, sop_idx)
def __iter__(self):
"""'Cooked' iteration
Returns either a CScriptOP instance, an integer, or bytes, as
appropriate.
See raw_iter() if you need to distinguish the different possible
PUSHDATA encodings.
"""
for (opcode, data, sop_idx) in self.raw_iter():
if data is not None:
yield data
else:
opcode = CScriptOp(opcode)
if opcode.is_small_int():
yield opcode.decode_op_n()
else:
yield CScriptOp(opcode)
def __repr__(self):
# For Python3 compatibility add b before strings so testcases don't
# need to change
def _repr(o):
if isinstance(o, bytes):
return b"x('%s')" % hexlify(o).decode('ascii')
else:
return repr(o)
ops = []
i = iter(self)
while True:
op = None
try:
op = _repr(next(i))
except CScriptTruncatedPushDataError as err:
op = '%s...<ERROR: %s>' % (_repr(err.data), err)
break
except CScriptInvalidError as err:
op = '<ERROR: %s>' % err
break
except StopIteration:
break
finally:
if op is not None:
ops.append(op)
return "CScript([%s])" % ', '.join(ops)
def GetSigOpCount(self, fAccurate):
"""Get the SigOp count.
fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details.
Note that this is consensus-critical.
"""
n = 0
lastOpcode = OP_INVALIDOPCODE
for (opcode, data, sop_idx) in self.raw_iter():
if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY):
n += 1
elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY):
if fAccurate and (OP_1 <= lastOpcode <= OP_16):
n += opcode.decode_op_n()
else:
n += 20
lastOpcode = opcode
return n
SIGHASH_ALL = 1
SIGHASH_NONE = 2
SIGHASH_SINGLE = 3
SIGHASH_ANYONECANPAY = 0x80
def FindAndDelete(script, sig):
"""Consensus critical, see FindAndDelete() in Satoshi codebase"""
r = b''
last_sop_idx = sop_idx = 0
skip = True
for (opcode, data, sop_idx) in script.raw_iter():
if not skip:
r += script[last_sop_idx:sop_idx]
last_sop_idx = sop_idx
if script[sop_idx:sop_idx + len(sig)] == sig:
skip = True
else:
skip = False
if not skip:
r += script[last_sop_idx:]
return CScript(r)
def SignatureHash(script, txTo, inIdx, hashtype):
"""Consensus-correct SignatureHash
Returns (hash, err) to precisely match the consensus-critical behavior of
the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity)
"""
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
if inIdx >= len(txTo.vin):
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
txtmp = CTransaction(txTo)
for txin in txtmp.vin:
txin.scriptSig = b''
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
if (hashtype & 0x1f) == SIGHASH_NONE:
txtmp.vout = []
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
outIdx = inIdx
if outIdx >= len(txtmp.vout):
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
tmp = txtmp.vout[outIdx]
txtmp.vout = []
for i in range(outIdx):
txtmp.vout.append(CTxOut())
txtmp.vout.append(tmp)
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
if hashtype & SIGHASH_ANYONECANPAY:
tmp = txtmp.vin[inIdx]
txtmp.vin = []
txtmp.vin.append(tmp)
s = txtmp.serialize()
s += struct.pack(b"<I", hashtype)
hash = hash256(s)
return (hash, None)
# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided.
# Performance optimization probably not necessary for python tests, however.
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
# for version 0 witnesses.
def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount):
hashPrevouts = 0
hashSequence = 0
hashOutputs = 0
if not (hashtype & SIGHASH_ANYONECANPAY):
serialize_prevouts = bytes()
for i in txTo.vin:
serialize_prevouts += i.prevout.serialize()
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_sequence = bytes()
for i in txTo.vin:
serialize_sequence += struct.pack("<I", i.nSequence)
hashSequence = uint256_from_str(hash256(serialize_sequence))
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_outputs = bytes()
for o in txTo.vout:
serialize_outputs += o.serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
serialize_outputs = txTo.vout[inIdx].serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
ss = bytes()
ss += struct.pack("<i", txTo.nVersion)
ss += ser_uint256(hashPrevouts)
ss += ser_uint256(hashSequence)
ss += txTo.vin[inIdx].prevout.serialize()
ss += ser_string(script)
ss += struct.pack("<q", amount)
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
ss += ser_uint256(hashOutputs)
ss += struct.pack("<i", txTo.nLockTime)
ss += struct.pack("<I", hashtype)
return hash256(ss)

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# Copyright (c) 2016-2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Specialized SipHash-2-4 implementations.
This implements SipHash-2-4 for 256-bit integers.
"""
def rotl64(n, b):
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
def siphash_round(v0, v1, v2, v3):
v0 = (v0 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 13)
v1 ^= v0
v0 = rotl64(v0, 32)
v2 = (v2 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 16)
v3 ^= v2
v0 = (v0 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 21)
v3 ^= v0
v2 = (v2 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 17)
v1 ^= v2
v2 = rotl64(v2, 32)
return (v0, v1, v2, v3)
def siphash256(k0, k1, h):
n0 = h & ((1 << 64) - 1)
n1 = (h >> 64) & ((1 << 64) - 1)
n2 = (h >> 128) & ((1 << 64) - 1)
n3 = (h >> 192) & ((1 << 64) - 1)
v0 = 0x736f6d6570736575 ^ k0
v1 = 0x646f72616e646f6d ^ k1
v2 = 0x6c7967656e657261 ^ k0
v3 = 0x7465646279746573 ^ k1 ^ n0
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n0
v3 ^= n1
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n1
v3 ^= n2
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n2
v3 ^= n3
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n3
v3 ^= 0x2000000000000000
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= 0x2000000000000000
v2 ^= 0xFF
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
return v0 ^ v1 ^ v2 ^ v3

View File

@@ -0,0 +1,700 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Helpful routines for regression testing
#
import os
import sys
from binascii import hexlify, unhexlify
from base64 import b64encode
from decimal import Decimal, ROUND_DOWN
import json
import http.client
import random
import shutil
import subprocess
import time
import re
import errno
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
COVERAGE_DIR = None
# The maximum number of nodes a single test can spawn
MAX_NODES = 8
# Don't assign rpc or p2p ports lower than this
PORT_MIN = 11000
# The number of ports to "reserve" for p2p and rpc, each
PORT_RANGE = 5000
NAVCOIND_PROC_WAIT_TIMEOUT = 60
class PortSeed:
# Must be initialized with a unique integer for each process
n = None
#Set Mocktime default to OFF.
#MOCKTIME is only needed for scripts that use the
#cached version of the blockchain. If the cached
#version of the blockchain is used without MOCKTIME
#then the mempools will not sync due to IBD.
MOCKTIME = 0
def enable_mocktime():
#For backwared compatibility of the python scripts
#with previous versions of the cache, set MOCKTIME
#to Jan 1, 2014 + (201 * 10 * 60)
global MOCKTIME
MOCKTIME = 1388534400 + (201 * 10 * 60)
def disable_mocktime():
global MOCKTIME
MOCKTIME = 0
def get_mocktime():
return MOCKTIME
def enable_coverage(dirname):
"""Maintain a log of which RPC calls are made during testing."""
global COVERAGE_DIR
COVERAGE_DIR = dirname
def get_rpc_proxy(url, node_number, timeout=None):
"""
Args:
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
Kwargs:
timeout (int): HTTP timeout in seconds
Returns:
AuthServiceProxy. convenience object for making RPC calls.
"""
proxy_kwargs = {}
if timeout is not None:
proxy_kwargs['timeout'] = timeout
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(
COVERAGE_DIR, node_number) if COVERAGE_DIR else None
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
def p2p_port(n):
assert(n <= MAX_NODES)
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def rpc_port(n):
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def check_json_precision():
"""Make sure json library being used does not lose precision converting NAV values"""
n = Decimal("20000000.00000003")
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
if satoshis != 2000000000000003:
raise RuntimeError("JSON encode/decode loses precision")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def bytes_to_hex_str(byte_str):
return hexlify(byte_str).decode('ascii')
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def sync_blocks(rpc_connections, wait=1, timeout=60):
"""
Wait until everybody has the same tip
"""
while timeout > 0:
tips = [ x.getbestblockhash() for x in rpc_connections ]
if tips == [ tips[0] ]*len(tips):
#if all x.getblockhash() in tips are the same return True
return True
time.sleep(wait)
timeout -= wait
raise AssertionError("Block sync failed")
def sync_mempools(rpc_connections, wait=1, timeout=60):
"""
Wait until everybody has the same transactions in their memory
pools
"""
while timeout > 0:
pool = set(rpc_connections[0].getrawmempool())
num_match = 1
for i in range(1, len(rpc_connections)):
if set(rpc_connections[i].getrawmempool()) == pool:
num_match = num_match+1
if num_match == len(rpc_connections):
return True
time.sleep(wait)
timeout -= wait
raise AssertionError("Mempool sync failed")
navcoind_processes = {}
def initialize_datadir(dirname, n):
datadir = os.path.join(dirname, "node"+str(n))
if not os.path.isdir(datadir):
os.makedirs(datadir)
rpc_u, rpc_p = rpc_auth_pair(n)
with open(os.path.join(datadir, "navcoin.conf"), 'w') as f:
f.write("devnet=1\n")
f.write("rpcuser=" + rpc_u + "\n")
f.write("rpcpassword=" + rpc_p + "\n")
f.write("port="+str(p2p_port(n))+"\n")
f.write("rpcport="+str(rpc_port(n))+"\n")
f.write("listenonion=0\n")
f.write("dandelion=0\n")
f.write("ntpminmeasures=-1\n")
f.write("torserver=0\n")
f.write("suppressblsctwarning=1\n")
return datadir
def rpc_auth_pair(n):
return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n)
def rpc_url(i, rpchost=None):
rpc_u, rpc_p = rpc_auth_pair(i)
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, rpchost or '127.0.0.1', rpc_port(i))
def wait_for_navcoind_start(process, url, i):
'''
Wait for navcoind to start. This means that RPC is accessible and fully initialized.
Raise an exception if navcoind exits during initialization.
'''
polls_interval = 1.0 / 4
runtime = 60
while runtime > 0:
if process.poll() is not None:
raise Exception('navcoind exited with status %i during initialization' % process.returncode)
try:
# print('Checking RPC')
rpc = get_rpc_proxy(url, i)
blocks = rpc.getblockcount()
# print('RPC replied with blocks: %i' % blocks)
return # break out of loop on success
except IOError as e:
if e.errno != errno.ECONNREFUSED: # Port not yet open?
raise # unknown IO error
# else:
# print('Waiting for port')
except JSONRPCException as e: # Initialization phase
if e.error['code'] != -28: # RPC in warmup?
raise # unkown JSON RPC exception
# else:
# print('RPC in warmup')
time.sleep(polls_interval)
runtime -= polls_interval
raise Exception('navcoind RPC timeout')
def initialize_chain(test_dir, num_nodes):
"""
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
Afterward, create num_nodes copies from the cache
"""
assert num_nodes <= MAX_NODES
create_cache = False
for i in range(MAX_NODES):
if not os.path.isdir(os.path.join('cache', 'node'+str(i))):
create_cache = True
break
if create_cache:
#find and delete old cache directories if any exist
for i in range(MAX_NODES):
if os.path.isdir(os.path.join("cache","node"+str(i))):
shutil.rmtree(os.path.join("cache","node"+str(i)))
# Create cache directories, run navcoinds:
for i in range(MAX_NODES):
datadir=initialize_datadir("cache", i)
args = [ os.getenv("NAVCOIND", "navcoind"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ]
if i > 0:
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
navcoind_processes[i] = subprocess.Popen(args)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: navcoind started, waiting for RPC to come up")
wait_for_navcoind_start(navcoind_processes[i], rpc_url(i), i)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: RPC succesfully started")
rpcs = []
for i in range(MAX_NODES):
try:
rpcs.append(get_rpc_proxy(rpc_url(i), i))
except:
sys.stderr.write("Error connecting to "+url+"\n")
sys.exit(1)
# Create a 200-block-long chain; each of the 4 first nodes
# gets 25 mature blocks and 25 immature.
# Note: To preserve compatibility with older versions of
# initialize_chain, only 4 nodes will generate coins.
#
# blocks are created with timestamps 10 minutes apart
# starting from 2010 minutes in the past
enable_mocktime()
block_time = get_mocktime() - (201 * 10 * 60)
for i in range(2):
for peer in range(4):
for j in range(25):
set_node_times(rpcs, block_time)
slow_gen(rpcs[peer], 1)
block_time += 10*60
# Must sync before next peer starts generating blocks
sync_blocks(rpcs)
# Shut them down, and clean up cache directories:
stop_nodes(rpcs)
wait_navcoinds()
disable_mocktime()
for i in range(MAX_NODES):
os.remove(log_filename("cache", i, "debug.log"))
os.remove(log_filename("cache", i, "db.log"))
os.remove(log_filename("cache", i, "peers.dat"))
os.remove(log_filename("cache", i, "fee_estimates.dat"))
for i in range(num_nodes):
from_dir = os.path.join("cache", "node"+str(i))
to_dir = os.path.join(test_dir, "node"+str(i))
shutil.copytree(from_dir, to_dir)
initialize_datadir(test_dir, i) # Overwrite port/rpcport in navcoin.conf
def initialize_chain_clean(test_dir, num_nodes):
"""
Create an empty blockchain and num_nodes wallets.
Useful if a test case wants complete control over initialization.
"""
for i in range(num_nodes):
datadir=initialize_datadir(test_dir, i)
def _rpchost_to_args(rpchost):
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
if rpchost is None:
return []
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
if not match:
raise ValueError('Invalid RPC host spec ' + rpchost)
rpcconnect = match.group(1)
rpcport = match.group(2)
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
rpcconnect = rpcconnect[1:-1]
rv = ['-rpcconnect=' + rpcconnect]
if rpcport:
rv += ['-rpcport=' + rpcport]
return rv
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
"""
Start a navcoind and return RPC connection to it
"""
datadir = os.path.join(dirname, "node"+str(i))
if binary is None:
binary = os.getenv("NAVCOIND", "navcoind")
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-mocktime="+str(get_mocktime()) ]
if extra_args is not None: args.extend(extra_args)
navcoind_processes[i] = subprocess.Popen(args)
if os.getenv("PYTHON_DEBUG", ""):
print("start_node: navcoind started, waiting for RPC to come up")
url = rpc_url(i, rpchost)
wait_for_navcoind_start(navcoind_processes[i], url, i)
if os.getenv("PYTHON_DEBUG", ""):
print("start_node: RPC succesfully started")
proxy = get_rpc_proxy(url, i, timeout=timewait)
if COVERAGE_DIR:
coverage.write_all_rpc_commands(COVERAGE_DIR, proxy)
return proxy
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None):
"""
Start multiple navcoinds, return RPC connections to them
"""
if extra_args is None: extra_args = [ None for _ in range(num_nodes) ]
if binary is None: binary = [ None for _ in range(num_nodes) ]
rpcs = []
try:
for i in range(num_nodes):
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, binary=binary[i]))
except: # If one node failed to start, stop the others
stop_nodes(rpcs)
raise
return rpcs
def log_filename(dirname, n_node, logname):
return os.path.join(dirname, "node"+str(n_node), "devnet", logname)
def stop_node(node, i):
try:
node.stop()
except http.client.CannotSendRequest as e:
print("WARN: Unable to stop node: " + repr(e))
navcoind_processes[i].wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT)
del navcoind_processes[i]
def stop_nodes(nodes):
for node in nodes:
try:
node.stop()
except http.client.CannotSendRequest as e:
print("WARN: Unable to stop node: " + repr(e))
del nodes[:] # Emptying array closes connections as a side effect
def set_node_times(nodes, t):
for node in nodes:
node.setmocktime(t)
def wait_navcoinds():
# Wait for all navcoinds to cleanly exit
for navcoind in navcoind_processes.values():
navcoind.wait(timeout=NAVCOIND_PROC_WAIT_TIMEOUT)
navcoind_processes.clear()
def connect_nodes(from_connection, node_num):
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()):
time.sleep(0.1)
def connect_nodes_bi(nodes, a, b):
connect_nodes(nodes[a], b)
connect_nodes(nodes[b], a)
def find_output(node, txid, amount):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
def gather_inputs(from_node, amount_needed, confirmations_required=1):
"""
Return a random set of unspent txouts that are enough to pay amount_needed
"""
assert(confirmations_required >=0)
utxo = from_node.listunspent(confirmations_required)
random.shuffle(utxo)
inputs = []
total_in = Decimal("0.00000000")
while total_in < amount_needed and len(utxo) > 0:
t = utxo.pop()
total_in += t["amount"]
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
if total_in < amount_needed:
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in))
return (total_in, inputs)
def make_change(from_node, amount_in, amount_out, fee):
"""
Create change output(s), return them
"""
outputs = {}
amount = amount_out+fee
change = amount_in - amount
if change > amount*2:
# Create an extra change output to break up big inputs
change_address = from_node.getnewaddress()
# Split change in two, being careful of rounding:
outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
change = amount_in - amount - outputs[change_address]
if change > 0:
outputs[from_node.getnewaddress()] = change
return outputs
def send_zeropri_transaction(from_node, to_node, amount, fee):
"""
Create&broadcast a zero-priority transaction.
Returns (txid, hex-encoded-txdata)
Ensures transaction is zero-priority by first creating a send-to-self,
then using its output
"""
# Create a send-to-self with confirmed inputs:
self_address = from_node.getnewaddress()
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
outputs = make_change(from_node, total_in, amount+fee, fee)
outputs[self_address] = float(amount+fee)
self_rawtx = from_node.createrawtransaction(inputs, outputs)
self_signresult = from_node.signrawtransaction(self_rawtx)
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
vout = find_output(from_node, self_txid, amount+fee)
# Now immediately spend the output to create a 1-input, 1-output
# zero-priority transaction:
inputs = [ { "txid" : self_txid, "vout" : vout } ]
outputs = { to_node.getnewaddress() : float(amount) }
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"])
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random zero-priority transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
return (txid, txhex, fee)
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(total_in, inputs) = gather_inputs(from_node, amount+fee)
outputs = make_change(from_node, total_in, amount, fee)
outputs[to_node.getnewaddress()] = float(amount)
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"], fee)
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = tx_size * fee_per_kB / 1000
if fee < target_fee:
raise AssertionError("Fee of %s NAV too low! (Should be %s NAV)"%(str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
if fee > (tx_size + 2) * fee_per_kB / 1000:
raise AssertionError("Fee of %s NAV too high! (Should be %s NAV)"%(str(fee), str(target_fee)))
def assert_equal(thing1, thing2):
if thing1 != thing2:
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
def assert_greater_than(thing1, thing2):
if thing1 <= thing2:
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
def assert_raises(exc, fun, *args, **kwds):
try:
fun(*args, **kwds)
except exc:
pass
except Exception as e:
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
else:
raise AssertionError("No exception raised")
def assert_is_hex_string(string):
try:
int(string, 16)
except Exception as e:
raise AssertionError(
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
def assert_is_hash_string(string, length=64):
if not isinstance(string, str):
raise AssertionError("Expected a string, got type %r" % type(string))
elif length and len(string) != length:
raise AssertionError(
"String of length %d expected; got %d" % (length, len(string)))
elif not re.match('[abcdef0-9]+$', string):
raise AssertionError(
"String %r contains invalid characters for a hash." % string)
def assert_array_result(object_array, to_match, expected, should_not_find = False):
"""
Pass in array of JSON objects, a dictionary with key/value pairs
to match against, and another dictionary with expected key/value
pairs.
If the should_not_find flag is true, to_match should not be found
in object_array
"""
if should_not_find == True:
assert_equal(expected, { })
num_matched = 0
for item in object_array:
all_match = True
for key,value in to_match.items():
if item[key] != value:
all_match = False
if not all_match:
continue
elif should_not_find == True:
num_matched = num_matched+1
for key,value in expected.items():
if item[key] != value:
raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value)))
num_matched = num_matched+1
if num_matched == 0 and should_not_find != True:
raise AssertionError("No objects matched %s"%(str(to_match)))
if num_matched > 0 and should_not_find == True:
raise AssertionError("Objects were found %s"%(str(to_match)))
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
and verifies that the error code and message are as expected. Throws AssertionError if
no JSONRPCException was raised or if the error code/message are not as expected.
Args:
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required.
fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
def try_rpc(code, message, fun, *args, **kwds):
"""Tries to run an rpc command.
Test against error code and message if the rpc fails.
Returns whether a JSONRPCException was raised."""
try:
fun(*args, **kwds)
except JSONRPCException as e:
# JSONRPCException was thrown as expected. Check the code and message values are correct.
if (code is not None) and (code != e.error["code"]):
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
if (message is not None) and (message not in e.error['message']):
raise AssertionError("Expected substring not found:" + e.error['message'])
return True
except Exception as e:
raise AssertionError("Unexpected exception raised: " + type(e).__name__)
else:
return False
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
# Helper to create at least "count" utxos
# Pass in a fee that is sufficient for relay and mining new transactions.
def create_confirmed_utxos(fee, node, count):
node.generate(int(0.5*count)+101)
utxos = node.listunspent()
iterations = count - len(utxos)
addr1 = node.getnewaddress()
addr2 = node.getnewaddress()
if iterations <= 0:
return utxos
for i in range(iterations):
t = utxos.pop()
inputs = []
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr1] = satoshi_round(send_value/2)
outputs[addr2] = satoshi_round(send_value/2)
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransaction(raw_tx)["hex"]
txid = node.sendrawtransaction(signed_tx)
while (node.getmempoolinfo()['size'] > 0):
node.generate(1)
utxos = node.listunspent()
assert(len(utxos) >= count)
return utxos
# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions).
def gen_return_txouts():
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
# So we have big transactions (and therefore can't fit very many into each block)
# create one script_pubkey
script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes
for i in range (512):
script_pubkey = script_pubkey + "01"
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
txouts = "81"
for k in range(128):
# add txout value
txouts = txouts + "0000000000000000"
# add length of script_pubkey
txouts = txouts + "fd0402"
# add script_pubkey
txouts = txouts + script_pubkey
return txouts
def create_tx(node, coinbase, to_address, amount):
inputs = [{ "txid" : coinbase, "vout" : 0}]
outputs = { to_address : amount }
rawtx = node.createrawtransaction(inputs, outputs)
signresult = node.signrawtransaction(rawtx)
assert_equal(signresult["complete"], True)
return signresult["hex"]
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above.
def create_lots_of_big_transactions(node, txouts, utxos, fee):
addr = node.getnewaddress()
txids = []
for i in range(len(utxos)):
t = utxos.pop()
inputs = []
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr] = satoshi_round(send_value)
rawtx = node.createrawtransaction(inputs, outputs)
newtx = rawtx[0:92]
newtx = newtx + txouts
newtx = newtx + rawtx[94:]
signresult = node.signrawtransaction(newtx, None, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], True)
txids.append(txid)
return txids
def get_bip9_status(node, key):
info = node.getblockchaininfo()
return info['bip9_softforks'][key]
def slow_gen(node, count, sleep = 0.1):
total = count
blocks = []
while total > 0:
now = min(total, 10)
blocks.extend(node.generate(now))
total -= now
time.sleep(sleep)
return blocks

View File

@@ -51,9 +51,6 @@ MSG_TYPE_MASK = 0xffffffff >> 2
def sha256(s):
return hashlib.new('sha256', s).digest()
def ripemd160(s):
return hashlib.new('ripemd160', s).digest()
def hash256(s):
return sha256(sha256(s))

View File

@@ -1,17 +1,21 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# 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.
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from mnemonic import Mnemonic
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,62 @@ 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
def seedToMnemonic(self, key):
return Mnemonic('english').to_mnemonic(key)
self._wallet_v20_compatible = (
False
if not swap_client
else swap_client.getChainClientSettings(self.coin_type()).get(
"wallet_v20_compatible", False
)
)
def initialiseWallet(self, key):
words = self.seedToMnemonic(key)
mnemonic_passphrase = ''
self.rpc_callback('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
self._have_checked_seed = False
if self._wallet_passphrase != '':
self.unlockWallet(self._wallet_passphrase)
def decodeAddress(self, address):
def decodeAddress(self, address: str) -> bytes:
return decodeAddress(address)[1:]
def checkExpectedSeed(self, key_hash):
try:
rv = self.rpc_callback('dumphdinfo')
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
def getWalletSeedID(self) -> str:
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)
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.")
words = self.entropyToMnemonic(key_bytes)
mnemonic_passphrase = ""
self.rpc_wallet(
"upgradetohd", [words, mnemonic_passphrase, self._wallet_passphrase]
)
self._have_checked_seed = False
if self._wallet_passphrase != "":
self.unlockWallet(self._wallet_passphrase)
return
key_wif = self.encodeKey(key_bytes)
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(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
self._have_checked_seed = True
return entropy_hash == key_hash
except Exception as e:
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
return False
have_expected_seed: bool = expect_seedid == entropy_hash
else:
have_expected_seed: bool = expect_seedid == self.getWalletSeedID()
self._have_checked_seed = True
return have_expected_seed
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
return self.rpc_callback('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_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc_wallet("getwalletinfo")["balance"])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
@@ -66,29 +91,38 @@ 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_callback('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
def unlockWallet(self, password: str):
super().unlockWallet(password)
# Store password for initialiseWallet
self._wallet_passphrase = password
if self._wallet_v20_compatible:
# Store password for initialiseWallet
self._wallet_passphrase = password
if not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
try:
self._sc.checkWalletSeed(self.coin_type())
except Exception as ex:
# dumphdinfo can fail if the wallet is not initialised
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
def lockWallet(self):
super().lockWallet()
self._wallet_passphrase = ''
self._wallet_passphrase = ""

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
from enum import IntEnum
from basicswap.util.crypto import blake256
from basicswap.util.integer import decode_compactsize, encode_compactsize
class TxSerializeType(IntEnum):
Full = 0
NoWitness = 1
OnlyWitness = 2
class SigHashType(IntEnum):
SigHashAll = 0x1
SigHashNone = 0x2
SigHashSingle = 0x3
SigHashAnyOneCanPay = 0x80
SigHashMask = 0x1F
class SignatureType(IntEnum):
STEcdsaSecp256k1 = 0
STEd25519 = 1
STSchnorrSecp256k1 = 2
class COutPoint:
__slots__ = ("hash", "n", "tree")
def __init__(self, hash=0, n=0, tree=0):
self.hash = hash
self.n = n
self.tree = tree
def get_hash(self) -> bytes:
return self.hash.to_bytes(32, "big")
class CTxIn:
__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.signature_script = bytes()
class CTxOut:
__slots__ = ("value", "version", "script_pubkey")
def __init__(self, value=0, script_pubkey=bytes()):
self.value = value
self.version = 0
self.script_pubkey = script_pubkey
class CTransaction:
__slots__ = ("hash", "version", "vin", "vout", "locktime", "expiry")
def __init__(self, tx=None):
if tx is None:
self.version = 1
self.vin = []
self.vout = []
self.locktime = 0
self.expiry = 0
else:
self.version = tx.version
self.vin = copy.deepcopy(tx.vin)
self.vout = copy.deepcopy(tx.vout)
self.locktime = tx.locktime
self.expiry = tx.expiry
def deserialize(self, data: bytes) -> None:
version = int.from_bytes(data[:4], "little")
self.version = version & 0xFFFF
ser_type: int = version >> 16
o = 4
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
num_txin, nb = decode_compactsize(data, o)
o += nb
for i in range(num_txin):
txi = CTxIn()
txi.prevout = COutPoint()
txi.prevout.hash = int.from_bytes(data[o : o + 32], "little")
o += 32
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")
o += 4
self.vin.append(txi)
num_txout, nb = decode_compactsize(data, o)
o += nb
for i in range(num_txout):
txo = CTxOut()
txo.value = int.from_bytes(data[o : o + 8], "little")
o += 8
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]
o += script_bytes
self.vout.append(txo)
self.locktime = int.from_bytes(data[o : o + 4], "little")
o += 4
self.expiry = int.from_bytes(data[o : o + 4], "little")
o += 4
if ser_type == TxSerializeType.NoWitness:
return
num_wit_scripts, nb = decode_compactsize(data, o)
o += nb
if ser_type == TxSerializeType.OnlyWitness:
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")
for i in range(num_wit_scripts):
txi = self.vin[i]
txi.value_in = int.from_bytes(data[o : o + 8], "little")
o += 8
txi.block_height = int.from_bytes(data[o : o + 4], "little")
o += 4
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]
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")
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 += 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 += encode_compactsize(len(txo.script_pubkey))
data += txo.script_pubkey
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")
data += encode_compactsize(len(txi.signature_script))
data += txi.signature_script
return data
def TxHash(self) -> bytes:
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
def TxHashWitness(self) -> bytes:
raise ValueError("todo")
def TxHashFull(self) -> bytes:
raise ValueError("todo")
def findOutput(tx, script_pk: bytes):
for i in range(len(tx.vout)):
if tx.vout[i].script_pubkey == script_pk:
return i
return None

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import traceback
from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
try:
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"))
except Exception as ex:
traceback.print_exc()
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"]))
return r["result"]
def openrpc(rpc_port, auth, host="127.0.0.1"):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError("RPC error " + str(ex))
def make_rpc_func(port, auth, host="127.0.0.1"):
port = port
auth = auth
host = host
def rpc_func(method, params=None):
nonlocal port, auth, host
return callrpc(port, auth, method, params, host)
return rpc_func

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
OP_0 = 0x00
OP_DATA_1 = 0x01
OP_1NEGATE = 0x4F
OP_1 = 0x51
OP_IF = 0x63
OP_ELSE = 0x67
OP_ENDIF = 0x68
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
def push_script_data(data_array: bytearray, data: bytes) -> None:
len_data: int = len(data)
if len_data == 0 or (len_data == 1 and data[0] == 0):
data_array += bytes((OP_0,))
return
if len_data == 1 and data[0] <= 16:
data_array += bytes((OP_1 - 1 + data[0],))
return
if len_data == 1 and data[0] == 0x81:
data_array += bytes((OP_1NEGATE,))
return
if len_data < OP_PUSHDATA1:
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")
else:
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, "little")
data_array += data

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import select
import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info("Creating DCR wallet")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt":
str_args = " ".join(args)
p = subprocess.Popen(
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
)
else:
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
def readOutput():
buf = os.read(pipe_r, 1024).decode("utf-8")
response = None
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:
pass
elif "Upgrading database from version" in buf:
pass
elif "Ticket commitments db upgrade done" in buf:
pass
elif "The wallet has been created successfully" in buf:
pass
else:
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":
readOutput()
delay_event.wait(0.1)
continue
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
readOutput()
delay_event.wait(0.1)
except Exception as e:
logging.error(f"dcrwallet --create failed: {e}")
finally:
if p.poll() is None:
p.terminate()
os.close(pipe_r)
os.close(pipe_w)
p.stdin.close()

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

@@ -1,26 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# 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.
import hashlib
from .btc import BTCInterface, find_vout_for_address_from_txobj
from basicswap.chainparams import Coins
import random
from .btc import BTCInterface, find_vout_for_address_from_txobj
from basicswap.util import (
i2b,
ensure,
)
from basicswap.rpc import make_rpc_func
from basicswap.util.crypto import hash160
from basicswap.util.address import decodeAddress
from basicswap.contrib.test_framework.script import (
from basicswap.chainparams import Coins
from basicswap.interface.contrib.firo_test_framework.script import (
CScript,
OP_0,
OP_DUP,
OP_EQUAL,
OP_HASH160,
OP_CHECKSIG,
OP_EQUALVERIFY,
hash160,
)
from basicswap.contrib.test_framework.messages import (
from basicswap.interface.contrib.firo_test_framework.mininode import (
CBlock,
FromHex,
CTransaction,
)
@@ -30,96 +38,135 @@ class FIROInterface(BTCInterface):
def coin_type():
return Coins.FIRO
def getExchangeName(self, exchange_name):
return 'zcoin'
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
)
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
def getExchangeName(self, exchange_name: str) -> str:
return "zcoin"
def initialiseWallet(self, key):
# load with -hdseed= parameter
pass
def getNewAddress(self, use_segwit, label='swap_receive'):
return self.rpc_callback('getnewaddress', [label])
# addr_plain = self.rpc_callback('getnewaddress', [label])
# return self.rpc_callback('addwitnessaddress', [addr_plain])
def checkWallets(self) -> int:
return 1
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])
def decodeAddress(self, address):
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_callback('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_callback('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):
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest)
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc_callback('validateaddress', [address])
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True])
return address
def getLockTxHeightFiro(self, txid, lock_script, bid_amount, rescan_from, find_index=False):
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
# Add watchonly address and rescan if required
lock_tx_dest = self.getScriptDest(lock_script)
dest_address = self.encodeScriptDest(lock_tx_dest)
if not self.isAddressMine(dest_address, or_watch_only=True):
self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.rpc_callback('rescanblockchain', [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_callback('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_callback('gettransaction', [txid.hex()])
tx = self.rpc("gettransaction", [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc_callback('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_callback('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)))
@@ -130,80 +177,278 @@ class FIROInterface(BTCInterface):
return self.fundTx(tx_bytes, feerate)
def signTxWithWallet(self, tx):
rv = self.rpc_callback('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_callback('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_callback('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_callback('signrawtransaction', [txn_funded])['hex']
return self.rpc("signrawtransaction", [txn_funded])["hex"]
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptDest(self, script: bytearray) -> bytearray:
# P2WSH nested in BIP16_P2SH
# P2SH
script_hash = hashlib.sha256(script).digest()
assert len(script_hash) == 32
script_hash_hash = hash160(script_hash)
assert len(script_hash_hash) == 20
script_hash = hash160(script)
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash_hash, OP_EQUAL])
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def getSeedHash(self, seed) -> bytes:
def getSeedHash(self, seed: bytes) -> bytes:
return hash160(seed)[::-1]
def encodeScriptDest(self, script):
def encodeScriptDest(self, script_dest: bytes) -> str:
# Extract hash from script
script_hash = script[2:-1]
script_hash = script_dest[2:-1]
return self.sh_to_address(script_hash)
def getScriptScriptSig(self, script):
return CScript([OP_0, hashlib.sha256(script).digest()])
def getDestForScriptHash(self, script_hash):
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee]
return self.rpc_callback('sendtoaddress', params)
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getWalletSeedID(self):
return self.rpc_callback('getwalletinfo')['hdmasterkeyid']
return self.rpc("getwalletinfo")["hdmasterkeyid"]
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('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_callback('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_callback('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")
for u in unspents:
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"])
)
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data["total"] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
utxos_req += 1
if sum_value >= amount_for:
break
if utxos_req <= max_utxos:
viable_addrs.append(addr)
continue
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)
prove_utxos = []
sorted_utxos = sorted(
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
)
hasher = hashlib.sha256()
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
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
# '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)
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)
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
hasher = hashlib.sha256()
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
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")
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"])
return sum_value
def rescanBlockchainForAddress(self, height_start: int, addr_find: str):
# Very ugly workaround for missing `rescanblockchain` rpc command
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
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 = self.rpc("getblock", [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
for txo in tx.vout:
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])
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])
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_rv.append(tx_dec)
block_rv = {
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv

View File

@@ -1,15 +1,160 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert
# 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.
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins, chainparams
class LTCInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
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 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)
def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance()
if spendable_balance < value_sats:
raise ValueError("Balance too low")
address = self.getNewAddress(self._use_segwit, "create_utxo")
return (
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
address,
)
def 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"]
return rv
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet("listunspent")
for u in unspent:
if u.get("spendable", False) is False:
continue
if u.get("solvable", False) is False: # Filter out mweb outputs
continue
if "address" not in u:
continue
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
continue
else:
if not desc.startswith("wpkh"):
continue
else:
if not desc.startswith("pkh"):
continue
unspent_addr[u["address"]] = unspent_addr.get(
u["address"], 0
) + self.make_int(u["amount"], r=1)
return unspent_addr
class LTCInterfaceMWEB(LTCInterface):
def interface_type(self) -> int:
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
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]
def chainparams_network(self):
return chainparams[Coins.LTC][self._network]
def coin_name(self) -> str:
coin_chainparams = chainparams[Coins.LTC]
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"
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")
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(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])
if password is not None:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
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"])
if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str):
if password == "":
return
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._sc.checkWalletSeed(self.coin_type())

998
basicswap/interface/nav.py Normal file
View File

@@ -0,0 +1,998 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 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.
import random
import hashlib
from io import BytesIO
from coincurve.keys import (
PublicKey,
PrivateKey,
)
from basicswap.interface.btc import (
BTCInterface,
extractScriptLockRefundScriptValues,
findOutput,
find_vout_for_address_from_txobj,
)
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.contrib.mnemonic import Mnemonic
from basicswap.interface.contrib.nav_test_framework.mininode import (
CTxIn,
CTxOut,
CBlock,
COutPoint,
CTransaction,
CTxInWitness,
FromHex,
)
from basicswap.util.crypto import hash160
from basicswap.util.address import (
decodeWif,
pubkeyToAddress,
encodeAddress,
)
from basicswap.util import (
b2i,
i2b,
ensure,
)
from basicswap.basicswap_util import (
getVoutByScriptPubKey,
)
from basicswap.interface.contrib.nav_test_framework.script import (
CScript,
OP_0,
OP_EQUAL,
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
OP_CHECKSIG,
SIGHASH_ALL,
SegwitVersion1SignatureHash,
)
class NAVInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.NAV
@staticmethod
def txVersion() -> int:
return 3
@staticmethod
def txoType():
return CTxOut
def __init__(self, coin_settings, network, swap_client=None):
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
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 use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return True
def initialiseWallet(self, key):
# Load with -importmnemonic= parameter
pass
def checkWallets(self) -> int:
return 1
def getWalletSeedID(self):
return self.rpc("getwalletinfo")["hdmasterkeyid"]
def withdrawCoin(self, value, addr_to: str, subfee: bool):
strdzeel = ""
params = [addr_to, value, "", "", strdzeel, subfee]
return self.rpc("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc("getwalletinfo")["balance"])
def signTxWithWallet(self, tx: bytes) -> bytes:
rv = self.rpc("signrawtransaction", [tx.hex()])
return bytes.fromhex(rv["hex"])
def checkExpectedSeed(self, key_hash: str):
try:
rv = self.rpc("dumpmnemonic")
entropy = Mnemonic("english").to_entropy(rv.split(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
self._have_checked_seed = True
return entropy_hash == key_hash
except Exception as e:
self._log.warning("checkExpectedSeed failed: {}".format(str(e)))
return False
def getScriptForP2PKH(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2SH-p2wpkh
script = CScript([OP_0, pkh])
script_hash = hash160(script)
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def getInputScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
script = CScript([OP_0, pkh])
return bytes((len(script),)) + script
def encodeSegwitAddress(self, pkh: bytes) -> str:
# P2SH-p2wpkh
script = CScript([OP_0, pkh])
script_hash = hash160(script)
assert len(script_hash) == 20
return encodeAddress(
bytes((self.chainparams_network()["script_address"],)) + script_hash
)
def encodeSegwitAddressScript(self, script: bytes) -> str:
if (
len(script) == 23
and script[0] == OP_HASH160
and script[1] == 20
and script[22] == OP_EQUAL
):
script_hash = script[2:22]
return encodeAddress(
bytes((self.chainparams_network()["script_address"],)) + script_hash
)
raise ValueError("Unknown Script")
def loadTx(self, tx_bytes: bytes) -> CTransaction:
# Load tx from bytes to internal representation
tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes))
return tx
def signTx(
self,
key_bytes: bytes,
tx_bytes: bytes,
input_n: int,
prevout_script,
prevout_value: int,
):
tx = self.loadTx(tx_bytes)
sig_hash = SegwitVersion1SignatureHash(
prevout_script, tx, input_n, SIGHASH_ALL, prevout_value
)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear()
tx.wit.vtxinwit.append(CTxInWitness())
tx.wit.vtxinwit[0].scriptWitness.stack = stack
return tx.serialize_with_witness()
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")
for u in unspents:
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"])
)
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data["total"] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
utxos_req += 1
if sum_value >= amount_for:
break
if utxos_req <= max_utxos:
viable_addrs.append(addr)
continue
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)
prove_utxos = []
sorted_utxos = sorted(
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
)
hasher = hashlib.sha256()
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
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
# 'Address does not refer to key' for non p2pkh
addr_info = self.rpc(
"validateaddress",
[
addr,
],
)
if "isscript" in addr_info and addr_info["isscript"] and "hex" in addr_info:
pkh = bytes.fromhex(addr_info["hex"])[2:]
sign_for_addr = self.pkh_to_address(pkh)
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(),
],
)
return (sign_for_addr, signature, prove_utxos)
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
hasher = hashlib.sha256()
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
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")
if self.using_segwit():
address = self.encodeSegwitAddress(self.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"])
return sum_value
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}"
)
if sub_fee:
raise ValueError(
"Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter"
)
# options['subtractFeeFromOutputs'] = [0,]
fee_rate = self.make_int(fee_rate, r=1)
return self.fundTx(txn, fee_rate, lock_unspents).hex()
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc("validateaddress", [address])
if not or_watch_only:
return addr_info["ismine"]
return addr_info["ismine"] or addr_info["iswatchonly"]
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc("signrawtransaction", [txn_funded])["hex"]
def getBlockchainInfo(self):
rv = self.rpc("getblockchaininfo")
synced = round(rv["verificationprogress"], 3)
if synced >= 0.997:
rv["verificationprogress"] = 1.0
return rv
def encodeScriptDest(self, script_dest: bytes) -> str:
script_hash = script_dest[2:-1] # Extract hash from script
return self.sh_to_address(script_hash)
def encode_p2wsh(self, script: bytes) -> str:
return pubkeyToAddress(self.chainparams_network()["script_address"], script)
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
txjs = self.rpc("decoderawtransaction", [txn_hex])
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
return {
"txid": txjs["txid"],
"vout": n,
"scriptPubKey": txjs["vout"][n]["scriptPubKey"]["hex"],
"redeemScript": txn_script.hex(),
"amount": txjs["vout"][n]["value"],
}
def getNewAddress(self, use_segwit: bool, label: str = "swap_receive") -> str:
address: str = self.rpc(
"getnewaddress",
[
label,
],
)
if use_segwit:
return self.rpc(
"addwitnessaddress",
[
address,
],
)
return address
def createRedeemTxn(
self, prevout, output_addr: str, output_value: int, txn_script: bytes
) -> str:
tx = CTransaction()
tx.nVersion = self.txVersion()
prev_txid = b2i(bytes.fromhex(prevout["txid"]))
tx.vin.append(
CTxIn(
COutPoint(prev_txid, prevout["vout"]),
scriptSig=self.getScriptScriptSig(txn_script),
)
)
pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh)
tx.vout.append(self.txoType()(output_value, script))
tx.rehash()
return tx.serialize().hex()
def createRefundTxn(
self,
prevout,
output_addr: str,
output_value: int,
locktime: int,
sequence: int,
txn_script: bytes,
) -> str:
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.nLockTime = locktime
prev_txid = b2i(bytes.fromhex(prevout["txid"]))
tx.vin.append(
CTxIn(
COutPoint(prev_txid, prevout["vout"]),
nSequence=sequence,
scriptSig=self.getScriptScriptSig(txn_script),
)
)
pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh)
tx.vout.append(self.txoType()(output_value, script))
tx.rehash()
return tx.serialize().hex()
def getTxSignature(self, tx_hex: str, prevout_data, key_wif: str) -> str:
key = decodeWif(key_wif)
redeem_script = bytes.fromhex(prevout_data["redeemScript"])
sig = self.signTx(
key,
bytes.fromhex(tx_hex),
0,
redeem_script,
self.make_int(prevout_data["amount"]),
)
return sig.hex()
def verifyTxSig(
self,
tx_bytes: bytes,
sig: bytes,
K: bytes,
input_n: int,
prevout_script: bytes,
prevout_value: int,
) -> bool:
tx = self.loadTx(tx_bytes)
sig_hash = SegwitVersion1SignatureHash(
prevout_script, tx, input_n, SIGHASH_ALL, prevout_value
)
pubkey = PublicKey(K)
return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte
def verifyRawTransaction(self, tx_hex: str, prevouts):
# Only checks signature
# verifyrawtransaction
self._log.warning("NAV verifyRawTransaction only checks signature")
inputs_valid: bool = False
validscripts: int = 0
tx_bytes = bytes.fromhex(tx_hex)
tx = self.loadTx(bytes.fromhex(tx_hex))
signature = tx.wit.vtxinwit[0].scriptWitness.stack[0]
pubkey = tx.wit.vtxinwit[0].scriptWitness.stack[1]
input_n: int = 0
prevout_data = prevouts[input_n]
redeem_script = bytes.fromhex(prevout_data["redeemScript"])
prevout_value = self.make_int(prevout_data["amount"])
if self.verifyTxSig(
tx_bytes, signature, pubkey, input_n, redeem_script, prevout_value
):
validscripts += 1
# TODO: validate inputs
inputs_valid = True
return {
"inputs_valid": inputs_valid,
"validscripts": validscripts,
}
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
tx_vsize = (
5 # Add a few bytes, sequence in script takes variable amount of bytes
)
tx_vsize += 184 if redeem else 187
return tx_vsize
def getTxid(self, tx) -> bytes:
if isinstance(tx, str):
tx = bytes.fromhex(tx)
if isinstance(tx, bytes):
tx = self.loadTx(tx)
tx.rehash()
return i2b(tx.sha256)
def rescanBlockchainForAddress(self, height_start: int, addr_find: str):
# Very ugly workaround for missing `rescanblockchain` rpc command
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
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 = self.rpc("getblock", [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
for txo in tx.vout:
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])
return
current_height -= 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(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,
],
],
)
for tx in txns:
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()])
block_height = 0
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,
}
except Exception as 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)
if return_txid:
rv["txid"] = txid.hex()
return rv
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
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_rv.append(tx_dec)
block_rv = {
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
def getScriptScriptSig(self, script: bytes) -> bytes:
return self.getP2SHP2WSHScriptSig(script)
def getScriptDest(self, script):
return self.getP2SHP2WSHDest(script)
def getDestForScriptHash(self, script_hash):
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def pubkey_to_segwit_address(self, pk: bytes) -> str:
pkh = hash160(pk)
script_out = self.getScriptForPubkeyHash(pkh)
return self.encodeSegwitAddressScript(script_out)
def createBLockTx(self, Kbs: bytes, output_amount: int, vkbv=None) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
script_pk = self.getPkDest(Kbs)
tx.vout.append(self.txoType()(output_amount, script_pk))
return tx.serialize()
def spendBLockTx(
self,
chain_b_lock_txid: bytes,
address_to: str,
kbv: bytes,
kbs: bytes,
cb_swap_value: int,
b_fee: int,
restore_height: int,
spend_actual_balance: bool = False,
lock_tx_vout=None,
) -> bytes:
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
wtx = self.rpc(
"gettransaction",
[
chain_b_lock_txid.hex(),
],
)
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs)
locked_n = findOutput(lock_tx, script_pk)
ensure(locked_n is not None, "Output not found in tx")
pkh_to = self.decodeAddress(address_to)
tx = CTransaction()
tx.nVersion = self.txVersion()
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
tx.vin.append(
CTxIn(
COutPoint(chain_b_lock_txid_int, locked_n),
nSequence=0,
scriptSig=script_sig,
)
)
tx.vout.append(
self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to))
)
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
tx.vout[0].nValue = cb_swap_value - pay_fee
b_lock_spend_tx = tx.serialize()
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs, cb_swap_value)
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount: int) -> bytes:
Key = self.getPubkey(key)
pkh = self.getPubkeyHash(Key)
script = self.getScriptForP2PKH(pkh)
sig = self.signTx(key, tx, 0, script, prev_amount)
stack = [
sig,
Key,
]
return self.setTxSignature(tx, stack)
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
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}
return None
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)))
return tx.serialize()
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
"lockUnspents": lock_unspents,
"feeRate": feerate_str,
}
rv = self.rpc("fundrawtransaction", [tx_hex, options])
# Sign transaction then strip witness data to fill scriptsig
rv = self.rpc("signrawtransaction", [rv["hex"]])
tx_signed = self.loadTx(bytes.fromhex(rv["hex"]))
if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit):
raise ValueError("txn has non segwit input")
for witness_data in tx_signed.wit.vtxinwit:
if len(witness_data.scriptWitness.stack) < 2:
raise ValueError("txn has non segwit input")
return tx_signed.serialize_without_witness()
def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None) -> bytes:
tx_funded = self.fundTx(tx_bytes.hex(), feerate)
return tx_funded
def createSCLockRefundTx(
self,
tx_lock_bytes,
script_lock,
Kal,
Kaf,
lock1_value,
csv_val,
tx_fee_rate,
vkbv=None,
):
tx_lock = CTransaction()
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_id_int = tx_lock.sha256
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(
CTxIn(
COutPoint(tx_lock_id_int, locked_n),
nSequence=lock1_value,
scriptSig=self.getScriptScriptSig(script_lock),
)
)
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(tx_fee_rate * vsize / 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
self._log.info(
"createSCLockRefundTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize(), refund_script, tx.vout[0].nValue
def createSCLockRefundSpendTx(
self,
tx_lock_refund_bytes,
script_lock_refund,
pkh_refund_to,
tx_fee_rate,
vkbv=None,
):
# Returns the coinA locked coin to the leader
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
output_script = self.getScriptDest(script_lock_refund)
locked_n = findOutput(tx_lock_refund, output_script)
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock_refund.vout[locked_n].nValue
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(
CTxIn(
COutPoint(tx_lock_refund_hash_int, locked_n),
nSequence=0,
scriptSig=self.getScriptScriptSig(script_lock_refund),
)
)
tx.vout.append(
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))
)
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(
script_lock_refund
)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(tx_fee_rate * vsize / 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
self._log.info(
"createSCLockRefundSpendTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()
def createSCLockRefundSpendToFTx(
self,
tx_lock_refund_bytes,
script_lock_refund,
pkh_dest,
tx_fee_rate,
vkbv=None,
kbsf=None,
):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
output_script = self.getScriptDest(script_lock_refund)
locked_n = findOutput(tx_lock_refund, output_script)
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock_refund.vout[locked_n].nValue
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(
CTxIn(
COutPoint(tx_lock_refund_hash_int, locked_n),
nSequence=lock2_value,
scriptSig=self.getScriptScriptSig(script_lock_refund),
)
)
tx.vout.append(
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))
)
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(
script_lock_refund
)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(tx_fee_rate * vsize / 1000)
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
self._log.info(
"createSCLockRefundSpendToFTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()
def createSCLockSpendTx(
self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}
):
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_id_int = tx_lock.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(
CTxIn(
COutPoint(tx_lock_id_int, locked_n),
scriptSig=self.getScriptScriptSig(script_lock),
)
)
tx.vout.append(
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))
)
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(tx_fee_rate * vsize / 1000)
tx.vout[0].nValue = locked_coin - pay_fee
fee_info["fee_paid"] = pay_fee
fee_info["rate_used"] = tx_fee_rate
fee_info["witness_bytes"] = witness_bytes
fee_info["vsize"] = vsize
tx.rehash()
self._log.info(
"createSCLockSpendTx {}{}.".format(
self._log.id(i2b(tx.sha256)),
(
""
if self._log.safe_logs
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
),
)
)
return tx.serialize()

View File

@@ -7,9 +7,6 @@
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.util import (
make_int,
)
class NMCInterface(BTCInterface):
@@ -17,26 +14,38 @@ class NMCInterface(BTCInterface):
def coin_type():
return Coins.NMC
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc_callback('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 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,19 +2,17 @@
# -*- 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.
from io import BytesIO
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,
@@ -29,58 +27,82 @@ class PIVXInterface(BTCInterface):
def coin_type():
return Coins.PIVX
def signTxWithWallet(self, tx):
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
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
)
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
def checkWallets(self) -> int:
return 1
def signTxWithWallet(self, tx):
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)}]
)
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_callback('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_callback('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_callback('getblock', [block_hash, False])
block_header = self.rpc_callback('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_callback('decoderawtransaction', [ToHex(tx)])
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
tx_rv.append(tx_dec)
block_rv = {
'hash': block_hash,
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'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_callback('sendtoaddress', params)
params = [addr_to, value, "", "", subfee]
return self.rpc("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc("getwalletinfo")["balance"])
def loadTx(self, tx_bytes):
# Load tx from bytes to internal representation
@@ -96,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_callback('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_callback('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,19 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
# 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
class VEILInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.VEIL
@staticmethod
def txVersion() -> int:
return 2

View File

@@ -0,0 +1,32 @@
#!/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 basicswap.chainparams import WOW_COIN, Coins
from .xmr import XMRInterface
class WOWInterface(XMRInterface):
@staticmethod
def coin_type():
return Coins.WOW
@staticmethod
def ticker_str() -> int:
return Coins.WOW.name
@staticmethod
def COIN():
return WOW_COIN
@staticmethod
def exp() -> int:
return 11
@staticmethod
def depth_spendable() -> int:
return 3

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# 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.
import json
import logging
import basicswap.contrib.ed25519_fast as edf
@@ -24,20 +24,14 @@ from coincurve.dleag import (
verify_ed25519_point,
)
from basicswap.interface import (
Curves)
from basicswap.util import (
i2b,
dumpj,
ensure,
make_int,
TemporaryError)
from basicswap.rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func)
from basicswap.util import (
b2i, b2h)
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
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.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface
class XMRInterface(CoinInterface):
@@ -49,6 +43,10 @@ class XMRInterface(CoinInterface):
def coin_type():
return Coins.XMR
@staticmethod
def ticker_str() -> int:
return Coins.XMR.name
@staticmethod
def COIN():
return XMR_COIN
@@ -71,74 +69,179 @@ 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:
# TODO: Estimate with ringsize
return 1507
return 1604
def is_transient_error(self, ex) -> bool:
str_error: str = str(ex).lower()
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)
daemon_login = None
if coin_settings.get('rpcuser', '') != '':
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
self.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1'))
self.rpc_cb2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1')) # non-json endpoint
self.rpc_wallet_cb = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'))
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._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._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", ""),
)
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"]
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 = (
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)"
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}."
)
else:
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._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 ",
)
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_cb('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_cb('close_wallet')
except Exception:
pass
self.rpc_wallet_cb('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, key_spend, restore_height=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)
Kbs = self.getPubkey(key_spend)
address_b58 = xmr_util.encode_address(Kbv, Kbs)
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)
@@ -147,80 +250,98 @@ class XMRInterface(CoinInterface):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
def testDaemonRPC(self, with_wallet=True):
self.rpc_wallet_cb('get_languages')
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet("get_languages")
def getDaemonVersion(self):
return self.rpc_wallet_cb('get_version')['version']
return self.rpc_wallet("get_version")["version"]
def getBlockchainInfo(self):
get_height = self.rpc_cb2('get_height', timeout=30)
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.rpc_cb2('get_info', timeout=30)
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_cb('get_block_count', timeout=30)['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('XMR get_block_count failed with: %s', str(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.rpc_cb2('get_height', timeout=30)['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_cb('refresh')
balance_info = self.rpc_wallet_cb('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
return rv
balance_info = self.rpc_wallet("get_balance")
def walletRestoreHeight(self):
return self._restore_height
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_cb('get_address')['address']
return self.rpc_wallet("get_address")["address"]
def getNewAddress(self, placeholder) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
return self.rpc_wallet_cb('create_address', {'account_index': 0})['address']
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):
self._log.warning('TODO - estimate fee rate?')
return 0.0, 'unused'
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
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
def getNewSecretKey(self) -> bytes:
return float(self.format_amount(fee_per_k_bytes)), "get_fee_estimate"
def getNewRandomKey(self) -> bytes:
# Note: Returned bytes are in big endian order
return i2b(edu.get_secret())
@@ -245,11 +366,11 @@ class XMRInterface(CoinInterface):
def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str:
pk_view = self.getPubkey(key_view)
pk_spend = self.getPubkey(key_spend)
return xmr_util.encode_address(pk_view, pk_spend)
return xmr_util.encode_address(pk_view, pk_spend, self._addr_prefix)
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
@@ -273,201 +394,291 @@ class XMRInterface(CoinInterface):
return ed25519_add(Ka, Kb)
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return xmr_util.encode_address(Kbv, Kbs)
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, 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_cb('refresh')
Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs)
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_cb('transfer', params)
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
tx_hash = bytes.fromhex(rv['tx_hash'])
if self._sc.debug:
i = 0
while not self._sc.delay_event.is_set():
gt_params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
rv = self.rpc_wallet_cb('get_transfers', gt_params)
self._log.debug('get_transfers {}'.format(dumpj(rv)))
if 'pending' not in rv:
break
if i >= delay_for:
break
self._sc.delay_event.wait(1.0)
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)
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_cb('refresh', timeout=600)
'''
"""
# Debug
try:
current_height = self.rpc_wallet_cb('get_height')['height']
current_height = self.rpc_wallet('get_height')['height']
self._log.info('findTxB XMR current_height %d\nAddress: %s', current_height, address_b58)
except Exception as e:
self._log.info('rpc_cb failed %s', str(e))
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_cb('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_cb('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_cb('refresh', timeout=600)
try:
current_height = self.rpc_cb2('get_height', timeout=30)['height']
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, 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_cb 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_cb('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) -> 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)
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_cb('refresh')
rv = self.rpc_wallet_cb('get_balance')
if rv['balance'] < cb_swap_value:
self._log.warning('Balance is too low, checking for existing spend.')
txns = self.rpc_wallet_cb('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}.')
if txns[0]['address'] == address_b58:
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:
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_cb('sweep_all', params)
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
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: int, addr_to: str, subfee: bool) -> str:
def withdrawCoin(
self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False
) -> str:
with self._mx_wallet:
value_sats = make_int(value, self.exp())
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('refresh')
if subfee:
balance = self.rpc_wallet_cb('get_balance')
diff = balance['unlocked_balance'] - value_sats
if diff >= 0 and diff <= 10:
self._log.info('subfee enabled and value close to total, using sweep_all.')
params = {'address': addr_to}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('sweep_all', params)
return rv['tx_hash_list'][0]
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
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,
}
if self._fee_priority > 0:
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]
params = {'destinations': [{'amount': value_sats, 'address': addr_to}]}
value_sats: int = self.make_int(value)
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_cb('transfer', params)
return rv['tx_hash']
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"]
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
def showLockTransfers(self, kbv, Kbs, restore_height):
with self._mx_wallet:
try:
Kbv = self.getPubkey(kbv)
address_b58 = xmr_util.encode_address(Kbv, Kbs)
wallet_file = address_b58 + '_spend'
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
wallet_file = address_b58 + "_spend"
try:
self.openWallet(wallet_file)
except Exception:
@@ -475,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_cb('refresh')
rv = self.rpc_wallet_cb('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_cb('refresh')
balance_info = self.rpc_wallet_cb('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_cb('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):
@@ -531,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.rpc_cb2('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

@@ -1,152 +0,0 @@
syntax = "proto3";
package basicswap;
/* Step 1, seller -> network */
message OfferMessage {
uint32 coin_from = 1;
uint32 coin_to = 2;
uint64 amount_from = 3;
uint64 rate = 4;
uint64 min_bid_amount = 5;
uint64 time_valid = 6;
enum LockType {
NOT_SET = 0;
SEQUENCE_LOCK_BLOCKS = 1;
SEQUENCE_LOCK_TIME = 2;
ABS_LOCK_BLOCKS = 3;
ABS_LOCK_TIME = 4;
}
LockType lock_type = 7;
uint32 lock_value = 8;
uint32 swap_type = 9;
/* optional */
string proof_address = 10;
string proof_signature = 11;
bytes pkhash_seller = 12;
bytes secret_hash = 13;
uint64 fee_rate_from = 14;
uint64 fee_rate_to = 15;
uint32 protocol_version = 16;
bool amount_negotiable = 17;
bool rate_negotiable = 18;
}
/* Step 2, buyer -> seller */
message BidMessage {
bytes offer_msg_id = 1;
uint64 time_valid = 2; /* seconds bid is valid for */
uint64 amount = 3; /* amount of amount_from bid is for */
uint64 rate = 4;
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
string proof_address = 6;
string proof_signature = 7;
uint32 protocol_version = 8;
}
/* Step 3, seller -> buyer */
message BidAcceptMessage {
bytes bid_msg_id = 1;
bytes initiate_txid = 2;
bytes contract_script = 3;
}
message OfferRevokeMessage {
bytes offer_msg_id = 1;
bytes signature = 2;
}
message BidRejectMessage {
bytes bid_msg_id = 1;
uint32 reject_code = 2;
}
message XmrBidMessage {
/* MSG1L, F -> L */
bytes offer_msg_id = 1;
uint64 time_valid = 2; /* seconds bid is valid for */
uint64 amount = 3; /* amount of amount_from bid is for */
uint64 rate = 4;
bytes pkaf = 5;
bytes kbvf = 6;
bytes kbsf_dleag = 7;
bytes dest_af = 8;
uint32 protocol_version = 9;
}
message XmrSplitMessage {
bytes msg_id = 1;
uint32 msg_type = 2; /* 1 XmrBid, 2 XmrBidAccept */
uint32 sequence = 3;
bytes dleag = 4;
}
message XmrBidAcceptMessage {
bytes bid_msg_id = 1;
bytes pkal = 3;
bytes kbvl = 4;
bytes kbsl_dleag = 5;
/* MSG2F */
bytes a_lock_tx = 6;
bytes a_lock_tx_script = 7;
bytes a_lock_refund_tx = 8;
bytes a_lock_refund_tx_script = 9;
bytes a_lock_refund_spend_tx = 10;
bytes al_lock_refund_tx_sig = 11;
}
message XmrBidLockTxSigsMessage {
/* MSG3L */
bytes bid_msg_id = 1;
bytes af_lock_refund_spend_tx_esig = 2;
bytes af_lock_refund_tx_sig = 3;
}
message XmrBidLockSpendTxMessage {
/* MSG4F */
bytes bid_msg_id = 1;
bytes a_lock_spend_tx = 2;
bytes kal_sig = 3;
}
message XmrBidLockReleaseMessage {
/* MSG5F */
bytes bid_msg_id = 1;
bytes al_lock_spend_tx_esig = 2;
}
message ADSBidIntentMessage {
/* L -> F Sent from bidder, construct a reverse bid */
bytes offer_msg_id = 1;
uint64 time_valid = 2; /* seconds bid is valid for */
uint64 amount_from = 3; /* amount of offer.coin_from bid is for */
uint64 amount_to = 4; /* amount of offer.coin_to bid is for, equivalent to bid.amount */
uint64 rate = 5; /* amount of offer.coin_from bid is for */
uint32 protocol_version = 6;
}
message ADSBidIntentAcceptMessage {
/* F -> L Sent from offerer, construct a reverse bid */
bytes bid_msg_id = 1;
bytes pkaf = 2;
bytes kbvf = 3;
bytes kbsf_dleag = 4;
bytes dest_af = 5;
}

266
basicswap/messages_npb.py Normal file
View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
syntax = "proto3";
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 I64 fixed64, sfixed64, double
2 LEN string, bytes, embedded messages, packed repeated fields
5 I32 fixed32, sfixed32, float
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:
def __init__(self, init_all: bool = True, **kwargs):
for key, value in kwargs.items():
found_field: bool = False
for field_num, v in self._map.items():
field_name, wire_type, field_type = v
if field_name == key:
setattr(self, field_name, value)
found_field = True
break
if found_field is False:
raise ValueError(f"got an unexpected keyword argument '{key}'")
if init_all:
self.init_fields()
def init_fields(self) -> None:
# Set default values for missing fields
for field_num, v in self._map.items():
field_name, wire_type, field_type = v
if hasattr(self, field_name):
continue
if wire_type == 0:
setattr(self, field_name, 0)
elif wire_type == 2:
if field_type == 1:
setattr(self, field_name, str())
else:
setattr(self, field_name, bytes())
else:
raise ValueError(f"Unknown wire_type {wire_type}")
def to_bytes(self) -> bytes:
rv = bytes()
for field_num, v in self._map.items():
field_name, wire_type, field_type = v
if not hasattr(self, field_name):
continue
field_value = getattr(self, field_name)
tag = (field_num << 3) | wire_type
if wire_type == 0:
if field_value == 0:
continue
rv += encode_varint(tag)
rv += encode_varint(field_value)
elif wire_type == 2:
if len(field_value) == 0:
continue
rv += encode_varint(tag)
if isinstance(field_value, str):
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}")
return rv
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
max_len: int = len(b)
o: int = 0
while o < max_len:
tag, lv = decode_varint(b, o)
o += lv
wire_type = tag & 7
field_num = tag >> 3
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}"
)
if wire_type == 0:
field_value, lv = decode_varint(b, o)
o += lv
elif wire_type == 2:
field_len, lv = decode_varint(b, o)
o += lv
field_value = b[o : o + field_len]
o += field_len
if field_type == 1:
field_value = field_value.decode("utf-8")
else:
raise ValueError(f"Unknown wire_type {wire_type}")
setattr(self, field_name, field_value)
if init_all:
self.init_fields()
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),
}
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),
}
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),
}
class OfferRevokeMessage(NonProtobufClass):
_map = {
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),
}
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),
}
class XmrSplitMessage(NonProtobufClass):
_map = {
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),
# 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),
}
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),
}
class XmrBidLockSpendTxMessage(NonProtobufClass):
# MSG4F
_map = {
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),
}
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),
}
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),
}

View File

@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: messages.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_OFFERMESSAGE._serialized_start=30
_OFFERMESSAGE._serialized_end=580
_OFFERMESSAGE_LOCKTYPE._serialized_start=467
_OFFERMESSAGE_LOCKTYPE._serialized_end=580
_BIDMESSAGE._serialized_start=583
_BIDMESSAGE._serialized_end=763
_BIDACCEPTMESSAGE._serialized_start=765
_BIDACCEPTMESSAGE._serialized_end=851
_OFFERREVOKEMESSAGE._serialized_start=853
_OFFERREVOKEMESSAGE._serialized_end=914
_BIDREJECTMESSAGE._serialized_start=916
_BIDREJECTMESSAGE._serialized_end=975
_XMRBIDMESSAGE._serialized_start=978
_XMRBIDMESSAGE._serialized_end=1156
_XMRSPLITMESSAGE._serialized_start=1158
_XMRSPLITMESSAGE._serialized_end=1242
_XMRBIDACCEPTMESSAGE._serialized_start=1245
_XMRBIDACCEPTMESSAGE._serialized_end=1501
_XMRBIDLOCKTXSIGSMESSAGE._serialized_start=1503
_XMRBIDLOCKTXSIGSMESSAGE._serialized_end=1617
_XMRBIDLOCKSPENDTXMESSAGE._serialized_start=1619
_XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707
_XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709
_XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786
_ADSBIDINTENTMESSAGE._serialized_start=1789
_ADSBIDINTENTMESSAGE._serialized_end=1932
_ADSBIDINTENTACCEPTMESSAGE._serialized_start=1934
_ADSBIDINTENTACCEPTMESSAGE._serialized_end=2046
# @@protoc_insertion_point(module_scope)

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,14 +17,13 @@
Both nodes are initialised
XChaCha20_Poly1305 mac is 16bytes
'''
"""
import time
import queue
import random
import select
import socket
import struct
import hashlib
import logging
import secrets
@@ -37,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 = struct.pack('>H', START_TOKEN)
START_TOKEN = 0xABCD
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
MSG_MAX_SIZE = 0x200000 # 2MB
@@ -64,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 struct.pack('>H', NetMessageTypes.HANDSHAKE) + \
struct.pack('>Q', self._timestamp) + \
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 = struct.unpack('>Q', msg_mv[o: o + 8])[0]
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()
@@ -142,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)
@@ -161,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
@@ -176,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)
@@ -194,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)
@@ -202,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())
@@ -212,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
@@ -279,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)
@@ -295,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()
@@ -306,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)
@@ -314,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)
@@ -333,7 +386,7 @@ class Network:
ss = k.ecdh(peer._pubkey)
hashed = hashlib.sha512(ss + struct.pack('>Q', msg._timestamp)).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -362,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)
@@ -376,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 + struct.pack('>Q', msg._timestamp)).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -396,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:]
@@ -415,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 = struct.unpack('>I', plaintext[:4])[0]
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()
@@ -444,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 = struct.unpack('>I', plaintext[:4])[0]
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 = struct.pack('>H', NetMessageTypes.PING)
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 = struct.pack('>I', ping_nonce)
payload = ping_nonce.to_bytes(4, "big")
if peer._last_ping_at == 0:
payload += self._sc._version
ct, mac = cipher.encrypt_and_digest(payload)
@@ -484,14 +547,14 @@ class Network:
self.send_msg(peer, msg_bytes)
def send_pong(self, peer, ping_nonce):
msg_bytes = struct.pack('>H', NetMessageTypes.PONG)
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 = struct.pack('>I', ping_nonce)
payload = ping_nonce.to_bytes(4, "big")
ct, mac = cipher.encrypt_and_digest(payload)
msg_bytes += ct + mac
@@ -503,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) + struct.pack('>I', len_encoded) + 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 = struct.unpack('>H', mv[o: o + 2])[0]
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:
@@ -523,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()
@@ -534,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
@@ -542,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 = struct.unpack('>I', mv[o: o + 4])[0]
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 = struct.unpack('>H', mv[o: o + 2])[0]
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)
@@ -577,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 = {}
@@ -590,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,96 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
O6GuapL0x1I9SeIk9WX9VRrpZmENhlri
=JrM6
-----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-----

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