212 Commits
wow ... v0.14.2

Author SHA1 Message Date
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
168 changed files with 38996 additions and 19018 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 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.2.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}"

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

@@ -0,0 +1,62 @@
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 .
- 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
CACHE_KEY: $(printf $(python bin/basicswap-prepare.py --version --withcoins=bitcoin) | sha256sum | head -c 64)
with:
path: $BIN_DIR
key: $CACHE_KEY
- if: ${{ steps.cache-yarn.outputs.cache-hit != 'true' }}
name: Running basicswap-prepare
run: |
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
- 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

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ __pycache__
# 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 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.2.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,18 +5,12 @@ 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;
ARG COINCURVE_VERSION=v0.2
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;
# 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; \

View File

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

View File

@@ -35,9 +35,9 @@ Built as a low-friction, highly secure solution to the frequent losses of funds
## 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** 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.
**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.
@@ -122,7 +122,7 @@ If youd like to add a cryptocurrency to BasicSwap, refer to how other cryptoc
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

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.13.2"
__version__ = "0.14.2"

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -18,6 +19,9 @@ import subprocess
from sockshandler import SocksiPyHandler
from .db import (
DBMethods,
)
from .rpc import (
callrpc,
)
@@ -34,8 +38,8 @@ def getaddrinfo_tor(*args):
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
class BaseApp:
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
class BaseApp(DBMethods):
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
self.log_name = log_name
self.fp = fp
self.fail_code = 0
@@ -47,19 +51,19 @@ class BaseApp:
self.coin_clients = {}
self.coin_interfaces = {}
self.mxDB = threading.Lock()
self.debug = self.settings.get('debug', False)
self.debug = self.settings.get("debug", False)
self.delay_event = threading.Event()
self.chainstate_delay_event = threading.Event()
self._network = None
self.prepareLogging()
self.log.info('Network: {}'.format(self.chain))
self.log.info("Network: {}".format(self.chain))
self.use_tor_proxy = self.settings.get('use_tor', False)
self.tor_proxy_host = self.settings.get('tor_proxy_host', '127.0.0.1')
self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050)
self.tor_control_password = self.settings.get('tor_control_password', None)
self.tor_control_port = self.settings.get('tor_control_port', 9051)
self.use_tor_proxy = self.settings.get("use_tor", False)
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
self.tor_proxy_port = self.settings.get("tor_proxy_port", 9050)
self.tor_control_password = self.settings.get("tor_control_password", None)
self.tor_control_port = self.settings.get("tor_control_port", 9051)
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
@@ -77,10 +81,17 @@ class BaseApp:
# Remove any existing handlers
self.log.handlers = []
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S')
formatter = logging.Formatter(
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
)
stream_stdout = logging.StreamHandler()
if self.log_name != 'BasicSwap':
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S'))
if self.log_name != "BasicSwap":
stream_stdout.setFormatter(
logging.Formatter(
"%(asctime)s %(name)s %(levelname)s : %(message)s",
"%Y-%m-%d %H:%M:%S",
)
)
else:
stream_stdout.setFormatter(formatter)
stream_fp = logging.StreamHandler(self.fp)
@@ -92,67 +103,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)
@@ -162,12 +197,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()
@@ -178,7 +220,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()
@@ -190,23 +234,23 @@ class BaseApp:
c.close()
return response
except Exception as e:
self.log.error(f'torControl {e}')
self.log.error(f"torControl {e}")
return
def getTime(self) -> int:
return int(time.time()) + self.mock_time_offset
def setMockTimeOffset(self, new_offset: int) -> None:
self.log.warning(f'Setting mocktime to {new_offset}')
self.log.warning(f"Setting mocktime to {new_offset}")
self.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
value: int = self.settings.get(name, default_v)
if value < min_v:
self.log.warning(f'Setting {name} to {min_v}')
self.log.warning(f"Setting {name} to {min_v}")
value = min_v
if value > max_v:
self.log.warning(f'Setting {name} to {max_v}')
self.log.warning(f"Setting {name} to {max_v}")
value = max_v
return value

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -64,6 +65,7 @@ class SwapTypes(IntEnum):
SELLER_FIRST_2MSG = auto()
BUYER_FIRST_2MSG = auto()
XMR_SWAP = auto()
XMR_BCH_SWAP = auto()
class OfferStates(IntEnum):
@@ -75,13 +77,13 @@ class OfferStates(IntEnum):
class BidStates(IntEnum):
BID_SENT = 1
BID_RECEIVING = 2 # Partially received
BID_RECEIVING = 2 # Partially received
BID_RECEIVED = 3
BID_RECEIVING_ACC = 4 # Partially received accept message
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
SWAP_INITIATED = 6 # Initiate txn validated
SWAP_PARTICIPATING = 7 # Participate txn validated
SWAP_COMPLETED = 8 # All swap txns spent
BID_RECEIVING_ACC = 4 # Partially received accept message
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
SWAP_INITIATED = 6 # Initiate txn validated
SWAP_PARTICIPATING = 7 # Participate txn validated
SWAP_COMPLETED = 8 # All swap txns spent
XMR_SWAP_SCRIPT_COIN_LOCKED = 9
XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = 10
XMR_SWAP_NOSCRIPT_COIN_LOCKED = 11
@@ -95,16 +97,18 @@ class BidStates(IntEnum):
XMR_SWAP_FAILED = 19
SWAP_DELAYING = 20
SWAP_TIMEDOUT = 21
BID_ABANDONED = 22 # Bid will no longer be processed
BID_ERROR = 23 # An error occurred
BID_ABANDONED = 22 # Bid will no longer be processed
BID_ERROR = 23 # An error occurred
BID_STALLED_FOR_TEST = 24
BID_REJECTED = 25
BID_STATE_UNKNOWN = 26
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
BID_REQUEST_SENT = 29
BID_REQUEST_ACCEPTED = 30
BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
class TxStates(IntEnum):
@@ -136,6 +140,8 @@ class TxTypes(IntEnum):
ITX_PRE_FUNDED = auto()
BCH_MERCY = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
@@ -183,6 +189,8 @@ class EventLogTypes(IntEnum):
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):
@@ -194,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()
@@ -204,6 +213,10 @@ class DebugTypes(IntEnum):
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):
@@ -221,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):
@@ -237,244 +250,257 @@ class VisibilityOverrideOptions(IntEnum):
def strVisibilityOverrideOption(option):
if option == VisibilityOverrideOptions.DEFAULT:
return 'Default'
return "Default"
if option == VisibilityOverrideOptions.HIDE:
return 'Hide'
return "Hide"
if option == VisibilityOverrideOptions.BLOCK:
return 'Block'
return 'Unknown'
return "Block"
return "Unknown"
def strOfferState(state):
if state == OfferStates.OFFER_SENT:
return 'Sent'
return "Sent"
if state == OfferStates.OFFER_RECEIVED:
return 'Received'
return "Received"
if state == OfferStates.OFFER_ABANDONED:
return 'Abandoned'
return "Abandoned"
if state == OfferStates.OFFER_EXPIRED:
return 'Expired'
return 'Unknown'
return "Expired"
return "Unknown"
def strBidState(state):
if state == BidStates.BID_SENT:
return 'Sent'
return "Sent"
if state == BidStates.BID_RECEIVING:
return 'Receiving'
return "Receiving"
if state == BidStates.BID_RECEIVING_ACC:
return 'Receiving accept'
return "Receiving accept"
if state == BidStates.BID_RECEIVED:
return 'Received'
return "Received"
if state == BidStates.BID_ACCEPTED:
return 'Accepted'
return "Accepted"
if state == BidStates.SWAP_INITIATED:
return 'Initiated'
return "Initiated"
if state == BidStates.SWAP_PARTICIPATING:
return 'Participating'
return "Participating"
if state == BidStates.SWAP_COMPLETED:
return 'Completed'
return "Completed"
if state == BidStates.SWAP_TIMEDOUT:
return 'Timed-out'
return "Timed-out"
if state == BidStates.BID_ABANDONED:
return 'Abandoned'
return "Abandoned"
if state == BidStates.BID_STALLED_FOR_TEST:
return 'Stalled (debug)'
return "Stalled (debug)"
if state == BidStates.BID_ERROR:
return 'Error'
return "Error"
if state == BidStates.BID_REJECTED:
return 'Rejected'
return "Rejected"
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
return 'Script coin locked'
return "Script coin locked"
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
return 'Script coin spend tx valid'
return "Script coin spend tx valid"
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
return 'Scriptless coin locked'
return "Scriptless coin locked"
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
return 'Script coin lock released'
return "Script coin lock released"
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
return 'Script tx redeemed'
return "Script tx redeemed"
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
return 'Script pre-refund tx in chain'
return "Script pre-refund tx in chain"
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
return 'Scriptless tx redeemed'
return "Scriptless tx redeemed"
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
return 'Scriptless tx recovered'
return "Scriptless tx recovered"
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
return 'Failed, refunded'
return "Failed, refunded"
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
return 'Failed, swiped'
return "Failed, swiped"
if state == BidStates.XMR_SWAP_FAILED:
return 'Failed'
return "Failed"
if state == BidStates.SWAP_DELAYING:
return 'Delaying'
return "Delaying"
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
return 'Exchanged script lock tx sigs msg'
return "Exchanged script lock tx sigs msg"
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
return 'Exchanged script lock spend tx msg'
return "Exchanged script lock spend tx msg"
if state == BidStates.BID_REQUEST_SENT:
return 'Request sent'
return "Request sent"
if state == BidStates.BID_REQUEST_ACCEPTED:
return 'Request accepted'
return "Request accepted"
if state == BidStates.BID_STATE_UNKNOWN:
return 'Unknown bid state'
return "Unknown bid state"
if state == BidStates.BID_EXPIRED:
return 'Expired'
return 'Unknown' + ' ' + str(state)
return "Expired"
if state == BidStates.BID_AACCEPT_DELAY:
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
return "Unknown" + " " + str(state)
def strTxState(state):
if state == TxStates.TX_NONE:
return 'None'
return "None"
if state == TxStates.TX_SENT:
return 'Sent'
return "Sent"
if state == TxStates.TX_CONFIRMED:
return 'Confirmed'
return "Confirmed"
if state == TxStates.TX_REDEEMED:
return 'Redeemed'
return "Redeemed"
if state == TxStates.TX_REFUNDED:
return 'Refunded'
return "Refunded"
if state == TxStates.TX_IN_MEMPOOL:
return 'In Mempool'
return "In Mempool"
if state == TxStates.TX_IN_CHAIN:
return 'In Chain'
return 'Unknown'
return "In Chain"
return "Unknown"
def strTxType(tx_type):
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
return 'Chain A Lock Tx'
return "Chain A Lock Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
return 'Chain A Lock Spend Tx'
return "Chain A Lock Spend Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
return 'Chain A Lock Refund Tx'
return "Chain A Lock Refund Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
return 'Chain A Lock Refund Spend Tx'
return "Chain A Lock Refund Spend Tx"
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
return 'Chain A Lock Refund Swipe Tx'
return "Chain A Lock Refund Swipe Tx"
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
return 'Chain B Lock Tx'
return "Chain B Lock Tx"
if tx_type == TxTypes.ITX_PRE_FUNDED:
return 'Funded mock initiate tx'
return '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'
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 getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
for o in txjs['vout']:
for o in txjs["vout"]:
try:
if scriptPubKey_hex == o['scriptPubKey']['hex']:
return o['n']
if scriptPubKey_hex == o["scriptPubKey"]["hex"]:
return o["n"]
except Exception:
pass
raise ValueError('scriptPubKey output not found in txn')
raise ValueError("scriptPubKey output not found in txn")
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type="pubkey_address"):
return encodeAddress(
bytes((chainparams[coin_type][chain_name][addr_type],))
+ decodeAddress(addr)[1:]
)
def getOfferProofOfFundsHash(offer_msg, offer_addr):
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
h = hashlib.sha256()
h.update(offer_addr.encode('utf-8'))
h.update(offer_addr.encode("utf-8"))
offer_bytes = offer_msg.to_bytes()
h.update(offer_bytes)
return h.digest()
@@ -484,33 +510,48 @@ def getLastBidState(packed_states):
num_states = len(packed_states) // 12
if num_states < 2:
return BidStates.BID_STATE_UNKNOWN
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
try:
num_states = len(packed_states) // 12
if num_states < 2:
return BidStates.BID_STATE_UNKNOWN
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
except Exception:
return BidStates.BID_STATE_UNKNOWN
def strSwapType(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return 'seller_first'
return "seller_first"
if swap_type == SwapTypes.XMR_SWAP:
return 'xmr_swap'
return "xmr_swap"
return None
def strSwapDesc(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return 'Secret Hash'
return "Secret Hash"
if swap_type == SwapTypes.XMR_SWAP:
return 'Adaptor Sig'
return "Adaptor Sig"
return None
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED]
inactive_states = [
BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR,
BidStates.BID_REJECTED,
BidStates.SWAP_TIMEDOUT,
BidStates.BID_ABANDONED,
BidStates.BID_EXPIRED,
]
def canAcceptBidState(state):
return state in (
BidStates.BID_RECEIVED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
)
def isActiveBidState(state):

View File

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

2659
basicswap/bin/prepare.py Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,626 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import json
import shutil
import signal
import logging
import traceback
import subprocess
import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
swap_client = None
class Daemon:
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v["name"]:
return True
return False
def signal_handler(sig, frame):
global swap_client
logger.info("Signal %d detected, ending program." % (sig))
if swap_client is not None:
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 for 0.21.3
# TODO: Remove
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
config_to_add = ["blockfilterindex=0", "peerblockfilters=0"]
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line in config_to_add:
config_to_add.remove(line)
if len(config_to_add) > 0:
logger.info("Rewriting litecoin.conf")
shutil.copyfile(ltc_conf_path, ltc_conf_path + ".last")
with open(ltc_conf_path, "a") as fp:
for line in config_to_add:
fp.write(line + "\n")
args = [
daemon_bin,
]
add_datadir: bool = extra_config.get("add_datadir", True)
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):
extra_args = []
if "config_filename" in coin_settings:
extra_args.append("--conf=" + coin_settings["config_filename"])
if "port" in coin_settings:
extra_args.append("--port=" + str(int(coin_settings["port"])))
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)
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("Interrupting %d, error %s", d.handle.pid, str(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 ex:
swap_client.log.error("Error: {}".format(ex))
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

@@ -9,8 +9,8 @@ from .util import (
COIN,
)
XMR_COIN = 10 ** 12
WOW_COIN = 10 ** 11
XMR_COIN = 10**12
WOW_COIN = 10**11
class Coins(IntEnum):
@@ -30,419 +30,464 @@ class Coins(IntEnum):
NAV = 14
LTC_MWEB = 15
# ZANO = 16
BCH = 17
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,
},
'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",
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "bcrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'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,
},
'regtest': {
'rpcport': 19443,
'pubkey_address': 111,
'script_address': 196,
'script_address2': 58,
'key_prefix': 239,
'hrp': 'rltc',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.DCR: {
'name': 'decred',
'ticker': 'DCR',
'message_magic': 'Decred Signed Message:\n',
'blocks_target': 60 * 5,
'decimal_places': 8,
'mainnet': {
'rpcport': 9109,
'pubkey_address': 0x073f,
'script_address': 0x071a,
'key_prefix': 0x22de,
'bip44': 42,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "decred",
"ticker": "DCR",
"message_magic": "Decred Signed Message:\n",
"blocks_target": 60 * 5,
"decimal_places": 8,
"mainnet": {
"rpcport": 9109,
"pubkey_address": 0x073F,
"script_address": 0x071A,
"key_prefix": 0x22DE,
"bip44": 42,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 19109,
'pubkey_address': 0x0f21,
'script_address': 0x0efc,
'key_prefix': 0x230e,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 19109,
"pubkey_address": 0x0F21,
"script_address": 0x0EFC,
"key_prefix": 0x230E,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
},
"regtest": { # simnet
"rpcport": 18656,
"pubkey_address": 0x0E91,
"script_address": 0x0E6C,
"key_prefix": 0x2307,
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': { # simnet
'rpcport': 18656,
'pubkey_address': 0x0e91,
'script_address': 0x0e6c,
'key_prefix': 0x2307,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.NMC: {
'name': 'namecoin',
'ticker': 'NMC',
'message_magic': 'Namecoin Signed Message:\n',
'blocks_target': 60 * 10,
'decimal_places': 8,
'mainnet': {
'rpcport': 8336,
'pubkey_address': 52,
'script_address': 13,
'hrp': 'nc',
'bip44': 7,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "namecoin",
"ticker": "NMC",
"message_magic": "Namecoin Signed Message:\n",
"blocks_target": 60 * 10,
"decimal_places": 8,
"mainnet": {
"rpcport": 8336,
"pubkey_address": 52,
"script_address": 13,
"hrp": "nc",
"bip44": 7,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 18336,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'tn',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
"testnet": {
"rpcport": 18336,
"pubkey_address": 111,
"script_address": 196,
"hrp": "tn",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"hrp": "ncrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'ncrt',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.XMR: {
'name': 'monero',
'ticker': 'XMR',
'client': 'xmr',
'decimal_places': 12,
'mainnet': {
'rpcport': 18081,
'walletrpcport': 18082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
"name": "monero",
"ticker": "XMR",
"client": "xmr",
"decimal_places": 12,
"mainnet": {
"rpcport": 18081,
"walletrpcport": 18082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
'testnet': {
'rpcport': 28081,
'walletrpcport': 28082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
"testnet": {
"rpcport": 28081,
"walletrpcport": 28082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
"regtest": {
"rpcport": 18081,
"walletrpcport": 18082,
"min_amount": 1000000000,
"max_amount": 10000000 * XMR_COIN,
"address_prefix": 18,
},
'regtest': {
'rpcport': 18081,
'walletrpcport': 18082,
'min_amount': 100000,
'max_amount': 10000 * XMR_COIN,
'address_prefix': 18,
}
},
Coins.WOW: {
'name': 'wownero',
'ticker': 'WOW',
'client': 'wow',
'decimal_places': 11,
'mainnet': {
'rpcport': 34568,
'walletrpcport': 34572, # todo
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
"name": "wownero",
"ticker": "WOW",
"client": "wow",
"decimal_places": 11,
"mainnet": {
"rpcport": 34568,
"walletrpcport": 34572, # todo
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
'testnet': {
'rpcport': 44568,
'walletrpcport': 44572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
"testnet": {
"rpcport": 44568,
"walletrpcport": 44572,
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
"regtest": {
"rpcport": 54568,
"walletrpcport": 54572,
"min_amount": 100000000,
"max_amount": 10000000 * WOW_COIN,
"address_prefix": 4146,
},
'regtest': {
'rpcport': 54568,
'walletrpcport': 54572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
}
},
Coins.PIVX: {
'name': 'pivx',
'ticker': 'PIVX',
'message_magic': 'DarkNet Signed Message:\n',
'blocks_target': 60 * 1,
'decimal_places': 8,
'has_cltv': True,
'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_cltv': False,
'has_csv': False,
'has_segwit': False,
'mainnet': {
'rpcport': 8888,
'pubkey_address': 82,
'script_address': 7,
'key_prefix': 210,
'hrp': '',
'bip44': 136,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "firo",
"ticker": "FIRO",
"message_magic": "Zcoin Signed Message:\n",
"blocks_target": 60 * 10,
"decimal_places": 8,
"has_cltv": False,
"has_csv": False,
"has_segwit": False,
"mainnet": {
"rpcport": 8888,
"pubkey_address": 82,
"script_address": 7,
"key_prefix": 210,
"hrp": "",
"bip44": 136,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 18888,
'pubkey_address': 65,
'script_address': 178,
'key_prefix': 185,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 18888,
"pubkey_address": 65,
"script_address": 178,
"key_prefix": 185,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
"regtest": {
"rpcport": 28888,
"pubkey_address": 65,
"script_address": 178,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 28888,
'pubkey_address': 65,
'script_address': 178,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.NAV: {
'name': 'navcoin',
'ticker': 'NAV',
'message_magic': 'Navcoin Signed Message:\n',
'blocks_target': 30,
'decimal_places': 8,
'has_csv': True,
'has_segwit': True,
'mainnet': {
'rpcport': 44444,
'pubkey_address': 53,
'script_address': 85,
'key_prefix': 150,
'hrp': '',
'bip44': 130,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"name": "navcoin",
"ticker": "NAV",
"message_magic": "Navcoin Signed Message:\n",
"blocks_target": 30,
"decimal_places": 8,
"has_csv": True,
"has_segwit": True,
"mainnet": {
"rpcport": 44444,
"pubkey_address": 53,
"script_address": 85,
"key_prefix": 150,
"hrp": "",
"bip44": 130,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'testnet': {
'rpcport': 44445,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
"testnet": {
"rpcport": 44445,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
},
'regtest': {
'rpcport': 44446,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': '',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * 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,
},
},
}
ticker_map = {}
for c, params in chainparams.items():
ticker_map[params['ticker'].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("Unknown coin")

View File

@@ -1,38 +1,52 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 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', os.path.join('~', '.basicswap'))
CONFIG_FILENAME = "basicswap.json"
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
DEFAULT_ALLOW_CORS = False
TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', os.path.join('~', '.basicswap', 'bin')))
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
DEFAULT_TEST_BINDIR = os.path.expanduser(
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
)
bin_suffix = ('.exe' if os.name == 'nt' else '')
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
PARTICLD = os.getenv('PARTICLD', 'particld' + bin_suffix)
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + bin_suffix)
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + bin_suffix)
bin_suffix = ".exe" if os.name == "nt" else ""
PARTICL_BINDIR = os.path.expanduser(
os.getenv("PARTICL_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "particl"))
)
PARTICLD = os.getenv("PARTICLD", "particld" + bin_suffix)
PARTICL_CLI = os.getenv("PARTICL_CLI", "particl-cli" + bin_suffix)
PARTICL_TX = os.getenv("PARTICL_TX", "particl-tx" + bin_suffix)
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoin')))
BITCOIND = os.getenv('BITCOIND', 'bitcoind' + bin_suffix)
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + bin_suffix)
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + bin_suffix)
BITCOIN_BINDIR = os.path.expanduser(
os.getenv("BITCOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "bitcoin"))
)
BITCOIND = os.getenv("BITCOIND", "bitcoind" + bin_suffix)
BITCOIN_CLI = os.getenv("BITCOIN_CLI", "bitcoin-cli" + bin_suffix)
BITCOIN_TX = os.getenv("BITCOIN_TX", "bitcoin-tx" + bin_suffix)
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'litecoin')))
LITECOIND = os.getenv('LITECOIND', 'litecoind' + bin_suffix)
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + bin_suffix)
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + bin_suffix)
LITECOIN_BINDIR = os.path.expanduser(
os.getenv("LITECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "litecoin"))
)
LITECOIND = os.getenv("LITECOIND", "litecoind" + bin_suffix)
LITECOIN_CLI = os.getenv("LITECOIN_CLI", "litecoin-cli" + bin_suffix)
LITECOIN_TX = os.getenv("LITECOIN_TX", "litecoin-tx" + bin_suffix)
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'namecoin')))
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + bin_suffix)
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + bin_suffix)
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
NAMECOIN_BINDIR = os.path.expanduser(
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
)
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)
XMR_BINDIR = os.path.expanduser(
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
)
XMRD = os.getenv("XMRD", "monerod" + bin_suffix)
XMR_WALLET_RPC = os.getenv("XMR_WALLET_RPC", "monero-wallet-rpc" + bin_suffix)
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.

File diff suppressed because it is too large Load Diff

View File

@@ -1,326 +1,427 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
from sqlalchemy.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.setIntKV('db_data_version', self.db_data_version, session)
session.commit()
self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
finally:
session.close()
session.remove()
if data_version > 0 and data_version < 5:
for state in (
BidStates.BID_EXPIRED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
):
addBidState(self, state, now, cursor)
self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKV("db_data_version", self.db_data_version, cursor)
self.commitDB()
self.log.info(
"Upgraded database records to version {}".format(self.db_data_version)
)
finally:
self.closeDB(cursor, commit=False)
def upgradeDatabase(self, db_version):
if db_version >= CURRENT_DB_VERSION:
return
self.log.info('Upgrading database from version %d to %d.', db_version, CURRENT_DB_VERSION)
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
)
while True:
session = scoped_session(self.session_factory)
try:
cursor = self.openDB()
current_version = db_version
if current_version == 6:
session.execute('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')
elif current_version == 21:
db_version += 1
session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
elif current_version == 22:
db_version += 1
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
elif current_version == 23:
db_version += 1
session.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))''')
session.execute('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB')
if current_version != db_version:
self.db_version = db_version
self.setIntKV('db_version', db_version, session)
session.commit()
session.close()
session.remove()
self.log.info('Upgraded database to version {}'.format(self.db_version))
continue
msg_type INTEGER,
msg_sequence INTEGER,
msg_id BLOB,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
elif current_version == 21:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
elif current_version == 22:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
elif current_version == 23:
db_version += 1
cursor.execute(
"""
CREATE TABLE checkedblocks (
record_id INTEGER NOT NULL,
created_at BIGINT,
coin_type INTEGER,
block_height INTEGER,
block_hash BLOB,
block_time INTEGER,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
elif current_version == 24:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
if current_version != db_version:
self.db_version = db_version
self.setIntKV("db_version", db_version, cursor)
self.commitDB()
self.log.info("Upgraded database to version {}".format(self.db_version))
continue
except Exception as e:
self.log.error("Upgrade failed {}".format(e))
self.rollbackDB()
finally:
self.closeDB(cursor, commit=False)
break
if db_version != CURRENT_DB_VERSION:
raise ValueError('Unable to upgrade database.')
raise ValueError("Unable to upgrade database.")

View File

@@ -1,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,47 +12,122 @@ from .db import (
def remove_expired_data(self, time_offset: int = 0):
now: int = self.getTime()
try:
session = self.openSession()
cursor = self.openDB()
active_bids_insert = self.activeBidsQueryStr(now, '', 'b2')
query_str = f'''
active_bids_insert: str = self.activeBidsQueryStr("", "b2")
query_str = f"""
SELECT o.offer_id FROM offers o
WHERE o.expire_at <= :expired_at AND 0 = (SELECT COUNT(*) FROM bids b2 WHERE b2.offer_id = o.offer_id AND {active_bids_insert})
'''
"""
num_offers = 0
num_bids = 0
offer_rows = session.execute(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 "",
)
)
session.execute('DELETE FROM checkedblocks WHERE created_at <= :expired_at', {'expired_at': now - time_offset})
cursor.execute(
"DELETE FROM checkedblocks WHERE created_at <= :expired_at",
{"expired_at": now - time_offset},
)
finally:
self.closeSession(session)
self.closeDB(cursor)

View File

@@ -13,8 +13,8 @@ def encodepoint(P):
zi = edf.inv(P[2])
x = (P[0] * zi) % edf.q
y = (P[1] * zi) % edf.q
y += ((x & 1) << 255)
return y.to_bytes(32, byteorder='little')
y += (x & 1) << 255
return y.to_bytes(32, byteorder="little")
def hashToEd25519(bytes_in):
@@ -22,8 +22,8 @@ def hashToEd25519(bytes_in):
for i in range(1000):
h255 = bytearray(hashed)
x_sign = 0 if h255[31] & 0x80 == 0 else 1
h255[31] &= 0x7f # Clear top bit
y = int.from_bytes(h255, byteorder='little')
h255[31] &= 0x7F # Clear top bit
y = int.from_bytes(h255, byteorder="little")
x = edf.xrecover(y, x_sign)
if x == 0 and y == 1: # Skip infinity point
continue
@@ -33,4 +33,4 @@ def hashToEd25519(bytes_in):
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
return P
hashed = hashlib.sha256(hashed).digest()
raise ValueError('hashToEd25519 failed')
raise ValueError("hashToEd25519 failed")

View File

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

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -56,12 +57,12 @@ from .ui.page_identity import page_identity
from .ui.page_smsgaddresses import page_smsgaddresses
from .ui.page_debug import page_debug
env = Environment(loader=PackageLoader('basicswap', 'templates'))
env.filters['formatts'] = format_timestamp
env = Environment(loader=PackageLoader("basicswap", "templates"))
env.filters["formatts"] = format_timestamp
def extractDomain(url):
return url.split('://', 1)[1].split('/', 1)[0]
return url.split("://", 1)[1].split("/", 1)[0]
def listAvailableExplorers(swap_client):
@@ -69,40 +70,47 @@ def listAvailableExplorers(swap_client):
for c in Coins:
if c not in chainparams:
continue
for i, e in enumerate(swap_client.coin_clients[c]['explorers']):
explorers.append(('{}_{}'.format(int(c), i), getCoinName(c) + ' - ' + extractDomain(e.base_url)))
for i, e in enumerate(swap_client.coin_clients[c]["explorers"]):
explorers.append(
(
"{}_{}".format(int(c), i),
getCoinName(c) + " - " + extractDomain(e.base_url),
)
)
return explorers
def listExplorerActions(swap_client):
actions = [('height', 'Chain Height'),
('block', 'Get Block'),
('tx', 'Get Transaction'),
('balance', 'Address Balance'),
('unspent', 'List Unspent')]
actions = [
("height", "Chain Height"),
("block", "Get Block"),
("tx", "Get Transaction"),
("balance", "Address Balance"),
("unspent", "List Unspent"),
]
return actions
def parse_cmd(cmd: str, type_map: str):
params = shlex.split(cmd)
if len(params) < 1:
return '', []
return "", []
method = params[0]
typed_params = []
params = params[1:]
for i, param in enumerate(params):
if i >= len(type_map):
type_ind = 's'
type_ind = "s"
else:
type_ind = type_map[i]
if type_ind == 'i':
if type_ind == "i":
typed_params.append(int(param))
elif type_ind == 'f':
elif type_ind == "f":
typed_params.append(float(param))
elif type_ind == 'b':
elif type_ind == "b":
typed_params.append(toBool(param))
elif type_ind == 'j':
elif type_ind == "j":
typed_params.append(json.loads(param))
else:
typed_params.append(param)
@@ -111,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)
@@ -123,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
@@ -224,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
@@ -267,34 +297,33 @@ class HttpHandler(BaseHTTPRequestHandler):
summary = swap_client.getSummary()
result = None
cmd = ''
cmd = ""
coin_type_selected = -1
coin_type = -1
coin_id = -1
call_type = 'cli'
type_map = ''
call_type = "cli"
type_map = ""
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'rpc', err_messages)
form_data = self.checkForm(post_string, "rpc", err_messages)
if form_data:
try:
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
type_map = get_data_entry_or(form_data, 'type_map', '')
call_type = get_data_entry_or(form_data, "call_type", "cli")
type_map = get_data_entry_or(form_data, "type_map", "")
try:
coin_type_selected = get_data_entry(form_data, 'coin_type')
coin_type_split = coin_type_selected.split(',')
coin_type_selected = get_data_entry(form_data, "coin_type")
coin_type_split = coin_type_selected.split(",")
coin_type = Coins(int(coin_type_split[0]))
coin_variant = int(coin_type_split[1])
except Exception:
raise ValueError('Unknown Coin Type')
raise ValueError("Unknown Coin Type")
if coin_type in (Coins.DCR,):
call_type = 'http'
call_type = "http"
try:
cmd = get_data_entry(form_data, 'cmd')
cmd = get_data_entry(form_data, "cmd")
except Exception:
raise ValueError('Invalid command')
raise ValueError("Invalid command")
if coin_type in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_type)
arr = cmd.split(None, 1)
@@ -309,10 +338,10 @@ class HttpHandler(BaseHTTPRequestHandler):
params = None
rv = ci.rpc2(method, params)
else:
raise ValueError('Unknown RPC variant')
raise ValueError("Unknown RPC variant")
result = json.dumps(rv, indent=4)
else:
if call_type == 'http':
if call_type == "http":
ci = swap_client.ci(coin_type)
method, params = parse_cmd(cmd, type_map)
if coin_variant == 1:
@@ -320,50 +349,56 @@ class HttpHandler(BaseHTTPRequestHandler):
elif coin_variant == 2:
rv = ci.rpc_wallet_mweb(method, params)
else:
if coin_type in (Coins.DCR, ):
if coin_type in (Coins.DCR,):
rv = ci.rpc(method, params)
else:
rv = ci.rpc_wallet(method, params)
if not isinstance(rv, str):
rv = json.dumps(rv, indent=4)
result = cmd + '\n' + rv
result = cmd + "\n" + rv
else:
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
result = cmd + "\n" + swap_client.callcoincli(coin_type, cmd)
except Exception as ex:
result = cmd + '\n' + str(ex)
result = cmd + "\n" + str(ex)
if self.server.swap_client.debug is True:
self.server.swap_client.log.error(traceback.format_exc())
template = env.get_template('rpc.html')
template = env.get_template("rpc.html")
coin_available = listAvailableCoins(swap_client, with_variants=False)
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, Coins.WOW)]
coins = [
(str(c[0]) + ",0", c[1])
for c in coin_available
if c[0] not in (Coins.XMR, Coins.WOW)
]
if any(c[0] == Coins.DCR for c in coin_available):
coins.append((str(int(Coins.DCR)) + ',1', 'Decred Wallet'))
coins.append((str(int(Coins.DCR)) + ",1", "Decred Wallet"))
if any(c[0] == Coins.LTC for c in coin_available):
coins.append((str(int(Coins.LTC)) + ',2', 'Litecoin MWEB Wallet'))
coins.append((str(int(Coins.LTC)) + ",2", "Litecoin MWEB Wallet"))
if with_xmr:
coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
coins.append((str(int(Coins.XMR)) + ",0", "Monero"))
coins.append((str(int(Coins.XMR)) + ",1", "Monero JSON"))
coins.append((str(int(Coins.XMR)) + ",2", "Monero Wallet"))
if with_wow:
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
coins.append((str(int(Coins.WOW)) + ",0", "Wownero"))
coins.append((str(int(Coins.WOW)) + ",1", "Wownero JSON"))
coins.append((str(int(Coins.WOW)) + ",2", "Wownero Wallet"))
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'coins': coins,
'coin_type': coin_type_selected,
'call_type': call_type,
'result': result,
'messages': messages,
'summary': summary,
})
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"coins": coins,
"coin_type": coin_type_selected,
"call_type": call_type,
"result": result,
"summary": summary,
},
)
def page_active(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -371,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
@@ -384,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:
@@ -447,37 +500,42 @@ class HttpHandler(BaseHTTPRequestHandler):
swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex))
if len(url_split) > 1 and url_split[1] == 'static':
if len(url_split) > 1 and url_split[1] == "static":
try:
static_path = os.path.join(os.path.dirname(__file__), 'static')
if len(url_split) > 3 and url_split[2] == 'sequence_diagrams':
with open(os.path.join(static_path, 'sequence_diagrams', url_split[3]), 'rb') as fp:
self.putHeaders(status_code, 'image/svg+xml')
static_path = os.path.join(os.path.dirname(__file__), "static")
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
with open(
os.path.join(static_path, "sequence_diagrams", url_split[3]),
"rb",
) as fp:
self.putHeaders(status_code, "image/svg+xml")
return fp.read()
elif len(url_split) > 3 and url_split[2] == 'images':
elif len(url_split) > 3 and url_split[2] == "images":
filename = os.path.join(*url_split[3:])
_, extension = os.path.splitext(filename)
mime_type = {
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
}.get(extension, '')
if mime_type == '':
raise ValueError('Unknown file type ' + filename)
with open(os.path.join(static_path, 'images', filename), 'rb') as fp:
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
}.get(extension, "")
if mime_type == "":
raise ValueError("Unknown file type " + filename)
with open(
os.path.join(static_path, "images", filename), "rb"
) as fp:
self.putHeaders(status_code, mime_type)
return fp.read()
elif len(url_split) > 3 and url_split[2] == 'css':
elif len(url_split) > 3 and url_split[2] == "css":
filename = os.path.join(*url_split[3:])
with open(os.path.join(static_path, 'css', filename), 'rb') as fp:
self.putHeaders(status_code, 'text/css; charset=utf-8')
with open(os.path.join(static_path, "css", filename), "rb") as fp:
self.putHeaders(status_code, "text/css; charset=utf-8")
return fp.read()
elif len(url_split) > 3 and url_split[2] == 'js':
elif len(url_split) > 3 and url_split[2] == "js":
filename = os.path.join(*url_split[3:])
with open(os.path.join(static_path, 'js', filename), 'rb') as fp:
self.putHeaders(status_code, 'application/javascript')
with open(os.path.join(static_path, "js", filename), "rb") as fp:
self.putHeaders(status_code, "application/javascript")
return fp.read()
else:
return self.page_404(url_split)
@@ -492,63 +550,63 @@ 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':
if page == "receivedbids":
return page_bids(self, url_split, post_string, received=True)
if page == 'sentbids':
if page == "sentbids":
return page_bids(self, url_split, post_string, sent=True)
if page == 'availablebids':
if page == "availablebids":
return page_bids(self, url_split, post_string, available=True)
if page == 'watched':
if page == "watched":
return self.page_watched(url_split, post_string)
if page == 'smsgaddresses':
if page == "smsgaddresses":
return page_smsgaddresses(self, url_split, post_string)
if page == 'identity':
if page == "identity":
return page_identity(self, url_split, post_string)
if page == 'tor':
if page == "tor":
return page_tor(self, url_split, post_string)
if page == 'automation':
if page == "automation":
return page_automation_strategies(self, url_split, post_string)
if page == 'automationstrategy':
if page == "automationstrategy":
return page_automation_strategy(self, url_split, post_string)
if page == 'newautomationstrategy':
if page == "newautomationstrategy":
return page_automation_strategy_new(self, url_split, post_string)
if page == 'shutdown':
if page == "shutdown":
return self.page_shutdown(url_split, post_string)
if page == 'changepassword':
if page == "changepassword":
return page_changepassword(self, url_split, post_string)
if page == 'unlock':
if page == "unlock":
return page_unlock(self, url_split, post_string)
if page == 'lock':
if page == "lock":
return page_lock(self, url_split, post_string)
if page != '':
if page != "":
return self.page_404(url_split)
return self.page_index(url_split)
except LockedCoinError:
@@ -563,20 +621,20 @@ class HttpHandler(BaseHTTPRequestHandler):
self.wfile.write(response)
def do_POST(self):
post_string = self.rfile.read(int(self.headers.get('Content-Length')))
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
is_json = True if 'json' in self.headers.get('Content-Type', '') else False
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
self.wfile.write(response)
def do_HEAD(self):
self.putHeaders(200, 'text/html')
self.putHeaders(200, "text/html")
def do_OPTIONS(self):
self.send_response(200, 'ok')
self.send_response(200, "ok")
if self.server.allow_cors:
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Headers', '*')
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "*")
self.end_headers()
@@ -590,7 +648,7 @@ class HttpThread(threading.Thread, HTTPServer):
self.port_no = port_no
self.allow_cors = allow_cors
self.swap_client = swap_client
self.title = 'BasicSwap - ' + __version__
self.title = "BasicSwap - " + __version__
self.last_form_id = dict()
self.session_tokens = dict()
self.env = env
@@ -605,9 +663,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

@@ -14,7 +14,8 @@ from basicswap.chainparams import (
)
from basicswap.util import (
ensure,
i2b, b2i,
i2b,
b2i,
make_int,
format_amount,
TemporaryError,
@@ -26,9 +27,7 @@ from basicswap.util.ecc import (
ep,
getSecretInt,
)
from coincurve.dleag import (
verify_secp256k1_point
)
from coincurve.dleag import verify_secp256k1_point
from coincurve.keys import (
PublicKey,
)
@@ -52,6 +51,7 @@ class CoinInterface:
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
self._altruistic = True
def setDefaults(self):
self._unknown_wallet_seed = True
@@ -66,33 +66,33 @@ class CoinInterface:
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()
if "display_name" in coin_chainparams:
return coin_chainparams["display_name"]
return coin_chainparams["name"].capitalize()
def ticker(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
ticker = chainparams[self.coin_type()]["ticker"]
if self._network == "testnet":
ticker = "t" + ticker
elif self._network == "regtest":
ticker = "rt" + ticker
return ticker
def getExchangeTicker(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['ticker']
return chainparams[self.coin_type()]["ticker"]
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['name']
return chainparams[self.coin_type()]["name"]
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
ticker = chainparams[self.coin_type()]["ticker"]
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['min_amount']
return chainparams[self.coin_type()][self._network]["min_amount"]
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['max_amount']
return chainparams[self.coin_type()][self._network]["max_amount"]
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
@@ -110,7 +110,7 @@ class CoinInterface:
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
return chainparams[self.coin_type()].get("has_segwit", True)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
@@ -120,24 +120,26 @@ class CoinInterface:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if 'not enough unlocked money' in str_error:
if "not enough unlocked money" in str_error:
return True
if 'no unlocked balance' in str_error:
if "no unlocked balance" in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
if "transaction was rejected by daemon" in str_error:
return True
if 'invalid unlocked_balance' in str_error:
if "invalid unlocked_balance" in str_error:
return True
if 'daemon is busy' in str_error:
if "daemon is busy" in str_error:
return True
if 'timed out' in str_error:
if "timed out" in str_error:
return True
if 'request-sent' in str_error:
if "request-sent" in str_error:
return True
return False
def setConfTarget(self, new_conf_target: int) -> None:
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
ensure(
new_conf_target >= 1 and new_conf_target < 33, "Invalid conf_target value"
)
self._conf_target = new_conf_target
def walletRestoreHeight(self) -> int:
@@ -166,31 +168,19 @@ class CoinInterface:
def checkWallets(self) -> int:
return 1
def altruistic(self) -> bool:
return self._altruistic
class AdaptorSigInterface():
class AdaptorSigInterface:
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
return [b"", bytes(72), bytes(72), bytes(len(script))]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
return [bytes(72), b"", bytes(len(script))]
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
@@ -209,7 +199,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return (i < ep.o and i > 0)
return i < ep.o and i > 0
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)

1126
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

@@ -1,7 +1,8 @@
#!/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.
@@ -11,7 +12,10 @@ from basicswap.util.address import decodeAddress
from basicswap.contrib.mnemonic import Mnemonic
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
OP_CHECKSIG,
)
@@ -22,41 +26,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 entropyToMnemonic(self, key: bytes) -> str:
return Mnemonic('english').to_mnemonic(key)
def initialiseWallet(self, key: bytes):
words = self.entropyToMnemonic(key)
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)
self._wallet_v20_compatible = (
False
if not swap_client
else swap_client.getChainClientSettings(self.coin_type()).get(
"wallet_v20_compatible", False
)
)
def decodeAddress(self, address: str) -> bytes:
return decodeAddress(address)[1:]
def checkExpectedSeed(self, key_hash: str):
try:
rv = self.rpc_wallet('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_wallet('sendtoaddress', params)
params = [addr_to, value, "", "", subfee, False, False, self._conf_target]
return self.rpc_wallet("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
return self.make_int(self.rpc_wallet("getwalletinfo")["balance"])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
@@ -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_wallet('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
rv = self.rpc_wallet("gettransaction", [txid_hex])
except Exception as e: # noqa: F841
self._log.debug(
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
)
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 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.
@@ -38,7 +39,9 @@ from basicswap.util.address import (
encodeAddress,
)
from basicswap.util import (
b2i, i2b, i2h,
b2i,
i2b,
i2h,
ensure,
)
from basicswap.basicswap_util import (
@@ -49,7 +52,10 @@ from basicswap.interface.contrib.nav_test_framework.script import (
CScript,
OP_0,
OP_EQUAL,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
OP_CHECKSIG,
SIGHASH_ALL,
SegwitVersion1SignatureHash,
)
@@ -71,15 +77,14 @@ class NAVInterface(BTCInterface):
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)
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return True
def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic('english').to_mnemonic(key)
def initialiseWallet(self, key):
# Load with -importmnemonic= parameter
pass
@@ -88,31 +93,31 @@ class NAVInterface(BTCInterface):
return 1
def getWalletSeedID(self):
return self.rpc('getwalletinfo')['hdmasterkeyid']
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)
strdzeel = ""
params = [addr_to, value, "", "", strdzeel, subfee]
return self.rpc("sendtoaddress", params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc('getwalletinfo')['balance'])
return self.make_int(self.rpc("getwalletinfo")["balance"])
def signTxWithWallet(self, tx: bytes) -> bytes:
rv = self.rpc('signrawtransaction', [tx.hex()])
rv = self.rpc("signrawtransaction", [tx.hex()])
return bytes.fromhex(rv['hex'])
return bytes.fromhex(rv["hex"])
def checkExpectedSeed(self, key_hash: str):
try:
rv = self.rpc('dumpmnemonic')
entropy = Mnemonic('english').to_entropy(rv.split(' '))
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)))
self._log.warning("checkExpectedSeed failed: {}".format(str(e)))
return False
def getScriptForP2PKH(self, pkh: bytes) -> bytearray:
@@ -137,13 +142,22 @@ class NAVInterface(BTCInterface):
script = CScript([OP_0, pkh])
script_hash = hash160(script)
assert len(script_hash) == 20
return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash)
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:
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')
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
@@ -151,9 +165,18 @@ class NAVInterface(BTCInterface):
tx.deserialize(BytesIO(tx_bytes))
return tx
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script, prevout_value: int):
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)
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,))
@@ -168,23 +191,25 @@ class NAVInterface(BTCInterface):
# TODO: Lock unspent and use same output/s to fund bid
unspents_by_addr = dict()
unspents = self.rpc('listunspent')
unspents = self.rpc("listunspent")
for u in unspents:
if u['spendable'] is not True:
if u["spendable"] is not True:
continue
if u['address'] not in unspents_by_addr:
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
utxo_amount: int = self.make_int(u['amount'], r=1)
unspents_by_addr[u['address']]['total'] += utxo_amount
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
if u["address"] not in unspents_by_addr:
unspents_by_addr[u["address"]] = {"total": 0, "utxos": []}
utxo_amount: int = self.make_int(u["amount"], r=1)
unspents_by_addr[u["address"]]["total"] += utxo_amount
unspents_by_addr[u["address"]]["utxos"].append(
(utxo_amount, u["txid"], u["vout"])
)
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data['total'] >= amount_for:
if data["total"] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
@@ -199,13 +224,17 @@ class NAVInterface(BTCInterface):
viable_addrs.append(addr)
continue
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
ensure(
len(viable_addrs) > 0, "Could not find address with enough funds for proof"
)
sign_for_addr: str = random.choice(viable_addrs)
self._log.debug('sign_for_addr %s', sign_for_addr)
self._log.debug("sign_for_addr %s", sign_for_addr)
prove_utxos = []
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
sorted_utxos = sorted(
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
)
hasher = hashlib.sha256()
@@ -215,20 +244,36 @@ class NAVInterface(BTCInterface):
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[1].to_bytes(2, "big"))
if sum_value >= amount_for:
break
utxos_hash = hasher.digest()
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
if (
self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
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:]
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)
self._log.debug("sign_for_addr converted %s", sign_for_addr)
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
signature = self.rpc(
"signmessage",
[
sign_for_addr,
sign_for_addr
+ "_swap_proof_"
+ utxos_hash.hex()
+ extra_commit_bytes.hex(),
],
)
return (sign_for_addr, signature, prove_utxos)
@@ -237,48 +282,64 @@ class NAVInterface(BTCInterface):
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
hasher.update(outpoint[1].to_bytes(2, "big"))
utxos_hash = hasher.digest()
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
passed = self.verifyMessage(
address,
address + "_swap_proof_" + utxos_hash.hex() + extra_commit_bytes.hex(),
signature,
)
ensure(passed is True, "Proof of funds signature invalid")
if self.using_segwit():
address = self.encodeSegwitAddress(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'])
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)}])
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}"
)
if sub_fee:
raise ValueError('Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter')
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])
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 createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc('signrawtransaction', [txn_funded])['hex']
return self.rpc("signrawtransaction", [txn_funded])["hex"]
def getBlockchainInfo(self):
rv = self.rpc('getblockchaininfo')
synced = round(rv['verificationprogress'], 3)
rv = self.rpc("getblockchaininfo")
synced = round(rv["verificationprogress"], 3)
if synced >= 0.997:
rv['verificationprogress'] = 1.0
rv["verificationprogress"] = 1.0
return rv
def encodeScriptDest(self, script_dest: bytes) -> str:
@@ -286,47 +347,75 @@ class NAVInterface(BTCInterface):
return self.sh_to_address(script_hash)
def encode_p2wsh(self, script: bytes) -> str:
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
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])
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']
"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,])
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 self.rpc(
"addwitnessaddress",
[
address,
],
)
return address
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str:
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']))
prev_txid = b2i(bytes.fromhex(prevout["txid"]))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
scriptSig=self.getScriptScriptSig(txn_script)))
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:
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)))
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))
@@ -335,22 +424,38 @@ class NAVInterface(BTCInterface):
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']))
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:
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)
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
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')
self._log.warning("NAV verifyRawTransaction only checks signature")
inputs_valid: bool = False
validscripts: int = 0
@@ -362,22 +467,26 @@ class NAVInterface(BTCInterface):
input_n: int = 0
prevout_data = prevouts[input_n]
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
prevout_value = self.make_int(prevout_data['amount'])
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):
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,
"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 = (
5 # Add a few bytes, sequence in script takes variable amount of bytes
)
tx_vsize += 184 if redeem else 187
return tx_vsize
@@ -396,15 +505,15 @@ class NAVInterface(BTCInterface):
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
block_hash = self.rpc('getblockhash', [current_height])
block_hash = self.rpc("getblockhash", [current_height])
script_hash: bytes = self.decodeAddress(addr_find)
find_scriptPubKey = self.getDestForScriptHash(script_hash)
while current_height > height_start:
block_hash = self.rpc('getblockhash', [current_height])
block_hash = self.rpc("getblockhash", [current_height])
block = self.rpc('getblock', [block_hash, False])
block = self.rpc("getblock", [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
@@ -412,84 +521,116 @@ class NAVInterface(BTCInterface):
if txo.scriptPubKey == find_scriptPubKey:
tx.rehash()
txid = i2b(tx.sha256)
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
self.rpc('invalidateblock', [block_hash])
self.rpc('reconsiderblock', [block_hash])
self._log.info(
"Found output to addr: {} in tx {} in block {}".format(
addr_find, txid.hex(), block_hash
)
)
self._log.info(
"rescanblockchain hack invalidateblock {}".format(
block_hash
)
)
self.rpc("invalidateblock", [block_hash])
self.rpc("reconsiderblock", [block_hash])
return
current_height -= 1
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
# Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.importWatchOnlyAddress(dest_address, "bid")
self._log.info("Imported watch-only addr: {}".format(dest_address))
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from
)
)
self.rescanBlockchainForAddress(rescan_from, dest_address)
return_txid = True if txid is None else False
if txid is None:
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
txns = self.rpc(
"listunspent",
[
0,
9999999,
[
dest_address,
],
],
)
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
txid = bytes.fromhex(tx['txid'])
if self.make_int(tx["amount"]) == bid_amount:
txid = bytes.fromhex(tx["txid"])
break
if txid is None:
return None
try:
tx = self.rpc('gettransaction', [txid.hex()])
tx = self.rpc("gettransaction", [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
if "blockhash" in tx:
block_header = self.rpc("getblockheader", [tx["blockhash"]])
block_height = block_header["height"]
rv = {
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
'height': block_height}
"depth": 0 if "confirmations" not in tx else tx["confirmations"],
"height": block_height,
}
except Exception as e:
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
self._log.debug(
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
)
return None
if find_index:
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
rv['txid'] = txid.hex()
rv["txid"] = txid.hex()
return rv
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = []
for tx in decoded_block.vtx:
tx_hex = tx.serialize_with_witness().hex()
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
if 'hex' not in tx_dec:
tx_dec['hex'] = tx_hex
tx_dec = self.rpc("decoderawtransaction", [tx_hex])
if "hex" not in tx_dec:
tx_dec["hex"] = tx_hex
tx_rv.append(tx_dec)
block_rv = {
'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
@@ -516,15 +657,30 @@ class NAVInterface(BTCInterface):
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, 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']))
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,
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')
ensure(locked_n is not None, "Output not found in tx")
pkh_to = self.decodeAddress(address_to)
tx = CTransaction()
@@ -534,10 +690,16 @@ class NAVInterface(BTCInterface):
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)))
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
@@ -563,16 +725,20 @@ class NAVInterface(BTCInterface):
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
rv = self.rpc("gettransaction", [txid_hex])
except Exception as e: # noqa: F841
self._log.debug(
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
)
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
return None
def 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)))
@@ -583,20 +749,20 @@ class NAVInterface(BTCInterface):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
'lockUnspents': lock_unspents,
'feeRate': feerate_str,
"lockUnspents": lock_unspents,
"feeRate": feerate_str,
}
rv = self.rpc('fundrawtransaction', [tx_hex, options])
rv = self.rpc("fundrawtransaction", [tx_hex, options])
# Sign transaction then strip witness data to fill scriptsig
rv = self.rpc('signrawtransaction', [rv['hex']])
rv = self.rpc("signrawtransaction", [rv["hex"]])
tx_signed = self.loadTx(bytes.fromhex(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')
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')
raise ValueError("txn has non segwit input")
return tx_signed.serialize_without_witness()
@@ -604,13 +770,23 @@ class NAVInterface(BTCInterface):
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):
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')
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
@@ -619,9 +795,13 @@ class NAVInterface(BTCInterface):
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.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)
@@ -631,12 +811,24 @@ class NAVInterface(BTCInterface):
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
self._log.info(
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
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):
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
@@ -645,7 +837,7 @@ class NAVInterface(BTCInterface):
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')
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock_refund.vout[locked_n].nValue
tx_lock_refund.rehash()
@@ -653,25 +845,46 @@ class NAVInterface(BTCInterface):
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.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)))
tx.vout.append(
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))
)
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
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 %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
self._log.info(
"createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
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):
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
@@ -679,7 +892,7 @@ class NAVInterface(BTCInterface):
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')
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)
@@ -689,29 +902,44 @@ class NAVInterface(BTCInterface):
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.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)))
tx.vout.append(
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))
)
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
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 %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
self._log.info(
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
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={}):
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')
ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
@@ -719,10 +947,16 @@ class NAVInterface(BTCInterface):
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
scriptSig=self.getScriptScriptSig(script_lock)))
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)))
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)
@@ -730,13 +964,18 @@ class NAVInterface(BTCInterface):
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
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 %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
self._log.info(
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
i2h(tx.sha256),
tx_fee_rate,
vsize,
pay_fee,
)
return tx.serialize()

View File

@@ -14,26 +14,38 @@ class NMCInterface(BTCInterface):
def coin_type():
return Coins.NMC
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end')
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
ro = self.rpc(
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
) # TODO: Use combo(address) where possible
self._log.debug("[rm] scantxoutset end")
return_txid = True if txid is None else False
for o in ro['unspents']:
if txid and o['txid'] != txid.hex():
for o in ro["unspents"]:
if txid and o["txid"] != txid.hex():
continue
# Verify amount
if self.make_int(o['amount']) != int(bid_amount):
self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
if self.make_int(o["amount"]) != int(bid_amount):
self._log.warning(
"Found output to lock tx address of incorrect value: %s, %s",
str(o["amount"]),
o["txid"],
)
continue
rv = {
'depth': 0,
'height': o['height']}
if o['height'] > 0:
rv['depth'] = ro['height'] - o['height']
rv = {"depth": 0, "height": o["height"]}
if o["height"] > 0:
rv["depth"] = ro["height"] - o["height"]
if find_index:
rv['index'] = o['vout']
rv["index"] = o["vout"]
if return_txid:
rv['txid'] = o['txid']
rv["txid"] = o["txid"]
return rv

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -25,3 +25,7 @@ class WOWInterface(XMRInterface):
@staticmethod
def exp() -> int:
return 11
@staticmethod
def depth_spendable() -> int:
return 3

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
'''
"""
syntax = "proto3";
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
@@ -18,12 +18,12 @@ Don't encode fields of default values.
When decoding initialise all fields not set from data.
protobuf ParseFromString would reset the whole object, from_bytes won't.
'''
"""
from basicswap.util.integer import encode_varint, decode_varint
class NonProtobufClass():
class NonProtobufClass:
def __init__(self, init_all: bool = True, **kwargs):
for key, value in kwargs.items():
found_field: bool = False
@@ -34,7 +34,7 @@ class NonProtobufClass():
found_field = True
break
if found_field is False:
raise ValueError(f'got an unexpected keyword argument \'{key}\'')
raise ValueError(f"got an unexpected keyword argument '{key}'")
if init_all:
self.init_fields()
@@ -53,7 +53,7 @@ class NonProtobufClass():
else:
setattr(self, field_name, bytes())
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
def to_bytes(self) -> bytes:
rv = bytes()
@@ -74,11 +74,11 @@ class NonProtobufClass():
continue
rv += encode_varint(tag)
if isinstance(field_value, str):
field_value = field_value.encode('utf-8')
field_value = field_value.encode("utf-8")
rv += encode_varint(len(field_value))
rv += field_value
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
return rv
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
@@ -92,7 +92,9 @@ class NonProtobufClass():
field_name, wire_type_expect, field_type = self._map[field_num]
if wire_type != wire_type_expect:
raise ValueError(f'Unexpected wire_type {wire_type} for field {field_num}')
raise ValueError(
f"Unexpected wire_type {wire_type} for field {field_num}"
)
if wire_type == 0:
field_value, lv = decode_varint(b, o)
@@ -100,12 +102,12 @@ class NonProtobufClass():
elif wire_type == 2:
field_len, lv = decode_varint(b, o)
o += lv
field_value = b[o: o + field_len]
field_value = b[o : o + field_len]
o += field_len
if field_type == 1:
field_value = field_value.decode('utf-8')
field_value = field_value.decode("utf-8")
else:
raise ValueError(f'Unknown wire_type {wire_type}')
raise ValueError(f"Unknown wire_type {wire_type}")
setattr(self, field_name, field_value)
@@ -115,151 +117,150 @@ class NonProtobufClass():
class OfferMessage(NonProtobufClass):
_map = {
1: ('protocol_version', 0, 0),
2: ('coin_from', 0, 0),
3: ('coin_to', 0, 0),
4: ('amount_from', 0, 0),
5: ('amount_to', 0, 0),
6: ('min_bid_amount', 0, 0),
7: ('time_valid', 0, 0),
8: ('lock_type', 0, 0),
9: ('lock_value', 0, 0),
10: ('swap_type', 0, 0),
11: ('proof_address', 2, 1),
12: ('proof_signature', 2, 1),
13: ('pkhash_seller', 2, 0),
14: ('secret_hash', 2, 0),
15: ('fee_rate_from', 0, 0),
16: ('fee_rate_to', 0, 0),
17: ('amount_negotiable', 0, 2),
18: ('rate_negotiable', 0, 2),
19: ('proof_utxos', 2, 0),
1: ("protocol_version", 0, 0),
2: ("coin_from", 0, 0),
3: ("coin_to", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
6: ("min_bid_amount", 0, 0),
7: ("time_valid", 0, 0),
8: ("lock_type", 0, 0),
9: ("lock_value", 0, 0),
10: ("swap_type", 0, 0),
11: ("proof_address", 2, 1),
12: ("proof_signature", 2, 1),
13: ("pkhash_seller", 2, 0),
14: ("secret_hash", 2, 0),
15: ("fee_rate_from", 0, 0),
16: ("fee_rate_to", 0, 0),
17: ("amount_negotiable", 0, 2),
18: ("rate_negotiable", 0, 2),
19: ("proof_utxos", 2, 0),
}
class BidMessage(NonProtobufClass):
_map = {
1: ('protocol_version', 0, 0),
2: ('offer_msg_id', 2, 0),
3: ('time_valid', 0, 0),
4: ('amount', 0, 0),
5: ('amount_to', 0, 0),
6: ('pkhash_buyer', 2, 0),
7: ('proof_address', 2, 1),
8: ('proof_signature', 2, 1),
9: ('proof_utxos', 2, 0),
10: ('pkhash_buyer_to', 2, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkhash_buyer", 2, 0),
7: ("proof_address", 2, 1),
8: ("proof_signature", 2, 1),
9: ("proof_utxos", 2, 0),
10: ("pkhash_buyer_to", 2, 0),
}
class BidAcceptMessage(NonProtobufClass):
# Step 3, seller -> buyer
_map = {
1: ('bid_msg_id', 2, 0),
2: ('initiate_txid', 2, 0),
3: ('contract_script', 2, 0),
4: ('pkhash_seller', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("initiate_txid", 2, 0),
3: ("contract_script", 2, 0),
4: ("pkhash_seller", 2, 0),
}
class OfferRevokeMessage(NonProtobufClass):
_map = {
1: ('offer_msg_id', 2, 0),
2: ('signature', 2, 0),
1: ("offer_msg_id", 2, 0),
2: ("signature", 2, 0),
}
class BidRejectMessage(NonProtobufClass):
_map = {
1: ('bid_msg_id', 2, 0),
2: ('reject_code', 0, 0),
1: ("bid_msg_id", 2, 0),
2: ("reject_code", 0, 0),
}
class XmrBidMessage(NonProtobufClass):
# MSG1L, F -> L
_map = {
1: ('protocol_version', 0, 0),
2: ('offer_msg_id', 2, 0),
3: ('time_valid', 0, 0),
4: ('amount', 0, 0),
5: ('amount_to', 0, 0),
6: ('pkaf', 2, 0),
7: ('kbvf', 2, 0),
8: ('kbsf_dleag', 2, 0),
9: ('dest_af', 2, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkaf", 2, 0),
7: ("kbvf", 2, 0),
8: ("kbsf_dleag", 2, 0),
9: ("dest_af", 2, 0),
}
class XmrSplitMessage(NonProtobufClass):
_map = {
1: ('msg_id', 2, 0),
2: ('msg_type', 0, 0),
3: ('sequence', 0, 0),
4: ('dleag', 2, 0),
1: ("msg_id", 2, 0),
2: ("msg_type", 0, 0),
3: ("sequence", 0, 0),
4: ("dleag", 2, 0),
}
class XmrBidAcceptMessage(NonProtobufClass):
_map = {
1: ('bid_msg_id', 2, 0),
2: ('pkal', 2, 0),
3: ('kbvl', 2, 0),
4: ('kbsl_dleag', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("pkal", 2, 0),
3: ("kbvl", 2, 0),
4: ("kbsl_dleag", 2, 0),
# MSG2F
5: ('a_lock_tx', 2, 0),
6: ('a_lock_tx_script', 2, 0),
7: ('a_lock_refund_tx', 2, 0),
8: ('a_lock_refund_tx_script', 2, 0),
9: ('a_lock_refund_spend_tx', 2, 0),
10: ('al_lock_refund_tx_sig', 2, 0),
5: ("a_lock_tx", 2, 0),
6: ("a_lock_tx_script", 2, 0),
7: ("a_lock_refund_tx", 2, 0),
8: ("a_lock_refund_tx_script", 2, 0),
9: ("a_lock_refund_spend_tx", 2, 0),
10: ("al_lock_refund_tx_sig", 2, 0),
}
class XmrBidLockTxSigsMessage(NonProtobufClass):
# MSG3L
_map = {
1: ('bid_msg_id', 2, 0),
2: ('af_lock_refund_spend_tx_esig', 2, 0),
3: ('af_lock_refund_tx_sig', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("af_lock_refund_spend_tx_esig", 2, 0),
3: ("af_lock_refund_tx_sig", 2, 0),
}
class XmrBidLockSpendTxMessage(NonProtobufClass):
# MSG4F
_map = {
1: ('bid_msg_id', 2, 0),
2: ('a_lock_spend_tx', 2, 0),
3: ('kal_sig', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("a_lock_spend_tx", 2, 0),
3: ("kal_sig", 2, 0),
}
class XmrBidLockReleaseMessage(NonProtobufClass):
# MSG5F
_map = {
1: ('bid_msg_id', 2, 0),
2: ('al_lock_spend_tx_esig', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("al_lock_spend_tx_esig", 2, 0),
}
class ADSBidIntentMessage(NonProtobufClass):
# L -> F Sent from bidder, construct a reverse bid
_map = {
1: ('protocol_version', 0, 0),
2: ('offer_msg_id', 2, 0),
3: ('time_valid', 0, 0),
4: ('amount_from', 0, 0),
5: ('amount_to', 0, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
}
class ADSBidIntentAcceptMessage(NonProtobufClass):
# F -> L Sent from offerer, construct a reverse bid
_map = {
1: ('bid_msg_id', 2, 0),
2: ('pkaf', 2, 0),
3: ('kbvf', 2, 0),
4: ('kbsf_dleag', 2, 0),
5: ('dest_af', 2, 0),
1: ("bid_msg_id", 2, 0),
2: ("pkaf", 2, 0),
3: ("kbvf", 2, 0),
4: ("kbsf_dleag", 2, 0),
5: ("dest_af", 2, 0),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,64 +1,63 @@
.padded_row td
{
padding-top:1.5em;
/* General Styles */
.bold {
font-weight: bold;
}
.bold
{
font-weight:bold;
.monospace {
font-family: monospace;
}
.monospace
{
font-family:monospace;
}
.floatright
{
.floatright {
position: fixed;
top:1.25rem;
right:1.25rem;
top: 1.25rem;
right: 1.25rem;
z-index: 9999;
}
.error_msg
{
/* Table Styles */
.padded_row td {
padding-top: 1.5em;
}
/* Modal Styles */
.modal-highest {
z-index: 9999;
}
/* Animation */
#hide {
-moz-animation: cssAnimation 0s ease-in 15s forwards;
/* Firefox */
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
/* Safari and Chrome */
-o-animation: cssAnimation 0s ease-in 15s forwards;
/* Opera */
animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
@keyframes cssAnimation {
to {
width:0;
height:0;
overflow:hidden;
}
}
@-webkit-keyframes cssAnimation {
to {
width:0;
height:0;
visibility:hidden;
}
-moz-animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
-o-animation: cssAnimation 0s ease-in 15s forwards;
animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
.custom-select .select {
appearance: none;
background-image: url('/static/images/other/coin.png');
background-position: 10px center;
background-repeat: no-repeat;
position: relative;
@keyframes cssAnimation {
to {
width: 0;
height: 0;
overflow: hidden;
}
}
@-webkit-keyframes cssAnimation {
to {
width: 0;
height: 0;
visibility: hidden;
}
}
/* Custom Select Styles */
.custom-select .select {
appearance: none;
background-image: url('/static/images/other/coin.png');
background-position: 10px center;
background-repeat: no-repeat;
position: relative;
}
.custom-select select::-webkit-scrollbar {
width: 0;
@@ -95,19 +94,20 @@
display: block;
}
/* Blur and Overlay Styles */
.blurred {
filter: blur(4px);
pointer-events: none;
user-select: none;
filter: blur(3px);
pointer-events: none;
user-select: none;
}
.error-overlay.non-blurred {
filter: none;
pointer-events: auto;
user-select: auto;
filter: none;
pointer-events: auto;
user-select: auto;
}
/* Disable opacity on disabled form elements in Chrome */
/* Form Element Styles */
@media screen and (-webkit-min-device-pixel-ratio:0) {
select:disabled,
input:disabled,
@@ -120,6 +120,7 @@
border: 1px solid red !important;
}
/* Active Container Styles */
.active-container {
position: relative;
border-radius: 10px;
@@ -137,6 +138,7 @@
pointer-events: none;
}
/* Center Spin Animation */
.center-spin {
display: flex;
justify-content: center;
@@ -148,10 +150,11 @@
100% { transform: rotate(360deg); }
}
.hover-container:hover #coin_to_button, .hover-container:hover #coin_to {
border-color: #3b82f6;
}
.hover-container:hover #coin_from_button, .hover-container:hover #coin_from {
/* Hover Container Styles */
.hover-container:hover #coin_to_button,
.hover-container:hover #coin_to,
.hover-container:hover #coin_from_button,
.hover-container:hover #coin_from {
border-color: #3b82f6;
}
@@ -161,6 +164,7 @@
background-size: 20px 20px;
}
/* Input-like Container Styles */
.input-like-container {
max-width: 100%;
background-color: #ffffff;
@@ -172,41 +176,45 @@
line-height: 1.25rem;
outline: none;
word-wrap: break-word;
height: 90px;
color: #374151;
border-radius: 0.375rem;
font-size: 0.875rem;
line-height: 1.25rem;
outline: none;
overflow-wrap: break-word;
word-break: break-all;
height: auto;
min-height: 90px;
max-height: 150px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow-y: auto;
}
.input-like-container.dark {
background-color: #374151;
color: #ffffff;
}
.input-like-container.copying {
width: inherit;
}
/* QR Code Styles */
.qrcode {
position: relative;
display: inline-block;
padding: 10px;
overflow: hidden;
position: relative;
}
.qrcode-border {
border: 2px solid;
border-color: rgba(59, 130, 246, var(--tw-border-opacity));
border-radius: 20px;
background-color: #ffffff;
border-radius: 0px;
}
.qrcode img {
width: 100%;
height: auto;
border-radius: 15px;
border-radius: 0px;
}
#showQR {
@@ -214,62 +222,138 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height:25px;
height: 25px;
}
select.select-disabled {
opacity: 0.40 !important;
.qrcode-container {
margin-top: 25px;
}
.disabled-input-enabled {
opacity: 0.40 !important;
}
/* Disabled Element Styles */
select.select-disabled,
.disabled-input-enabled,
select.disabled-select-enabled {
opacity: 0.40 !important;
opacity: 0.40 !important;
}
/* Shutdown Modal Styles */
#shutdownModal {
z-index: 50;
}
#shutdownModal > div:first-child {
z-index: 40;
}
#shutdownModal > div:last-child {
z-index: 50;
}
#shutdownModal > div {
transition: opacity 0.3s ease-out;
}
#shutdownModal.hidden > div {
opacity: 0;
}
#shutdownModal:not(.hidden) > div {
opacity: 1;
}
.shutdown-button {
transition: all 0.3s ease;
}
.shutdown-button.shutdown-disabled {
opacity: 0.6;
cursor: not-allowed;
color: #a0aec0;
}
.shutdown-button.shutdown-disabled:hover {
background-color: #4a5568;
}
.shutdown-button.shutdown-disabled svg {
opacity: 0.5;
}
.custom-select .select {
appearance: none;
background-position: 10px center;
background-repeat: no-repeat;
position: relative;
/* Loading line animation */
.loading-line {
width: 100%;
height: 2px;
background-color: #ccc;
overflow: hidden;
position: relative;
}
.loading-line::before {
content: '';
display: block;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, #007bff, transparent);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/* Hide the loading line once data is loaded */
.usd-value:not(.loading) .loading-line,
.profit-loss:not(.loading) .loading-line {
display: none;
}
.resolution-button {
background: none;
border: none;
color: #4B5563; /* gray-600 */
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s;
outline: 2px solid transparent;
outline-offset: 2px;
}
.custom-select select::-webkit-scrollbar {
width: 0;
.resolution-button:hover {
color: #1F2937; /* gray-800 */
}
.custom-select .select option {
padding-left: 0;
text-indent: 0;
background-repeat: no-repeat;
background-position: 0 50%;
.resolution-button:focus {
outline: 2px solid #3B82F6; /* blue-500 */
}
.custom-select .select option.no-space {
padding-left: 0;
.resolution-button.active {
color: #3B82F6; /* blue-500 */
outline: 2px solid #3B82F6; /* blue-500 */
}
.custom-select .select option[data-image] {
background-image: url('');
.dark .resolution-button {
color: #9CA3AF; /* gray-400 */
}
.custom-select .select-icon {
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
.dark .resolution-button:hover {
color: #F3F4F6; /* gray-100 */
}
.custom-select .select-image {
display: none;
margin-top: 10px;
}
.custom-select .select:focus+.select-dropdown .select-image {
display: block;
.dark .resolution-button.active {
color: #60A5FA; /* blue-400 */
outline-color: #60A5FA; /* blue-400 */
color: #fff;
}
#toggle-volume.active {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@
<p class="font-normal text-coolGray-200 dark:text-white"><span class="bold">BID ID:</span> {{ bid_id }}</p>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
{% if refresh %}
{% if refresh %}
<a id="refresh" href="/bid/{{ bid_id }}" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ circular_arrows_svg | safe }}
<span>Refresh {{ refresh }} seconds</span>
@@ -378,13 +378,13 @@
{% if data.xmr_b_half_privatekey %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Key Half (WARNING key data!):</td>
<td class="py-3 px-6 monospace">{{ data.xmr_b_half_privatekey }}</td>
<td class="py-3 px-6 monospace" id="localkeyhalf">{{ data.xmr_b_half_privatekey }}</td>
</tr>
{% endif %}
{% if data.xmr_b_half_privatekey_remote %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Remote Key Half:</td>
<td class="py-3 px-6 monospace">{{ data.xmr_b_half_privatekey_remote }}</td>
<td class="py-3 px-6 monospace" id="remotekeyhalf">{{ data.xmr_b_half_privatekey_remote }}</td>
</tr>
{% endif %}
</table>
@@ -845,4 +845,4 @@
</div>
{% include 'footer.html' %}
</body>
</html>
</html>

View File

@@ -5,14 +5,9 @@
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
@@ -37,8 +32,8 @@
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change Password</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Update your BasicSwap / Wallets password.</p>
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change/Set your Password</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Change or Set your BasicSwap / Wallets password.</p>
</div>
</div>
</div>
@@ -187,4 +182,4 @@ toggles.forEach(function (toggle_id, index) {
});
</script>
</body>
</html>
</html>

View File

@@ -1,13 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
<body>
<h2>{{ title_str }}</h2>
<p>Info: {{ message_str }}</p>
<p><a href="/">home</a></p>
</body>
</html>
<head>
<meta charset="UTF-8">
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script>
<script>
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
}
document.documentElement.classList.toggle('dark', isDarkMode);
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - Info</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.container {
width: 100%;
max-width: 600px;
}
</style>
</head>
<body class="dark:bg-gray-700">
<div class="container px-4 mx-auto">
<div class="text-center">
<a class="inline-block mb-6" href="#">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
</a>
<div class="p-6 bg-coolGray-100 dark:bg-gray-800 rounded-lg shadow-md">
<h2 class="text-xl font-bold mb-4 text-coolGray-900 dark:text-white">ERROR:</h2>
<p class="text-lg text-coolGray-500 dark:text-white mb-6">{{ message_str }}</p>
<a href="/" class="inline-block py-2 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition duration-300">Home</a>
</div>
<p class="mt-10">
<span class="text-xs font-medium dark:text-white">Need help?</span>
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
</p>
<p>
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Image toggling function
function toggleImages() {
const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image');
const lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
}
function toggleImageDisplay(images, display) {
images.forEach(img => {
img.style.display = display;
});
}
// Theme toggle functionality
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
// Initialize theme
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
}
// Call toggleImages on load
toggleImages();
});
</script>
</body>
</html>

View File

@@ -11,6 +11,7 @@
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-dex/basicswap_explained.html" target="_blank">BasicSwap Explained</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_installation.html" target="_blank">Tutorials and Guides</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Get Support</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://basicswapdex.com/terms" target="_blank">Terms and Conditions</a></div>
</div>
</div>
<div class="w-full md:w-1/4 px-4"> </div>
@@ -24,16 +25,16 @@
<div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2024~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.0.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.1.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }}
<span class="ml-2 text-sm font-bold dark:text-white text-gray-90 ">by TV and CRZ</span></div>
</div>
</div>
</div>
<div class="w-full md:w-1/2">
<div class="flex flex-wrap md:justify-end -mx-5">
<div class="px-5">
<a class="inline-block text-coolGray-300 hover:text-coolGray-400" href="https://github.com/tecnovert/basicswap" target="_blank">
<a class="inline-block text-coolGray-300 hover:text-coolGray-400" href="https://github.com/basicswap/basicswap" target="_blank">
{{ github_svg | safe }}
</a>
</div>
@@ -43,69 +44,67 @@
</div>
</section>
<script>
(function() {
var toggleImages = function() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
var toggleImages = function() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
};
var toggleImageDisplay = function(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
}
document.addEventListener('DOMContentLoaded', function() {
var themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
toggleImages();
});
}
toggleImages();
var toggleImageDisplay = function(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
})();
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
};
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
document.addEventListener('DOMContentLoaded', function() {
var themeToggle = document.getElementById('theme-toggle');
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
toggleImages();
});
}
</script>
toggleImages();
});
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
</script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
{% from 'style.html' import notifications_network_offer_svg, notifications_bid_accepted_svg, notifications_unknow_event_svg, notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, bids_sent_svg, header_arrow_down_svg, love_svg %}
{% from 'style.html' import change_password_svg, notifications_network_offer_svg, notifications_bid_accepted_svg, notifications_unknow_event_svg, notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, bids_sent_svg, header_arrow_down_svg, love_svg %}
<html lang="en">
<head>
<meta charset="UTF-8">
@@ -7,11 +7,13 @@
<meta http-equiv="refresh" content="{{ refresh }}">
{% endif %}
<script src="/static/js/libs/chart.js"></script>
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script>
<script>
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
@@ -19,7 +21,7 @@
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
localStorage.setItem('color-theme', 'dark');
}
document.documentElement.classList.toggle('dark', isDarkMode);
@@ -84,8 +86,73 @@
}
},
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const shutdownButtons = document.querySelectorAll('.shutdown-button');
const shutdownModal = document.getElementById('shutdownModal');
const closeModalButton = document.getElementById('closeShutdownModal');
const confirmShutdownButton = document.getElementById('confirmShutdown');
const shutdownWarning = document.getElementById('shutdownWarning');
function updateShutdownButtons() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
shutdownButtons.forEach(button => {
if (activeSwaps > 0) {
button.classList.add('shutdown-disabled');
button.setAttribute('data-disabled', 'true');
button.setAttribute('title', 'Caution: Swaps in progress');
} else {
button.classList.remove('shutdown-disabled');
button.removeAttribute('data-disabled');
button.removeAttribute('title');
}
});
}
function showShutdownModal() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
if (activeSwaps > 0) {
shutdownWarning.classList.remove('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
} else {
shutdownWarning.classList.add('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down';
}
shutdownModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideShutdownModal() {
shutdownModal.classList.add('hidden');
document.body.style.overflow = '';
}
shutdownButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
showShutdownModal();
});
});
closeModalButton.addEventListener('click', hideShutdownModal);
confirmShutdownButton.addEventListener('click', function() {
const shutdownToken = document.querySelector('.shutdown-button').getAttribute('href').split('/').pop();
window.location.href = '/shutdown/' + shutdownToken;
});
shutdownModal.addEventListener('click', function(e) {
if (e.target === this) {
hideShutdownModal();
}
});
updateShutdownButtons();
});
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
@@ -128,6 +195,10 @@
<li>
<a href="/settings" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Settings</span>
{{ cog_svg | safe }} Settings</a>
</li>
<li>
<a href="/changepassword" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Change/Set Password</span>
{{ change_password_svg | safe }} Change/Set Password</a>
</li>
{% if debug_mode == true %}
<li>
@@ -165,17 +236,42 @@
</li>
{% if debug_mode == true %}
<li>
<a href="/automation" class="flex items-center block py-4 px-4 hover:bg-gray-10 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Automation Strategies</span>
<a href="/automation" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Automation Strategies</span>
{{ automation_svg | safe }} Automation Strategies</a>
</li>
{% endif %}
</ul>
<div class="text-sm text-gray-700">
<a href="/shutdown/{{ shutdown_token }}" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Shutdown</span>
{{ shutdown_svg | safe }} Shutdown </a>
</div>
</div>
<div class="text-sm text-gray-700">
<a href="/shutdown/{{ shutdown_token }}"
class="shutdown-button flex items-center block py-4 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
data-active-swaps="{{ summary.num_swapping }}">
{{ shutdown_svg | safe }}
<span class="ml-2">Shutdown</span>
</a>
</div>
</div>
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
<div class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"></div>
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">
<div class="bg-white dark:bg-gray-500 rounded-lg shadow-xl max-w-md w-full">
<div class="p-6 text-center">
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
<h3 class="mb-5 text-lg font-normal text-gray-700 dark:text-gray-300">Are you sure you want to shut down?</h3>
<p id="shutdownWarning" class="mb-5 text-sm text-red-500 font-bold hidden">Warning: Swaps are in progress. Please wait for swaps to complete before shutting down.</p>
<p class="mb-5 text-sm text-gray-500 dark:text-gray-300">This action will shut down the application. Are you sure you want to proceed?</p>
<button id="confirmShutdown" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-0 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
Yes, Shut Down
</button>
<button id="closeShutdownModal" type="button" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-0 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
</div>
</div>
</div>
<!-- dropdown settings & tools -->
<!-- notifications -->
@@ -260,10 +356,10 @@
<!-- dev mode icons on/off -->
<ul class="xl:flex">
<li>
<div data-tooltip-target="tooltip-debug" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
<div data-tooltip-target="tooltip-DEV" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
{{ debug_nerd_svg | safe }}
<span data-tooltip-target="tooltip-DEV" ></span> </div>
<div id="tooltip-debug" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
</div>
<div id="tooltip-DEV" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<p><b>Debug mode:</b> Active</p>
{% if debug_ui_mode == true %}
<p><b>Debug UI mode:</b> Active</p>
@@ -278,8 +374,7 @@
<ul class="xl:flex"><li>
{% if locked == true %}
<div data-tooltip-target="tooltip-locked-wallets" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
{{ wallet_locked_svg | safe }}
<span data-tooltip-target="tooltip-locked-wallets" ></span> </div>
{{ wallet_locked_svg | safe }}</div>
<div id="tooltip-locked-wallets" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<p><b>Wallets:</b> Locked </p>
</div>
@@ -287,8 +382,7 @@
<a href='/lock'>
<div data-tooltip-target="tooltip-unlocked-wallets" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
{{ wallet_unlocked_svg | safe }}
<span data-tooltip-target="tooltip-unlocked-wallets" ></span> </div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-unlocked-wallets" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<p><b>Wallets:</b> Unlocked </p>
</div>
@@ -303,7 +397,7 @@
<a href="/tor">
<div data-tooltip-target="tooltip-tor" class="flex items-center text-gray-50 hover:text-gray-100 text-sm">
{{ tor_purple_svg | safe }}
</a> <span data-tooltip-target="tooltip-tor"></span></div>
</a></div>
<div id="tooltip-tor" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"><b>Tor mode:</b> Active
{% if tor_established == true %}
<br><b>Tor:</b> Connected
@@ -316,7 +410,6 @@
<button data-tooltip-target="tooltip-darkmode" id="theme-toggle" type="button" class="text-gray-500 dark:text-gray-400 focus:outline-none rounded-lg text-sm ml-5">
{{ sun_svg | safe }}
{{ moon_svg | safe }}
<span data-tooltip-target="tooltip-darkmode"></span>
<div id="tooltip-darkmode" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">Dark mode</div>
</button>
</div>
@@ -453,6 +546,11 @@
{{ settings_svg | safe }}
</span><span>Settings</span></a>
</li>
<li>
<a class="flex items-center pl-3 py-3 pr-2 text-gray-50 hover:bg-gray-900 rounded" href="/changepassword"> <span class="inline-block mr-3">
{{ change_password_svg | safe }}
</span><span>Change/Set Password</span></a>
</li>
{% if debug_mode == true %}
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/rpc"> <span class="inline-block mr-3">
@@ -500,9 +598,12 @@
{% endif %}
</ul>
<div class="pt-8 text-sm font-medium">
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/shutdown/{{ shutdown_token }}"> <span class="inline-block mr-3">
{{ shutdown_svg | safe }}
</span><span>Shutdown</span></a>
<a href="/shutdown/{{ shutdown_token }}"
class="shutdown-button flex items-center block py-4 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
data-active-swaps="{{ summary.num_swapping }}">
{{ shutdown_svg | safe }}
<span class="ml-2">Shutdown</span>
</a>
</div>
</div>
</nav>
@@ -510,40 +611,57 @@
<!-- mobile sidebar -->
</section>
{% if ws_url %}
{% if ws_port %}
<script>
var ws = new WebSocket("{{ ws_url }}"),
// Configuration object
const notificationConfig = {
showNewOffers: false,
showNewBids: true,
showBidAccepted: true
};
var ws = new WebSocket("ws://" + window.location.hostname + ":{{ ws_port }}"),
floating_div = document.createElement('div');
floating_div.classList.add('floatright');
messages = document.createElement('ul');
messages.setAttribute('id', 'ul_updates');
ws.onmessage = function(event) {
let json = JSON.parse(event.data);
let event_message = 'Unknown event';
if (json['event'] == 'new_offer') {
floating_div.classList.add('floatright');
messages = document.createElement('ul');
messages.setAttribute('id', 'ul_updates');
floating_div.appendChild(messages);
ws.onmessage = function(event) {
let json = JSON.parse(event.data);
let event_message = 'Unknown event';
let should_display = false;
if (json['event'] == 'new_offer' && notificationConfig.showNewOffers) {
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-blue-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round"><circle cx="5" cy="5" r="4"></circle> <circle cx="19" cy="19" r="4"></circle> <polyline data-cap="butt" points="13,5 21,5 21,11 " stroke="#ffffff"></polyline> <polyline data-cap="butt" points="11,19 3,19 3,13 " stroke="#ffffff"></polyline> <polyline points=" 16,2 13,5 16,8 " stroke="#ffffff"></polyline> <polyline points=" 8,16 11,19 8,22 " stroke="#ffffff"></polyline></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">New network <a class="underline" href=/offer/' + json['offer_id'] + '>offer</a></div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><span class="sr-only">Close</span><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
}
else
if (json['event'] == 'new_bid') {
should_display = true;
}
else if (json['event'] == 'new_bid' && notificationConfig.showNewBids) {
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-violet-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round"><rect x="9.843" y="5.379" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -0.7635 13.1569)" width="11.314" height="4.243"></rect> <polyline points="3,23 3,19 15,19 15,23 "></polyline> <line x1="4" y1="15" x2="1" y2="15" stroke="#ffffff"></line> <line x1="5.757" y1="10.757" x2="3.636" y2="8.636" stroke="#ffffff"></line> <line x1="1" y1="23" x2="17" y2="23"></line> <line x1="17" y1="9" x2="23" y2="15"></line></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-normal"><span class="mb-1 text-sm font-semibold text-gray-900"><a class="underline" href=/bid/' + json['bid_id'] + '>New bid</a> on <a class="underline" href=/offer/' + json['offer_id'] + '>offer</a></span></div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-nonefocus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
}
else
if (json['event'] == 'bid_accepted') {
should_display = true;
}
else if (json['event'] == 'bid_accepted' && notificationConfig.showBidAccepted) {
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-violet-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g fill="#ffffff"><path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z" fill="#ffffff"></path></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900"><a class="underline" href=/bid/' + json['bid_id'] + '>Bid</a> accepted</div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><span class="sr-only">Close</span><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
}
let messages = document.getElementById('ul_updates'),
message = document.createElement('li');
message.innerHTML = event_message;
messages.appendChild(message);
};
floating_div.appendChild(messages);
document.body.appendChild(floating_div);
function closeAlert(event){
should_display = true;
}
if (should_display) {
let messages = document.getElementById('ul_updates'),
message = document.createElement('li');
message.innerHTML = event_message;
messages.appendChild(message);
}
};
document.body.appendChild(floating_div);
function closeAlert(event){
let element = event.target;
while(element.nodeName !== "BUTTON"){
element = element.parentNode;
element = element.parentNode;
}
element.parentNode.parentNode.removeChild(element.parentNode);
}
}
</script>
{% endif %}

View File

@@ -10,14 +10,11 @@
<div class="w-auto p-1">
{{ circular_info_messages_svg | safe }}
</div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-green-500 error_msg"><span class="bold">INFO:</span></li>
<li class="font-medium text-sm text-green-500 infomsg">{{ m[1] }}</li>
</ul>
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_{{ m[0] }}" aria-label="Close"><span class="sr-only">Close</span>

View File

@@ -4,9 +4,10 @@
<section class="relative py-24 overflow-hidden">
<div class="container px-4 mx-auto mb-16 md:mb-0">
<div class="md:w-1/2 pl-4">
<span class="inline-block py-1 px-3 mb-4 text-xs leading-5 bg-blue-500 text-white font-medium rounded-full shadow-sm">(BSX) BasicSwap v{{ version }} - (GUI) v.3.0.0</span>
<span class="inline-block py-1 px-3 mb-4 text-xs leading-5 bg-blue-500 text-white font-medium rounded-full shadow-sm">(BSX) BasicSwap v{{ version }} - (GUI) v.3.1.1</span>
<h3 class="mb-6 text-4xl md:text-5xl leading-tight text-coolGray-900 font-bold tracking-tighter dark:text-white">Welcome to BasicSwap DEX</h3>
<p class="mb-12 text-lg md:text-xl text-coolGray-500 dark:text-gray-300 font-medium">Swap cryptocurrencies in <span class="underline">total privacy</span> with no middlemen, fees, <br> or restrictions. </p>
<p class="mb-12 text-lg md:text-xl text-coolGray-500 dark:text-gray-300 font-medium">The World's Most Secure and Decentralized DEX, Safely swap cryptocurrencies without central points of failure.
Its free, completely trustless, and highly secure.</p>
<div class="flex flex-wrap mb-10 text-center md:text-left">
<div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
<a href="/wallets">
@@ -69,4 +70,4 @@
</div>
{% include 'footer.html' %}
</body>
</html>
</html>

View File

@@ -1,13 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
<body>
<h2>{{ title_str }}</h2>
<p>Info: {{ message_str }}</p>
<p><a href="/">home</a></p>
</body>
</html>
<head>
<meta charset="UTF-8">
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script>
<script>
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
}
document.documentElement.classList.toggle('dark', isDarkMode);
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - Info</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.container {
width: 100%;
max-width: 600px;
}
</style>
</head>
<body class="dark:bg-gray-700">
<div class="container px-4 mx-auto">
<div class="text-center">
<a class="inline-block mb-6" href="#">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
</a>
<div class="p-6 bg-coolGray-100 dark:bg-gray-800 rounded-lg shadow-md">
<h2 class="text-xl font-bold mb-4 text-coolGray-900 dark:text-white">INFO:</h2>
<p class="text-lg text-coolGray-500 dark:text-white mb-6">{{ message_str }}</p>
<a href="/" class="inline-block py-2 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition duration-300">Home</a>
</div>
<p class="mt-10">
<span class="text-xs font-medium dark:text-white">Need help?</span>
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
</p>
<p>
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Image toggling function
function toggleImages() {
const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image');
const lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
}
function toggleImageDisplay(images, display) {
images.forEach(img => {
img.style.display = display;
});
}
// Theme toggle functionality
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
// Initialize theme
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
}
// Call toggleImages on load
toggleImages();
});
</script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg %}
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg, circular_info_messages_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
@@ -57,19 +57,21 @@
{% if sent_bid_id %}
<section class="py-4" id="messages_send_bid_id" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 bg-green-100 border border-green-200 rounded-md">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1">
{{ confirm_green_svg | safe }}
{{ circular_info_messages_svg | safe }}
</div>
<div class="flex-1 p-1">
<h3 class="infomsg font-medium text-sm text-green-900">Sent Bid {{ sent_bid_id }}</h3></div>
</div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-green-500 error_msg"><span class="bold">INFO:</span></li>
<li class="font-medium text-sm text-green-500 infomsg">Sent Bid {{ sent_bid_id }}</li>
</ul>
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ml-auto bg-green-100 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex h-8 w-8 focus:outline-none" data-dismiss-target="#messages_send_bid_id" aria-label="Close"><span class="sr-only">Close</span>
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_send_bid_id" aria-label="Close"><span class="sr-only">Close</span>
{{ green_cross_close_svg | safe }}
</button>
</div>
@@ -304,7 +306,7 @@
</div>
</div>
</div>
<form method="post">
<form id="bidForm" action="/offer/{{ offer_id }}" method="post">
</section>
{% if data.show_edit_form %}
<section class="p-6 bg-body dark:bg-gray-700">
@@ -405,22 +407,74 @@
<td class="py-3 px-6">Amount you will send <span class="bold" id="bid_amt_to">{{ data.amt_to }}</span> {{ data.tla_to }}
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Send From Address</td>
<td class="py-3 px-6">
<div class="w-full md:flex-1">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
{% for a in addrs %}
<option value="{{ a[0] }}" {% if data.nb_addr_from==a[0] %} selected{% endif %}>{{ a[0] }} {{ a[1] }}</option>
{% endfor %}
<option value="-1" {% if data.nb_addr_from=="-1" %} selected{% endif %}>New Address</option>
</select>
</div>
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Send From Address</td>
<td class="py-3 px-6">
<div class="w-full md:flex-1">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
<option value="-1" {% if data.nb_addr_from=="-1" %} selected{% endif %}>New Address</option>
{% for a in addrs %}
<option value="{{ a[0] }}" {% if data.nb_addr_from==a[0] %} selected{% endif %}>{{ a[0] }} {{ a[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</td>
</tr>
<script>
function handleBidsPageAddress() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
if (!selectElement) return;
function loadInitialAddress() {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
function saveAddress(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
}
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleBidsPageAddress);
} else {
handleBidsPageAddress();
}
</script>
{% if data.amount_negotiable == true %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Amount</td>
@@ -467,6 +521,7 @@
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="w-full md:w-auto p-1.5 ml-2">
<input type="hidden" name="confirm" value="true">
<button name="sendbid" value="Send Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Send Bid</button>
</div>
<div class="w-full md:w-auto p-1.5">
@@ -483,6 +538,172 @@
</div>
</div>
</section>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Confirm Bid</h2>
<div class="space-y-4 text-left mb-8">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Amount you will get:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">
<span id="modal-amt-receive"></span>
<span id="modal-receive-currency"></span>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1" id="modal-fee-info"></div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Amount you will send:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">
<span id="modal-amt-send"></span>
<span id="modal-send-currency"></span>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Send From Address:</div>
<div class="font-mono text-sm p-2 bg-white dark:bg-gray-500 rounded border border-gray-300 dark:border-gray-400 overflow-x-auto text-gray-900 dark:text-white">
<span id="modal-addr-from"></span>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Minutes valid:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="modal-valid-mins"></div>
</div>
</div>
<div class="flex justify-center gap-4">
<button type="submit" name="sendbid" value="confirm"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Confirm
</button>
<button type="button" onclick="hideConfirmModal()"
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<script>
function updateBidParams(value_changed) {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
const amt_var = document.getElementById('amt_var').value;
const rate_var = document.getElementById('rate_var').value;
let amt_from = '';
let rate = '';
if (amt_var == 'True') {
amt_from = document.getElementById('bid_amount').value;
} else {
amt_from = document.getElementById('amount_from').value;
}
if (rate_var == 'True') {
rate = document.getElementById('bid_rate').value;
} else {
rate = document.getElementById('offer_rate').value;
}
if (value_changed == 'amount') {
document.getElementById('bid_amt_from').innerHTML = amt_from;
}
xhr_bid_params.open('POST', '/json/rate');
xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_bid_params.send('coin_from=' + coin_from + '&coin_to=' + coin_to + '&rate=' + rate + '&amt_from=' + amt_from);
}
function updateModalValues() {
document.getElementById('modal-amt-receive').textContent = document.getElementById('bid_amt_from').textContent;
document.getElementById('modal-amt-send').textContent = document.getElementById('bid_amt_to').textContent;
document.getElementById('modal-receive-currency').textContent = '{{ data.tla_from }}';
document.getElementById('modal-send-currency').textContent = '{{ data.tla_to }}';
{% if data.xmr_type == true %}
document.getElementById('modal-fee-info').textContent = `(excluding estimated {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees)`;
{% else %}
document.getElementById('modal-fee-info').textContent = '(excluding a tx fee)';
{% endif %}
const addrSelect = document.querySelector('select[name="addr_from"]');
const selectedText = addrSelect.options[addrSelect.selectedIndex].text;
const addrText = selectedText === 'New Address' ? selectedText : selectedText.split(' ')[0];
document.getElementById('modal-addr-from').textContent = addrText;
document.getElementById('modal-valid-mins').textContent = document.querySelector('input[name="validmins"]').value;
}
function showConfirmModal() {
updateModalValues();
document.getElementById('confirmModal').classList.remove('hidden');
return false;
}
function hideConfirmModal() {
document.getElementById('confirmModal').classList.add('hidden');
return false;
}
function handleCancelClick(event) {
event.preventDefault();
window.location.href = `/offer/${window.location.pathname.split('/')[2]}`;
}
document.addEventListener('DOMContentLoaded', function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
if (sendBidBtn) {
sendBidBtn.onclick = showConfirmModal;
}
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
if (modalCancelBtn) {
modalCancelBtn.onclick = hideConfirmModal;
}
const mainCancelBtn = document.querySelector('button[name="cancel"]');
if (mainCancelBtn) {
mainCancelBtn.onclick = handleCancelClick;
}
// Input change listeners
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', updateModalValues);
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', updateModalValues);
}
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountInput) {
bidAmountInput.addEventListener('change', () => {
updateBidParams('amount');
updateModalValues();
});
}
const bidRateInput = document.getElementById('bid_rate');
if (bidRateInput) {
bidRateInput.addEventListener('change', () => {
updateBidParams('rate');
updateModalValues();
});
}
});
</script>
{% else %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
@@ -528,6 +749,9 @@
</div>
</section>
{% endif %}
<input type="hidden" name="sendbid" value="true">
<input type="hidden" name="confirm" value="true">
<input type="hidden" id="coin_from" value="{{ data.coin_from_ind }}">
<input type="hidden" id="coin_to" value="{{ data.coin_to_ind }}">
<input type="hidden" id="amt_var" value="{{ data.amount_negotiable }}">

View File

@@ -94,29 +94,87 @@
</div>
</div>
</div>
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Select Address</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="relative">
{{ input_down_arrow_offer_svg | safe }}
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
{{ select_address_svg | safe }}
</div>
<select class="hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
{% for a in addrs %}
<option{% if data.addr_from==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option>
{% endfor %}
<option{% if data.addr_from=="-1" %} selected{% endif %} value="-1">New Address</option>
</select>
</div>
</div>
</div>
</div>
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Select Address</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="relative">
{{ input_down_arrow_offer_svg | safe }}
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
{{ select_address_svg | safe }}
</div>
<select class="hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
<option{% if data.addr_from=="-1" %} selected{% endif %} value="-1">New Address</option>
{% for a in addrs %}
<option{% if data.addr_from==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
<script>
function handleNewOfferAddress() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressNewOffer';
const form = selectElement?.closest('form');
if (!selectElement || !form) return;
function loadInitialAddress() {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
function saveAddress(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
}
form.addEventListener('submit', async (e) => {
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
});
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleNewOfferAddress);
} else {
handleNewOfferAddress();
}
</script>
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -248,31 +306,68 @@
</div>
</div>
</div>
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline">
<div class="flex items-center h-5"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" {% if data.amt_var==true %} checked="checked" {% endif %}> </div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different amount to the offer.</p>
</div>
</div>
<div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" {% if data.rate_var==true %} checked="checked" {% endif %}> </div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different rate to the offer.</p>
</div>
</div>
</div>
</div>
</div>
{% if debug_mode == true %}
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline">
<div class="flex items-center h-5">
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av">
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different amount to the offer.</p>
</div>
</div>
<div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5">
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv">
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different rate to the offer.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline">
<div class="flex items-center h-5">
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2" style="opacity: 0.40;">Amount Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different amount to the offer.</p>
</div>
</div>
<div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5">
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3" style="opacity: 0.40;">Rate Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different rate to the offer.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="pricejsonhidden hidden py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -409,7 +504,7 @@
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */];
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */, '17' /* BCH */];
const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */];
let make_hidden = false;
if (adaptor_sig_only_coins.includes(coin_from) || adaptor_sig_only_coins.includes(coin_to)) {

File diff suppressed because it is too large Load Diff

View File

@@ -694,4 +694,12 @@
</g>
</svg>
' %}
{% set change_password_svg = '
<svg class="text-gray-500 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24">
<g fill="#6b7280">
<path d="M19,10H5a3,3,0,0,0-3,3v8a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V13A3,3,0,0,0,19,10Zm-7,9a2,2,0,1,1,2-2A2,2,0,0,1,12,19Z" fill="#6b7280"></path>
<path data-color="color-2" d="M18,8H16V6a3.958,3.958,0,0,0-3.911-4h-.042A3.978,3.978,0,0,0,8,5.911V8H6V5.9A5.961,5.961,0,0,1,11.949,0h.061A5.979,5.979,0,0,1,18,6.01Z"></path>
</g>
</svg>
' %}
{% set select_box_class = 'hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0' %}

View File

@@ -1,3 +1,4 @@
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
<!DOCTYPE html>
<html lang="en">
<head>
@@ -5,25 +6,25 @@
{% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}">
{% endif %}
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script>
<script>
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script>
<script>
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
}
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
}
document.documentElement.classList.toggle('dark', isDarkMode);
</script>
document.documentElement.classList.toggle('dark', isDarkMode);
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
<body class="dark:bg-gray-700">
<section class="py-24 md:py-32">
@@ -31,25 +32,22 @@
<div class="max-w-sm mx-auto">
<div class="mb-3 text-center">
<a class="inline-block mb-6" href="#">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
</a>
<p class="text-lg text-coolGray-500 font-medium mb-6 dark:text-white" contenteditable="false">Unlock your wallets</p>
<p class="text-lg text-coolGray-500 font-medium mb-6 dark:text-white">Unlock your wallets</p>
{% for m in messages %}
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 bg-green-100 border border-green-200 rounded-md">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1">
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4732 4.80667C12.4112 4.74418 12.3375 4.69458 12.2563 4.66074C12.175 4.62689 12.0879 4.60947 11.9999 4.60947C11.9119 4.60947 11.8247 4.62689 11.7435 4.66074C11.6623 4.69458 11.5885 4.74418 11.5266 4.80667L6.55989 9.78L4.47322 7.68667C4.40887 7.62451 4.33291 7.57563 4.24967 7.54283C4.16644 7.51003 4.07755 7.49394 3.9881 7.49549C3.89865 7.49703 3.81037 7.51619 3.72832 7.55185C3.64627 7.58751 3.57204 7.63898 3.50989 7.70333C3.44773 7.76768 3.39885 7.84364 3.36605 7.92688C3.33324 8.01011 3.31716 8.099 3.31871 8.18845C3.32025 8.2779 3.3394 8.36618 3.37507 8.44823C3.41073 8.53028 3.4622 8.60451 3.52655 8.66667L6.08655 11.2267C6.14853 11.2892 6.22226 11.3387 6.3035 11.3726C6.38474 11.4064 6.47188 11.4239 6.55989 11.4239C6.64789 11.4239 6.73503 11.4064 6.81627 11.3726C6.89751 11.3387 6.97124 11.2892 7.03322 11.2267L12.4732 5.78667C12.5409 5.72424 12.5949 5.64847 12.6318 5.56414C12.6688 5.4798 12.6878 5.38873 12.6878 5.29667C12.6878 5.2046 12.6688 5.11353 12.6318 5.02919C12.5949 4.94486 12.5409 4.86909 12.4732 4.80667Z" fill="#2AD168"></path>
</svg>
</div>
<div class="flex-1 p-1">
<h3 class="font-medium text-sm text-green-900 text-left">{{ m[1] }}</h3>
</div>
<div class="w-auto p-1"> {{ circular_info_messages_svg | safe }} </div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-green-500 error_msg text-left"><span class="bold">ALERT:</span></li>
<li class="font-medium text-sm text-green-500 infomsg">This will unlock the system for all users!</li>
</ul>
</div>
</div>
</div>
@@ -60,18 +58,15 @@
{% for m in err_messages %}
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 bg-red-100 border border-red-200 rounded-md">
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1">
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4733 5.52667C10.4114 5.46419 10.3376 5.41459 10.2564 5.38075C10.1751 5.3469 10.088 5.32947 9.99999 5.32947C9.91198 5.32947 9.82485 5.3469 9.74361 5.38075C9.66237 5.41459 9.58863 5.46419 9.52666 5.52667L7.99999 7.06001L6.47333 5.52667C6.34779 5.40114 6.17753 5.33061 5.99999 5.33061C5.82246 5.33061 5.65219 5.40114 5.52666 5.52667C5.40112 5.65221 5.3306 5.82247 5.3306 6.00001C5.3306 6.17754 5.40112 6.3478 5.52666 6.47334L7.05999 8.00001L5.52666 9.52667C5.46417 9.58865 5.41458 9.66238 5.38073 9.74362C5.34689 9.82486 5.32946 9.912 5.32946 10C5.32946 10.088 5.34689 10.1752 5.38073 10.2564C5.41458 10.3376 5.46417 10.4114 5.52666 10.4733C5.58863 10.5358 5.66237 10.5854 5.74361 10.6193C5.82485 10.6531 5.91198 10.6705 5.99999 10.6705C6.088 10.6705 6.17514 10.6531 6.25638 10.6193C6.33762 10.5854 6.41135 10.5358 6.47333 10.4733L7.99999 8.94001L9.52666 10.4733C9.58863 10.5358 9.66237 10.5854 9.74361 10.6193C9.82485 10.6531 9.91198 10.6705 9.99999 10.6705C10.088 10.6705 10.1751 10.6531 10.2564 10.6193C10.3376 10.5854 10.4114 10.5358 10.4733 10.4733C10.5358 10.4114 10.5854 10.3376 10.6193 10.2564C10.6531 10.1752 10.6705 10.088 10.6705 10C10.6705 9.912 10.6531 9.82486 10.6193 9.74362C10.5854 9.66238 10.5358 9.58865 10.4733 9.52667L8.93999 8.00001L10.4733 6.47334C10.5358 6.41137 10.5854 6.33763 10.6193 6.25639C10.6531 6.17515 10.6705 6.08802 10.6705 6.00001C10.6705 5.912 10.6531 5.82486 10.6193 5.74362C10.5854 5.66238 10.5358 5.58865 10.4733 5.52667ZM12.7133 3.28667C12.0983 2.64994 11.3627 2.14206 10.5494 1.79266C9.736 1.44327 8.8612 1.25936 7.976 1.25167C7.0908 1.24398 6.21294 1.41266 5.39363 1.74786C4.57432 2.08307 3.82998 2.57809 3.20403 3.20404C2.57807 3.82999 2.08305 4.57434 1.74785 5.39365C1.41264 6.21296 1.24396 7.09082 1.25166 7.97602C1.25935 8.86121 1.44326 9.73601 1.79265 10.5494C2.14204 11.3627 2.64992 12.0984 3.28666 12.7133C3.90164 13.3501 4.63727 13.858 5.45063 14.2074C6.26399 14.5567 7.13879 14.7407 8.02398 14.7483C8.90918 14.756 9.78704 14.5874 10.6064 14.2522C11.4257 13.9169 12.17 13.4219 12.796 12.796C13.4219 12.17 13.9169 11.4257 14.2521 10.6064C14.5873 9.78706 14.756 8.90919 14.7483 8.024C14.7406 7.1388 14.5567 6.264 14.2073 5.45064C13.8579 4.63728 13.3501 3.90165 12.7133 3.28667ZM11.7733 11.7733C10.9014 12.6463 9.75368 13.1899 8.52585 13.3115C7.29802 13.4332 6.066 13.1254 5.03967 12.4405C4.01335 11.7557 3.25623 10.7361 2.89731 9.55566C2.53838 8.37518 2.59986 7.10677 3.07127 5.96653C3.54267 4.82629 4.39484 3.88477 5.48259 3.30238C6.57033 2.71999 7.82635 2.53276 9.03666 2.77259C10.247 3.01242 11.3367 3.66447 12.1202 4.61765C12.9036 5.57083 13.3324 6.76617 13.3333 8.00001C13.3357 8.70087 13.1991 9.39524 12.9313 10.0429C12.6635 10.6906 12.2699 11.2788 11.7733 11.7733Z" fill="#EF5944"></path>
</svg>
</div>
<div class="flex-1 p-1">
<h3 class="text-left font-medium text-sm text-red-900 error_msg">Error: {{ m[1] }}</h3>
</div>
<div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-red-500 error_msg text-left"><span class="bold">ERROR:</span></li>
<li class="font-medium text-sm text-red-500 error_msg">{{ m[1] }}</li>
</ul>
</div>
</div>
</div>
@@ -82,7 +77,7 @@
</div>
<form method="post" autocomplete="off">
<div class="mb-4">
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="" contenteditable="false">Your Password</label>
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="">Your Password</label>
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle" type="checkbox" />
@@ -99,41 +94,40 @@
</div>
<button type="submit" name="unlock" value="Unlock" class="appearance-none focus:outline-none inline-block py-3 px-7 mb-6 w-full text-base text-blue-50 font-medium text-center leading-6 bg-blue-500 hover:bg-blue-600 focus:ring-0 rounded-md shadow-sm">Unlock</button>
<p class="text-center">
<span class="text-xs font-medium dark:text-white" contenteditable="false">Need help?</span>
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank" contenteditable="false">Help / Tutorials</a>
<span class="text-xs font-medium dark:text-white">Need help?</span>
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
</p>
<p class="text-center">
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500" contenteditable="false">{{ title }} - GUI 3.0.0</span>
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
</p>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
</div>
</div>
</section>
<script>
const passwordToggle = document.querySelector('.js-password-toggle')
passwordToggle.addEventListener('change', function() {
const password = document.querySelector('.js-password'),
passwordLabel = document.querySelector('.js-password-label')
if (password.type === 'password') {
password.type = 'text'
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>'
} else {
password.type = 'password'
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>'
}
password.focus()
})
</script>
<script>
window.onload = () => {
toggleImages();
};
document.getElementById('theme-toggle').addEventListener('click', () => {
toggleImages();
});
document.addEventListener('DOMContentLoaded', () => {
// Password toggle functionality
const passwordToggle = document.querySelector('.js-password-toggle');
if (passwordToggle) {
passwordToggle.addEventListener('change', function() {
const password = document.querySelector('.js-password');
const passwordLabel = document.querySelector('.js-password-label');
if (password && passwordLabel) {
if (password.type === 'password') {
password.type = 'text';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>';
} else {
password.type = 'password';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>';
}
password.focus();
}
});
}
// Image toggling function
function toggleImages() {
const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image');
@@ -153,17 +147,8 @@ passwordToggle.addEventListener('change', function() {
img.style.display = display;
});
}
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
// Theme toggle functionality
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
@@ -174,17 +159,33 @@ passwordToggle.addEventListener('change', function() {
}
}
document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
// Initialize theme
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
</script>
</body>
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
}
// Call toggleImages on load
toggleImages();
});
</script>
</body>
</html>

View File

@@ -163,6 +163,12 @@
{% endif %}
</td>
</tr>
{% if w.wallet_blocks %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Wallet Blocks:</td>
<td class="py-3 px-6">{{ w.wallet_blocks }}{% if w.known_block_count %} / {{ w.known_block_count }}{% endif %}</td>
</tr>
{% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Synced:</td>
<td class="py-3 px-6">{{ w.synced }}</td>
@@ -388,8 +394,8 @@
var qrCodeStealth = new QRCode(document.getElementById("qrcode-stealth"), {
text: stealthAddress,
width: 170,
height: 170,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
@@ -404,8 +410,8 @@
var qrCodeMWEB = new QRCode(document.getElementById("qrcode-mweb"), {
text: mwebAddress,
width: 170,
height: 170,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
@@ -420,8 +426,8 @@
var qrCodeMoneroSub = new QRCode(document.getElementById("qrcode-monero-sub"), {
text: moneroSubAddress,
width: 170,
height: 170,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
@@ -433,8 +439,8 @@
var qrCodeMoneroMain = new QRCode(document.getElementById("qrcode-monero-main"), {
text: moneroMainAddress,
width: 170,
height: 170,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
@@ -447,8 +453,8 @@
var qrCodeDepost = new QRCode(document.getElementById("qrcode-deposit"), {
text: defaultAddress,
width: 170,
height: 170,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
@@ -456,63 +462,63 @@
</script>
{% endif %}
<script>
function copyToClipboard(text) {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
let clickTimeout = null;
function copyAndShowMessage(elementId) {
const addressElement = document.getElementById(elementId);
if (!addressElement) return;
const addressText = addressElement.innerText.trim();
copyToClipboard(addressText);
addressElement.innerText = 'Copied to clipboard';
function copyToClipboard(text) {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
const originalWidth = addressElement.offsetWidth;
function copyAndShowMessage(elementId) {
const addressElement = document.getElementById(elementId);
if (!addressElement) return;
const addressText = addressElement.innerText.trim();
addressElement.classList.add('copying');
if (addressText === 'Copied to clipboard') return;
addressElement.parentElement.style.width = `${originalWidth}px`;
copyToClipboard(addressText);
addressElement.innerText = 'Copied to clipboard';
const originalWidth = addressElement.offsetWidth;
addressElement.classList.add('copying');
addressElement.parentElement.style.width = `${originalWidth}px`;
setTimeout(function () {
addressElement.innerText = addressText;
addressElement.classList.remove('copying');
addressElement.parentElement.style.width = '';
}, 1000);
}
setTimeout(function () {
addressElement.innerText = addressText;
addressElement.classList.remove('copying');
addressElement.parentElement.style.width = '';
}, 2000);
}
const stealthAddressElement = document.getElementById('stealth_address');
if (stealthAddressElement) {
stealthAddressElement.addEventListener('click', function () {
copyAndShowMessage('stealth_address');
});
}
const mainDepositAddressElement = document.getElementById('main_deposit_address');
if (mainDepositAddressElement) {
mainDepositAddressElement.addEventListener('click', function () {
copyAndShowMessage('main_deposit_address');
});
}
const moneroMainAddressElement = document.getElementById('monero_main_address');
if (moneroMainAddressElement) {
moneroMainAddressElement.addEventListener('click', function () {
copyAndShowMessage('monero_main_address');
});
}
const moneroSubAddressElement = document.getElementById('monero_sub_address');
if (moneroSubAddressElement) {
moneroSubAddressElement.addEventListener('click', function () {
copyAndShowMessage('monero_sub_address');
});
}
</script>
document.addEventListener('DOMContentLoaded', function() {
const stealthAddressElement = document.getElementById('stealth_address');
if (stealthAddressElement) {
stealthAddressElement.addEventListener('click', function() {
copyAndShowMessage('stealth_address');
});
}
const mainDepositAddressElement = document.getElementById('main_deposit_address');
if (mainDepositAddressElement) {
mainDepositAddressElement.addEventListener('click', function() {
copyAndShowMessage('main_deposit_address');
});
}
const moneroMainAddressElement = document.getElementById('monero_main_address');
if (moneroMainAddressElement) {
moneroMainAddressElement.addEventListener('click', function() {
copyAndShowMessage('monero_main_address');
});
}
const moneroSubAddressElement = document.getElementById('monero_sub_address');
if (moneroSubAddressElement) {
moneroSubAddressElement.addEventListener('click', function() {
copyAndShowMessage('monero_sub_address');
});
}
});
</script>
<section class="p-6">
<div class="flex items-center">
<h4 class="font-semibold text-2xl text-black dark:text-white">Withdraw</h4>
@@ -564,122 +570,145 @@
<td class="py-3 px-6">
<div class="flex"> <input placeholder="{{ w.ticker }} Amount" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" id="amount" name="amt_{{ w.cid }}" value="{{ w.wd_value }}">
<div class="ml-2 flex">
{% if w.cid == '1' %}
{# PART #}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
<script>
function setAmount(percent, balance, blindBalance, anonBalance) {
var amountInput = document.getElementById('amount');
var typeSelect = document.getElementById('withdraw_type');
var selectedType = typeSelect.value;
var floatBalance;
var calculatedAmount;
{% if w.cid == '1' %}
{# PART #}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
<script>
function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var amountInput = document.getElementById('amount');
var typeSelect = document.getElementById('withdraw_type');
var selectedType = typeSelect.value;
var floatBalance;
var calculatedAmount;
switch(selectedType) {
case 'plain':
floatBalance = parseFloat(balance);
calculatedAmount = floatBalance * percent;
break;
case 'blind':
floatBalance = parseFloat(blindBalance);
calculatedAmount = floatBalance * percent;
break;
case 'anon':
floatBalance = parseFloat(anonBalance);
calculatedAmount = floatBalance * percent;
break;
default:
floatBalance = parseFloat(balance);
calculatedAmount = floatBalance * percent;
break;
}
switch(selectedType) {
case 'plain':
floatBalance = parseFloat(balance);
break;
case 'blind':
floatBalance = parseFloat(blindBalance);
break;
case 'anon':
floatBalance = parseFloat(anonBalance);
break;
default:
floatBalance = parseFloat(balance);
break;
}
calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
amountInput.value = calculatedAmount.toFixed(8);
}
</script>
{# / PART #}
{% elif w.cid == '3' %}
{# LTC #}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">100%</button>
<script>
function setAmount(percent, balance, mwebBalance, selectedType) {
var amountInput = document.getElementById('amount');
var typeSelect = document.getElementById('withdraw_type');
var selectedType = typeSelect.value;
var floatBalance;
var calculatedAmount;
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
}
</script>
{# / PART #}
{% elif w.cid == '3' %}
{# LTC #}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">25%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
<script>
function setAmount(percent, balance, cid, mwebBalance) {
var amountInput = document.getElementById('amount');
var typeSelect = document.getElementById('withdraw_type');
var selectedType = typeSelect.value;
var floatBalance;
var calculatedAmount;
switch(selectedType) {
case 'plain':
floatBalance = parseFloat(balance);
calculatedAmount = floatBalance * percent;
break;
case 'mweb':
floatBalance = parseFloat(mwebBalance);
calculatedAmount = floatBalance * percent;
break;
default:
floatBalance = parseFloat(balance);
calculatedAmount = floatBalance * percent;
break;
}
switch(selectedType) {
case 'plain':
floatBalance = parseFloat(balance);
break;
case 'mweb':
floatBalance = parseFloat(mwebBalance);
break;
default:
floatBalance = parseFloat(balance);
break;
}
calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
amountInput.value = calculatedAmount.toFixed(8);
}
</script>
{# / LTC #}
{% else %}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.cid }}')">100%</button>
<script>
function setAmount(percent, balance, cid) {
var amountInput = document.getElementById('amount');
var floatBalance;
var calculatedAmount;
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
}
</script>
{# / LTC #}
{% else %}
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }})">100%</button>
<script>
function setAmount(percent, balance, cid) {
var amountInput = document.getElementById('amount');
var floatBalance = parseFloat(balance);
var calculatedAmount = floatBalance * percent;
floatBalance = parseFloat(balance);
calculatedAmount = floatBalance * percent;
if (cid in '6, 9' && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
} else if (amountInput.getAttribute('data-hidden') === 'true' && percent !== 1) {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
amountInput.placeholder = '';
amountInput.disabled = false;
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.placeholder = '';
amountInput.disabled = false;
}
if (cid in '6, 9' && percent === 1) {
var sweepAllCheckbox = document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = true;
}
} else {
var sweepAllCheckbox = document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = false;
}
}
}
const specialCids = [6, 9];
</script>
{% endif %}
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
{% if w.cid in '6, 9' %} {# XMR | WOW #}
<td class="py-3 px-6 bold">Sweep All:</td>
<td class="py-3 px-6"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="sweepall" name="sweepall_{{ w.cid }}" {% if w.wd_sweepall==true %} checked=checked{% endif %}> </td> {% else %} <td class="py-3 px-6 bold">Subtract Fee:</td>
<td class="py-3 px-6"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked=checked{% endif %}> </td>
{% endif %}
<td>
</td>
</tr>
console.log("CID:", cid);
console.log("Percent:", percent);
console.log("Balance:", balance);
console.log("Calculated Amount:", calculatedAmount);
if (specialCids.includes(parseInt(cid)) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
console.log("Sweep All activated for special CID:", cid);
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
amountInput.placeholder = '';
amountInput.disabled = false;
}
let sweepAllCheckbox = document.getElementById('sweepall');
if (sweepAllCheckbox) {
if (specialCids.includes(parseInt(cid)) && percent === 1) {
sweepAllCheckbox.checked = true;
console.log("Sweep All checkbox checked");
} else {
sweepAllCheckbox.checked = false;
console.log("Sweep All checkbox unchecked");
}
}
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
}
}
</script>
{% endif %}
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
{% if w.cid in '6, 9' %} {# XMR | WOW #}
<td class="py-3 px-6 bold">Sweep All:</td>
<td class="py-3 px-6">
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="sweepall" name="sweepall_{{ w.cid }}" {% if w.wd_sweepall==true %} checked=checked{% endif %}>
</td>
{% else %}
<td class="py-3 px-6 bold">Subtract Fee:</td>
<td class="py-3 px-6">
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked=checked{% endif %}>
</td>
{% endif %}
<td>
</td>
</tr>
{% if w.cid == '1' %}
{# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
@@ -842,30 +871,47 @@
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<script>
const coinNameToSymbol = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Litecoin': 'LTC',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'DECRED': 'DCR',
'WOWNERO': 'WOW'
};
const coinNameToSymbol = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH',
};
const getUsdValue = (cryptoValue, coinSymbol) => fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
.then(response => response.json())
.then(data => {
const exchangeRate = data.USD;
if (!isNaN(exchangeRate)) {
return cryptoValue * exchangeRate;
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
}
});
const getUsdValue = (cryptoValue, coinSymbol) => {
if (coinSymbol === 'WOW') {
return fetch(`https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`)
.then(response => response.json())
.then(data => {
const exchangeRate = data.wownero.usd;
if (!isNaN(exchangeRate)) {
return cryptoValue * exchangeRate;
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
}
});
} else {
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
.then(response => response.json())
.then(data => {
const exchangeRate = data.USD;
if (!isNaN(exchangeRate)) {
return cryptoValue * exchangeRate;
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
}
});
}
};
const updateUsdValue = async (cryptoCell, coinFullName, usdValueSpan) => {
const coinSymbol = coinNameToSymbol[coinFullName] || '';

View File

@@ -35,7 +35,7 @@
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change Password</span></a>
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">{{ circular_arrows_svg | safe }}<span>Refresh</span></a>
</div>
</div>
@@ -64,7 +64,7 @@
<h4 class="text-xl font-bold dark:text-white">{{ w.name }}
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
</h4>
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
{% endif %}
</div>
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
@@ -207,245 +207,442 @@
{% include 'footer.html' %}
<script>
const coinNameToSymbol = {
const CONFIG = {
MAX_RETRIES: 3,
BASE_DELAY: 1000,
CACHE_EXPIRATION: 5 * 60 * 1000,
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
API_TIMEOUT: 30000,
DEBOUNCE_DELAY: 500,
CACHE_MIN_INTERVAL: 60 * 1000
};
const STATE_KEYS = {
LAST_UPDATE: 'last-update-time',
PREVIOUS_TOTAL: 'previous-total-usd',
CURRENT_TOTAL: 'current-total-usd',
BALANCES_VISIBLE: 'balancesVisible'
};
const COIN_SYMBOLS = {
'Bitcoin': 'bitcoin',
'Particl': 'particl',
'Monero': 'monero',
'Wownero': 'wownero',
'Litecoin': 'litecoin',
'Firo': 'zcoin',
'Dash': 'dash',
'PIVX': 'pivx',
'Decred': 'decred',
'Zano': 'zano',
'Bitcoin Cash': 'bitcoin-cash'
};
const SHORT_NAMES = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Litecoin MWEB': 'LTC MWEB',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH'
};
const getUsdValue = (cryptoValue, coinSymbol) => {
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
.then(response => response.json())
.then(data => {
const exchangeRate = data.USD;
if (!isNaN(exchangeRate)) {
return {
usdValue: cryptoValue * exchangeRate,
btcValue: cryptoValue / exchangeRate
};
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
class Cache {
constructor(expirationTime) {
this.data = null;
this.timestamp = null;
this.expirationTime = expirationTime;
}
isValid() {
return Boolean(
this.data &&
this.timestamp &&
(Date.now() - this.timestamp < this.expirationTime)
);
}
set(data) {
this.data = data;
this.timestamp = Date.now();
}
get() {
if (this.isValid()) {
return this.data;
}
return null;
}
clear() {
this.data = null;
this.timestamp = null;
}
}
class ApiClient {
constructor() {
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
this.lastFetchTime = 0;
}
makeRequest(url, headers = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/json/readurl');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = CONFIG.API_TIMEOUT;
xhr.ontimeout = () => {
reject(new Error('Request timed out'));
};
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.Error) {
reject(new Error(response.Error));
} else {
resolve(response);
}
} catch (error) {
reject(new Error(`Invalid JSON response: ${error.message}`));
}
} else {
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => {
reject(new Error('Network error occurred'));
};
xhr.send(JSON.stringify({ url, headers }));
});
}
async fetchPrices(forceUpdate = false) {
const now = Date.now();
const timeSinceLastFetch = now - this.lastFetchTime;
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
}
let lastError = null;
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
try {
const prices = await this.makeRequest(
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'
);
this.cache.set(prices);
this.lastFetchTime = now;
return prices;
} catch (error) {
lastError = error;
if (attempt < CONFIG.MAX_RETRIES - 1) {
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
throw lastError || new Error('Failed to fetch prices');
}
}
class UiManager {
constructor() {
this.api = new ApiClient();
this.toggleInProgress = false;
this.toggleDebounceTimer = null;
this.priceUpdateInterval = null;
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
}
getShortName(fullName) {
return SHORT_NAMES[fullName] || fullName;
}
storeOriginalValues() {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const value = el.textContent?.trim() || '';
if (coinName) {
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
const shortName = this.getShortName(coinName);
if (coinId) {
if (coinId === 'particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinId === 'litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
el.setAttribute('data-original-value', `${amount} ${shortName}`);
}
}
});
};
const updateValues = async () => {
const coinNameValues = document.querySelectorAll('.coinname-value');
for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname');
const cryptoValue = parseFloat(coinNameValue.textContent);
const coinSymbol = coinNameToSymbol[coinFullName];
if (coinSymbol) {
try {
const { usdValue, btcValue } = await getUsdValue(cryptoValue, coinSymbol);
const usdValueElement = coinNameValue.nextElementSibling.querySelector('.usd-value');
if (usdValueElement) {
usdValueElement.textContent = `$${usdValue.toFixed(2)}`;
}
const btcValueElement = coinNameValue.nextElementSibling.querySelector('.btc-value');
if (btcValueElement) {
btcValueElement.textContent = `${btcValue.toFixed(8)} BTC`;
}
} catch (error) {
console.error(`Error retrieving exchange rate for ${coinSymbol}`);
}
} else {
console.error(`Coin symbol not found for full name: ${coinFullName}`);
}
}
calculateTotalUsdValue();
calculateTotalBtcValue();
};
async updatePrices(forceUpdate = false) {
try {
const prices = await this.api.fetchPrices(forceUpdate);
let newTotal = 0;
const toggleUsdAmount = async (usdCell, isVisible) => {
const currentTime = Date.now();
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
this.lastUpdateTime = currentTime;
const usdText = document.getElementById('usd-text');
if (prices) {
Object.entries(COIN_SYMBOLS).forEach(([coinName, coinId]) => {
if (prices[coinId]?.usd) {
localStorage.setItem(`${coinId}-price`, prices[coinId].usd.toString());
}
});
}
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
if (usdText) {
usdText.style.display = isVisible ? 'inline' : 'none';
}
if (!coinName) return;
if (isVisible) {
const coinNameValueElement = usdCell.parentElement.querySelector('.coinname-value');
const amount = amountStr ? parseFloat(amountStr.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
if (!coinId) return;
const price = prices?.[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
const usdValue = (amount * price).toFixed(2);
if (coinNameValueElement) {
const coinFullName = coinNameValueElement.getAttribute('data-coinname');
console.log("coinFullName:", coinFullName);
if (coinFullName) {
const cryptoValue = parseFloat(coinNameValueElement.textContent);
console.log("cryptoValue:", cryptoValue);
const coinSymbol = coinNameToSymbol[coinFullName.trim()];
console.log("coinSymbol:", coinSymbol);
if (coinSymbol) {
await updateValues();
} else {
console.error(`Coin symbol not found for full name: ${coinFullName}`);
}
} else {
console.error(`Data attribute 'data-coinname' not found.`);
}
if (coinId === 'particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinId === 'litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-last-value`, usdValue);
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
} else {
usdCell.textContent = "******";
const btcValueElement = usdCell.nextElementSibling;
if (btcValueElement) {
btcValueElement.textContent = "****";
}
}
};
const toggleUsdIcon = (isVisible) => {
const usdIcon = document.querySelector("#hide-usd-amount-toggle svg");
if (usdIcon) {
newTotal += parseFloat(usdValue);
const usdEl = el.closest('.flex')?.nextElementSibling?.querySelector('.usd-value');
if (usdEl) {
usdEl.textContent = `$${usdValue} USD`;
}
});
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
return true;
} catch (error) {
console.error('Price update failed:', error);
return false;
}
}
updateTotalValues(totalUsd, btcPrice) {
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
localStorage.setItem('total-usd', totalUsd.toString());
}
if (btcPrice) {
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
const totalBtcEl = document.getElementById('total-btc-value');
if (totalBtcEl) {
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
}
}
}
async toggleBalances() {
if (this.toggleInProgress) return;
try {
this.toggleInProgress = true;
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
const newVisibility = !balancesVisible;
localStorage.setItem('balancesVisible', newVisibility.toString());
this.updateVisibility(newVisibility);
if (this.toggleDebounceTimer) {
clearTimeout(this.toggleDebounceTimer);
}
this.toggleDebounceTimer = window.setTimeout(async () => {
this.toggleInProgress = false;
if (newVisibility) {
await this.updatePrices(true);
}
}, CONFIG.DEBOUNCE_DELAY);
} catch (error) {
console.error('Failed to toggle balances:', error);
this.toggleInProgress = false;
}
}
updateVisibility(isVisible) {
if (isVisible) {
usdIcon.innerHTML = `
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>
`;
this.showBalances();
} else {
usdIcon.innerHTML = `
<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path>
<path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path>
<path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>
`;
this.hideBalances();
}
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
if (eyeIcon) {
eyeIcon.innerHTML = isVisible ?
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
}
}
};
const calculateTotalUsdValue = async () => {
const coinNameValues = document.querySelectorAll('.coinname-value');
let totalUsdValue = 0;
showBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'inline';
}
for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname');
const cryptoValue = parseFloat(coinNameValue.textContent);
const coinSymbol = coinNameToSymbol[coinFullName];
document.querySelectorAll('.coinname-value').forEach(el => {
const originalValue = el.getAttribute('data-original-value');
if (originalValue) {
el.textContent = originalValue;
}
});
if (coinSymbol) {
try {
const { usdValue } = await getUsdValue(cryptoValue, coinSymbol);
totalUsdValue += usdValue;
document.querySelectorAll('.usd-value').forEach(el => {
const storedValue = el.getAttribute('data-original-value');
if (storedValue) {
el.textContent = `$${parseFloat(storedValue).toFixed(2)} USD`;
el.style.color = 'white';
}
});
const usdValueElement = coinNameValue.parentElement.nextElementSibling.querySelector('.usd-value');
if (usdValueElement) {
usdValueElement.textContent = `$${usdValue.toFixed(2)}`;
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
const originalValue = el?.getAttribute('data-original-value');
if (el && originalValue) {
if (id === 'total-usd-value') {
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
el.classList.add('font-extrabold');
} else {
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
}
} catch (error) {
console.error(`Error retrieving exchange rate for ${coinSymbol}`);
}
} else {
console.error(`Coin symbol not found for full name: ${coinFullName}`);
});
}
hideBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'none';
}
}
const totalUsdValueElement = document.getElementById('total-usd-value');
if (totalUsdValueElement) {
totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`;
}
};
document.querySelectorAll('.coinname-value, .usd-value').forEach(el => {
el.textContent = '****';
});
const calculateTotalBtcValue = async () => {
const usdValueElements = document.getElementsByClassName('usd-value');
let totalBtcValue = 0;
try {
const btcToUsdRate = await fetch('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD')
.then(response => response.json())
.then(data => data.USD);
for (const usdValueElement of usdValueElements) {
const usdValue = parseFloat(usdValueElement.textContent.replace('$', ''));
const btcValue = usdValue / btcToUsdRate;
if (!isNaN(btcValue)) {
totalBtcValue += btcValue;
} else {
console.error(`Invalid USD value: ${usdValue}`);
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.textContent = '****';
}
});
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.classList.remove('font-extrabold');
}
}
async initialize() {
this.storeOriginalValues();
if (localStorage.getItem('balancesVisible') === null) {
localStorage.setItem('balancesVisible', 'true');
}
const totalBtcValueElement = document.getElementById('total-btc-value');
if (totalBtcValueElement) {
if (localStorage.getItem('usdAmountVisible') === 'false') {
totalBtcValueElement.textContent = "****";
} else {
totalBtcValueElement.textContent = `~ ${totalBtcValue.toFixed(8)} BTC`;
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
if (hideBalancesToggle) {
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
}
await this.loadBalanceVisibility();
if (this.priceUpdateInterval) {
clearInterval(this.priceUpdateInterval);
}
this.priceUpdateInterval = setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
this.updatePrices(false);
}
}
} catch (error) {
console.error(`Error retrieving BTC to USD exchange rate: ${error}`);
}
};
const loadUsdAmountVisibility = async () => {
let usdAmountVisible = localStorage.getItem('usdAmountVisible') === 'true';
toggleUsdIcon(usdAmountVisible);
const usdValueElements = document.getElementsByClassName('usd-value');
for (const usdValueElement of usdValueElements) {
toggleUsdAmount(usdValueElement, usdAmountVisible, !usdAmountVisible);
}, CONFIG.PRICE_UPDATE_INTERVAL);
}
const totalUsdValueElement = document.getElementById('total-usd-value');
if (!usdAmountVisible && totalUsdValueElement) {
totalUsdValueElement.textContent = "*****";
} else if (usdAmountVisible && totalUsdValueElement) {
await calculateTotalUsdValue();
calculateTotalBtcValue();
async loadBalanceVisibility() {
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
this.updateVisibility(balancesVisible);
if (balancesVisible) {
await this.updatePrices(true);
}
}
};
}
window.onload = async () => {
const hideUsdAmountToggle = document.getElementById('hide-usd-amount-toggle');
let usdAmountVisible = localStorage.getItem('usdAmountVisible') === 'true';
window.addEventListener('beforeunload', () => {
const uiManager = window.uiManager;
if (uiManager?.priceUpdateInterval) {
clearInterval(uiManager.priceUpdateInterval);
}
});
hideUsdAmountToggle.addEventListener('click', async () => {
usdAmountVisible = !usdAmountVisible;
localStorage.setItem('usdAmountVisible', usdAmountVisible);
toggleUsdIcon(usdAmountVisible);
const usdValueElements = document.getElementsByClassName('usd-value');
for (const usdValueElement of usdValueElements) {
toggleUsdAmount(usdValueElement, usdAmountVisible);
}
const totalUsdValueElement = document.getElementById('total-usd-value');
if (!usdAmountVisible && totalUsdValueElement) {
totalUsdValueElement.textContent = "*****";
} else if (usdAmountVisible && totalUsdValueElement) {
await calculateTotalUsdValue();
}
const totalBtcValueElement = document.getElementById('total-btc-value');
if (!usdAmountVisible && totalBtcValueElement) {
totalBtcValueElement.textContent = "";
} else if (usdAmountVisible && totalBtcValueElement) {
await calculateTotalBtcValue();
}
window.addEventListener('load', () => {
const uiManager = new UiManager();
window.uiManager = uiManager;
uiManager.initialize().catch(error => {
console.error('Failed to initialize application:', error);
});
await loadUsdAmountVisibility();
};
});
</script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -28,33 +29,33 @@ def page_automation_strategies(self, url_split, post_string):
summary = swap_client.getSummary()
filters = {
'page_no': 1,
'limit': PAGE_LIMIT,
'sort_by': 'created_at',
'sort_dir': 'desc',
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
}
messages = []
form_data = self.checkForm(post_string, 'automationstrategies', messages)
form_data = self.checkForm(post_string, "automationstrategies", messages)
if form_data:
if have_data_entry(form_data, 'clearfilters'):
swap_client.clearFilters('page_automation_strategies')
if have_data_entry(form_data, "clearfilters"):
swap_client.clearFilters("page_automation_strategies")
else:
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if have_data_entry(form_data, "sort_by"):
sort_by = get_data_entry(form_data, "sort_by")
ensure(sort_by in ["created_at", "rate"], "Invalid sort by")
filters["sort_by"] = sort_by
if have_data_entry(form_data, "sort_dir"):
sort_dir = get_data_entry(form_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
set_pagination_filters(form_data, filters)
if have_data_entry(form_data, 'applyfilters'):
swap_client.setFilters('page_automation_strategies', filters)
if have_data_entry(form_data, "applyfilters"):
swap_client.setFilters("page_automation_strategies", filters)
else:
saved_filters = swap_client.getFilters('page_automation_strategies')
saved_filters = swap_client.getFilters("page_automation_strategies")
if saved_filters:
filters.update(saved_filters)
@@ -62,13 +63,16 @@ def page_automation_strategies(self, url_split, post_string):
for s in swap_client.listAutomationStrategies(filters):
formatted_strategies.append((s[0], s[1], strConcepts(s[2])))
template = server.env.get_template('automation_strategies.html')
return self.render_template(template, {
'messages': messages,
'filters': filters,
'strategies': formatted_strategies,
'summary': summary,
})
template = server.env.get_template("automation_strategies.html")
return self.render_template(
template,
{
"messages": messages,
"filters": filters,
"strategies": formatted_strategies,
"summary": summary,
},
)
def page_automation_strategy_new(self, url_split, post_string):
@@ -78,21 +82,24 @@ def page_automation_strategy_new(self, url_split, post_string):
summary = swap_client.getSummary()
messages = []
form_data = self.checkForm(post_string, 'automationstrategynew', messages)
_ = self.checkForm(post_string, "automationstrategynew", messages)
template = server.env.get_template('automation_strategy_new.html')
return self.render_template(template, {
'messages': messages,
'summary': summary,
})
template = server.env.get_template("automation_strategy_new.html")
return self.render_template(
template,
{
"messages": messages,
"summary": summary,
},
)
def page_automation_strategy(self, url_split, post_string):
ensure(len(url_split) > 2, 'Strategy ID not specified')
ensure(len(url_split) > 2, "Strategy ID not specified")
try:
strategy_id = int(url_split[2])
except Exception:
raise ValueError('Bad strategy ID')
raise ValueError("Bad strategy ID")
server = self.server
swap_client = server.swap_client
@@ -101,17 +108,19 @@ def page_automation_strategy(self, url_split, post_string):
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'automation_strategy', err_messages)
form_data = self.checkForm(post_string, "automation_strategy", err_messages)
show_edit_form = False
if form_data:
if have_data_entry(form_data, 'edit'):
if have_data_entry(form_data, "edit"):
show_edit_form = True
if have_data_entry(form_data, 'apply'):
if have_data_entry(form_data, "apply"):
try:
data = json.loads(get_data_entry_or(form_data, 'data', ''))
note = get_data_entry_or(form_data, 'note', '')
swap_client.updateAutomationStrategy(strategy_id, data, note)
messages.append('Updated')
data = {
"data": json.loads(get_data_entry_or(form_data, "data", "")),
"note": get_data_entry_or(form_data, "note", ""),
}
swap_client.updateAutomationStrategy(strategy_id, data)
messages.append("Updated")
except Exception as e:
err_messages.append(str(e))
show_edit_form = True
@@ -119,19 +128,24 @@ def page_automation_strategy(self, url_split, post_string):
strategy = swap_client.getAutomationStrategy(strategy_id)
formatted_strategy = {
'label': strategy.label,
'type': strConcepts(strategy.type_ind),
'only_known_identities': 'True' if strategy.only_known_identities is True else 'False',
'data': strategy.data.decode('utf-8'),
'note': '' if not strategy.note else strategy.note,
'created_at': strategy.created_at,
"label": strategy.label,
"type": strConcepts(strategy.type_ind),
"only_known_identities": (
"True" if strategy.only_known_identities is True else "False"
),
"data": strategy.data.decode("utf-8"),
"note": "" if not strategy.note else strategy.note,
"created_at": strategy.created_at,
}
template = server.env.get_template('automation_strategy.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'strategy': formatted_strategy,
'show_edit_form': show_edit_form,
'summary': summary,
})
template = server.env.get_template("automation_strategy.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"strategy": formatted_strategy,
"show_edit_form": show_edit_form,
"summary": summary,
},
)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -24,18 +25,19 @@ from basicswap.basicswap_util import (
BidStates,
SwapTypes,
DebugTypes,
canAcceptBidState,
strTxState,
strBidState,
)
def page_bid(self, url_split, post_string):
ensure(len(url_split) > 2, 'Bid ID not specified')
ensure(len(url_split) > 2, "Bid ID not specified")
try:
bid_id = bytes.fromhex(url_split[2])
assert len(bid_id) == 28
except Exception:
raise ValueError('Bad bid ID')
raise ValueError("Bad bid ID")
server = self.server
swap_client = server.swap_client
swap_client.checkSystemStatus()
@@ -49,129 +51,163 @@ def page_bid(self, url_split, post_string):
show_lock_transfers = False
edit_bid = False
view_tx_ind = None
form_data = self.checkForm(post_string, 'bid', err_messages)
form_data = self.checkForm(post_string, "bid", err_messages)
if form_data:
if b'abandon_bid' in form_data:
if b"abandon_bid" in form_data:
try:
swap_client.abandonBid(bid_id)
messages.append('Bid abandoned')
messages.append("Bid abandoned")
except Exception as ex:
err_messages.append('Abandon failed ' + str(ex))
elif b'accept_bid' in form_data:
err_messages.append("Abandon failed " + str(ex))
elif b"accept_bid" in form_data:
try:
swap_client.acceptBid(bid_id)
messages.append('Bid accepted')
messages.append("Bid accepted")
except Exception as ex:
err_messages.append('Accept failed ' + str(ex))
elif b'show_txns' in form_data:
err_messages.append("Accept failed " + str(ex))
elif b"show_txns" in form_data:
show_txns = True
elif b'show_offerer_seq_diagram' in form_data:
elif b"show_offerer_seq_diagram" in form_data:
show_offerer_seq_diagram = True
elif b'show_bidder_seq_diagram' in form_data:
elif b"show_bidder_seq_diagram" in form_data:
show_bidder_seq_diagram = True
elif b'edit_bid' in form_data:
elif b"edit_bid" in form_data:
edit_bid = True
elif b'edit_bid_submit' in form_data:
elif b"edit_bid_submit" in form_data:
data = {
'bid_state': int(form_data[b'new_state'][0]),
'bid_action': int(get_data_entry_or(form_data, 'new_action', -1)),
'debug_ind': int(get_data_entry_or(form_data, 'debugind', -1)),
'kbs_other': get_data_entry_or(form_data, 'kbs_other', None),
"bid_state": int(form_data[b"new_state"][0]),
"bid_action": int(get_data_entry_or(form_data, "new_action", -1)),
"debug_ind": int(get_data_entry_or(form_data, "debugind", -1)),
"kbs_other": get_data_entry_or(form_data, "kbs_other", None),
}
try:
swap_client.manualBidUpdate(bid_id, data)
messages.append('Bid edited')
messages.append("Bid edited")
except Exception as ex:
err_messages.append('Edit failed ' + str(ex))
elif b'view_tx_submit' in form_data:
err_messages.append("Edit failed " + str(ex))
elif b"view_tx_submit" in form_data:
show_txns = True
view_tx_ind = form_data[b'view_tx'][0].decode('utf-8')
view_tx_ind = form_data[b"view_tx"][0].decode("utf-8")
if len(view_tx_ind) != 64:
err_messages.append('Invalid transaction selected.')
err_messages.append("Invalid transaction selected.")
view_tx_ind = None
elif b'view_lock_transfers' in form_data:
elif b"view_lock_transfers" in form_data:
show_txns = True
show_lock_transfers = True
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
ensure(bid, 'Unknown bid ID')
ensure(bid, "Unknown bid ID")
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, view_tx_ind, show_lock_transfers=show_lock_transfers)
data = describeBid(
swap_client,
bid,
xmr_swap,
offer,
xmr_offer,
events,
edit_bid,
show_txns,
view_tx_ind,
show_lock_transfers=show_lock_transfers,
)
if bid.debug_ind is not None and bid.debug_ind > 0:
messages.append('Debug flag set: {}, {}'.format(bid.debug_ind, DebugTypes(bid.debug_ind).name))
messages.append(
"Debug flag set: {}, {}".format(
bid.debug_ind, DebugTypes(bid.debug_ind).name
)
)
data['show_bidder_seq_diagram'] = show_bidder_seq_diagram
data['show_offerer_seq_diagram'] = show_offerer_seq_diagram
data["show_bidder_seq_diagram"] = show_bidder_seq_diagram
data["show_offerer_seq_diagram"] = show_offerer_seq_diagram
old_states = listOldBidStates(bid)
if len(data['addr_from_label']) > 0:
data['addr_from_label'] = '(' + data['addr_from_label'] + ')'
data['can_accept_bid'] = True if bid.state == BidStates.BID_RECEIVED else False
if len(data["addr_from_label"]) > 0:
data["addr_from_label"] = "(" + data["addr_from_label"] + ")"
data["can_accept_bid"] = True if canAcceptBidState(bid.state) else False
if swap_client.debug_ui:
data['bid_actions'] = [(-1, 'None'), ] + listBidActions()
data["bid_actions"] = [
(-1, "None"),
] + listBidActions()
template = server.env.get_template('bid_xmr.html' if offer.swap_type == SwapTypes.XMR_SWAP else 'bid.html')
return self.render_template(template, {
'bid_id': bid_id.hex(),
'messages': messages,
'err_messages': err_messages,
'data': data,
'edit_bid': edit_bid,
'old_states': old_states,
'summary': summary,
})
template = server.env.get_template(
"bid_xmr.html" if offer.swap_type == SwapTypes.XMR_SWAP else "bid.html"
)
return self.render_template(
template,
{
"bid_id": bid_id.hex(),
"messages": messages,
"err_messages": err_messages,
"data": data,
"edit_bid": edit_bid,
"old_states": old_states,
"summary": summary,
},
)
def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
def page_bids(
self, url_split, post_string, sent=False, available=False, received=False
):
server = self.server
swap_client = server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
filters = {
'page_no': 1,
'bid_state_ind': -1,
'with_expired': True,
'limit': PAGE_LIMIT,
'sort_by': 'created_at',
'sort_dir': 'desc',
"page_no": 1,
"bid_state_ind": -1,
"with_expired": True,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
}
if available:
filters['bid_state_ind'] = BidStates.BID_RECEIVED
filters['with_expired'] = False
filters["bid_state_ind"] = BidStates.BID_RECEIVED
filters["with_expired"] = False
filter_prefix = 'page_bids_sent' if sent else 'page_bids_available' if available else 'page_bids_received'
filter_prefix = (
"page_bids_sent"
if sent
else "page_bids_available" if available else "page_bids_received"
)
messages = []
form_data = self.checkForm(post_string, 'bids', messages)
form_data = self.checkForm(post_string, "bids", messages)
if form_data:
if have_data_entry(form_data, 'clearfilters'):
if have_data_entry(form_data, "clearfilters"):
swap_client.clearFilters(filter_prefix)
else:
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
ensure(sort_by in ['created_at', ], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if have_data_entry(form_data, 'state'):
state_ind = int(get_data_entry(form_data, 'state'))
if have_data_entry(form_data, "sort_by"):
sort_by = get_data_entry(form_data, "sort_by")
ensure(
sort_by
in [
"created_at",
],
"Invalid sort by",
)
filters["sort_by"] = sort_by
if have_data_entry(form_data, "sort_dir"):
sort_dir = get_data_entry(form_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(form_data, "state"):
state_ind = int(get_data_entry(form_data, "state"))
if state_ind != -1:
try:
state = BidStates(state_ind)
except Exception:
raise ValueError('Invalid state')
filters['bid_state_ind'] = state_ind
if have_data_entry(form_data, 'with_expired'):
with_expired = toBool(get_data_entry(form_data, 'with_expired'))
filters['with_expired'] = with_expired
_ = BidStates(state_ind)
except Exception as e: # noqa: F841
raise ValueError("Invalid state")
filters["bid_state_ind"] = state_ind
if have_data_entry(form_data, "with_expired"):
with_expired = toBool(get_data_entry(form_data, "with_expired"))
filters["with_expired"] = with_expired
set_pagination_filters(form_data, filters)
if have_data_entry(form_data, 'applyfilters'):
if have_data_entry(form_data, "applyfilters"):
swap_client.setFilters(filter_prefix, filters)
else:
saved_filters = swap_client.getFilters(filter_prefix)
@@ -181,21 +217,40 @@ def page_bids(self, url_split, post_string, sent=False, available=False, receive
bids = swap_client.listBids(sent=sent, filters=filters)
page_data = {
'bid_states': listBidStates(),
"bid_states": listBidStates(),
}
template = server.env.get_template('bids.html')
return self.render_template(template, {
'page_type_sent': 'Bids Sent' if sent else '',
'page_type_available': 'Bids Available' if available else '',
'page_type_received': 'Received Bids' if received else '',
'page_type_sent_description': 'All the bids you have placed on offers.' if sent else '',
'page_type_available_description': 'Bids available for you to accept.' if available else '',
'page_type_received_description': 'All the bids placed on your offers.' if received else '',
'messages': messages,
'filters': filters,
'data': page_data,
'summary': summary,
'bids': [(format_timestamp(b[0]),
b[2].hex(), b[3].hex(), strBidState(b[5]), strTxState(b[7]), strTxState(b[8]), b[11]) for b in bids],
'bids_count': len(bids),
})
template = server.env.get_template("bids.html")
return self.render_template(
template,
{
"page_type_sent": "Bids Sent" if sent else "",
"page_type_available": "Bids Available" if available else "",
"page_type_received": "Received Bids" if received else "",
"page_type_sent_description": (
"All the bids you have placed on offers." if sent else ""
),
"page_type_available_description": (
"Bids available for you to accept." if available else ""
),
"page_type_received_description": (
"All the bids placed on your offers." if received else ""
),
"messages": messages,
"filters": filters,
"data": page_data,
"summary": summary,
"bids": [
(
format_timestamp(b[0]),
b[2].hex(),
b[3].hex(),
strBidState(b[5]),
strTxState(b[7]),
strTxState(b[8]),
b[11],
)
for b in bids
],
"bids_count": len(bids),
},
)

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.
@@ -25,31 +25,34 @@ def page_debug(self, url_split, post_string):
result = None
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'wallets', err_messages)
form_data = self.checkForm(post_string, "wallets", err_messages)
if form_data:
if have_data_entry(form_data, 'reinit_xmr'):
if have_data_entry(form_data, "reinit_xmr"):
try:
swap_client.initialiseWallet(Coins.XMR)
messages.append('Done.')
messages.append("Done.")
except Exception as e:
err_messages.append('Failed.')
err_messages.append(f"Failed: {e}.")
if have_data_entry(form_data, 'remove_expired'):
if have_data_entry(form_data, "remove_expired"):
try:
swap_client.log.warning('Removing expired data.')
swap_client.log.warning("Removing expired data.")
remove_expired_data(swap_client)
messages.append('Done.')
messages.append("Done.")
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
else:
swap_client.log.error(f'remove_expired_data: {e}')
err_messages.append('Failed.')
swap_client.log.error(f"remove_expired_data: {e}")
err_messages.append("Failed.")
template = server.env.get_template('debug.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'result': result,
'summary': summary,
})
template = server.env.get_template("debug.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"result": result,
"summary": summary,
},
)

View File

@@ -17,47 +17,52 @@ def page_changepassword(self, url_split, post_string):
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'changepassword', err_messages)
form_data = self.checkForm(post_string, "changepassword", err_messages)
if form_data:
old_password = get_data_entry_or(form_data, 'oldpassword', '')
new_password = get_data_entry_or(form_data, 'newpassword', '')
confirm_password = get_data_entry_or(form_data, 'confirmpassword', '')
old_password = get_data_entry_or(form_data, "oldpassword", "")
new_password = get_data_entry_or(form_data, "newpassword", "")
confirm_password = get_data_entry_or(form_data, "confirmpassword", "")
try:
if new_password == '':
raise ValueError('New password must be entered.')
if new_password == "":
raise ValueError("New password must be entered.")
if new_password != confirm_password:
raise ValueError('New password and confirm password must match.')
raise ValueError("New password and confirm password must match.")
swap_client.changeWalletPasswords(old_password, new_password)
messages.append('Password changed')
messages.append("Password changed")
except Exception as e:
err_messages.append(str(e))
template = server.env.get_template('changepassword.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'summary': swap_client.getSummary(),
})
template = server.env.get_template("changepassword.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"summary": swap_client.getSummary(),
},
)
def page_unlock(self, url_split, post_string):
server = self.server
swap_client = server.swap_client
messages = ['Warning: This will unlock the system for all users!', ]
messages = [
"Warning: This will unlock the system for all users!",
]
err_messages = []
form_data = self.checkForm(post_string, 'unlock', err_messages)
form_data = self.checkForm(post_string, "unlock", err_messages)
if form_data:
password = get_data_entry_or(form_data, 'password', '')
password = get_data_entry_or(form_data, "password", "")
try:
if password == '':
raise ValueError('Password must be entered.')
if password == "":
raise ValueError("Password must be entered.")
swap_client.unlockWallets(password)
self.send_response(302)
self.send_header('Location', '/')
self.send_header("Location", "/")
self.end_headers()
return bytes()
except Exception as e:
@@ -65,11 +70,14 @@ def page_unlock(self, url_split, post_string):
swap_client.log.error(str(e))
err_messages.append(str(e))
template = server.env.get_template('unlock.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
})
template = server.env.get_template("unlock.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
},
)
def page_lock(self, url_split, post_string):
@@ -79,6 +87,6 @@ def page_lock(self, url_split, post_string):
swap_client.lockWallets()
self.send_response(302)
self.send_header('Location', '/')
self.send_header("Location", "/")
self.end_headers()
return bytes()

View File

@@ -25,54 +25,71 @@ def page_identity(self, url_split, post_string):
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
ensure(len(url_split) > 2, 'Address not specified')
ensure(len(url_split) > 2, "Address not specified")
identity_address = url_split[2]
page_data = {'identity_address': identity_address}
page_data = {"identity_address": identity_address}
messages = []
err_messages = []
form_data = self.checkForm(post_string, 'identity', err_messages)
form_data = self.checkForm(post_string, "identity", err_messages)
if form_data:
if have_data_entry(form_data, 'edit'):
page_data['show_edit_form'] = True
if have_data_entry(form_data, 'apply'):
if have_data_entry(form_data, "edit"):
page_data["show_edit_form"] = True
if have_data_entry(form_data, "apply"):
try:
data = {
'label': get_data_entry_or(form_data, 'label', ''),
'note': get_data_entry_or(form_data, 'note', ''),
'automation_override': get_data_entry(form_data, 'automation_override'),
"label": get_data_entry_or(form_data, "label", ""),
"note": get_data_entry_or(form_data, "note", ""),
"automation_override": get_data_entry(
form_data, "automation_override"
),
}
swap_client.setIdentityData({'address': identity_address}, data)
messages.append('Updated')
swap_client.setIdentityData({"address": identity_address}, data)
messages.append("Updated")
except Exception as e:
err_messages.append(str(e))
try:
identity = swap_client.getIdentity(identity_address)
if identity is None:
raise ValueError('Unknown address')
raise ValueError("Unknown address")
automation_override = zeroIfNone(identity.automation_override)
page_data.update({
'label': '' if identity.label is None else identity.label,
'num_sent_bids_successful': zeroIfNone(identity.num_sent_bids_successful),
'num_recv_bids_successful': zeroIfNone(identity.num_recv_bids_successful),
'num_sent_bids_rejected': zeroIfNone(identity.num_sent_bids_rejected),
'num_recv_bids_rejected': zeroIfNone(identity.num_recv_bids_rejected),
'num_sent_bids_failed': zeroIfNone(identity.num_sent_bids_failed),
'num_recv_bids_failed': zeroIfNone(identity.num_recv_bids_failed),
'automation_override': automation_override,
'str_automation_override': strAutomationOverrideOption(automation_override),
'note': '' if identity.note is None else identity.note,
})
page_data.update(
{
"label": "" if identity.label is None else identity.label,
"num_sent_bids_successful": zeroIfNone(
identity.num_sent_bids_successful
),
"num_recv_bids_successful": zeroIfNone(
identity.num_recv_bids_successful
),
"num_sent_bids_rejected": zeroIfNone(identity.num_sent_bids_rejected),
"num_recv_bids_rejected": zeroIfNone(identity.num_recv_bids_rejected),
"num_sent_bids_failed": zeroIfNone(identity.num_sent_bids_failed),
"num_recv_bids_failed": zeroIfNone(identity.num_recv_bids_failed),
"automation_override": automation_override,
"str_automation_override": strAutomationOverrideOption(
automation_override
),
"note": "" if identity.note is None else identity.note,
}
)
except Exception as e:
err_messages.append(e)
template = server.env.get_template('identity.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'data': page_data,
'automation_override_options': [(int(opt), strAutomationOverrideOption(opt)) for opt in AutomationOverrideOptions if opt > 0],
'summary': summary,
})
template = server.env.get_template("identity.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"data": page_data,
"automation_override_options": [
(int(opt), strAutomationOverrideOption(opt))
for opt in AutomationOverrideOptions
if opt > 0
],
"summary": summary,
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -28,140 +28,195 @@ def page_settings(self, url_split, post_string):
messages = []
err_messages = []
active_tab = 'default'
form_data = self.checkForm(post_string, 'settings', err_messages)
active_tab = "default"
form_data = self.checkForm(post_string, "settings", err_messages)
if form_data:
try:
if have_data_entry(form_data, 'apply_general'):
active_tab = 'general'
if have_data_entry(form_data, "apply_general"):
active_tab = "general"
data = {
'debug': toBool(get_data_entry(form_data, 'debugmode')),
'debug_ui': toBool(get_data_entry(form_data, 'debugui')),
'expire_db_records': toBool(get_data_entry(form_data, 'expire_db_records')),
"debug": toBool(get_data_entry(form_data, "debugmode")),
"debug_ui": toBool(get_data_entry(form_data, "debugui")),
"expire_db_records": toBool(
get_data_entry(form_data, "expire_db_records")
),
}
swap_client.editGeneralSettings(data)
elif have_data_entry(form_data, 'apply_chart'):
active_tab = 'general'
elif have_data_entry(form_data, "apply_chart"):
active_tab = "general"
data = {
'show_chart': toBool(get_data_entry(form_data, 'showchart')),
'chart_api_key': html.unescape(get_data_entry_or(form_data, 'chartapikey', '')),
'coingecko_api_key': html.unescape(get_data_entry_or(form_data, 'coingeckoapikey', '')),
'enabled_chart_coins': get_data_entry_or(form_data, 'enabledchartcoins', ''),
"show_chart": toBool(get_data_entry(form_data, "showchart")),
"chart_api_key": html.unescape(
get_data_entry_or(form_data, "chartapikey", "")
),
"coingecko_api_key": html.unescape(
get_data_entry_or(form_data, "coingeckoapikey", "")
),
"enabled_chart_coins": get_data_entry_or(
form_data, "enabledchartcoins", ""
),
}
swap_client.editGeneralSettings(data)
elif have_data_entry(form_data, 'apply_tor'):
active_tab = 'tor'
elif have_data_entry(form_data, "apply_tor"):
active_tab = "tor"
# TODO: Detect if running in docker
raise ValueError('TODO: If running in docker see doc/tor.md to enable/disable tor.')
raise ValueError(
"TODO: If running in docker see doc/tor.md to enable/disable tor."
)
for name, c in swap_client.settings['chainclients'].items():
if have_data_entry(form_data, 'apply_' + name):
data = {'lookups': get_data_entry(form_data, 'lookups_' + name)}
if name in ('monero', 'wownero'):
data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name))
data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
data['rpcport'] = int(get_data_entry(form_data, 'rpcport_' + name))
data['remotedaemonurls'] = get_data_entry_or(form_data, 'remotedaemonurls_' + name, '')
data['automatically_select_daemon'] = True if get_data_entry(form_data, 'autosetdaemon_' + name) == 'true' else False
for name, c in swap_client.settings["chainclients"].items():
if have_data_entry(form_data, "apply_" + name):
data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
if name in ("monero", "wownero"):
data["fee_priority"] = int(
get_data_entry(form_data, "fee_priority_" + name)
)
data["manage_daemon"] = (
True
if get_data_entry(form_data, "managedaemon_" + name)
== "true"
else False
)
data["rpchost"] = get_data_entry(form_data, "rpchost_" + name)
data["rpcport"] = int(
get_data_entry(form_data, "rpcport_" + name)
)
data["remotedaemonurls"] = get_data_entry_or(
form_data, "remotedaemonurls_" + name, ""
)
data["automatically_select_daemon"] = (
True
if get_data_entry(form_data, "autosetdaemon_" + name)
== "true"
else False
)
else:
data['conf_target'] = int(get_data_entry(form_data, 'conf_target_' + name))
if name == 'particl':
data['anon_tx_ring_size'] = int(get_data_entry(form_data, 'rct_ring_size_' + name))
data["conf_target"] = int(
get_data_entry(form_data, "conf_target_" + name)
)
if name == "particl":
data["anon_tx_ring_size"] = int(
get_data_entry(form_data, "rct_ring_size_" + name)
)
settings_changed, suggest_reboot = swap_client.editSettings(name, data)
settings_changed, suggest_reboot = swap_client.editSettings(
name, data
)
if settings_changed is True:
messages.append('Settings applied.')
messages.append("Settings applied.")
if suggest_reboot is True:
messages.append('Please restart BasicSwap.')
elif have_data_entry(form_data, 'enable_' + name):
messages.append("Please restart BasicSwap.")
elif have_data_entry(form_data, "enable_" + name):
swap_client.enableCoin(name)
display_name = getCoinName(swap_client.getCoinIdFromName(name))
messages.append(display_name + ' enabled, shutting down.')
messages.append(display_name + " enabled, shutting down.")
swap_client.stopRunning()
elif have_data_entry(form_data, 'disable_' + name):
elif have_data_entry(form_data, "disable_" + name):
swap_client.disableCoin(name)
display_name = getCoinName(swap_client.getCoinIdFromName(name))
messages.append(display_name + ' disabled, shutting down.')
messages.append(display_name + " disabled, shutting down.")
swap_client.stopRunning()
except InactiveCoin as ex:
err_messages.append('InactiveCoin {}'.format(Coins(ex.coinid).name))
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
except Exception as e:
err_messages.append(str(e))
chains_formatted = []
sorted_names = sorted(swap_client.settings['chainclients'].keys())
sorted_names = sorted(swap_client.settings["chainclients"].keys())
for name in sorted_names:
c = swap_client.settings['chainclients'][name]
c = swap_client.settings["chainclients"][name]
try:
display_name = getCoinName(swap_client.getCoinIdFromName(name))
except Exception:
display_name = name
chains_formatted.append({
'name': name,
'display_name': display_name,
'lookups': c.get('chain_lookups', 'local'),
'manage_daemon': c.get('manage_daemon', 'Unknown'),
'connection_type': c.get('connection_type', 'Unknown'),
})
if name in ('monero', 'wownero'):
chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0)
chains_formatted[-1]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown')
chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost')
chains_formatted[-1]['rpcport'] = int(c.get('rpcport', 18081))
chains_formatted[-1]['remotedaemonurls'] = '\n'.join(c.get('remote_daemon_urls', []))
chains_formatted[-1]['autosetdaemon'] = c.get('automatically_select_daemon', False)
chains_formatted.append(
{
"name": name,
"display_name": display_name,
"lookups": c.get("chain_lookups", "local"),
"manage_daemon": c.get("manage_daemon", "Unknown"),
"connection_type": c.get("connection_type", "Unknown"),
}
)
if name in ("monero", "wownero"):
chains_formatted[-1]["fee_priority"] = c.get("fee_priority", 0)
chains_formatted[-1]["manage_wallet_daemon"] = c.get(
"manage_wallet_daemon", "Unknown"
)
chains_formatted[-1]["rpchost"] = c.get("rpchost", "localhost")
chains_formatted[-1]["rpcport"] = int(c.get("rpcport", 18081))
chains_formatted[-1]["remotedaemonurls"] = "\n".join(
c.get("remote_daemon_urls", [])
)
chains_formatted[-1]["autosetdaemon"] = c.get(
"automatically_select_daemon", False
)
else:
chains_formatted[-1]['conf_target'] = c.get('conf_target', 2)
chains_formatted[-1]["conf_target"] = c.get("conf_target", 2)
if name == 'particl':
chains_formatted[-1]['anon_tx_ring_size'] = c.get('anon_tx_ring_size', 12)
if name == "particl":
chains_formatted[-1]["anon_tx_ring_size"] = c.get("anon_tx_ring_size", 12)
else:
if c.get('connection_type', 'Unknown') == 'none':
if 'connection_type_prev' in c:
chains_formatted[-1]['can_reenable'] = True
if c.get("connection_type", "Unknown") == "none":
if "connection_type_prev" in c:
chains_formatted[-1]["can_reenable"] = True
else:
chains_formatted[-1]['can_disable'] = True
chains_formatted[-1]["can_disable"] = True
general_settings = {
'debug': swap_client.debug,
'debug_ui': swap_client.debug_ui,
'expire_db_records': swap_client._expire_db_records,
"debug": swap_client.debug,
"debug_ui": swap_client.debug_ui,
"expire_db_records": swap_client._expire_db_records,
}
if 'chart_api_key_enc' in swap_client.settings:
chart_api_key = html.escape(bytes.fromhex(swap_client.settings.get('chart_api_key_enc', '')).decode('utf-8'))
if "chart_api_key_enc" in swap_client.settings:
chart_api_key = html.escape(
bytes.fromhex(swap_client.settings.get("chart_api_key_enc", "")).decode(
"utf-8"
)
)
else:
chart_api_key = swap_client.settings.get('chart_api_key', '')
chart_api_key = swap_client.settings.get("chart_api_key", "")
if 'coingecko_api_key_enc' in swap_client.settings:
coingecko_api_key = html.escape(bytes.fromhex(swap_client.settings.get('coingecko_api_key_enc', '')).decode('utf-8'))
if "coingecko_api_key_enc" in swap_client.settings:
coingecko_api_key = html.escape(
bytes.fromhex(swap_client.settings.get("coingecko_api_key_enc", "")).decode(
"utf-8"
)
)
else:
coingecko_api_key = swap_client.settings.get('coingecko_api_key', '')
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
chart_settings = {
'show_chart': swap_client.settings.get('show_chart', True),
'chart_api_key': chart_api_key,
'coingecko_api_key': coingecko_api_key,
'enabled_chart_coins': swap_client.settings.get('enabled_chart_coins', ''),
"show_chart": swap_client.settings.get("show_chart", True),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
"enabled_chart_coins": swap_client.settings.get("enabled_chart_coins", ""),
}
tor_control_password = '' if swap_client.tor_control_password is None else swap_client.tor_control_password
tor_control_password = (
""
if swap_client.tor_control_password is None
else swap_client.tor_control_password
)
tor_settings = {
'use_tor': swap_client.use_tor_proxy,
'proxy_host': swap_client.tor_proxy_host,
'proxy_port': swap_client.tor_proxy_port,
'control_password': html.escape(tor_control_password),
'control_port': swap_client.tor_control_port,
"use_tor": swap_client.use_tor_proxy,
"proxy_host": swap_client.tor_proxy_host,
"proxy_port": swap_client.tor_proxy_port,
"control_password": html.escape(tor_control_password),
"control_port": swap_client.tor_control_port,
}
template = server.env.get_template('settings.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'summary': swap_client.getSummary(),
'chains': chains_formatted,
'general_settings': general_settings,
'chart_settings': chart_settings,
'tor_settings': tor_settings,
'active_tab': active_tab,
})
template = server.env.get_template("settings.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"summary": swap_client.getSummary(),
"chains": chains_formatted,
"general_settings": general_settings,
"chart_settings": chart_settings,
"tor_settings": tor_settings,
"active_tab": active_tab,
},
)

View File

@@ -28,11 +28,11 @@ def page_smsgaddresses(self, url_split, post_string):
summary = swap_client.getSummary()
filters = {
'page_no': 1,
'limit': PAGE_LIMIT,
'sort_by': 'created_at',
'sort_dir': 'desc',
'addr_type': -1,
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
"addr_type": -1,
}
page_data = {}
@@ -41,96 +41,114 @@ def page_smsgaddresses(self, url_split, post_string):
smsgaddresses = []
listaddresses = True
form_data = self.checkForm(post_string, 'smsgaddresses', err_messages)
form_data = self.checkForm(post_string, "smsgaddresses", err_messages)
if form_data:
edit_address_id = None
for key in form_data:
if key.startswith(b'editaddr_'):
edit_address_id = int(key.split(b'_')[1])
if key.startswith(b"editaddr_"):
edit_address_id = int(key.split(b"_")[1])
break
if edit_address_id is not None:
listaddresses = False
page_data['edit_address'] = edit_address_id
page_data['addr_data'] = swap_client.listAllSMSGAddresses({'addr_id': edit_address_id})[0]
elif have_data_entry(form_data, 'saveaddr'):
edit_address_id = int(get_data_entry(form_data, 'edit_address_id'))
edit_addr = get_data_entry(form_data, 'edit_address')
active_ind = int(get_data_entry(form_data, 'active_ind'))
ensure(active_ind in (0, 1), 'Invalid sort by')
addressnote = get_data_entry_or(form_data, 'addressnote', '')
if not validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
page_data["edit_address"] = edit_address_id
page_data["addr_data"] = swap_client.listAllSMSGAddresses(
{"addr_id": edit_address_id}
)[0]
elif have_data_entry(form_data, "saveaddr"):
edit_address_id = int(get_data_entry(form_data, "edit_address_id"))
edit_addr = get_data_entry(form_data, "edit_address")
active_ind = int(get_data_entry(form_data, "active_ind"))
ensure(active_ind in (0, 1), "Invalid sort by")
addressnote = get_data_entry_or(form_data, "addressnote", "")
if not validateTextInput(
addressnote, "Address note", err_messages, max_length=30
):
listaddresses = False
page_data['edit_address'] = edit_address_id
page_data["edit_address"] = edit_address_id
else:
swap_client.editSMSGAddress(edit_addr, active_ind=active_ind, addressnote=addressnote)
messages.append(f'Edited address {edit_addr}')
elif have_data_entry(form_data, 'shownewaddr'):
swap_client.editSMSGAddress(
edit_addr, active_ind=active_ind, addressnote=addressnote
)
messages.append(f"Edited address {edit_addr}")
elif have_data_entry(form_data, "shownewaddr"):
listaddresses = False
page_data['new_address'] = True
elif have_data_entry(form_data, 'showaddaddr'):
page_data["new_address"] = True
elif have_data_entry(form_data, "showaddaddr"):
listaddresses = False
page_data['new_send_address'] = True
elif have_data_entry(form_data, 'createnewaddr'):
addressnote = get_data_entry_or(form_data, 'addressnote', '')
if not validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
page_data["new_send_address"] = True
elif have_data_entry(form_data, "createnewaddr"):
addressnote = get_data_entry_or(form_data, "addressnote", "")
if not validateTextInput(
addressnote, "Address note", err_messages, max_length=30
):
listaddresses = False
page_data['new_address'] = True
page_data["new_address"] = True
else:
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
messages.append(f'Created address {new_addr}, pubkey {pubkey}')
elif have_data_entry(form_data, 'createnewsendaddr'):
pubkey_hex = get_data_entry(form_data, 'addresspubkey')
addressnote = get_data_entry_or(form_data, 'addressnote', '')
if not validateTextInput(addressnote, 'Address note', messages, max_length=30) or \
not validateTextInput(pubkey_hex, 'Pubkey', messages, max_length=66):
messages.append(f"Created address {new_addr}, pubkey {pubkey}")
elif have_data_entry(form_data, "createnewsendaddr"):
pubkey_hex = get_data_entry(form_data, "addresspubkey")
addressnote = get_data_entry_or(form_data, "addressnote", "")
if not validateTextInput(
addressnote, "Address note", messages, max_length=30
) or not validateTextInput(pubkey_hex, "Pubkey", messages, max_length=66):
listaddresses = False
page_data['new_send_address'] = True
page_data["new_send_address"] = True
else:
new_addr = swap_client.addSMSGAddress(pubkey_hex, addressnote=addressnote)
messages.append(f'Added address {new_addr}')
new_addr = swap_client.addSMSGAddress(
pubkey_hex, addressnote=addressnote
)
messages.append(f"Added address {new_addr}")
if have_data_entry(form_data, 'clearfilters'):
swap_client.clearFilters('page_smsgaddresses')
if have_data_entry(form_data, "clearfilters"):
swap_client.clearFilters("page_smsgaddresses")
else:
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if have_data_entry(form_data, 'filter_addressnote'):
addressnote = get_data_entry(form_data, 'filter_addressnote')
if validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
filters['addressnote'] = addressnote
if have_data_entry(form_data, 'filter_addr_type'):
filters['addr_type'] = int(get_data_entry(form_data, 'filter_addr_type'))
if have_data_entry(form_data, "sort_by"):
sort_by = get_data_entry(form_data, "sort_by")
ensure(sort_by in ["created_at", "rate"], "Invalid sort by")
filters["sort_by"] = sort_by
if have_data_entry(form_data, "sort_dir"):
sort_dir = get_data_entry(form_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(form_data, "filter_addressnote"):
addressnote = get_data_entry(form_data, "filter_addressnote")
if validateTextInput(
addressnote, "Address note", err_messages, max_length=30
):
filters["addressnote"] = addressnote
if have_data_entry(form_data, "filter_addr_type"):
filters["addr_type"] = int(
get_data_entry(form_data, "filter_addr_type")
)
set_pagination_filters(form_data, filters)
if have_data_entry(form_data, 'applyfilters'):
swap_client.setFilters('page_smsgaddresses', filters)
if have_data_entry(form_data, "applyfilters"):
swap_client.setFilters("page_smsgaddresses", filters)
else:
saved_filters = swap_client.getFilters('page_smsgaddresses')
saved_filters = swap_client.getFilters("page_smsgaddresses")
if saved_filters:
filters.update(saved_filters)
if listaddresses is True:
smsgaddresses = swap_client.listAllSMSGAddresses(filters)
page_data['addr_types'] = [(int(t), strAddressType(t)) for t in AddressTypes]
page_data['network_addr'] = swap_client.network_addr
page_data["addr_types"] = [(int(t), strAddressType(t)) for t in AddressTypes]
page_data["network_addr"] = swap_client.network_addr
for addr in smsgaddresses:
addr['type'] = strAddressType(addr['type'])
addr["type"] = strAddressType(addr["type"])
template = self.server.env.get_template('smsgaddresses.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'filters': filters,
'data': page_data,
'smsgaddresses': smsgaddresses,
'page_data': page_data,
'summary': summary,
})
template = self.server.env.get_template("smsgaddresses.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"filters": filters,
"data": page_data,
"smsgaddresses": smsgaddresses,
"page_data": page_data,
"summary": summary,
},
)

View File

@@ -1,54 +1,53 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
def extract_data(bytes_in):
str_in = bytes_in.decode('utf-8')
start = str_in.find('=')
if bytes_in is None:
return None
str_in = bytes_in.decode("utf-8")
start = str_in.find("=")
if start < 0:
return None
start += 1
end = str_in.find('\r', start)
end = str_in.find("\r", start)
if end < 0:
return None
return str_in[start: end]
return str_in[start:end]
def get_tor_established_state(swap_client):
rv = swap_client.torControl('GETINFO status/circuit-established')
rv = swap_client.torControl("GETINFO status/circuit-established")
return extract_data(rv)
def page_tor(self, url_split, post_string):
swap_client = self.server.swap_client
summary = swap_client.getSummary()
page_data = {}
try:
page_data['circuit_established'] = get_tor_established_state(swap_client)
page_data["circuit_established"] = get_tor_established_state(swap_client)
except Exception:
page_data['circuit_established'] = 'error'
page_data["circuit_established"] = "error"
try:
rv = swap_client.torControl('GETINFO traffic/read')
page_data['bytes_written'] = extract_data(rv)
rv = swap_client.torControl("GETINFO traffic/read")
page_data["bytes_written"] = extract_data(rv)
except Exception:
page_data['bytes_written'] = 'error'
page_data["bytes_written"] = "error"
try:
rv = swap_client.torControl('GETINFO traffic/written')
page_data['bytes_read'] = extract_data(rv)
rv = swap_client.torControl("GETINFO traffic/written")
page_data["bytes_read"] = extract_data(rv)
except Exception:
page_data['bytes_read'] = 'error'
page_data["bytes_read"] = "error"
messages = []
template = self.server.env.get_template('tor.html')
return self.render_template(template, {
'messages': messages,
'data': page_data,
'summary': summary,
})
template = self.server.env.get_template("tor.html")
return self.render_template(
template,
{
"messages": messages,
"data": page_data,
"summary": summary,
},
)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -23,52 +24,55 @@ from basicswap.chainparams import (
def format_wallet_data(swap_client, ci, w):
wf = {
'name': ci.coin_name(),
'version': w.get('version', '?'),
'ticker': ci.ticker_mainnet(),
'cid': str(int(ci.coin_type())),
'balance': w.get('balance', '?'),
'blocks': w.get('blocks', '?'),
'synced': w.get('synced', '?'),
'expected_seed': w.get('expected_seed', '?'),
'encrypted': w.get('encrypted', '?'),
'locked': w.get('locked', '?'),
'updating': w.get('updating', '?'),
'havedata': True,
"name": ci.coin_name(),
"version": w.get("version", "?"),
"ticker": ci.ticker_mainnet(),
"cid": str(int(ci.coin_type())),
"balance": w.get("balance", "?"),
"blocks": w.get("blocks", "?"),
"synced": w.get("synced", "?"),
"expected_seed": w.get("expected_seed", "?"),
"encrypted": w.get("encrypted", "?"),
"locked": w.get("locked", "?"),
"updating": w.get("updating", "?"),
"havedata": True,
}
if w.get('bootstrapping', False) is True:
wf['bootstrapping'] = True
if 'known_block_count' in w:
wf['known_block_count'] = w['known_block_count']
if 'locked_utxos' in w:
wf['locked_utxos'] = w['locked_utxos']
if "wallet_blocks" in w:
wf["wallet_blocks"] = w["wallet_blocks"]
if 'balance' in w and 'unconfirmed' in w:
wf['balance_all'] = float(w['balance']) + float(w['unconfirmed'])
if 'lastupdated' in w:
wf['lastupdated'] = format_timestamp(w['lastupdated'])
if w.get("bootstrapping", False) is True:
wf["bootstrapping"] = True
if "known_block_count" in w:
wf["known_block_count"] = w["known_block_count"]
if "locked_utxos" in w:
wf["locked_utxos"] = w["locked_utxos"]
if "balance" in w and "unconfirmed" in w:
wf["balance_all"] = float(w["balance"]) + float(w["unconfirmed"])
if "lastupdated" in w:
wf["lastupdated"] = format_timestamp(w["lastupdated"])
pending: int = 0
if 'unconfirmed' in w and float(w['unconfirmed']) > 0.0:
pending += ci.make_int(w['unconfirmed'])
if 'immature' in w and float(w['immature']) > 0.0:
pending += ci.make_int(w['immature'])
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
pending += ci.make_int(w["unconfirmed"])
if "immature" in w and float(w["immature"]) > 0.0:
pending += ci.make_int(w["immature"])
if pending > 0.0:
wf['pending'] = ci.format_amount(pending)
wf["pending"] = ci.format_amount(pending)
if ci.coin_type() == Coins.PART:
wf['stealth_address'] = w.get('stealth_address', '?')
wf['blind_balance'] = w.get('blind_balance', '?')
if 'blind_unconfirmed' in w and float(w['blind_unconfirmed']) > 0.0:
wf['blind_unconfirmed'] = w['blind_unconfirmed']
wf['anon_balance'] = w.get('anon_balance', '?')
if 'anon_pending' in w and float(w['anon_pending']) > 0.0:
wf['anon_pending'] = w['anon_pending']
wf["stealth_address"] = w.get("stealth_address", "?")
wf["blind_balance"] = w.get("blind_balance", "?")
if "blind_unconfirmed" in w and float(w["blind_unconfirmed"]) > 0.0:
wf["blind_unconfirmed"] = w["blind_unconfirmed"]
wf["anon_balance"] = w.get("anon_balance", "?")
if "anon_pending" in w and float(w["anon_pending"]) > 0.0:
wf["anon_pending"] = w["anon_pending"]
elif ci.coin_type() == Coins.LTC:
wf['mweb_address'] = w.get('mweb_address', '?')
wf['mweb_balance'] = w.get('mweb_balance', '?')
wf['mweb_pending'] = w.get('mweb_pending', '?')
wf["mweb_address"] = w.get("mweb_address", "?")
wf["mweb_balance"] = w.get("mweb_balance", "?")
wf["mweb_pending"] = w.get("mweb_pending", "?")
checkAddressesOwned(swap_client, ci, wf)
return wf
@@ -91,19 +95,18 @@ def page_wallets(self, url_split, post_string):
for k in sk:
w = wallets[k]
if 'error' in w:
wallets_formatted.append({
'cid': str(int(k)),
'error': w['error']
})
if "error" in w:
wallets_formatted.append({"cid": str(int(k)), "error": w["error"]})
continue
if 'no_data' in w:
wallets_formatted.append({
'name': w['name'],
'havedata': False,
'updating': w['updating'],
})
if "no_data" in w:
wallets_formatted.append(
{
"name": w["name"],
"havedata": False,
"updating": w["updating"],
}
)
continue
ci = swap_client.ci(k)
@@ -111,17 +114,20 @@ def page_wallets(self, url_split, post_string):
wallets_formatted.append(wf)
template = server.env.get_template('wallets.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'wallets': wallets_formatted,
'summary': summary,
})
template = server.env.get_template("wallets.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"wallets": wallets_formatted,
"summary": summary,
},
)
def page_wallet(self, url_split, post_string):
ensure(len(url_split) > 2, 'Wallet not specified')
ensure(len(url_split) > 2, "Wallet not specified")
wallet_ticker = url_split[2]
server = self.server
swap_client = server.swap_client
@@ -136,94 +142,130 @@ def page_wallet(self, url_split, post_string):
show_utxo_groups: bool = False
withdrawal_successful: bool = False
force_refresh: bool = False
form_data = self.checkForm(post_string, 'wallet', err_messages)
form_data = self.checkForm(post_string, "wallet", err_messages)
if form_data:
cid = str(int(coin_id))
estimate_fee: bool = have_data_entry(form_data, 'estfee_' + cid)
withdraw: bool = have_data_entry(form_data, 'withdraw_' + cid)
if have_data_entry(form_data, 'newaddr_' + cid):
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
withdraw: bool = have_data_entry(form_data, "withdraw_" + cid)
if have_data_entry(form_data, "newaddr_" + cid):
swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, 'forcerefresh'):
elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True
elif have_data_entry(form_data, 'newmwebaddr_' + cid):
elif have_data_entry(form_data, "newmwebaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, 'reseed_' + cid):
elif have_data_entry(form_data, "reseed_" + cid):
try:
swap_client.reseedWallet(coin_id)
messages.append('Reseed complete ' + str(coin_id))
messages.append("Reseed complete " + str(coin_id))
except Exception as ex:
err_messages.append('Reseed failed ' + str(ex))
err_messages.append("Reseed failed " + str(ex))
swap_client.updateWalletsInfo(True, coin_id)
elif withdraw or estimate_fee:
subfee = True if have_data_entry(form_data, 'subfee_' + cid) else False
page_data['wd_subfee_' + cid] = subfee
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
page_data["wd_subfee_" + cid] = subfee
sweepall = True if have_data_entry(form_data, 'sweepall_' + cid) else False
page_data['wd_sweepall_' + cid] = sweepall
sweepall = True if have_data_entry(form_data, "sweepall_" + cid) else False
page_data["wd_sweepall_" + cid] = sweepall
value = None
if not sweepall:
try:
value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_value_' + cid] = value
except Exception as e:
err_messages.append('Missing value')
value = form_data[bytes("amt_" + cid, "utf-8")][0].decode("utf-8")
page_data["wd_value_" + cid] = value
except Exception as e: # noqa: F841
err_messages.append("Missing value")
try:
address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_address_' + cid] = address
except Exception as e:
err_messages.append('Missing address')
address = form_data[bytes("to_" + cid, "utf-8")][0].decode("utf-8")
page_data["wd_address_" + cid] = address
except Exception as e: # noqa: F841
err_messages.append("Missing address")
if estimate_fee and withdraw:
err_messages.append('Estimate fee and withdraw can\'t be used together.')
err_messages.append("Estimate fee and withdraw can't be used together.")
if estimate_fee and coin_id not in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_id)
ticker: str = ci.ticker()
err_messages.append(f'Estimate fee unavailable for {ticker}.')
err_messages.append(f"Estimate fee unavailable for {ticker}.")
if coin_id == Coins.PART:
try:
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_type_from_' + cid] = type_from
page_data['wd_type_to_' + cid] = type_to
except Exception as e:
err_messages.append('Missing type')
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
0
].decode("utf-8")
type_to = form_data[bytes("withdraw_type_to_" + cid, "utf-8")][
0
].decode("utf-8")
page_data["wd_type_from_" + cid] = type_from
page_data["wd_type_to_" + cid] = type_to
except Exception as e: # noqa: F841
err_messages.append("Missing type")
elif coin_id == Coins.LTC:
try:
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_type_from_' + cid] = type_from
except Exception as e:
err_messages.append('Missing type')
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
0
].decode("utf-8")
page_data["wd_type_from_" + cid] = type_from
except Exception as e: # noqa: F841
err_messages.append("Missing type")
if len(err_messages) == 0:
ci = swap_client.ci(coin_id)
ticker: str = ci.ticker()
try:
if coin_id == Coins.PART:
txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
messages.append('Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, type_to, address, txid))
txid = swap_client.withdrawParticl(
type_from, type_to, value, address, subfee
)
messages.append(
"Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}".format(
value, ticker, type_from, type_to, address, txid
)
)
elif coin_id == Coins.LTC:
txid = swap_client.withdrawLTC(type_from, value, address, subfee)
messages.append('Withdrew {} {} (from {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, address, txid))
txid = swap_client.withdrawLTC(
type_from, value, address, subfee
)
messages.append(
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
value, ticker, type_from, address, txid
)
)
elif coin_id in (Coins.XMR, Coins.WOW):
if estimate_fee:
fee_estimate = ci.estimateFee(value, address, sweepall)
suffix = 's' if fee_estimate['num_txns'] > 1 else ''
sum_fees = ci.format_amount(fee_estimate['sum_fee'])
value_str = ci.format_amount(fee_estimate['sum_amount'])
messages.append(f'Estimated fee for {value_str} {ticker} to address {address}: {sum_fees} in {fee_estimate["num_txns"]} transaction{suffix}.')
page_data['fee_estimate'] = fee_estimate
suffix = "s" if fee_estimate["num_txns"] > 1 else ""
sum_fees = ci.format_amount(fee_estimate["sum_fee"])
value_str = ci.format_amount(fee_estimate["sum_amount"])
messages.append(
f'Estimated fee for {value_str} {ticker} to address {address}: {sum_fees} in {fee_estimate["num_txns"]} transaction{suffix}.'
)
page_data["fee_estimate"] = fee_estimate
else:
txid = swap_client.withdrawCoin(coin_id, value, address, sweepall)
txid = swap_client.withdrawCoin(
coin_id, value, address, sweepall
)
if sweepall:
messages.append('Swept all {} to address {}<br/>In txid: {}'.format(ticker, address, txid))
messages.append(
"Swept all {} to address {}<br/>In txid: {}".format(
ticker, address, txid
)
)
else:
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
messages.append('Note: The wallet balance can take a while to update.')
messages.append(
"Withdrew {} {} to address {}<br/>In txid: {}".format(
value, ticker, address, txid
)
)
messages.append(
"Note: The wallet balance can take a while to update."
)
else:
txid = swap_client.withdrawCoin(coin_id, value, address, subfee)
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
messages.append(
"Withdrew {} {} to address {}<br/>In txid: {}".format(
value, ticker, address, txid
)
)
if not estimate_fee:
withdrawal_successful = True
except Exception as e:
@@ -232,40 +274,44 @@ def page_wallet(self, url_split, post_string):
err_messages.append(str(e))
if not estimate_fee:
swap_client.updateWalletsInfo(True, only_coin=coin_id)
elif have_data_entry(form_data, 'showutxogroups'):
elif have_data_entry(form_data, "showutxogroups"):
show_utxo_groups = True
elif have_data_entry(form_data, 'create_utxo'):
elif have_data_entry(form_data, "create_utxo"):
show_utxo_groups = True
try:
value = get_data_entry(form_data, 'utxo_value')
page_data['utxo_value'] = value
value = get_data_entry(form_data, "utxo_value")
page_data["utxo_value"] = value
ci = swap_client.ci(coin_id)
value_sats = ci.make_int(value)
txid, address = ci.createUTXO(value_sats)
messages.append('Created new utxo of value {} and address {}<br/>In txid: {}'.format(value, address, txid))
messages.append(
"Created new utxo of value {} and address {}<br/>In txid: {}".format(
value, address, txid
)
)
except Exception as e:
err_messages.append(str(e))
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
swap_client.updateWalletsInfo(force_refresh, only_coin=coin_id, wait_for_complete=True)
wallets = swap_client.getCachedWalletsInfo({'coin_id': coin_id})
swap_client.updateWalletsInfo(
force_refresh, only_coin=coin_id, wait_for_complete=True
)
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
wallet_data = {}
for k in wallets.keys():
w = wallets[k]
if 'error' in w:
wallet_data = {
'cid': str(int(k)),
'error': w['error']
}
if "error" in w:
wallet_data = {"cid": str(int(k)), "error": w["error"]}
continue
if 'no_data' in w:
if "no_data" in w:
wallet_data = {
'name': w['name'],
'havedata': False,
'updating': w['updating'],
"name": w["name"],
"havedata": False,
"updating": w["updating"],
}
continue
@@ -276,55 +322,68 @@ def page_wallet(self, url_split, post_string):
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
wallet_data['fee_rate'] = ci.format_amount(int(fee_rate * ci.COIN()))
wallet_data['fee_rate_src'] = fee_src
wallet_data['est_fee'] = 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN()))
wallet_data['deposit_address'] = w.get('deposit_address', 'Refresh necessary')
wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN()))
wallet_data["fee_rate_src"] = fee_src
wallet_data["est_fee"] = (
"Unknown" if est_fee is None else ci.format_amount(int(est_fee * ci.COIN()))
)
wallet_data["deposit_address"] = w.get("deposit_address", "Refresh necessary")
if k in (Coins.XMR, Coins.WOW):
wallet_data['main_address'] = w.get('main_address', 'Refresh necessary')
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
elif k == Coins.LTC:
wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary')
wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary")
if 'wd_type_from_' + cid in page_data:
wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid]
if 'wd_type_to_' + cid in page_data:
wallet_data['wd_type_to'] = page_data['wd_type_to_' + cid]
if "wd_type_from_" + cid in page_data:
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
if "wd_type_to_" + cid in page_data:
wallet_data["wd_type_to"] = page_data["wd_type_to_" + cid]
if 'utxo_value' in page_data:
wallet_data['utxo_value'] = page_data['utxo_value']
if "utxo_value" in page_data:
wallet_data["utxo_value"] = page_data["utxo_value"]
if not withdrawal_successful:
if 'wd_value_' + cid in page_data:
wallet_data['wd_value'] = page_data['wd_value_' + cid]
if 'wd_address_' + cid in page_data:
wallet_data['wd_address'] = page_data['wd_address_' + cid]
if 'wd_subfee_' + cid in page_data:
wallet_data['wd_subfee'] = page_data['wd_subfee_' + cid]
if 'wd_sweepall_' + cid in page_data:
wallet_data['wd_sweepall'] = page_data['wd_sweepall_' + cid]
if 'fee_estimate' in page_data:
wallet_data['est_fee'] = ci.format_amount(page_data['fee_estimate']['sum_fee'])
wallet_data['fee_rate'] = ci.format_amount(page_data['fee_estimate']['sum_fee'] * 1000 // page_data['fee_estimate']['sum_weight'])
if "wd_value_" + cid in page_data:
wallet_data["wd_value"] = page_data["wd_value_" + cid]
if "wd_address_" + cid in page_data:
wallet_data["wd_address"] = page_data["wd_address_" + cid]
if "wd_subfee_" + cid in page_data:
wallet_data["wd_subfee"] = page_data["wd_subfee_" + cid]
if "wd_sweepall_" + cid in page_data:
wallet_data["wd_sweepall"] = page_data["wd_sweepall_" + cid]
if "fee_estimate" in page_data:
wallet_data["est_fee"] = ci.format_amount(
page_data["fee_estimate"]["sum_fee"]
)
wallet_data["fee_rate"] = ci.format_amount(
page_data["fee_estimate"]["sum_fee"]
* 1000
// page_data["fee_estimate"]["sum_weight"]
)
if show_utxo_groups:
utxo_groups = ''
utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr()
sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True)
sorted_unspent_by_addr = sorted(
unspent_by_addr.items(), key=lambda x: x[1], reverse=True
)
for kv in sorted_unspent_by_addr:
utxo_groups += kv[0] + ' ' + ci.format_amount(kv[1]) + '\n'
utxo_groups += kv[0] + " " + ci.format_amount(kv[1]) + "\n"
wallet_data['show_utxo_groups'] = True
wallet_data['utxo_groups'] = utxo_groups
wallet_data["show_utxo_groups"] = True
wallet_data["utxo_groups"] = utxo_groups
checkAddressesOwned(swap_client, ci, wallet_data)
template = server.env.get_template('wallet.html')
return self.render_template(template, {
'messages': messages,
'err_messages': err_messages,
'w': wallet_data,
'summary': summary,
'block_unknown_seeds': swap_client._restrict_unknown_seed_wallets,
})
template = server.env.get_template("wallet.html")
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"w": wallet_data,
"summary": summary,
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
},
)

View File

@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-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 struct
import traceback
from basicswap.util import (
hex_or_none,
make_int,
format_timestamp,
)
@@ -19,6 +20,7 @@ from basicswap.basicswap_util import (
ActionTypes,
BidStates,
DebugTypes,
canAcceptBidState,
getLastBidState,
strBidState,
strTxState,
@@ -31,9 +33,23 @@ from basicswap.basicswap_util import (
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
PAGE_LIMIT = 25
PAGE_LIMIT = 1000
invalid_coins_from = []
known_chart_coins = ['BTC', 'PART', 'XMR', 'LTC', 'FIRO', 'DASH', 'PIVX', 'DOGE', 'ETH', 'DCR', 'ZANO', 'WOW']
known_chart_coins = [
"BTC",
"PART",
"XMR",
"LTC",
"FIRO",
"DASH",
"PIVX",
"DOGE",
"ETH",
"DCR",
"ZANO",
"WOW",
"BCH",
]
def tickerToCoinId(ticker):
@@ -41,7 +57,7 @@ def tickerToCoinId(ticker):
for c in Coins:
if c.name == search_str:
return c.value
raise ValueError('Unknown coin')
raise ValueError("Unknown coin")
def getCoinType(coin_type_ind):
@@ -55,9 +71,9 @@ def getCoinType(coin_type_ind):
def validateAmountString(amount, ci):
if not isinstance(amount, str):
return
ar = amount.split('.')
ar = amount.split(".")
if len(ar) > 1 and len(ar[1]) > ci.exp():
raise ValueError('Too many decimal places in amount {}'.format(amount))
raise ValueError("Too many decimal places in amount {}".format(amount))
def inputAmount(amount_str, ci):
@@ -66,24 +82,24 @@ def inputAmount(amount_str, ci):
def get_data_entry_or(post_data, name, default):
if 'is_json' in post_data:
if "is_json" in post_data:
return post_data.get(name, default)
key_bytes = name.encode('utf-8')
key_bytes = name.encode("utf-8")
if key_bytes in post_data:
return post_data[key_bytes][0].decode('utf-8')
return post_data[key_bytes][0].decode("utf-8")
return default
def get_data_entry(post_data, name):
if 'is_json' in post_data:
if "is_json" in post_data:
return post_data[name]
return post_data[name.encode('utf-8')][0].decode('utf-8')
return post_data[name.encode("utf-8")][0].decode("utf-8")
def have_data_entry(post_data, name):
if 'is_json' in post_data:
if "is_json" in post_data:
return name in post_data
return name.encode('utf-8') in post_data
return name.encode("utf-8") in post_data
def setCoinFilter(form_data, field_name):
@@ -96,18 +112,18 @@ def setCoinFilter(form_data, field_name):
try:
return Coins(coin_type)
except Exception:
raise ValueError('Unknown Coin Type {}'.format(str(field_name)))
raise ValueError("Unknown Coin Type {}".format(str(field_name)))
def set_pagination_filters(form_data, filters):
if form_data and have_data_entry(form_data, 'pageback'):
filters['page_no'] = int(form_data[b'pageno'][0]) - 1
if filters['page_no'] < 1:
filters['page_no'] = 1
elif form_data and have_data_entry(form_data, 'pageforwards'):
filters['page_no'] = int(form_data[b'pageno'][0]) + 1
if filters['page_no'] > 1:
filters['offset'] = (filters['page_no'] - 1) * PAGE_LIMIT
if form_data and have_data_entry(form_data, "pageback"):
filters["page_no"] = int(form_data[b"pageno"][0]) - 1
if filters["page_no"] < 1:
filters["page_no"] = 1
elif form_data and have_data_entry(form_data, "pageforwards"):
filters["page_no"] = int(form_data[b"pageno"][0]) + 1
if filters["page_no"] > 1:
filters["offset"] = (filters["page_no"] - 1) * PAGE_LIMIT
def getTxIdHex(bid, tx_type, suffix):
@@ -116,12 +132,12 @@ def getTxIdHex(bid, tx_type, suffix):
elif tx_type == TxTypes.PTX:
obj = bid.participate_tx
else:
return 'Unknown Type'
return "Unknown Type"
if not obj:
return 'None'
return "None"
if not obj.txid:
return 'None'
return "None"
return obj.txid.hex() + suffix
@@ -131,13 +147,13 @@ def getTxSpendHex(bid, tx_type):
elif tx_type == TxTypes.PTX:
obj = bid.participate_tx
else:
return 'Unknown Type'
return "Unknown Type"
if not obj:
return 'None'
return "None"
if not obj.spend_txid:
return 'None'
return obj.spend_txid.hex() + ' {}'.format(obj.spend_n)
return "None"
return obj.spend_txid.hex() + " {}".format(obj.spend_n)
def listBidStates():
@@ -154,11 +170,23 @@ def listBidActions():
return rv
def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_bid, show_txns, view_tx_ind=None, for_api=False, show_lock_transfers=False):
def describeBid(
swap_client,
bid,
xmr_swap,
offer,
xmr_offer,
bid_events,
edit_bid,
show_txns,
view_tx_ind=None,
for_api=False,
show_lock_transfers=False,
):
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from)
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to
@@ -166,238 +194,404 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
bid_amount_to: int = bid.amount_to
bid_rate: int = offer.rate if bid.rate is None else bid.rate
initiator_role: str = 'offerer' # Leader
participant_role: str = 'bidder' # Follower
initiator_role: str = "offerer" # Leader
participant_role: str = "bidder" # Follower
if reverse_bid:
bid_amount = bid.amount_to
bid_amount_to = bid.amount
bid_rate = ci_from.make_int(bid.amount / bid.amount_to, r=1)
initiator_role = 'bidder'
participant_role = 'offerer'
initiator_role = "bidder"
participant_role = "offerer"
state_description = ''
state_description = ""
if offer.swap_type == SwapTypes.SELLER_FIRST:
if bid.state == BidStates.BID_SENT:
state_description = 'Waiting for seller to accept.'
state_description = "Waiting for seller to accept."
elif bid.state == BidStates.BID_RECEIVED:
state_description = 'Waiting for seller to accept.'
state_description = "Waiting for seller to accept."
elif bid.state == BidStates.BID_ACCEPTED:
if not bid.initiate_tx:
state_description = 'Waiting for seller to send initiate tx.'
state_description = "Waiting for seller to send initiate tx."
else:
state_description = 'Waiting for initiate tx to confirm.'
state_description = "Waiting for initiate tx to confirm."
elif bid.state == BidStates.SWAP_INITIATED:
state_description = 'Waiting for participate txn to be confirmed in {} chain'.format(ci_follower.ticker())
state_description = (
"Waiting for participate txn to be confirmed in {} chain".format(
ci_follower.ticker()
)
)
elif bid.state == BidStates.SWAP_PARTICIPATING:
if bid.was_sent:
state_description = 'Waiting for participate txn to be spent in {} chain'.format(ci_follower.ticker())
state_description = (
"Waiting for participate txn to be spent in {} chain".format(
ci_follower.ticker()
)
)
else:
state_description = 'Waiting for initiate txn to be spent in {} chain'.format(ci_leader.ticker())
state_description = (
"Waiting for initiate txn to be spent in {} chain".format(
ci_leader.ticker()
)
)
elif bid.state == BidStates.SWAP_COMPLETED:
state_description = 'Swap completed'
if bid.getITxState() == TxStates.TX_REDEEMED and bid.getPTxState() == TxStates.TX_REDEEMED:
state_description += ' successfully'
state_description = "Swap completed"
if (
bid.getITxState() == TxStates.TX_REDEEMED
and bid.getPTxState() == TxStates.TX_REDEEMED
):
state_description += " successfully"
else:
state_description += ', ITX ' + strTxState(bid.getITxState()) + ', PTX ' + strTxState(bid.getPTxState())
state_description += (
", ITX "
+ strTxState(bid.getITxState())
+ ", PTX "
+ strTxState(bid.getPTxState())
)
elif bid.state == BidStates.SWAP_TIMEDOUT:
state_description = 'Timed out waiting for initiate txn'
state_description = "Timed out waiting for initiate txn"
elif bid.state == BidStates.BID_ABANDONED:
state_description = 'Bid abandoned'
state_description = "Bid abandoned"
elif bid.state == BidStates.BID_ERROR:
state_description = bid.state_note
elif offer.swap_type == SwapTypes.XMR_SWAP:
if bid.state == BidStates.BID_SENT:
state_description = 'Waiting for offerer to accept'
state_description = "Waiting for offerer to accept"
if bid.state == BidStates.BID_RECEIVING:
# Offerer receiving bid from bidder
state_description = 'Waiting for bid to be fully received'
elif bid.state == BidStates.BID_RECEIVED:
state_description = "Waiting for bid to be fully received"
elif canAcceptBidState(bid.state):
# Offerer received bid from bidder
# TODO: Manual vs automatic
state_description = 'Bid must be accepted'
state_description = "Bid must be accepted"
elif bid.state == BidStates.BID_RECEIVING_ACC:
state_description = 'Receiving accepted bid message'
state_description = "Receiving accepted bid message"
elif bid.state == BidStates.BID_ACCEPTED:
state_description = 'Offerer has accepted bid, waiting for bidder to respond'
state_description = (
"Offerer has accepted bid, waiting for bidder to respond"
)
elif bid.state == BidStates.SWAP_DELAYING:
last_state = getLastBidState(bid.states)
if last_state == BidStates.BID_RECEIVED:
state_description = 'Delaying before accepting bid'
if canAcceptBidState(last_state):
state_description = "Delaying before accepting bid"
elif last_state == BidStates.BID_RECEIVING_ACC:
state_description = 'Delaying before responding to accepted bid'
state_description = "Delaying before responding to accepted bid"
elif last_state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
state_description = f'Delaying before spending from {ci_follower.ticker()} lock tx'
state_description = (
f"Delaying before spending from {ci_follower.ticker()} lock tx"
)
elif last_state == BidStates.BID_ACCEPTED:
state_description = f'Delaying before sending {ci_leader.ticker()} lock tx'
state_description = (
f"Delaying before sending {ci_leader.ticker()} lock tx"
)
else:
state_description = 'Delaying before automated action'
state_description = "Delaying before automated action"
elif bid.state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
state_description = f'Waiting for {ci_leader.ticker()} lock tx to confirm in chain ({ci_leader.blocks_confirmed} blocks)'
state_description = f"Waiting for {ci_leader.ticker()} lock tx to confirm in chain ({ci_leader.blocks_confirmed} blocks)"
elif bid.state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
if xmr_swap.b_lock_tx_id is None:
state_description = f'Waiting for {ci_follower.ticker()} lock tx'
state_description = f"Waiting for {ci_follower.ticker()} lock tx"
else:
state_description = f'Waiting for {ci_follower.ticker()} lock tx to confirm in chain ({ci_follower.blocks_confirmed} blocks)'
state_description = f"Waiting for {ci_follower.ticker()} lock tx to confirm in chain ({ci_follower.blocks_confirmed} blocks)"
elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
state_description = f'Waiting for {initiator_role} to unlock {ci_leader.ticker()} lock tx'
state_description = (
f"Waiting for {initiator_role} to unlock {ci_leader.ticker()} lock tx"
)
elif bid.state == BidStates.XMR_SWAP_LOCK_RELEASED:
state_description = f'Waiting for {participant_role} to spend from {ci_leader.ticker()} lock tx'
state_description = f"Waiting for {participant_role} to spend from {ci_leader.ticker()} lock tx"
elif bid.state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
state_description = f'Waiting for {initiator_role} to spend from {ci_follower.ticker()} lock tx'
state_description = f"Waiting for {initiator_role} to spend from {ci_follower.ticker()} lock tx"
elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
state_description = f'Waiting for {ci_follower.ticker()} lock tx spend tx to confirm in chain'
state_description = f"Waiting for {ci_follower.ticker()} lock tx spend tx to confirm in chain"
elif bid.state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
if bid.was_sent:
state_description = f'Waiting for {initiator_role} to redeem or locktime to expire'
state_description = (
f"Waiting for {initiator_role} to redeem or locktime to expire"
)
else:
state_description = 'Redeeming output'
state_description = "Redeeming output"
addr_label = swap_client.getAddressLabel([bid.bid_addr, ])[0]
addr_label = swap_client.getAddressLabel(
[
bid.bid_addr,
]
)[0]
can_abandon: bool = False
if swap_client.debug and bid.state not in (BidStates.BID_ABANDONED, BidStates.SWAP_COMPLETED):
if swap_client.debug and bid.state not in (
BidStates.BID_ABANDONED,
BidStates.SWAP_COMPLETED,
):
can_abandon = True
data = {
'coin_from': ci_from.coin_name(),
'coin_to': ci_to.coin_name(),
'amt_from': ci_from.format_amount(bid_amount),
'amt_to': ci_to.format_amount(bid_amount_to),
'bid_rate': ci_to.format_amount(bid_rate),
'ticker_from': ci_from.ticker(),
'ticker_to': ci_to.ticker(),
'bid_state': strBidState(bid.state),
'state_description': state_description,
'itx_state': strTxState(bid.getITxState()),
'ptx_state': strTxState(bid.getPTxState()),
'offer_id': bid.offer_id.hex(),
'addr_from': bid.bid_addr,
'addr_from_label': addr_label,
'addr_fund_proof': bid.proof_address,
'created_at': bid.created_at if for_api else format_timestamp(bid.created_at, with_seconds=True),
'expired_at': bid.expire_at if for_api else format_timestamp(bid.expire_at, with_seconds=True),
'was_sent': 'True' if bid.was_sent else 'False',
'was_received': 'True' if bid.was_received else 'False',
'initiate_tx': getTxIdHex(bid, TxTypes.ITX, ' ' + ci_leader.ticker()),
'initiate_conf': 'None' if (not bid.initiate_tx or not bid.initiate_tx.conf) else bid.initiate_tx.conf,
'participate_tx': getTxIdHex(bid, TxTypes.PTX, ' ' + ci_follower.ticker()),
'participate_conf': 'None' if (not bid.participate_tx or not bid.participate_tx.conf) else bid.participate_tx.conf,
'show_txns': show_txns,
'can_abandon': can_abandon,
'events': bid_events,
'debug_ui': swap_client.debug_ui,
'reverse_bid': reverse_bid,
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name(),
"amt_from": ci_from.format_amount(bid_amount),
"amt_to": ci_to.format_amount(bid_amount_to),
"bid_rate": ci_to.format_amount(bid_rate),
"ticker_from": ci_from.ticker(),
"ticker_to": ci_to.ticker(),
"bid_state": strBidState(bid.state),
"state_description": state_description,
"itx_state": strTxState(bid.getITxState()),
"ptx_state": strTxState(bid.getPTxState()),
"offer_id": bid.offer_id.hex(),
"addr_from": bid.bid_addr,
"addr_from_label": addr_label,
"addr_fund_proof": bid.proof_address,
"created_at": (
bid.created_at
if for_api
else format_timestamp(bid.created_at, with_seconds=True)
),
"expired_at": (
bid.expire_at
if for_api
else format_timestamp(bid.expire_at, with_seconds=True)
),
"was_sent": "True" if bid.was_sent else "False",
"was_received": "True" if bid.was_received else "False",
"initiate_tx": getTxIdHex(bid, TxTypes.ITX, " " + ci_leader.ticker()),
"initiate_conf": (
"None"
if (not bid.initiate_tx or not bid.initiate_tx.conf)
else bid.initiate_tx.conf
),
"participate_tx": getTxIdHex(bid, TxTypes.PTX, " " + ci_follower.ticker()),
"participate_conf": (
"None"
if (not bid.participate_tx or not bid.participate_tx.conf)
else bid.participate_tx.conf
),
"show_txns": show_txns,
"can_abandon": can_abandon,
"events": bid_events,
"debug_ui": swap_client.debug_ui,
"reverse_bid": reverse_bid,
}
if edit_bid:
data['bid_state_ind'] = int(bid.state)
data['bid_states'] = listBidStates()
data["bid_state_ind"] = int(bid.state)
data["bid_states"] = listBidStates()
if swap_client.debug_ui:
data['debug_ind'] = bid.debug_ind
data['debug_options'] = [(int(t), t.name) for t in DebugTypes]
data["debug_ind"] = bid.debug_ind
data["debug_options"] = [(int(t), t.name) for t in DebugTypes]
if show_txns:
if offer.swap_type == SwapTypes.XMR_SWAP:
txns = []
if bid.xmr_a_lock_tx:
confirms = None
if swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] and bid.xmr_a_lock_tx.chain_height:
confirms = (swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] - bid.xmr_a_lock_tx.chain_height) + 1
txns.append({'type': 'Chain A Lock', 'txid': bid.xmr_a_lock_tx.txid.hex(), 'confirms': confirms})
if (
swap_client.coin_clients[ci_leader.coin_type()]["chain_height"]
and bid.xmr_a_lock_tx.chain_height
):
confirms = (
swap_client.coin_clients[ci_leader.coin_type()]["chain_height"]
- bid.xmr_a_lock_tx.chain_height
) + 1
txns.append(
{
"type": "Chain A Lock",
"txid": hex_or_none(bid.xmr_a_lock_tx.txid),
"confirms": confirms,
}
)
if bid.xmr_a_lock_spend_tx:
txns.append({'type': 'Chain A Lock Spend', 'txid': bid.xmr_a_lock_spend_tx.txid.hex()})
txns.append(
{
"type": "Chain A Lock Spend",
"txid": bid.xmr_a_lock_spend_tx.txid.hex(),
}
)
if bid.xmr_b_lock_tx:
confirms = None
if swap_client.coin_clients[ci_follower.coin_type()]['chain_height'] and bid.xmr_b_lock_tx.chain_height:
confirms = (swap_client.coin_clients[ci_follower.coin_type()]['chain_height'] - bid.xmr_b_lock_tx.chain_height) + 1
txns.append({'type': 'Chain B Lock', 'txid': bid.xmr_b_lock_tx.txid.hex(), 'confirms': confirms})
if (
swap_client.coin_clients[ci_follower.coin_type()]["chain_height"]
and bid.xmr_b_lock_tx.chain_height
):
confirms = (
swap_client.coin_clients[ci_follower.coin_type()][
"chain_height"
]
- bid.xmr_b_lock_tx.chain_height
) + 1
txns.append(
{
"type": "Chain B Lock",
"txid": bid.xmr_b_lock_tx.txid.hex(),
"confirms": confirms,
}
)
if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.spend_txid:
txns.append({'type': 'Chain B Lock Spend', 'txid': bid.xmr_b_lock_tx.spend_txid.hex()})
txns.append(
{
"type": "Chain B Lock Spend",
"txid": bid.xmr_b_lock_tx.spend_txid.hex(),
}
)
if xmr_swap.a_lock_refund_tx:
txns.append({'type': strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND), 'txid': xmr_swap.a_lock_refund_tx_id.hex()})
txns.append(
{
"type": strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND),
"txid": xmr_swap.a_lock_refund_tx_id.hex(),
}
)
if xmr_swap.a_lock_refund_spend_tx:
txns.append({'type': strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND), 'txid': xmr_swap.a_lock_refund_spend_tx_id.hex()})
txns.append(
{
"type": strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND),
"txid": xmr_swap.a_lock_refund_spend_tx_id.hex(),
}
)
for tx_type, tx in bid.txns.items():
if tx_type in (TxTypes.XMR_SWAP_A_LOCK_REFUND, TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND):
if tx_type in (
TxTypes.XMR_SWAP_A_LOCK_REFUND,
TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND,
):
continue
txns.append({'type': strTxType(tx_type), 'txid': tx.txid.hex()})
data['txns'] = txns
txns.append({"type": strTxType(tx_type), "txid": tx.txid.hex()})
data["txns"] = txns
data['xmr_b_shared_address'] = ci_to.encodeSharedAddress(xmr_swap.pkbv, xmr_swap.pkbs) if xmr_swap.pkbs else None
data['xmr_b_shared_viewkey'] = ci_to.encodeKey(xmr_swap.vkbv) if xmr_swap.vkbv else None
data["xmr_b_shared_address"] = (
ci_to.encodeSharedAddress(xmr_swap.pkbv, xmr_swap.pkbs)
if xmr_swap.pkbs
else None
)
data["xmr_b_shared_viewkey"] = (
ci_to.encodeKey(xmr_swap.vkbv) if xmr_swap.vkbv else None
)
if swap_client.debug_ui:
try:
data['xmr_b_half_privatekey'] = getChainBSplitKey(swap_client, bid, xmr_swap, offer)
remote_split_key = getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer)
data["xmr_b_half_privatekey"] = getChainBSplitKey(
swap_client, bid, xmr_swap, offer
)
except Exception as e: # noqa: F841
swap_client.log.debug(
"Unable to get xmr_b_half_privatekey for bid: {}".format(
bid.bid_id.hex()
)
)
try:
remote_split_key = getChainBRemoteSplitKey(
swap_client, bid, xmr_swap, offer
)
if remote_split_key:
data['xmr_b_half_privatekey_remote'] = remote_split_key
except Exception as e:
swap_client.log.error(traceback.format_exc())
data["xmr_b_half_privatekey_remote"] = remote_split_key
except Exception as e: # noqa: F841
swap_client.log.debug(
"Unable to get xmr_b_half_privatekey_remote for bid: {}".format(
bid.bid_id.hex()
)
)
if show_lock_transfers:
if xmr_swap.pkbs:
data['lock_transfers'] = json.dumps(ci_to.showLockTransfers(xmr_swap.vkbv, xmr_swap.pkbs, bid.chain_b_height_start), indent=4)
data["lock_transfers"] = json.dumps(
ci_to.showLockTransfers(
xmr_swap.vkbv, xmr_swap.pkbs, bid.chain_b_height_start
),
indent=4,
)
else:
data['lock_transfers'] = 'Shared address not yet known.'
data["lock_transfers"] = "Shared address not yet known."
else:
data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex()
data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex()
data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX)
data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX)
data["initiate_tx_refund"] = (
"None" if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex()
)
data["participate_tx_refund"] = (
"None"
if not bid.participate_txn_refund
else bid.participate_txn_refund.hex()
)
data["initiate_tx_spend"] = getTxSpendHex(bid, TxTypes.ITX)
data["participate_tx_spend"] = getTxSpendHex(bid, TxTypes.PTX)
if bid.initiate_tx and bid.initiate_tx.tx_data is not None:
data['initiate_tx_inputs'] = ci_from.listInputs(bid.initiate_tx.tx_data)
data["initiate_tx_inputs"] = ci_from.listInputs(bid.initiate_tx.tx_data)
if bid.participate_tx and bid.participate_tx.tx_data is not None:
data['initiate_tx_inputs'] = ci_from.listInputs(bid.participate_tx.tx_data)
data["initiate_tx_inputs"] = ci_from.listInputs(
bid.participate_tx.tx_data
)
if offer.swap_type == SwapTypes.XMR_SWAP:
data['coin_a_lock_refund_tx_est_final'] = 'None'
data['coin_a_lock_refund_swipe_tx_est_final'] = 'None'
data["coin_a_lock_refund_tx_est_final"] = "None"
data["coin_a_lock_refund_swipe_tx_est_final"] = "None"
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.block_time:
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
raw_sequence = ci_leader.getExpectedSequence(
offer.lock_type, offer.lock_value
)
seconds_locked = ci_leader.decodeSequence(raw_sequence)
data['coin_a_lock_refund_tx_est_final'] = bid.xmr_a_lock_tx.block_time + seconds_locked
data['coin_a_last_median_time'] = swap_client.coin_clients[offer.coin_from]['chain_median_time']
data["coin_a_lock_refund_tx_est_final"] = (
bid.xmr_a_lock_tx.block_time + seconds_locked
)
data["coin_a_last_median_time"] = swap_client.coin_clients[
offer.coin_from
]["chain_median_time"]
if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns:
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
if refund_tx.block_time is not None:
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
raw_sequence = ci_leader.getExpectedSequence(
offer.lock_type, offer.lock_value
)
seconds_locked = ci_leader.decodeSequence(raw_sequence)
data['coin_a_lock_refund_swipe_tx_est_final'] = refund_tx.block_time + seconds_locked
data["coin_a_lock_refund_swipe_tx_est_final"] = (
refund_tx.block_time + seconds_locked
)
if view_tx_ind:
data['view_tx_ind'] = view_tx_ind
data["view_tx_ind"] = view_tx_ind
view_tx_id = bytes.fromhex(view_tx_ind)
if xmr_swap:
if view_tx_id == xmr_swap.a_lock_tx_id and xmr_swap.a_lock_tx:
data['view_tx_hex'] = xmr_swap.a_lock_tx.hex()
data['chain_a_lock_tx_inputs'] = ci_leader.listInputs(xmr_swap.a_lock_tx)
if view_tx_id == xmr_swap.a_lock_refund_tx_id and xmr_swap.a_lock_refund_tx:
data['view_tx_hex'] = xmr_swap.a_lock_refund_tx.hex()
if view_tx_id == xmr_swap.a_lock_refund_spend_tx_id and xmr_swap.a_lock_refund_spend_tx:
data['view_tx_hex'] = xmr_swap.a_lock_refund_spend_tx.hex()
if view_tx_id == xmr_swap.a_lock_spend_tx_id and xmr_swap.a_lock_spend_tx:
data['view_tx_hex'] = xmr_swap.a_lock_spend_tx.hex()
data["view_tx_hex"] = xmr_swap.a_lock_tx.hex()
data["chain_a_lock_tx_inputs"] = ci_leader.listInputs(
xmr_swap.a_lock_tx
)
if (
view_tx_id == xmr_swap.a_lock_refund_tx_id
and xmr_swap.a_lock_refund_tx
):
data["view_tx_hex"] = xmr_swap.a_lock_refund_tx.hex()
if (
view_tx_id == xmr_swap.a_lock_refund_spend_tx_id
and xmr_swap.a_lock_refund_spend_tx
):
data["view_tx_hex"] = xmr_swap.a_lock_refund_spend_tx.hex()
if (
view_tx_id == xmr_swap.a_lock_spend_tx_id
and xmr_swap.a_lock_spend_tx
):
data["view_tx_hex"] = xmr_swap.a_lock_spend_tx.hex()
if 'view_tx_hex' in data:
data['view_tx_desc'] = json.dumps(ci_leader.describeTx(data['view_tx_hex']), indent=4)
if "view_tx_hex" in data:
data["view_tx_desc"] = json.dumps(
ci_leader.describeTx(data["view_tx_hex"]), indent=4
)
else:
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
if bid.initiate_tx and bid.initiate_tx.block_time is not None:
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
raw_sequence = ci_leader.getExpectedSequence(
offer.lock_type, offer.lock_value
)
seconds_locked = ci_leader.decodeSequence(raw_sequence)
data['itx_refund_tx_est_final'] = bid.initiate_tx.block_time + seconds_locked
data["itx_refund_tx_est_final"] = (
bid.initiate_tx.block_time + seconds_locked
)
if bid.participate_tx and bid.participate_tx.block_time is not None:
raw_sequence = ci_follower.getExpectedSequence(offer.lock_type, offer.lock_value // 2)
raw_sequence = ci_follower.getExpectedSequence(
offer.lock_type, offer.lock_value // 2
)
seconds_locked = ci_follower.decodeSequence(raw_sequence)
data['ptx_refund_tx_est_final'] = bid.participate_tx.block_time + seconds_locked
data["ptx_refund_tx_est_final"] = (
bid.participate_tx.block_time + seconds_locked
)
return data
@@ -406,20 +600,24 @@ def listOldBidStates(bid):
old_states = []
num_states = len(bid.states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.states[i * 12:(i + 1) * 12])
old_states.append((up[1], 'Bid ' + strBidState(up[0])))
up = struct.unpack_from("<iq", bid.states[i * 12 : (i + 1) * 12])
old_states.append((up[1], "Bid " + strBidState(up[0])))
if bid.initiate_tx and bid.initiate_tx.states is not None:
num_states = len(bid.initiate_tx.states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.initiate_tx.states[i * 12:(i + 1) * 12])
up = struct.unpack_from(
"<iq", bid.initiate_tx.states[i * 12 : (i + 1) * 12]
)
if up[0] != TxStates.TX_NONE:
old_states.append((up[1], 'ITX ' + strTxState(up[0])))
old_states.append((up[1], "ITX " + strTxState(up[0])))
if bid.participate_tx and bid.participate_tx.states is not None:
num_states = len(bid.participate_tx.states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.participate_tx.states[i * 12:(i + 1) * 12])
up = struct.unpack_from(
"<iq", bid.participate_tx.states[i * 12 : (i + 1) * 12]
)
if up[0] != TxStates.TX_NONE:
old_states.append((up[1], 'PTX ' + strTxState(up[0])))
old_states.append((up[1], "PTX " + strTxState(up[0])))
if len(old_states) > 0:
old_states.sort(key=lambda x: x[0])
return old_states
@@ -427,16 +625,16 @@ def listOldBidStates(bid):
def getCoinName(c):
if c == Coins.PART_ANON:
return chainparams[Coins.PART]['name'].capitalize() + ' Anon'
return chainparams[Coins.PART]["name"].capitalize() + " Anon"
if c == Coins.PART_BLIND:
return chainparams[Coins.PART]['name'].capitalize() + ' Blind'
return chainparams[Coins.PART]["name"].capitalize() + " Blind"
if c == Coins.LTC_MWEB:
return chainparams[Coins.LTC]['name'].capitalize() + ' MWEB'
return chainparams[Coins.LTC]["name"].capitalize() + " MWEB"
coin_chainparams = chainparams[c]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
return coin_chainparams['name'].capitalize()
if "display_name" in coin_chainparams:
return coin_chainparams["display_name"]
return coin_chainparams["name"].capitalize()
def listAvailableCoins(swap_client, with_variants=True, split_from=False):
@@ -445,7 +643,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v['connection_type'] == 'rpc':
if v["connection_type"] == "rpc":
coins.append((int(k), getCoinName(k)))
if split_from and k not in invalid_coins_from:
coins_from.append(coins[-1])
@@ -455,7 +653,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
if split_from and v not in invalid_coins_from:
coins_from.append(coins[-1])
if with_variants and k == Coins.LTC:
for v in (Coins.LTC_MWEB, ):
for v in (Coins.LTC_MWEB,):
pass # Add when swappable
if split_from:
return coins_from, coins
@@ -463,29 +661,37 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
def checkAddressesOwned(swap_client, ci, wallet_info):
if 'stealth_address' in wallet_info:
if "stealth_address" in wallet_info:
if wallet_info['stealth_address'] != '?':
if not ci.isAddressMine(wallet_info['stealth_address']):
ci._log.error('Unowned stealth address: {}'.format(wallet_info['stealth_address']))
wallet_info['stealth_address'] = 'Error: unowned address'
elif swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
wallet_info['stealth_address'] = 'WARNING: Unknown wallet seed'
if wallet_info["stealth_address"] != "?":
if not ci.isAddressMine(wallet_info["stealth_address"]):
ci._log.error(
"Unowned stealth address: {}".format(wallet_info["stealth_address"])
)
wallet_info["stealth_address"] = "Error: unowned address"
elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
):
wallet_info["stealth_address"] = "WARNING: Unknown wallet seed"
if 'deposit_address' in wallet_info:
if wallet_info['deposit_address'] != 'Refresh necessary':
if not ci.isAddressMine(wallet_info['deposit_address']):
ci._log.error('Unowned deposit address: {}'.format(wallet_info['deposit_address']))
wallet_info['deposit_address'] = 'Error: unowned address'
elif swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
wallet_info['deposit_address'] = 'WARNING: Unknown wallet seed'
if "deposit_address" in wallet_info:
if wallet_info["deposit_address"] != "Refresh necessary":
if not ci.isAddressMine(wallet_info["deposit_address"]):
ci._log.error(
"Unowned deposit address: {}".format(wallet_info["deposit_address"])
)
wallet_info["deposit_address"] = "Error: unowned address"
elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
):
wallet_info["deposit_address"] = "WARNING: Unknown wallet seed"
def validateTextInput(text, name, messages, max_length=None):
if max_length is not None and len(text) > max_length:
messages.append(f'Error: {name} is too long')
messages.append(f"Error: {name} is too long")
return False
if len(text) > 0 and all(c.isalnum() or c.isspace() for c in text) is False:
messages.append(f'Error: {name} must consist of only letters and digits')
messages.append(f"Error: {name} must consist of only letters and digits")
return False
return True

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-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.
@@ -25,6 +26,10 @@ class AutomationConstraint(ValueError):
pass
class AutomationConstraintTemporary(ValueError):
pass
class InactiveCoin(Exception):
def __init__(self, coinid):
self.coinid = coinid
@@ -38,7 +43,7 @@ class LockedCoinError(Exception):
self.coinid = coinid
def __str__(self):
return 'Coin must be unlocked: ' + str(self.coinid)
return "Coin must be unlocked: " + str(self.coinid)
def ensure(v, err_string):
@@ -49,7 +54,7 @@ def ensure(v, err_string):
def toBool(s) -> bool:
if isinstance(s, bool):
return s
return s.lower() in ['1', 'true']
return s.lower() in ["1", "true"]
def jsonDecimal(obj):
@@ -75,8 +80,8 @@ def SerialiseNum(n: int) -> bytes:
rv = bytearray()
neg = n < 0
absvalue = -n if neg else n
while (absvalue):
rv.append(absvalue & 0xff)
while absvalue:
rv.append(absvalue & 0xFF)
absvalue >>= 8
if rv[-1] & 0x80:
rv.append(0x80 if neg else 0)
@@ -105,34 +110,36 @@ def DeserialiseNum(b: bytes, o: int = 0) -> int:
def float_to_str(f: float) -> str:
# stackoverflow.com/questions/38847690
d1 = decimal_ctx.create_decimal(repr(f))
return format(d1, 'f')
return format(d1, "f")
def make_int(v, scale: int = 8, r: int = 0) -> int: # r = 0, no rounding (fail), r > 0 round off, r < 0 floor
def make_int(
v, scale: int = 8, r: int = 0
) -> int: # r = 0, no rounding (fail), r > 0 round off, r < 0 floor
if isinstance(v, float):
v = float_to_str(v)
elif isinstance(v, int):
return v * 10 ** scale
return v * 10**scale
sign = 1
if v[0] == '-':
if v[0] == "-":
v = v[1:]
sign = -1
ep = 10 ** scale
ep = 10**scale
have_dp = False
rv = 0
for c in v:
if c == '.':
if c == ".":
rv *= ep
have_dp = True
continue
if not c.isdigit():
raise ValueError('Invalid char: ' + c)
raise ValueError("Invalid char: " + c)
if have_dp:
ep //= 10
if ep <= 0:
if r == 0:
raise ValueError('Mantissa too long')
raise ValueError("Mantissa too long")
if r > 0:
# Round off
if int(c) > 4:
@@ -150,51 +157,53 @@ def validate_amount(amount, scale: int = 8) -> bool:
str_amount = float_to_str(amount) if isinstance(amount, float) else str(amount)
has_decimal = False
for c in str_amount:
if c == '.' and not has_decimal:
if c == "." and not has_decimal:
has_decimal = True
continue
if not c.isdigit():
raise ValueError('Invalid amount')
raise ValueError("Invalid amount")
ar = str_amount.split('.')
ar = str_amount.split(".")
if len(ar) > 1 and len(ar[1]) > scale:
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
raise ValueError("Too many decimal places in amount {}".format(str_amount))
return True
def format_amount(i: int, display_scale: int, scale: int = None) -> str:
if not isinstance(i, int):
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
raise ValueError(
"Amount must be an integer."
) # Raise error instead of converting as amounts should always be integers
if scale is None:
scale = display_scale
ep = 10 ** scale
ep = 10**scale
n = abs(i)
quotient = n // ep
remainder = n % ep
if display_scale != scale:
remainder %= (10 ** display_scale)
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
remainder %= 10**display_scale
rv = "{}.{:0>{scale}}".format(quotient, remainder, scale=display_scale)
if i < 0:
rv = '-' + rv
rv = "-" + rv
return rv
def format_timestamp(value: int, with_seconds: bool = False) -> str:
str_format = '%Y-%m-%d %H:%M'
str_format = "%Y-%m-%d %H:%M"
if with_seconds:
str_format += ':%S'
str_format += ' %z'
str_format += ":%S"
str_format += " %z"
return time.strftime(str_format, time.localtime(value))
def b2i(b: bytes) -> int:
# bytes32ToInt
return int.from_bytes(b, byteorder='big')
return int.from_bytes(b, byteorder="big")
def i2b(i: int) -> bytes:
# intToBytes32
return i.to_bytes(32, byteorder='big')
return i.to_bytes(32, byteorder="big")
def b2h(b: bytes) -> str:
@@ -202,7 +211,7 @@ def b2h(b: bytes) -> str:
def h2b(h: str) -> bytes:
if h.startswith('0x'):
if h.startswith("0x"):
h = h[2:]
return bytes.fromhex(h)
@@ -215,3 +224,9 @@ def zeroIfNone(value) -> int:
if value is None:
return 0
return value
def hex_or_none(value: bytes) -> str:
if value is None:
return "None"
return value.hex()

View File

@@ -7,12 +7,12 @@
from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
from basicswap.util.crypto import ripemd160, sha256
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
__b58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def b58decode(v, length=None):
long_value = 0
for (i, c) in enumerate(v[::-1]):
for i, c in enumerate(v[::-1]):
ofs = __b58chars.find(c)
if ofs < 0:
return None
@@ -38,10 +38,10 @@ def b58decode(v, length=None):
def b58encode(v):
long_value = 0
for (i, c) in enumerate(v[::-1]):
for i, c in enumerate(v[::-1]):
long_value += (256**i) * c
result = ''
result = ""
while long_value >= 58:
div, mod = divmod(long_value, 58)
result = __b58chars[mod] + result
@@ -58,7 +58,9 @@ def b58encode(v):
return (__b58chars[0] * nPad) + result
def encodeStealthAddress(prefix_byte: int, scan_pubkey: bytes, spend_pubkey: bytes) -> str:
def encodeStealthAddress(
prefix_byte: int, scan_pubkey: bytes, spend_pubkey: bytes
) -> str:
data = bytes((0x00,))
data += scan_pubkey
data += bytes((0x01,))
@@ -114,7 +116,7 @@ def decodeAddress(address: str) -> bytes:
prefixed_data = addr_data[:-4]
checksum = addr_data[-4:]
if sha256(sha256(prefixed_data))[:4] != checksum:
raise ValueError('Checksum mismatch')
raise ValueError("Checksum mismatch")
return prefixed_data

View File

@@ -9,7 +9,7 @@ from basicswap.contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_sym
from . import i2b
class ECCParameters():
class ECCParameters:
def __init__(self, p, a, b, Gx, Gy, o):
self.p = p
self.a = a
@@ -20,12 +20,13 @@ class ECCParameters():
ep = ECCParameters(
p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,
p=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
a=0x0,
b=0x7,
Gx=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
Gy=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8,
o=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141)
Gx=0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
Gy=0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
o=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,
)
curve_secp256k1 = CurveFp(ep.p, ep.a, ep.b)
@@ -34,7 +35,11 @@ SECP256K1_ORDER_HALF = ep.o // 2
def ToDER(P) -> bytes:
return bytes((4, )) + int(P.x()).to_bytes(32, byteorder='big') + int(P.y()).to_bytes(32, byteorder='big')
return (
bytes((4,))
+ int(P.x()).to_bytes(32, byteorder="big")
+ int(P.y()).to_bytes(32, byteorder="big")
)
def getSecretBytes() -> bytes:
@@ -50,7 +55,7 @@ def getInsecureBytes() -> bytes:
while True:
s = os.urandom(32)
s_test = int.from_bytes(s, byteorder='big')
s_test = int.from_bytes(s, byteorder="big")
if s_test > 1 and s_test < ep.o:
return s
@@ -59,7 +64,7 @@ def getInsecureInt() -> int:
while True:
s = os.urandom(32)
s_test = int.from_bytes(s, byteorder='big')
s_test = int.from_bytes(s, byteorder="big")
if s_test > 1 and s_test < ep.o:
return s_test
@@ -77,7 +82,7 @@ def powMod(x, y, z) -> int:
def ExpandPoint(xb, sign):
x = int.from_bytes(xb, byteorder='big')
x = int.from_bytes(xb, byteorder="big")
a = (powMod(x, 3, ep.p) + 7) % ep.p
y = powMod(a, (ep.p + 1) // 4, ep.p)
@@ -89,7 +94,7 @@ def ExpandPoint(xb, sign):
def CPKToPoint(cpk):
y_parity = cpk[0] - 2
x = int.from_bytes(cpk[1:], byteorder='big')
x = int.from_bytes(cpk[1:], byteorder="big")
a = (powMod(x, 3, ep.p) + 7) % ep.p
y = powMod(a, (ep.p + 1) // 4, ep.p)
@@ -102,28 +107,29 @@ def CPKToPoint(cpk):
def pointToCPK2(point, ind=0x09):
# The function is_square(x), where x is an integer, returns whether or not x is a quadratic residue modulo p. Since p is prime, it is equivalent to the Legendre symbol (x / p) = x(p-1)/2 mod p being equal to 1[8].
ind = bytes((ind ^ (1 if jacobi_symbol(point.y(), ep.p) == 1 else 0),))
return ind + point.x().to_bytes(32, byteorder='big')
return ind + point.x().to_bytes(32, byteorder="big")
def pointToCPK(point):
y = point.y().to_bytes(32, byteorder='big')
y = point.y().to_bytes(32, byteorder="big")
ind = bytes((0x03,)) if y[31] % 2 else bytes((0x02,))
cpk = ind + point.x().to_bytes(32, byteorder='big')
cpk = ind + point.x().to_bytes(32, byteorder="big")
return cpk
def secretToCPK(secret):
secretInt = secret if isinstance(secret, int) \
else int.from_bytes(secret, byteorder='big')
secretInt = (
secret if isinstance(secret, int) else int.from_bytes(secret, byteorder="big")
)
R = G * secretInt
Y = R.y().to_bytes(32, byteorder='big')
Y = R.y().to_bytes(32, byteorder="big")
ind = bytes((0x03,)) if Y[31] % 2 else bytes((0x02,))
pubkey = ind + R.x().to_bytes(32, byteorder='big')
pubkey = ind + R.x().to_bytes(32, byteorder="big")
return pubkey
@@ -136,7 +142,7 @@ def getKeypair():
def hashToCurve(pubkey):
xBytes = hashlib.sha256(pubkey).digest()
x = int.from_bytes(xBytes, byteorder='big')
x = int.from_bytes(xBytes, byteorder="big")
for k in range(0, 100):
# get matching y element for point
@@ -155,12 +161,14 @@ def hashToCurve(pubkey):
x = (x + 1) % ep.p # % P?
continue
if R == INFINITY or R * ep.o != INFINITY: # is R * O != INFINITY check necessary? Validation of Elliptic Curve Public Keys says no if cofactor = 1
if (
R == INFINITY or R * ep.o != INFINITY
): # is R * O != INFINITY check necessary? Validation of Elliptic Curve Public Keys says no if cofactor = 1
x = (x + 1) % ep.p # % P?
continue
return R
raise ValueError('hashToCurve failed for 100 tries')
raise ValueError("hashToCurve failed for 100 tries")
def hash256(inb):
@@ -168,23 +176,35 @@ def hash256(inb):
def testEccUtils():
print('testEccUtils()')
print("testEccUtils()")
G_enc = ToDER(G)
assert (G_enc.hex() == '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')
assert (
G_enc.hex()
== "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
)
G_enc = pointToCPK(G)
assert (G_enc.hex() == '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
assert (
G_enc.hex()
== "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
)
G_dec = CPKToPoint(G_enc)
assert (G_dec == G)
assert G_dec == G
G_enc = pointToCPK2(G)
assert (G_enc.hex() == '0879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
assert (
G_enc.hex()
== "0879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
)
H = hashToCurve(ToDER(G))
assert (pointToCPK(H).hex() == '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0')
assert (
pointToCPK(H).hex()
== "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
)
print('Passed.')
print("Passed.")
if __name__ == "__main__":

View File

@@ -7,21 +7,30 @@
from .crypto import blake256, hash160, hmac_sha512, ripemd160
from coincurve.keys import (
PrivateKey,
PublicKey)
from coincurve.keys import PrivateKey, PublicKey
def BIP32Hash(chaincode: bytes, child_no: int, key_data_type: int, keydata: bytes):
return hmac_sha512(chaincode, key_data_type.to_bytes(1, 'big') + keydata + child_no.to_bytes(4, 'big'))
return hmac_sha512(
chaincode,
key_data_type.to_bytes(1, "big") + keydata + child_no.to_bytes(4, "big"),
)
def hash160_dcr(data: bytes) -> bytes:
return ripemd160(blake256(data))
class ExtKeyPair():
__slots__ = ('_depth', '_fingerprint', '_child_no', '_chaincode', '_key', '_pubkey', 'hash_func')
class ExtKeyPair:
__slots__ = (
"_depth",
"_fingerprint",
"_child_no",
"_chaincode",
"_key",
"_pubkey",
"hash_func",
)
def __init__(self, coin_type=1):
if coin_type == 4:
@@ -30,20 +39,20 @@ class ExtKeyPair():
self.hash_func = hash160
def set_seed(self, seed: bytes) -> None:
hashout: bytes = hmac_sha512(b'Bitcoin seed', seed)
hashout: bytes = hmac_sha512(b"Bitcoin seed", seed)
self._key = hashout[:32]
self._pubkey = None
self._chaincode = hashout[32:]
self._depth = 0
self._child_no = 0
self._fingerprint = b'\0' * 4
self._fingerprint = b"\0" * 4
def has_key(self) -> bool:
return False if self._key is None else True
def neuter(self) -> None:
if self._key is None:
raise ValueError('Already neutered')
raise ValueError("Already neutered")
self._pubkey = PublicKey.from_secret(self._key).format()
self._key = None
@@ -74,7 +83,11 @@ class ExtKeyPair():
out._pubkey = K.format()
else:
k = PrivateKey(self._key)
out._fingerprint = self.hash_func(self._pubkey if self._pubkey else PublicKey.from_secret(self._key).format())[:4]
out._fingerprint = self.hash_func(
self._pubkey
if self._pubkey
else PublicKey.from_secret(self._key).format()
)[:4]
new_hash = BIP32Hash(self._chaincode, child_no, 0, self._key)
out._chaincode = new_hash[32:]
k.add(new_hash[:32], update=True)
@@ -85,27 +98,35 @@ class ExtKeyPair():
return out
def encode_v(self) -> bytes:
return self._depth.to_bytes(1, 'big') + \
self._fingerprint + \
self._child_no.to_bytes(4, 'big') + \
self._chaincode + \
b'\x00' + \
self._key
return (
self._depth.to_bytes(1, "big")
+ self._fingerprint
+ self._child_no.to_bytes(4, "big")
+ self._chaincode
+ b"\x00"
+ self._key
)
def encode_p(self) -> bytes:
pubkey = PublicKey.from_secret(self._key).format() if self._pubkey is None else self._pubkey
return self._depth.to_bytes(1, 'big') + \
self._fingerprint + \
self._child_no.to_bytes(4, 'big') + \
self._chaincode + \
pubkey
pubkey = (
PublicKey.from_secret(self._key).format()
if self._pubkey is None
else self._pubkey
)
return (
self._depth.to_bytes(1, "big")
+ self._fingerprint
+ self._child_no.to_bytes(4, "big")
+ self._chaincode
+ pubkey
)
def decode(self, data: bytes) -> None:
if len(data) != 74:
raise ValueError('Unexpected extkey length')
raise ValueError("Unexpected extkey length")
self._depth = data[0]
self._fingerprint = data[1:5]
self._child_no = int.from_bytes(data[5:9], 'big')
self._child_no = int.from_bytes(data[5:9], "big")
self._chaincode = data[9:41]
if data[41] == 0:

View File

@@ -7,25 +7,25 @@
def decode_compactsize(b: bytes, offset: int = 0) -> (int, int):
i = b[offset]
if i < 0xfd:
if i < 0xFD:
return i, 1
offset += 1
if i == 0xfd:
return int.from_bytes(b[offset: offset + 2], 'little'), 3
if i == 0xfe:
return int.from_bytes(b[offset: offset + 4], 'little'), 5
if i == 0xFD:
return int.from_bytes(b[offset : offset + 2], "little"), 3
if i == 0xFE:
return int.from_bytes(b[offset : offset + 4], "little"), 5
# 0xff
return int.from_bytes(b[offset: offset + 8], 'little'), 9
return int.from_bytes(b[offset : offset + 8], "little"), 9
def encode_compactsize(i: int) -> bytes:
if i < 0xfd:
if i < 0xFD:
return bytes((i,))
if i <= 0xffff:
return bytes((0xfd,)) + i.to_bytes(2, 'little')
if i <= 0xffffffff:
return bytes((0xfe,)) + i.to_bytes(4, 'little')
return bytes((0xff,)) + i.to_bytes(8, 'little')
if i <= 0xFFFF:
return bytes((0xFD,)) + i.to_bytes(2, "little")
if i <= 0xFFFFFFFF:
return bytes((0xFE,)) + i.to_bytes(4, "little")
return bytes((0xFF,)) + i.to_bytes(8, "little")
def decode_varint(b: bytes, offset: int = 0) -> (int, int):
@@ -38,7 +38,7 @@ def decode_varint(b: bytes, offset: int = 0) -> (int, int):
if not c & 0x80:
break
if num_bytes > 8:
raise ValueError('Too many bytes')
raise ValueError("Too many bytes")
return i, num_bytes
@@ -46,6 +46,6 @@ def encode_varint(i: int) -> bytes:
b = bytearray()
while i > 0x7F:
b += bytes(((i & 0x7F) | 0x80,))
i = (i >> 7)
i = i >> 7
b += bytes((i,))
return b

View File

@@ -4,14 +4,140 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import contextlib
import ipaddress
import os
import time
from urllib.error import ContentTooShortError
from urllib.parse import _splittype
from urllib.request import Request, urlopen
def is_private_ip_address(addr: str):
# Will return false for all URLs
if addr == 'localhost':
if addr == "localhost":
return True
if addr.endswith(".local"):
return True
try:
return ipaddress.ip_address(addr).is_private
except Exception:
return False
def make_reporthook(read_start: int, logger):
read = read_start # Number of bytes read so far
last_percent_str = ""
time_last = time.time()
read_last = read_start
display_last = time_last
abo = 7
average_buffer = [-1] * 8
if read_start > 0:
logger.info(f"Attempting to resume from byte {read_start}")
def reporthook(blocknum, blocksize, totalsize):
nonlocal read, last_percent_str, time_last, read_last, display_last, read_start
nonlocal average_buffer, abo, logger
read += blocksize
# totalsize excludes read_start
use_size = totalsize + read_start
dl_complete: bool = totalsize > 0 and read >= use_size
time_now = time.time()
time_delta = time_now - time_last
if time_delta < 4.0 and not dl_complete:
return
# Avoid division by zero by picking a value
if time_delta <= 0.0:
time_delta = 0.01
bytes_delta = read - read_last
time_last = time_now
read_last = read
bits_per_second = (bytes_delta * 8) / time_delta
abo = 0 if abo >= 7 else abo + 1
average_buffer[abo] = bits_per_second
samples = 0
average_bits_per_second = 0
for sample in average_buffer:
if sample < 0:
continue
average_bits_per_second += sample
samples += 1
average_bits_per_second /= samples
speed_str: str
if average_bits_per_second > 1000**3:
speed_str = "{:.2f} Gbps".format(average_bits_per_second / (1000**3))
elif average_bits_per_second > 1000**2:
speed_str = "{:.2f} Mbps".format(average_bits_per_second / (1000**2))
else:
speed_str = "{:.2f} kbps".format(average_bits_per_second / 1000)
if totalsize > 0:
percent_str = "%5.0f%%" % (read * 1e2 / use_size)
if percent_str != last_percent_str or time_now - display_last > 10:
logger.info(percent_str + " " + speed_str)
last_percent_str = percent_str
display_last = time_now
else:
logger.info(f"Read {read}, {speed_str}")
return reporthook
def urlretrieve(url, filename, reporthook=None, data=None, resume_from=0):
"""urlretrieve with resume"""
url_type, path = _splittype(url)
req = Request(url)
if resume_from > 0:
req.add_header("Range", f"bytes={resume_from}-")
with contextlib.closing(urlopen(req)) as fp:
headers = fp.info()
# Just return the local path and the "headers" for file://
# URLs. No sense in performing a copy unless requested.
if url_type == "file" and not filename:
return os.path.normpath(path), headers
with open(filename, "ab" if resume_from > 0 else "wb") as tfp:
result = filename, headers
bs = 1024 * 8
size = -1
read = resume_from
blocknum = 0
range_from = 0
if "content-length" in headers:
size = int(headers["Content-Length"])
if "Content-Range" in headers:
range_str = headers["Content-Range"]
offset = range_str.find("-")
range_from = int(range_str[6:offset])
if resume_from != range_from:
raise ValueError("Download is not resuming from the expected byte")
if reporthook:
reporthook(blocknum, bs, size)
while True:
block = fp.read(bs)
if not block:
break
read += len(block)
tfp.write(block)
blocknum += 1
if reporthook:
reporthook(blocknum, bs, size)
if size >= 0 and read < size:
raise ContentTooShortError(
"retrieval incomplete: got only %i out of %i bytes" % (read, size), result
)
return result

View File

@@ -16,7 +16,7 @@ def rfc2440_hash_password(password, salt=None):
salt = secrets.token_bytes(8)
assert len(salt) == 8
hashbytes = salt + password.encode('utf-8')
hashbytes = salt + password.encode("utf-8")
len_hashbytes = len(hashbytes)
h = hashlib.sha1()
@@ -27,5 +27,5 @@ def rfc2440_hash_password(password, salt=None):
continue
h.update(hashbytes[:count])
break
rv = '16:' + salt.hex() + '60' + h.hexdigest()
rv = "16:" + salt.hex() + "60" + h.hexdigest()
return rv.upper()

View File

@@ -6,6 +6,13 @@
import struct
import hashlib
from basicswap.contrib.test_framework.script import (
OP_PUSHDATA1,
OP_PUSHDATA2,
OP_PUSHDATA4,
CScriptInvalidError,
CScriptTruncatedPushDataError,
)
from basicswap.script import OpCodes
@@ -16,15 +23,15 @@ def decodeScriptNum(script_bytes, o):
return ((num_len - OpCodes.OP_1) + 1, 1)
if num_len > 4:
raise ValueError('Bad scriptnum length') # Max 4 bytes
raise ValueError("Bad scriptnum length") # Max 4 bytes
if num_len + o >= len(script_bytes):
raise ValueError('Bad script length')
raise ValueError("Bad script length")
o += 1
for i in range(num_len):
b = script_bytes[o + i]
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
if i == num_len - 1 and b & 0x80:
b &= (~(0x80) & 0xFF)
b &= ~(0x80) & 0xFF
v += int(b) << 8 * i
v *= -1
else:
@@ -32,10 +39,58 @@ def decodeScriptNum(script_bytes, o):
return (v, 1 + num_len)
def decodePushData(script_bytes, o):
datasize = None
pushdata_type = None
i = o
opcode = script_bytes[i]
i += 1
if opcode < OP_PUSHDATA1:
pushdata_type = "PUSHDATA(%d)" % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = "PUSHDATA1"
if i >= len(script_bytes):
raise CScriptInvalidError("PUSHDATA1: missing data length")
datasize = script_bytes[i]
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = "PUSHDATA2"
if i + 1 >= len(script_bytes):
raise CScriptInvalidError("PUSHDATA2: missing data length")
datasize = script_bytes[i] + (script_bytes[i + 1] << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = "PUSHDATA4"
if i + 3 >= len(script_bytes):
raise CScriptInvalidError("PUSHDATA4: missing data length")
datasize = (
script_bytes[i]
+ (script_bytes[i + 1] << 8)
+ (script_bytes[i + 2] << 16)
+ (script_bytes[i + 3] << 24)
)
i += 4
else:
assert False # shouldn't happen
data = bytes(script_bytes[i : i + datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError("%s: truncated data" % pushdata_type, data)
# return data and the number of bytes to skip forward
return (data, i + datasize - o)
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \
+ bytes((OpCodes.OP_EQUAL,))
return bytes((OpCodes.OP_HASH160, 0x14)) + p2sh + bytes((OpCodes.OP_EQUAL,))
def getP2WSH(script):
@@ -45,26 +100,26 @@ def getP2WSH(script):
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
if v <= 0xffff: # USHRT_MAX
if v <= 0xFFFF: # USHRT_MAX
return struct.pack("<BH", 253, v)
if v <= 0xffffffff: # UINT_MAX
if v <= 0xFFFFFFFF: # UINT_MAX
return struct.pack("<BI", 254, v)
if v <= 0xffffffffffffffff: # UINT_MAX
if v <= 0xFFFFFFFFFFFFFFFF: # UINT_MAX
return struct.pack("<BQ", 255, v)
raise ValueError('Value too large')
raise ValueError("Value too large")
def getCompactSizeLen(v):
# Compact Size
if v < 253:
return 1
if v <= 0xffff: # USHRT_MAX
if v <= 0xFFFF: # USHRT_MAX
return 3
if v <= 0xffffffff: # UINT_MAX
if v <= 0xFFFFFFFF: # UINT_MAX
return 5
if v <= 0xffffffffffffffff: # UINT_MAX
if v <= 0xFFFFFFFFFFFFFFFF: # UINT_MAX
return 9
raise ValueError('Value too large')
raise ValueError("Value too large")
def getWitnessElementLen(v):

View File

@@ -7,13 +7,15 @@ from .contrib.MoneroPy.base58 import encode as xmr_b58encode
def cn_fast_hash(s):
k = Keccak.Keccak()
return k.Keccak((len(s) * 8, s.hex()), 1088, 512, 32 * 8, False).lower() # r = bitrate = 1088, c = capacity, n = output length in bits
return k.Keccak(
(len(s) * 8, s.hex()), 1088, 512, 32 * 8, False
).lower() # r = bitrate = 1088, c = capacity, n = output length in bits
def encode_address(view_point: bytes, spend_point: bytes, version=18) -> str:
prefix_bytes = version if isinstance(version, bytes) else encode_varint(version)
buf = prefix_bytes + spend_point + view_point
h = cn_fast_hash(buf)
buf = buf + bytes.fromhex(h[0: 8])
buf = buf + bytes.fromhex(h[0:8])
return xmr_b58encode(buf.hex())

View File

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

View File

@@ -1 +1 @@
basicswap_prepare.py
../basicswap/bin/prepare.py

View File

@@ -1 +1 @@
basicswap_run.py
../basicswap/bin/run.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,450 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-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 sys
import json
import shutil
import signal
import logging
import traceback
import subprocess
import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
swap_client = None
class Daemon:
__slots__ = ('handle', 'files')
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v['name']:
return True
return False
def signal_handler(sig, frame):
global swap_client
logger.info('Signal %d detected, ending program.' % (sig))
if swap_client is not None:
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 for 0.21.3
ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf')
if os.path.exists(ltc_conf_path):
config_to_add = ['blockfilterindex=0', 'peerblockfilters=0']
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line in config_to_add:
config_to_add.remove(line)
if len(config_to_add) > 0:
logging.info('Rewriting litecoin.conf')
shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last')
with open(ltc_conf_path, 'a') as fp:
for line in config_to_add:
fp.write(line + '\n')
args = [daemon_bin, ]
add_datadir: bool = extra_config.get('add_datadir', True)
if add_datadir:
args.append('-datadir=' + datadir_path)
args += opts
logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else ''))
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
if extra_config.get('use_shell', False):
str_args = ' '.join(args)
return Daemon(subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
return Daemon(subprocess.Popen(args, 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
logging.info('Starting node {} --data-dir={}'.format(daemon_path, node_dir))
# 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_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_bin, '--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):
logging.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
needs_rewrite = True
args += opts
if needs_rewrite:
logging.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)
logging.info('Starting wallet daemon {} --wallet-dir={}'.format(daemon_bin, node_dir))
# 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 runClient(fp, data_dir, chain, start_only_coins):
global swap_client
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)
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:
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 = c + 'd' + ('.exe' if os.name == 'nt' else '')
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 = c + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
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 = 'dcrd' + ('.exe' if os.name == 'nt' else '')
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 = 'dcrwallet' + ('.exe' if os.name == 'nt' else '')
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 = c + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
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)
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.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 ex:
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 ex:
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 ex:
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('Interrupting %d, error %s', d.handle.pid, str(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 ex:
swap_client.log.error('Error: {}'.format(ex))
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 not int(ln.split(':')[1]) 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)
logger.info('Done.')
return swap_client.fail_code if swap_client is not None else 0
if __name__ == '__main__':
main()

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