Merge conflict resolved - git add guix.scm

master
Mike Holloway 6 months ago
commit 8a3f626801
  1. 2
      .cirrus.yml
  2. 60
      .travis.yml
  3. 9
      Dockerfile
  4. 6
      README.md
  5. 2
      basicswap/__init__.py
  6. 2
      basicswap/base.py
  7. 1550
      basicswap/basicswap.py
  8. 4
      basicswap/basicswap_util.py
  9. 161
      basicswap/chainparams.py
  10. 4
      basicswap/config.py
  11. 1
      basicswap/contrib/blake256/__init__.py
  12. 533
      basicswap/contrib/blake256/blake256.py
  13. 37
      basicswap/contrib/blake256/test.py
  14. 3
      basicswap/contrib/mnemonic/__init__.py
  15. 298
      basicswap/contrib/mnemonic/mnemonic.py
  16. 1
      basicswap/contrib/mnemonic/py.typed
  17. 2048
      basicswap/contrib/mnemonic/wordlist/chinese_simplified.txt
  18. 2048
      basicswap/contrib/mnemonic/wordlist/chinese_traditional.txt
  19. 2048
      basicswap/contrib/mnemonic/wordlist/czech.txt
  20. 2048
      basicswap/contrib/mnemonic/wordlist/english.txt
  21. 2048
      basicswap/contrib/mnemonic/wordlist/french.txt
  22. 2048
      basicswap/contrib/mnemonic/wordlist/italian.txt
  23. 2048
      basicswap/contrib/mnemonic/wordlist/japanese.txt
  24. 2048
      basicswap/contrib/mnemonic/wordlist/korean.txt
  25. 2048
      basicswap/contrib/mnemonic/wordlist/portuguese.txt
  26. 2048
      basicswap/contrib/mnemonic/wordlist/russian.txt
  27. 2048
      basicswap/contrib/mnemonic/wordlist/spanish.txt
  28. 2048
      basicswap/contrib/mnemonic/wordlist/turkish.txt
  29. 19
      basicswap/db.py
  30. 16
      basicswap/db_upgrades.py
  31. 2
      basicswap/db_util.py
  32. 67
      basicswap/http_server.py
  33. 13
      basicswap/interface/__init__.py
  34. 238
      basicswap/interface/base.py
  35. 396
      basicswap/interface/btc.py
  36. 8
      basicswap/interface/dash.py
  37. 4
      basicswap/interface/dcr/__init__.py
  38. 1450
      basicswap/interface/dcr/dcr.py
  39. 204
      basicswap/interface/dcr/messages.py
  40. 47
      basicswap/interface/dcr/rpc.py
  41. 50
      basicswap/interface/dcr/script.py
  42. 66
      basicswap/interface/dcr/util.py
  43. 18
      basicswap/interface/firo.py
  44. 38
      basicswap/interface/nav.py
  45. 7
      basicswap/interface/nmc.py
  46. 69
      basicswap/interface/part.py
  47. 4
      basicswap/interface/pivx.py
  48. 27
      basicswap/interface/wow.py
  49. 35
      basicswap/interface/xmr.py
  50. 21
      basicswap/js_server.py
  51. 156
      basicswap/messages.proto
  52. 265
      basicswap/messages_npb.py
  53. 54
      basicswap/messages_pb2.py
  54. 53
      basicswap/protocols/atomic_swap_1.py
  55. 24
      basicswap/protocols/xmr_swap_1.py
  56. 16
      basicswap/rpc.py
  57. 2
      basicswap/script.py
  58. 13
      basicswap/templates/changepassword.html
  59. 110
      basicswap/templates/footer.html
  60. 33
      basicswap/templates/header.html
  61. 3
      basicswap/templates/inc_messages.html
  62. 5
      basicswap/templates/index.html
  63. 16
      basicswap/templates/offer.html
  64. 24
      basicswap/templates/offer_confirm.html
  65. 4
      basicswap/templates/offer_new_1.html
  66. 22
      basicswap/templates/offer_new_2.html
  67. 411
      basicswap/templates/offers.html
  68. 43
      basicswap/templates/rpc.html
  69. 4
      basicswap/templates/settings.html
  70. 8
      basicswap/templates/style.html
  71. 33
      basicswap/templates/unlock.html
  72. 430
      basicswap/templates/wallet.html
  73. 40
      basicswap/templates/wallets.html
  74. 20
      basicswap/ui/page_offers.py
  75. 4
      basicswap/ui/page_settings.py
  76. 6
      basicswap/ui/page_wallet.py
  77. 35
      basicswap/util/address.py
  78. 30
      basicswap/util/crypto.py
  79. 116
      basicswap/util/extkey.py
  80. 42
      basicswap/util/integer.py
  81. 470
      bin/basicswap_prepare.py
  82. 166
      bin/basicswap_run.py
  83. 9
      doc/install.md
  84. 2
      doc/notes.md
  85. 12
      doc/release-notes.md
  86. 2
      doc/tor.md
  87. 16
      docker/production/compose-fragments/1_decred-wallet.yml
  88. 16
      docker/production/compose-fragments/1_wownero-wallet.yml
  89. 16
      docker/production/compose-fragments/8_decred-daemon.yml
  90. 16
      docker/production/compose-fragments/8_wownero-daemon.yml
  91. 12
      docker/production/compose-fragments/9_swapprepare.yml
  92. 25
      docker/production/decred_daemon/Dockerfile
  93. 11
      docker/production/decred_daemon/entrypoint.sh
  94. 19
      docker/production/decred_wallet/Dockerfile
  95. 11
      docker/production/decred_wallet/entrypoint.sh
  96. 28
      docker/production/example.env
  97. 13
      docker/production/notes.md
  98. 15
      docker/production/scripts/build_yml_files.py
  99. 12
      docker/production/swapclient/Dockerfile
  100. 24
      docker/production/wownero_daemon/Dockerfile
  101. Some files were not shown because too many files have changed in this diff Show More

@ -21,7 +21,7 @@ test_task:
- XMR_BINDIR: ${BIN_DIR}/monero - XMR_BINDIR: ${BIN_DIR}/monero
setup_script: setup_script:
- apt-get update - apt-get update
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config - apt-get install -y wget python3-pip gnupg unzip automake libtool pkg-config
- pip install tox pytest - pip install tox pytest
- python3 setup.py install - python3 setup.py install
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip - wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip

@ -1,60 +0,0 @@
dist: bionic
os: linux
language: python
python: '3.7'
stages:
- lint
- test
env:
global:
- TEST_DIR=${HOME}/test_basicswap2
- TEST_RELOAD_PATH=~/test_basicswap1
- BIN_DIR=~/cached_bin
- PARTICL_BINDIR=${BIN_DIR}/particl
- BITCOIN_BINDIR=${BIN_DIR}/bitcoin
- LITECOIN_BINDIR=${BIN_DIR}/litecoin
- XMR_BINDIR=${BIN_DIR}/monero
cache:
directories:
- "$BIN_DIR"
before_install:
- sudo apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
install:
- travis_retry pip install tox pytest
before_script:
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.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:

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

@ -35,9 +35,9 @@ Built as a low-friction, highly secure solution to the frequent losses of funds
## Under the Hood ## 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. In essence, **BasicSwap** operates merely as a decentralized messaging protocol supplemented by a user-friendly interface.
@ -122,7 +122,7 @@ If you’d 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. 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 #### Community chat support

@ -1,3 +1,3 @@
name = "basicswap" name = "basicswap"
__version__ = "0.13.0" __version__ = "0.13.4"

@ -46,7 +46,7 @@ class BaseApp:
self.settings = settings self.settings = settings
self.coin_clients = {} self.coin_clients = {}
self.coin_interfaces = {} self.coin_interfaces = {}
self.mxDB = threading.RLock() self.mxDB = threading.Lock()
self.debug = self.settings.get('debug', False) self.debug = self.settings.get('debug', False)
self.delay_event = threading.Event() self.delay_event = threading.Event()
self.chainstate_delay_event = threading.Event() self.chainstate_delay_event = threading.Event()

File diff suppressed because it is too large Load Diff

@ -202,6 +202,8 @@ class DebugTypes(IntEnum):
SEND_LOCKED_XMR = auto() SEND_LOCKED_XMR = auto()
B_LOCK_TX_MISSED_SEND = auto() B_LOCK_TX_MISSED_SEND = auto()
DUPLICATE_ACTIONS = auto() DUPLICATE_ACTIONS = auto()
DONT_CONFIRM_PTX = auto()
OFFER_LOCK_2_VALUE_INC = auto()
class NotificationTypes(IntEnum): class NotificationTypes(IntEnum):
@ -473,7 +475,7 @@ def getOfferProofOfFundsHash(offer_msg, offer_addr):
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg # TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
h = hashlib.sha256() h = hashlib.sha256()
h.update(offer_addr.encode('utf-8')) h.update(offer_addr.encode('utf-8'))
offer_bytes = offer_msg.SerializeToString() offer_bytes = offer_msg.to_bytes()
h.update(offer_bytes) h.update(offer_bytes)
return h.digest() return h.digest()

@ -1,38 +1,35 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert # Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import threading
from enum import IntEnum from enum import IntEnum
from .util import ( from .util import (
COIN, COIN,
make_int,
format_amount,
TemporaryError,
) )
XMR_COIN = 10 ** 12 XMR_COIN = 10 ** 12
WOW_COIN = 10 ** 11
class Coins(IntEnum): class Coins(IntEnum):
PART = 1 PART = 1
BTC = 2 BTC = 2
LTC = 3 LTC = 3
# DCR = 4 DCR = 4
NMC = 5 NMC = 5
XMR = 6 XMR = 6
PART_BLIND = 7 PART_BLIND = 7
PART_ANON = 8 PART_ANON = 8
# ZANO = 9 WOW = 9
# NDAU = 10 # NDAU = 10
PIVX = 11 PIVX = 11
DASH = 12 DASH = 12
FIRO = 13 FIRO = 13
NAV = 14 NAV = 14
LTC_MWEB = 15 LTC_MWEB = 15
# ZANO = 16
chainparams = { chainparams = {
@ -155,6 +152,41 @@ chainparams = {
'max_amount': 100000 * COIN, '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,
},
'testnet': {
'rpcport': 19109,
'pubkey_address': 0x0f21,
'script_address': 0x0efc,
'key_prefix': 0x230e,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
},
'regtest': { # simnet
'rpcport': 18656,
'pubkey_address': 0x0e91,
'script_address': 0x0e6c,
'key_prefix': 0x2307,
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.NMC: { Coins.NMC: {
'name': 'namecoin', 'name': 'namecoin',
'ticker': 'NMC', 'ticker': 'NMC',
@ -217,6 +249,33 @@ chainparams = {
'address_prefix': 18, '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,
},
'testnet': {
'rpcport': 44568,
'walletrpcport': 44572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
},
'regtest': {
'rpcport': 54568,
'walletrpcport': 54572,
'min_amount': 100000,
'max_amount': 10000 * WOW_COIN,
'address_prefix': 4146,
}
},
Coins.PIVX: { Coins.PIVX: {
'name': 'pivx', 'name': 'pivx',
'ticker': 'PIVX', 'ticker': 'PIVX',
@ -387,89 +446,3 @@ def getCoinIdFromTicker(ticker):
return ticker_map[ticker.lower()] return ticker_map[ticker.lower()]
except Exception: except Exception:
raise ValueError('Unknown coin') raise ValueError('Unknown coin')
class CoinInterface:
def __init__(self, network):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
def setDefaults(self):
self._unknown_wallet_seed = True
self._restore_height = None
def make_int(self, amount_in: int, r: int = 0) -> int:
return make_int(amount_in, self.exp(), r=r)
def format_amount(self, amount_in, conv_int=False, r=0):
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
return format_amount(amount_int, self.exp())
def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
return coin_chainparams['name'].capitalize()
def ticker(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
return ticker
def getExchangeTicker(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['ticker']
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['name']
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['min_amount']
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['max_amount']
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
def setWalletRestoreHeight(self, value: int) -> None:
self._restore_height = value
def knownWalletSeed(self) -> bool:
return not self._unknown_wallet_seed
def chainparams(self):
return chainparams[self.coin_type()]
def chainparams_network(self):
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if 'not enough unlocked money' in str_error:
return True
if 'no unlocked balance' in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
return True
if 'invalid unlocked_balance' in str_error:
return True
if 'daemon is busy' in str_error:
return True
if 'timed out' in str_error:
return True
if 'request-sent' in str_error:
return True
return False

@ -7,10 +7,10 @@
import os import os
CONFIG_FILENAME = 'basicswap.json' CONFIG_FILENAME = 'basicswap.json'
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', '~/.basicswap') BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', os.path.join('~', '.basicswap'))
DEFAULT_ALLOW_CORS = False DEFAULT_ALLOW_CORS = False
TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap')) TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', '~/.basicswap/bin')) DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', os.path.join('~', '.basicswap', 'bin')))
bin_suffix = ('.exe' if os.name == 'nt' else '') bin_suffix = ('.exe' if os.name == 'nt' else '')
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl'))) PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))

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

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -11,7 +11,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 23 CURRENT_DB_VERSION = 24
CURRENT_DB_DATA_VERSION = 4 CURRENT_DB_DATA_VERSION = 4
Base = declarative_base() Base = declarative_base()
@ -127,6 +127,7 @@ class Bid(Base):
amount_to = sa.Column(sa.BigInteger) # amount * offer.rate amount_to = sa.Column(sa.BigInteger) # amount * offer.rate
pkhash_buyer = sa.Column(sa.LargeBinary) pkhash_buyer = sa.Column(sa.LargeBinary)
pkhash_buyer_to = sa.Column(sa.LargeBinary) # Used for the ptx if coin pubkey hashes differ
amount = sa.Column(sa.BigInteger) amount = sa.Column(sa.BigInteger)
rate = sa.Column(sa.BigInteger) rate = sa.Column(sa.BigInteger)
@ -191,6 +192,11 @@ class Bid(Base):
else: else:
self.states += pack_state(new_state, now) self.states += pack_state(new_state, now)
def getLockTXBVout(self):
if self.xmr_b_lock_tx:
return self.xmr_b_lock_tx.vout
return None
class SwapTx(Base): class SwapTx(Base):
__tablename__ = 'transactions' __tablename__ = 'transactions'
@ -522,3 +528,14 @@ class MessageLink(Base):
msg_type = sa.Column(sa.Integer) msg_type = sa.Column(sa.Integer)
msg_sequence = sa.Column(sa.Integer) msg_sequence = sa.Column(sa.Integer)
msg_id = sa.Column(sa.LargeBinary) msg_id = sa.Column(sa.LargeBinary)
class CheckedBlock(Base):
__tablename__ = 'checkedblocks'
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
created_at = sa.Column(sa.BigInteger)
coin_type = sa.Column(sa.Integer)
block_height = sa.Column(sa.Integer)
block_hash = sa.Column(sa.LargeBinary)
block_time = sa.Column(sa.BigInteger)

@ -93,7 +93,7 @@ def upgradeDatabaseData(self, data_version):
created_at=now)) created_at=now))
self.db_data_version = CURRENT_DB_DATA_VERSION self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKVInSession('db_data_version', self.db_data_version, session) self.setIntKV('db_data_version', self.db_data_version, session)
session.commit() session.commit()
self.log.info('Upgraded database records to version {}'.format(self.db_data_version)) self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
finally: finally:
@ -300,9 +300,21 @@ def upgradeDatabase(self, db_version):
elif current_version == 22: elif current_version == 22:
db_version += 1 db_version += 1
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER') 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: if current_version != db_version:
self.db_version = db_version self.db_version = db_version
self.setIntKVInSession('db_version', db_version, session) self.setIntKV('db_version', db_version, session)
session.commit() session.commit()
session.close() session.close()
session.remove() session.remove()

@ -52,5 +52,7 @@ def remove_expired_data(self, time_offset: int = 0):
if num_offers > 0 or num_bids > 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})
finally: finally:
self.closeSession(session) self.closeSession(session)

@ -98,6 +98,8 @@ def parse_cmd(cmd: str, type_map: str):
type_ind = type_map[i] type_ind = type_map[i]
if type_ind == 'i': if type_ind == 'i':
typed_params.append(int(param)) typed_params.append(int(param))
elif type_ind == 'f':
typed_params.append(float(param))
elif type_ind == 'b': elif type_ind == 'b':
typed_params.append(toBool(param)) typed_params.append(toBool(param))
elif type_ind == 'j': elif type_ind == 'j':
@ -265,6 +267,8 @@ class HttpHandler(BaseHTTPRequestHandler):
summary = swap_client.getSummary() summary = swap_client.getSummary()
result = None result = None
cmd = ''
coin_type_selected = -1
coin_type = -1 coin_type = -1
coin_id = -1 coin_id = -1
call_type = 'cli' call_type = 'cli'
@ -277,71 +281,84 @@ class HttpHandler(BaseHTTPRequestHandler):
call_type = get_data_entry_or(form_data, 'call_type', 'cli') call_type = get_data_entry_or(form_data, 'call_type', 'cli')
type_map = get_data_entry_or(form_data, 'type_map', '') type_map = get_data_entry_or(form_data, 'type_map', '')
try: try:
coin_id = int(get_data_entry(form_data, 'coin_type')) coin_type_selected = get_data_entry(form_data, 'coin_type')
if coin_id in (-2, -3, -4): coin_type_split = coin_type_selected.split(',')
coin_type = Coins(Coins.XMR) coin_type = Coins(int(coin_type_split[0]))
elif coin_id in (-5,): coin_variant = int(coin_type_split[1])
coin_type = Coins(Coins.LTC)
else:
coin_type = Coins(coin_id)
except Exception: except Exception:
raise ValueError('Unknown Coin Type') raise ValueError('Unknown Coin Type')
if coin_type in (Coins.DCR,):
call_type = 'http'
try: try:
cmd = get_data_entry(form_data, 'cmd') cmd = get_data_entry(form_data, 'cmd')
except Exception: except Exception:
raise ValueError('Invalid command') raise ValueError('Invalid command')
if coin_type == Coins.XMR: if coin_type in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_type) ci = swap_client.ci(coin_type)
arr = cmd.split(None, 1) arr = cmd.split(None, 1)
method = arr[0] method = arr[0]
params = json.loads(arr[1]) if len(arr) > 1 else [] params = json.loads(arr[1]) if len(arr) > 1 else []
if coin_id == -4: if coin_variant == 2:
rv = ci.rpc_wallet(method, params) rv = ci.rpc_wallet(method, params)
elif coin_id == -3: elif coin_variant == 0:
rv = ci.rpc(method, params) rv = ci.rpc(method, params)
elif coin_id == -2: elif coin_variant == 1:
if params == []: if params == []:
params = None params = None
rv = ci.rpc2(method, params) rv = ci.rpc2(method, params)
else: else:
raise ValueError('Unknown XMR RPC variant') raise ValueError('Unknown RPC variant')
result = json.dumps(rv, indent=4) result = json.dumps(rv, indent=4)
else: else:
if call_type == 'http': if call_type == 'http':
ci = swap_client.ci(coin_type)
method, params = parse_cmd(cmd, type_map) method, params = parse_cmd(cmd, type_map)
if coin_id == -5: if coin_variant == 1:
rv = swap_client.ci(coin_type).rpc_wallet_mweb(method, params) rv = ci.rpc_wallet(method, params)
elif coin_variant == 2:
rv = ci.rpc_wallet_mweb(method, params)
else: else:
rv = swap_client.ci(coin_type).rpc_wallet(method, params) if coin_type in (Coins.DCR, ):
rv = ci.rpc(method, params)
else:
rv = ci.rpc_wallet(method, params)
if not isinstance(rv, str): if not isinstance(rv, str):
rv = json.dumps(rv, indent=4) rv = json.dumps(rv, indent=4)
result = cmd + '\n' + rv result = cmd + '\n' + rv
else: else:
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd) result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
except Exception as ex: except Exception as ex:
result = str(ex) result = cmd + '\n' + str(ex)
if self.server.swap_client.debug is True: if self.server.swap_client.debug is True:
self.server.swap_client.log.error(traceback.format_exc()) self.server.swap_client.log.error(traceback.format_exc())
template = env.get_template('rpc.html') template = env.get_template('rpc.html')
coins = listAvailableCoins(swap_client, with_variants=False) coin_available = listAvailableCoins(swap_client, with_variants=False)
with_xmr: bool = any(c[0] == Coins.XMR for c in coins) with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
coins = [c for c in coins if c[0] != Coins.XMR] 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)]
if any(c[0] == Coins.LTC for c in coins): if any(c[0] == Coins.DCR for c in coin_available):
coins.append((-5, 'Litecoin MWEB 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'))
if with_xmr: if with_xmr:
coins.append((-2, 'Monero')) coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
coins.append((-3, 'Monero JSON')) coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
coins.append((-4, 'Monero Wallet')) coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
if with_wow:
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
return self.render_template(template, { return self.render_template(template, {
'messages': messages, 'messages': messages,
'err_messages': err_messages, 'err_messages': err_messages,
'coins': coins, 'coins': coins,
'coin_type': coin_id, 'coin_type': coin_type_selected,
'call_type': call_type, 'call_type': call_type,
'result': result, 'result': result,
'messages': messages, 'messages': messages,

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

@ -0,0 +1,238 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import threading
from enum import IntEnum
from basicswap.chainparams import (
chainparams,
)
from basicswap.util import (
ensure,
i2b, b2i,
make_int,
format_amount,
TemporaryError,
)
from basicswap.util.crypto import (
hash160,
)
from basicswap.util.ecc import (
ep,
getSecretInt,
)
from coincurve.dleag import (
verify_secp256k1_point
)
from coincurve.keys import (
PublicKey,
)
class Curves(IntEnum):
secp256k1 = 1
ed25519 = 2
class CoinInterface:
@staticmethod
def watch_blocks_for_scripts() -> bool:
return False
@staticmethod
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
def setDefaults(self):
self._unknown_wallet_seed = True
self._restore_height = None
def make_int(self, amount_in: int, r: int = 0) -> int:
return make_int(amount_in, self.exp(), r=r)
def format_amount(self, amount_in, conv_int=False, r=0):
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
return format_amount(amount_int, self.exp())
def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
return coin_chainparams['name'].capitalize()
def ticker(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
return ticker
def getExchangeTicker(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['ticker']
def getExchangeName(self, exchange_name: str) -> str:
return chainparams[self.coin_type()]['name']
def ticker_mainnet(self) -> str:
ticker = chainparams[self.coin_type()]['ticker']
return ticker
def min_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['min_amount']
def max_amount(self) -> int:
return chainparams[self.coin_type()][self._network]['max_amount']
def setWalletSeedWarning(self, value: bool) -> None:
self._unknown_wallet_seed = value
def setWalletRestoreHeight(self, value: int) -> None:
self._restore_height = value
def knownWalletSeed(self) -> bool:
return not self._unknown_wallet_seed
def chainparams(self):
return chainparams[self.coin_type()]
def chainparams_network(self):
return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return False
def is_transient_error(self, ex) -> bool:
if isinstance(ex, TemporaryError):
return True
str_error: str = str(ex).lower()
if 'not enough unlocked money' in str_error:
return True
if 'no unlocked balance' in str_error:
return True
if 'transaction was rejected by daemon' in str_error:
return True
if 'invalid unlocked_balance' in str_error:
return True
if 'daemon is busy' in str_error:
return True
if 'timed out' in str_error:
return True
if 'request-sent' in str_error:
return True
return False
def setConfTarget(self, new_conf_target: int) -> None:
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
self._conf_target = new_conf_target
def walletRestoreHeight(self) -> int:
return self._restore_height
def get_connection_type(self):
return self._connection_type
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
def use_tx_vsize(self) -> bool:
return self._use_segwit
def getLockTxSwapOutputValue(self, bid, xmr_swap) -> int:
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap) -> int:
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap) -> int:
# Only one prevout exists
return 0
def checkWallets(self) -> int:
return 1
class AdaptorSigInterface():
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
@staticmethod
def curve_type():
return Curves.secp256k1
def getNewSecretKey(self) -> bytes:
return i2b(getSecretInt())
def getPubkey(self, privkey: bytes) -> bytes:
return PublicKey.from_secret(privkey).format()
def pkh(self, pubkey: bytes) -> bytes:
return hash160(pubkey)
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return (i < ep.o and i > 0)
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)
def isValidAddressHash(self, address_hash: bytes) -> bool:
hash_len = len(address_hash)
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()

@ -5,25 +5,31 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import base64 import base64
import hashlib import hashlib
import json
import logging import logging
import traceback import traceback
from io import BytesIO
from basicswap.contrib.test_framework import segwit_addr from io import BytesIO
from basicswap.interface import ( from basicswap.basicswap_util import (
Curves) getVoutByAddress,
getVoutByScriptPubKey,
)
from basicswap.contrib.test_framework import (
segwit_addr,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
make_int, b2h, i2b, b2i, i2h,
b2h, i2b, b2i, i2h) )
from basicswap.util.ecc import ( from basicswap.util.ecc import (
ep,
pointToCPK, CPKToPoint, pointToCPK, CPKToPoint,
getSecretInt) )
from basicswap.util.script import ( from basicswap.util.script import (
decodeScriptNum, decodeScriptNum,
getCompactSizeLen, getCompactSizeLen,
@ -37,16 +43,20 @@ from basicswap.util.address import (
decodeAddress, decodeAddress,
pubkeyToAddress, pubkeyToAddress,
) )
from basicswap.util.crypto import (
hash160,
sha256,
)
from coincurve.keys import ( from coincurve.keys import (
PrivateKey, PrivateKey,
PublicKey) PublicKey,
from coincurve.dleag import ( )
verify_secp256k1_point)
from coincurve.ecdsaotves import ( from coincurve.ecdsaotves import (
ecdsaotves_enc_sign, ecdsaotves_enc_sign,
ecdsaotves_enc_verify, ecdsaotves_enc_verify,
ecdsaotves_dec_sig, ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key) ecdsaotves_rec_enc_key
)
from basicswap.contrib.test_framework.messages import ( from basicswap.contrib.test_framework.messages import (
COIN, COIN,
@ -55,8 +65,7 @@ from basicswap.contrib.test_framework.messages import (
CTxIn, CTxIn,
CTxInWitness, CTxInWitness,
CTxOut, CTxOut,
uint256_from_str) )
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScriptOp, CScript, CScriptOp,
OP_IF, OP_ELSE, OP_ENDIF, OP_IF, OP_ELSE, OP_ENDIF,
@ -68,12 +77,12 @@ from basicswap.contrib.test_framework.script import (
OP_HASH160, OP_EQUAL, OP_HASH160, OP_EQUAL,
SIGHASH_ALL, SIGHASH_ALL,
SegwitV0SignatureHash, SegwitV0SignatureHash,
hash160) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
TxLockTypes) TxLockTypes
)
from basicswap.chainparams import CoinInterface, Coins from basicswap.chainparams import Coins
from basicswap.rpc import make_rpc_func, openrpc from basicswap.rpc import make_rpc_func, openrpc
@ -109,10 +118,58 @@ def find_vout_for_address_from_txobj(tx_obj, addr: str) -> int:
raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr)) raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr))
class BTCInterface(CoinInterface): def extractScriptLockScriptValues(script_bytes: bytes) -> (bytes, bytes):
@staticmethod script_len = len(script_bytes)
def curve_type(): ensure(script_len == 71, 'Bad script length')
return Curves.secp256k1 o = 0
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == 33)
o += 2
pk1 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == 33)
o += 1
pk2 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
return pk1, pk2
def extractScriptLockRefundScriptValues(script_bytes: bytes):
script_len = len(script_bytes)
ensure(script_len > 73, 'Bad script length')
ensure_op(script_bytes[0] == OP_IF)
ensure_op(script_bytes[1] == OP_2)
ensure_op(script_bytes[2] == 33)
pk1 = script_bytes[3: 3 + 33]
ensure_op(script_bytes[36] == 33)
pk2 = script_bytes[37: 37 + 33]
ensure_op(script_bytes[70] == OP_2)
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
ensure_op(script_bytes[72] == OP_ELSE)
o = 73
csv_val, nb = decodeScriptNum(script_bytes, o)
o += nb
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
o += 1
ensure_op(script_bytes[o] == OP_DROP)
o += 1
ensure_op(script_bytes[o] == 33)
o += 1
pk3 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_CHECKSIG)
o += 1
ensure_op(script_bytes[o] == OP_ENDIF)
return pk1, pk2, csv_val, pk3
class BTCInterface(Secp256k1Interface):
@staticmethod @staticmethod
def coin_type(): def coin_type():
@ -149,10 +206,6 @@ class BTCInterface(CoinInterface):
rv += output.nValue rv += output.nValue
return rv return rv
@staticmethod
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
@staticmethod @staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int: def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 147 return 147
@ -167,7 +220,7 @@ class BTCInterface(CoinInterface):
@staticmethod @staticmethod
def getExpectedSequence(lockType: int, lockVal: int) -> int: def getExpectedSequence(lockType: int, lockVal: int) -> int:
assert (lockVal >= 1), 'Bad lockVal' ensure(lockVal >= 1, 'Bad lockVal')
if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS: if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
return lockVal return lockVal
if lockType == TxLockTypes.SEQUENCE_LOCK_TIME: if lockType == TxLockTypes.SEQUENCE_LOCK_TIME:
@ -206,34 +259,6 @@ class BTCInterface(CoinInterface):
self._log = self._sc.log if self._sc and self._sc.log else logging self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None self._expect_seedid_hex = None
def checkWallets(self) -> int:
wallets = self.rpc('listwallets')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.debug('Changing {} wallet name.'.format(self.ticker()))
for wallet_name in wallets:
# Skip over other expected wallets
if wallet_name in ('mweb', ):
continue
self._rpc_wallet = wallet_name
self._log.info('Switched {} wallet name to {}.'.format(self.ticker(), self._rpc_wallet))
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
break
return len(wallets)
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return False
def get_connection_type(self):
return self._connection_type
def open_rpc(self, wallet=None): def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@ -244,18 +269,29 @@ class BTCInterface(CoinInterface):
except Exception as ex: except Exception as ex:
traceback.print_exc() traceback.print_exc()
raise ValueError('RPC Server Error ' + str(ex)) raise ValueError('RPC Server Error ' + str(ex))
if 'error' in r and r['error'] is not None: if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error'])) raise ValueError('RPC error ' + str(r['error']))
return r['result'] return r['result']
def close_rpc(self, rpc_conn): def close_rpc(self, rpc_conn):
rpc_conn.close() rpc_conn.close()
def setConfTarget(self, new_conf_target: int) -> None: def checkWallets(self) -> int:
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value') wallets = self.rpc('listwallets')
self._conf_target = new_conf_target
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.debug('Changing {} wallet name.'.format(self.ticker()))
for wallet_name in wallets:
# Skip over other expected wallets
if wallet_name in ('mweb', ):
continue
self._rpc_wallet = wallet_name
self._log.info('Switched {} wallet name to {}.'.format(self.ticker(), self._rpc_wallet))
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
break
return len(wallets)
def testDaemonRPC(self, with_wallet=True) -> None: def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet('getwalletinfo' if with_wallet else 'getblockchaininfo') self.rpc_wallet('getwalletinfo' if with_wallet else 'getblockchaininfo')
@ -285,7 +321,7 @@ class BTCInterface(CoinInterface):
max_tries = 5000 max_tries = 5000
for i in range(max_tries): for i in range(max_tries):
prev_block_header = self.rpc('getblock', [last_block_header['previousblockhash']]) prev_block_header = self.rpc('getblockheader', [last_block_header['previousblockhash']])
if prev_block_header['time'] <= time: if prev_block_header['time'] <= time:
return last_block_header if block_after else prev_block_header return last_block_header if block_after else prev_block_header
@ -303,9 +339,6 @@ class BTCInterface(CoinInterface):
rv['locked_utxos'] = len(self.rpc_wallet('listlockunspent')) rv['locked_utxos'] = len(self.rpc_wallet('listlockunspent'))
return rv return rv
def walletRestoreHeight(self) -> int:
return self._restore_height
def getWalletRestoreHeight(self) -> int: def getWalletRestoreHeight(self) -> int:
start_time = self.rpc_wallet('getwalletinfo')['keypoololdest'] start_time = self.rpc_wallet('getwalletinfo')['keypoololdest']
@ -353,18 +386,6 @@ class BTCInterface(CoinInterface):
self._log.debug('validateaddress failed: {}'.format(address)) self._log.debug('validateaddress failed: {}'.format(address))
return False return False
def isValidAddressHash(self, address_hash: bytes) -> bool:
hash_len = len(address_hash)
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool: def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_wallet('getaddressinfo', [address]) addr_info = self.rpc_wallet('getaddressinfo', [address])
if not or_watch_only: if not or_watch_only:
@ -422,11 +443,11 @@ class BTCInterface(CoinInterface):
return segwit_addr.encode(bech32_prefix, version, pkh) return segwit_addr.encode(bech32_prefix, version, pkh)
def pkh_to_address(self, pkh: bytes) -> str: def pkh_to_address(self, pkh: bytes) -> str:
# pkh is hash160(pk) # pkh is ripemd160(sha256(pk))
assert (len(pkh) == 20) assert (len(pkh) == 20)
prefix = self.chainparams_network()['pubkey_address'] prefix = self.chainparams_network()['pubkey_address']
data = bytes((prefix,)) + pkh data = bytes((prefix,)) + pkh
checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() checksum = sha256(sha256(data))
return b58encode(data + checksum[0:4]) return b58encode(data + checksum[0:4])
def sh_to_address(self, sh: bytes) -> str: def sh_to_address(self, sh: bytes) -> str:
@ -452,12 +473,6 @@ class BTCInterface(CoinInterface):
assert (len(pk) == 33) assert (len(pk) == 33)
return self.pkh_to_address(hash160(pk)) return self.pkh_to_address(hash160(pk))
def getNewSecretKey(self) -> bytes:
return i2b(getSecretInt())
def getPubkey(self, privkey):
return PublicKey.from_secret(privkey).format()
def getAddressHashFromKey(self, key: bytes) -> bytes: def getAddressHashFromKey(self, key: bytes) -> bytes:
pk = self.getPubkey(key) pk = self.getPubkey(key)
return hash160(pk) return hash160(pk)
@ -465,13 +480,6 @@ class BTCInterface(CoinInterface):
def getSeedHash(self, seed) -> bytes: def getSeedHash(self, seed) -> bytes:
return self.getAddressHashFromKey(seed)[::-1] return self.getAddressHashFromKey(seed)[::-1]
def verifyKey(self, k: bytes) -> bool:
i = b2i(k)
return (i < ep.o and i > 0)
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes)
def encodeKey(self, key_bytes: bytes) -> str: def encodeKey(self, key_bytes: bytes) -> str:
wif_prefix = self.chainparams_network()['key_prefix'] wif_prefix = self.chainparams_network()['key_prefix']
return toWIF(wif_prefix, key_bytes) return toWIF(wif_prefix, key_bytes)
@ -491,13 +499,6 @@ class BTCInterface(CoinInterface):
def decodeKey(self, k: str) -> bytes: def decodeKey(self, k: str) -> bytes:
return decodeWif(k) return decodeWif(k)
def sumKeys(self, ka, kb):
# TODO: Add to coincurve
return i2b((b2i(ka) + b2i(kb)) % ep.o)
def sumPubkeys(self, Ka, Kb):
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
# p2wpkh # p2wpkh
return CScript([OP_0, pkh]) return CScript([OP_0, pkh])
@ -508,24 +509,6 @@ class BTCInterface(CoinInterface):
tx.deserialize(BytesIO(tx_bytes)) tx.deserialize(BytesIO(tx_bytes))
return tx return tx
def extractScriptLockScriptValues(self, script_bytes: bytes):
script_len = len(script_bytes)
ensure(script_len == 71, 'Bad script length')
o = 0
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == 33)
o += 2
pk1 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == 33)
o += 1
pk2 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_2)
ensure_op(script_bytes[o + 1] == OP_CHECKMULTISIG)
return pk1, pk2
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 = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
@ -535,37 +518,6 @@ class BTCInterface(CoinInterface):
def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): def fundSCLockTx(self, tx_bytes, feerate, vkbv=None):
return self.fundTx(tx_bytes, feerate) return self.fundTx(tx_bytes, feerate)
def extractScriptLockRefundScriptValues(self, script_bytes: bytes):
script_len = len(script_bytes)
ensure(script_len > 73, 'Bad script length')
ensure_op(script_bytes[0] == OP_IF)
ensure_op(script_bytes[1] == OP_2)
ensure_op(script_bytes[2] == 33)
pk1 = script_bytes[3: 3 + 33]
ensure_op(script_bytes[36] == 33)
pk2 = script_bytes[37: 37 + 33]
ensure_op(script_bytes[70] == OP_2)
ensure_op(script_bytes[71] == OP_CHECKMULTISIG)
ensure_op(script_bytes[72] == OP_ELSE)
o = 73
csv_val, nb = decodeScriptNum(script_bytes, o)
o += nb
ensure(script_len == o + 5 + 33, 'Bad script length') # Fails if script too long
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY)
o += 1
ensure_op(script_bytes[o] == OP_DROP)
o += 1
ensure_op(script_bytes[o] == 33)
o += 1
pk3 = script_bytes[o: o + 33]
o += 33
ensure_op(script_bytes[o] == OP_CHECKSIG)
o += 1
ensure_op(script_bytes[o] == OP_ENDIF)
return pk1, pk2, csv_val, pk3
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript: def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> CScript:
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal) Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
@ -657,7 +609,7 @@ class BTCInterface(CoinInterface):
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 locked_coin = tx_lock_refund.vout[locked_n].nValue
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund.rehash() tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256 tx_lock_refund_hash_int = tx_lock_refund.sha256
@ -744,7 +696,7 @@ class BTCInterface(CoinInterface):
ensure(locked_coin == swap_value, 'Bad locked value') ensure(locked_coin == swap_value, 'Bad locked value')
# Check script # Check script
A, B = self.extractScriptLockScriptValues(script_out) A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey') ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey')
@ -757,7 +709,7 @@ class BTCInterface(CoinInterface):
for pi in tx.vin: for pi in tx.vin:
ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True]) ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True])
prevout = ptx['vout'][pi.prevout.n] prevout = ptx['vout'][pi.prevout.n]
inputs_value += make_int(prevout['value']) inputs_value += self.make_int(prevout['value'])
prevout_type = prevout['scriptPubKey']['type'] prevout_type = prevout['scriptPubKey']['type']
if prevout_type == 'witness_v0_keyhash': if prevout_type == 'witness_v0_keyhash':
@ -812,7 +764,7 @@ class BTCInterface(CoinInterface):
locked_coin = tx.vout[locked_n].nValue locked_coin = tx.vout[locked_n].nValue
# Check script and values # Check script and values
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey') ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value') ensure(csv_val == csv_val_expect, 'Bad script csv value')
@ -924,27 +876,30 @@ class BTCInterface(CoinInterface):
return True return True
def signTx(self, key_bytes, tx_bytes, input_n, prevout_script, prevout_value): def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
eck = PrivateKey(key_bytes) eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,)) return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
def signTxOtVES(self, key_sign, pubkey_encrypt, tx_bytes, input_n, prevout_script, prevout_value): def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash) return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, sig_hash)
def verifyTxOtVES(self, tx_bytes, ct, Ks, Ke, input_n, prevout_script, prevout_value): def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value):
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct) return ecdsaotves_enc_verify(Ks, Ke, sig_hash, ct)
def decryptOtVES(self, k, esig): def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,)) return ecdsaotves_dec_sig(k, esig) + bytes((SIGHASH_ALL,))
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
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) tx = self.loadTx(tx_bytes)
sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value) sig_hash = SegwitV0SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
@ -952,11 +907,7 @@ class BTCInterface(CoinInterface):
pubkey = PublicKey(K) 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 verifySig(self, pubkey, signed_hash, sig): def fundTx(self, tx: bytes, feerate) -> bytes:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
def fundTx(self, tx, feerate):
feerate_str = self.format_amount(feerate) feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
options = { options = {
@ -966,7 +917,7 @@ class BTCInterface(CoinInterface):
rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), options]) rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), options])
return bytes.fromhex(rv['hex']) return bytes.fromhex(rv['hex'])
def listInputs(self, tx_bytes): def listInputs(self, tx_bytes: bytes):
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
all_locked = self.rpc_wallet('listlockunspent') all_locked = self.rpc_wallet('listlockunspent')
@ -1018,20 +969,20 @@ class BTCInterface(CoinInterface):
return hash160(K) return hash160(K)
def getScriptDest(self, script): def getScriptDest(self, script):
return CScript([OP_0, hashlib.sha256(script).digest()]) return CScript([OP_0, sha256(script)])
def getScriptScriptSig(self, script: bytes) -> bytes: def getScriptScriptSig(self, script: bytes) -> bytes:
return bytes() return bytes()
def getP2SHP2WSHDest(self, script): def getP2SHP2WSHDest(self, script):
script_hash = hashlib.sha256(script).digest() script_hash = sha256(script)
assert len(script_hash) == 32 assert len(script_hash) == 32
p2wsh_hash = hash160(CScript([OP_0, script_hash])) p2wsh_hash = hash160(CScript([OP_0, script_hash]))
assert len(p2wsh_hash) == 20 assert len(p2wsh_hash) == 20
return CScript([OP_HASH160, p2wsh_hash, OP_EQUAL]) return CScript([OP_HASH160, p2wsh_hash, OP_EQUAL])
def getP2SHP2WSHScriptSig(self, script): def getP2SHP2WSHScriptSig(self, script):
script_hash = hashlib.sha256(script).digest() script_hash = sha256(script)
assert len(script_hash) == 32 assert len(script_hash) == 32
return CScript([CScript([OP_0, script_hash, ]), ]) return CScript([CScript([OP_0, script_hash, ]), ])
@ -1050,7 +1001,7 @@ class BTCInterface(CoinInterface):
def getWalletTransaction(self, txid: bytes): def getWalletTransaction(self, txid: bytes):
try: try:
return bytes.fromhex(self.rpc('gettransaction', [txid.hex()])) return bytes.fromhex(self.rpc_wallet('gettransaction', [txid.hex()]))
except Exception as ex: except Exception as ex:
# TODO: filter errors # TODO: filter errors
return None return None
@ -1072,11 +1023,11 @@ class BTCInterface(CoinInterface):
tx.wit.vtxinwit.clear() tx.wit.vtxinwit.clear()
return tx.serialize() return tx.serialize()
def extractLeaderSig(self, tx_bytes) -> bytes: def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[1] return tx.wit.vtxinwit[0].scriptWitness.stack[1]
def extractFollowerSig(self, tx_bytes) -> bytes: def extractFollowerSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[2] return tx.wit.vtxinwit[0].scriptWitness.stack[2]
@ -1099,9 +1050,6 @@ class BTCInterface(CoinInterface):
return bytes.fromhex(self.publishTx(b_lock_tx)) return bytes.fromhex(self.publishTx(b_lock_tx))
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int: def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int:
wsf = self.witnessScaleFactor() wsf = self.witnessScaleFactor()
len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes
@ -1131,10 +1079,10 @@ class BTCInterface(CoinInterface):
witness_bytes = 109 witness_bytes = 109
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes) vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = round(fee_rate * vsize / 1000) pay_fee = round(fee_rate * vsize / 1000)
self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.') self._log.info(f'BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {vsize}, {pay_fee}.')
return pay_fee return pay_fee
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) -> bytes: 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()) self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ]) wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
@ -1149,7 +1097,7 @@ class BTCInterface(CoinInterface):
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
script_lock = self.getScriptForPubkeyHash(Kbs) script_lock = self.getScriptForPubkeyHash(Kbs)
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) chain_b_lock_txid_int = b2i(chain_b_lock_txid)
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n), tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n),
nSequence=0, nSequence=0,
@ -1171,11 +1119,11 @@ class BTCInterface(CoinInterface):
addr_info = self.rpc_wallet('getaddressinfo', [address]) addr_info = self.rpc_wallet('getaddressinfo', [address])
return addr_info['iswatchonly'] return addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script): def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
return self.encodeScriptDest(lock_tx_dest) return self.encodeScriptDest(lock_tx_dest)
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -1244,30 +1192,30 @@ class BTCInterface(CoinInterface):
'vout': utxo['vout']}) 'vout': utxo['vout']})
return rv, chain_height return rv, chain_height
def withdrawCoin(self, value, addr_to, subfee): def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
params = [addr_to, value, '', '', subfee, True, self._conf_target] params = [addr_to, value, '', '', subfee, True, self._conf_target]
return self.rpc_wallet('sendtoaddress', params) return self.rpc_wallet('sendtoaddress', params)
def signCompact(self, k, message): def signCompact(self, k, message: str) -> bytes:
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() message_hash = sha256(bytes(message, 'utf-8'))
privkey = PrivateKey(k) privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None)[:64] return privkey.sign_recoverable(message_hash, hasher=None)[:64]
def signRecoverable(self, k, message): def signRecoverable(self, k, message: str) -> bytes:
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() message_hash = sha256(bytes(message, 'utf-8'))
privkey = PrivateKey(k) privkey = PrivateKey(k)
return privkey.sign_recoverable(message_hash, hasher=None) return privkey.sign_recoverable(message_hash, hasher=None)
def verifyCompactSig(self, K, message, sig): def verifyCompactSig(self, K, message: str, sig) -> None:
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() message_hash = sha256(bytes(message, 'utf-8'))
pubkey = PublicKey(K) pubkey = PublicKey(K)
rv = pubkey.verify_compact(sig, message_hash, hasher=None) rv = pubkey.verify_compact(sig, message_hash, hasher=None)
assert (rv is True) assert (rv is True)
def verifySigAndRecover(self, sig, message): def verifySigAndRecover(self, sig, message: str) -> bytes:
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() message_hash = sha256(bytes(message, 'utf-8'))
pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None) pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None)
return pubkey.format() return pubkey.format()
@ -1276,7 +1224,7 @@ class BTCInterface(CoinInterface):
message_magic = self.chainparams()['message_magic'] message_magic = self.chainparams()['message_magic']
message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8') message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8')
message_hash = hashlib.sha256(hashlib.sha256(message_bytes).digest()).digest() message_hash = sha256(sha256(message_bytes))
signature_bytes = base64.b64decode(signature) signature_bytes = base64.b64decode(signature)
rec_id = (signature_bytes[0] - 27) & 3 rec_id = (signature_bytes[0] - 27) & 3
signature_bytes = signature_bytes[1:] + bytes((rec_id,)) signature_bytes = signature_bytes[1:] + bytes((rec_id,))
@ -1294,40 +1242,6 @@ class BTCInterface(CoinInterface):
def showLockTransfers(self, kbv, Kbs, restore_height): def showLockTransfers(self, kbv, Kbs, restore_height):
raise ValueError('Unimplemented') raise ValueError('Unimplemented')
def getLockTxSwapOutputValue(self, bid, xmr_swap):
return bid.amount
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
return xmr_swap.a_swap_refund_value
def getLockRefundTxSwapOutput(self, xmr_swap):
# Only one prevout exists
return 0
def getScriptLockTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes(len(script))
]
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
return [
b'',
bytes(72),
bytes(72),
bytes((1,)),
bytes(len(script))
]
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
return [
bytes(72),
b'',
bytes(len(script))
]
def getWitnessStackSerialisedLength(self, witness_stack): def getWitnessStackSerialisedLength(self, witness_stack):
length = getCompactSizeLen(len(witness_stack)) length = getCompactSizeLen(len(witness_stack))
for e in witness_stack: for e in witness_stack:
@ -1430,6 +1344,12 @@ class BTCInterface(CoinInterface):
prove_utxos = [] # TODO: Send specific utxos prove_utxos = [] # TODO: Send specific utxos
return (sign_for_addr, signature, prove_utxos) return (sign_for_addr, signature, prove_utxos)
def encodeProofUtxos(self, proof_utxos):
packed_utxos = bytes()
for utxo in proof_utxos:
packed_utxos += utxo[0] + utxo[1].to_bytes(2, 'big')
return packed_utxos
def decodeProofUtxos(self, msg_utxos): def decodeProofUtxos(self, msg_utxos):
proof_utxos = [] proof_utxos = []
if len(msg_utxos) > 0: if len(msg_utxos) > 0:
@ -1459,7 +1379,7 @@ class BTCInterface(CoinInterface):
return True return True
return False return False
def isWalletEncryptedLocked(self): def isWalletEncryptedLocked(self) -> (bool, bool):
wallet_info = self.rpc_wallet('getwalletinfo') wallet_info = self.rpc_wallet('getwalletinfo')
encrypted = 'unlocked_until' in wallet_info encrypted = 'unlocked_until' in wallet_info
locked = encrypted and wallet_info['unlocked_until'] <= 0 locked = encrypted and wallet_info['unlocked_until'] <= 0
@ -1502,7 +1422,7 @@ class BTCInterface(CoinInterface):
return CScript([OP_HASH160, script_hash, OP_EQUAL]) return CScript([OP_HASH160, script_hash, OP_EQUAL])
def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray: def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray:
return CScript([OP_0, hashlib.sha256(script).digest()]) return CScript([OP_0, sha256(script)])
def findTxnByHash(self, txid_hex: str): def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns # Only works for wallet txns
@ -1515,10 +1435,10 @@ class BTCInterface(CoinInterface):
return {'txid': txid_hex, 'amount': 0, 'height': rv['blockheight']} return {'txid': txid_hex, 'amount': 0, 'height': rv['blockheight']}
return None return None
def createRedeemTxn(self, prevout, output_addr: str, output_value: int) -> str: def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes = None) -> str:
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']))) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout'])))
pkh = self.decodeAddress(output_addr) pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh) script = self.getScriptForPubkeyHash(pkh)
@ -1526,11 +1446,11 @@ class BTCInterface(CoinInterface):
tx.rehash() tx.rehash()
return tx.serialize().hex() return tx.serialize().hex()
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int) -> str: def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes = None) -> str:
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
tx.nLockTime = locktime tx.nLockTime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), nSequence=sequence,)) tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), nSequence=sequence,))
pkh = self.decodeAddress(output_addr) pkh = self.decodeAddress(output_addr)
script = self.getScriptForPubkeyHash(pkh) script = self.getScriptForPubkeyHash(pkh)
@ -1550,6 +1470,30 @@ class BTCInterface(CoinInterface):
tx_vsize += 323 if redeem else 287 tx_vsize += 323 if redeem else 287
return tx_vsize return tx_vsize
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
txjs = self.rpc('decoderawtransaction', [txn_hex])
if self.using_segwit():
p2wsh = self.getScriptDest(txn_script)
n = getVoutByScriptPubKey(txjs, p2wsh.hex())
else:
addr_to = self.encode_p2sh(txn_script)
n = getVoutByAddress(txjs, addr_to)
return {
'txid': txjs['txid'],
'vout': n,
'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'],
'redeemScript': txn_script.hex(),
'amount': txjs['vout'][n]['value']
}
def isTxExistsError(self, err_str: str) -> bool:
return 'Transaction already in block chain' in err_str
def isTxNonFinalError(self, err_str: str) -> bool:
return 'non-BIP68-final' in err_str or 'non-final' in err_str
def testBTCInterface(): def testBTCInterface():
print('TODO: testBTCInterface') print('TODO: testBTCInterface')

@ -8,7 +8,7 @@
from .btc import BTCInterface from .btc import BTCInterface
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress from basicswap.util.address import decodeAddress
from mnemonic import Mnemonic from basicswap.contrib.mnemonic import Mnemonic
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
@ -25,11 +25,11 @@ class DASHInterface(BTCInterface):
self._wallet_passphrase = '' self._wallet_passphrase = ''
self._have_checked_seed = False self._have_checked_seed = False
def seedToMnemonic(self, key: bytes) -> str: def entropyToMnemonic(self, key: bytes) -> str:
return Mnemonic('english').to_mnemonic(key) return Mnemonic('english').to_mnemonic(key)
def initialiseWallet(self, key: bytes): def initialiseWallet(self, key: bytes):
words = self.seedToMnemonic(key) words = self.entropyToMnemonic(key)
mnemonic_passphrase = '' mnemonic_passphrase = ''
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase]) self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
@ -66,7 +66,7 @@ class DASHInterface(BTCInterface):
add_bytes = 107 add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000) 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 return pay_fee
def findTxnByHash(self, txid_hex: str): def findTxnByHash(self, txid_hex: str):

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

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import traceback
from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
x = Jsonrpc(url)
x.__handler = None
v = x.json_request(method, params)
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
return r['result']
def openrpc(rpc_port, auth, host='127.0.0.1'):
try:
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC error ' + str(ex))
def make_rpc_func(port, auth, host='127.0.0.1'):
port = port
auth = auth
host = host
def rpc_func(method, params=None):
nonlocal port, auth, host
return callrpc(port, auth, method, params, host)
return rpc_func

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
OP_0 = 0x00
OP_DATA_1 = 0x01
OP_1NEGATE = 0x4f
OP_1 = 0x51
OP_IF = 0x63
OP_ELSE = 0x67
OP_ENDIF = 0x68
OP_DROP = 0x75
OP_DUP = 0x76
OP_EQUAL = 0x87
OP_EQUALVERIFY = 0x88
OP_PUSHDATA1 = 0x4c
OP_PUSHDATA2 = 0x4d
OP_PUSHDATA4 = 0x4e
OP_HASH160 = 0xa9
OP_CHECKSIG = 0xac
OP_CHECKMULTISIG = 0xae
OP_CHECKSEQUENCEVERIFY = 0xb2
def push_script_data(data_array: bytearray, data: bytes) -> None:
len_data: int = len(data)
if len_data == 0 or (len_data == 1 and data[0] == 0):
data_array += bytes((OP_0,))
return
if len_data == 1 and data[0] <= 16:
data_array += bytes((OP_1 - 1 + data[0],))
return
if len_data == 1 and data[0] == 0x81:
data_array += bytes((OP_1NEGATE,))
return
if len_data < OP_PUSHDATA1:
data_array += len_data.to_bytes(1, 'little')
elif len_data <= 0xff:
data_array += bytes((OP_PUSHDATA1, len_data))
elif len_data <= 0xffff:
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
else:
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
data_array += data

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import select
import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info('Creating DCR wallet')
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == 'nt':
str_args = ' '.join(args)
p = subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
else:
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
def readOutput():
buf = os.read(pipe_r, 1024).decode('utf-8')
response = None
if 'Opened wallet' in buf:
pass
elif 'Use the existing configured private passphrase' in buf:
response = b'y\n'
elif 'Do you want to add an additional layer of encryption' in buf:
response = b'n\n'
elif 'Do you have an existing wallet seed' in buf:
response = b'y\n'
elif 'Enter existing wallet seed' in buf:
response = (hex_seed + '\n').encode('utf-8')
elif 'Seed input successful' in buf:
pass
elif 'Upgrading database from version' in buf:
pass
elif 'Ticket commitments db upgrade done' in buf:
pass
elif 'The wallet has been created successfully' in buf:
pass
else:
raise ValueError(f'Unexpected output: {buf}')
if response is not None:
p.stdin.write(response)
p.stdin.flush()
try:
while p.poll() is None:
if os.name == 'nt':
readOutput()
delay_event.wait(0.1)
continue
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
readOutput()
delay_event.wait(0.1)
except Exception as e:
logging.error(f'dcrwallet --create failed: {e}')
finally:
if p.poll() is None:
p.terminate()
os.close(pipe_r)
os.close(pipe_w)
p.stdin.close()

@ -5,8 +5,8 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import random
import hashlib import hashlib
import random
from .btc import BTCInterface, find_vout_for_address_from_txobj from .btc import BTCInterface, find_vout_for_address_from_txobj
from basicswap.util import ( from basicswap.util import (
@ -42,9 +42,6 @@ class FIROInterface(BTCInterface):
# No multiwallet support # 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 getExchangeName(self, exchange_name): def getExchangeName(self, exchange_name):
return 'zcoin' return 'zcoin'
@ -52,6 +49,9 @@ class FIROInterface(BTCInterface):
# load with -hdseed= parameter # load with -hdseed= parameter
pass pass
def checkWallets(self) -> int:
return 1
def getNewAddress(self, use_segwit, label='swap_receive'): def getNewAddress(self, use_segwit, label='swap_receive'):
return self.rpc('getnewaddress', [label]) return self.rpc('getnewaddress', [label])
# addr_plain = self.rpc('getnewaddress', [label]) # addr_plain = self.rpc('getnewaddress', [label])
@ -76,7 +76,7 @@ class FIROInterface(BTCInterface):
return addr_info['ismine'] return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly'] return addr_info['ismine'] or addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script): def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest) address = self.encodeScriptDest(lock_tx_dest)
@ -87,7 +87,7 @@ class FIROInterface(BTCInterface):
return address return address
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -201,7 +201,7 @@ class FIROInterface(BTCInterface):
add_bytes = 107 add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000) 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 return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
@ -337,7 +337,7 @@ class FIROInterface(BTCInterface):
return return
current_height -= 1 current_height -= 1
def getBlockWithTxns(self, block_hash): def getBlockWithTxns(self, block_hash: str):
# TODO: Bypass decoderawtransaction and getblockheader # TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc('getblock', [block_hash, False]) block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash]) block_header = self.rpc('getblockheader', [block_hash])
@ -355,9 +355,11 @@ class FIROInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }

@ -13,9 +13,15 @@ from coincurve.keys import (
PublicKey, PublicKey,
PrivateKey, PrivateKey,
) )
from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput from basicswap.interface.btc import (
BTCInterface,
extractScriptLockRefundScriptValues,
findOutput,
find_vout_for_address_from_txobj,
)
from basicswap.rpc import make_rpc_func from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.contrib.mnemonic import Mnemonic
from basicswap.interface.contrib.nav_test_framework.mininode import ( from basicswap.interface.contrib.nav_test_framework.mininode import (
CTxIn, CTxIn,
CTxOut, CTxOut,
@ -24,7 +30,6 @@ from basicswap.interface.contrib.nav_test_framework.mininode import (
CTransaction, CTransaction,
CTxInWitness, CTxInWitness,
FromHex, FromHex,
uint256_from_str,
) )
from basicswap.util.crypto import hash160 from basicswap.util.crypto import hash160
from basicswap.util.address import ( from basicswap.util.address import (
@ -33,7 +38,7 @@ from basicswap.util.address import (
encodeAddress, encodeAddress,
) )
from basicswap.util import ( from basicswap.util import (
i2b, i2h, b2i, i2b, i2h,
ensure, ensure,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
@ -48,7 +53,6 @@ from basicswap.interface.contrib.nav_test_framework.script import (
SIGHASH_ALL, SIGHASH_ALL,
SegwitVersion1SignatureHash, SegwitVersion1SignatureHash,
) )
from mnemonic import Mnemonic
class NAVInterface(BTCInterface): class NAVInterface(BTCInterface):
@ -69,20 +73,20 @@ class NAVInterface(BTCInterface):
# No multiwallet support # 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 use_p2shp2wsh(self) -> bool: def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh # p2sh-p2wsh
return True return True
def seedToMnemonic(self, key): def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic('english').to_mnemonic(key) return Mnemonic('english').to_mnemonic(key)
def initialiseWallet(self, key): def initialiseWallet(self, key):
# load with -importmnemonic= parameter # Load with -importmnemonic= parameter
pass pass
def checkWallets(self) -> int:
return 1
def getWalletSeedID(self): def getWalletSeedID(self):
return self.rpc('getwalletinfo')['hdmasterkeyid'] return self.rpc('getwalletinfo')['hdmasterkeyid']
@ -305,7 +309,7 @@ class NAVInterface(BTCInterface):
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 = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
scriptSig=self.getScriptScriptSig(txn_script))) scriptSig=self.getScriptScriptSig(txn_script)))
@ -319,7 +323,7 @@ class NAVInterface(BTCInterface):
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
tx.nLockTime = locktime tx.nLockTime = locktime
prev_txid = uint256_from_str(bytes.fromhex(prevout['txid'])[::-1]) prev_txid = b2i(bytes.fromhex(prevout['txid']))
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']), tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
nSequence=sequence, nSequence=sequence,
scriptSig=self.getScriptScriptSig(txn_script))) scriptSig=self.getScriptScriptSig(txn_script)))
@ -415,7 +419,7 @@ class NAVInterface(BTCInterface):
return return
current_height -= 1 current_height -= 1
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
@ -479,16 +483,18 @@ class NAVInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }
return block_rv return block_rv
def getScriptScriptSig(self, script: bytes) -> bytearray: def getScriptScriptSig(self, script: bytes) -> bytes:
return self.getP2SHP2WSHScriptSig(script) return self.getP2SHP2WSHScriptSig(script)
def getScriptDest(self, script): def getScriptDest(self, script):
@ -510,7 +516,7 @@ class NAVInterface(BTCInterface):
tx.vout.append(self.txoType()(output_amount, script_pk)) tx.vout.append(self.txoType()(output_amount, script_pk))
return tx.serialize() 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) -> bytes: 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()) self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ]) wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
@ -524,7 +530,7 @@ class NAVInterface(BTCInterface):
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) chain_b_lock_txid_int = b2i(chain_b_lock_txid)
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs)) script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
@ -676,7 +682,7 @@ class NAVInterface(BTCInterface):
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 locked_coin = tx_lock_refund.vout[locked_n].nValue
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
tx_lock_refund.rehash() tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256 tx_lock_refund_hash_int = tx_lock_refund.sha256

@ -7,9 +7,6 @@
from .btc import BTCInterface from .btc import BTCInterface
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.util import (
make_int,
)
class NMCInterface(BTCInterface): class NMCInterface(BTCInterface):
@ -17,7 +14,7 @@ class NMCInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.NMC return Coins.NMC
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False): 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 self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end') self._log.debug('[rm] scantxoutset end')
@ -26,7 +23,7 @@ class NMCInterface(BTCInterface):
if txid and o['txid'] != txid.hex(): if txid and o['txid'] != txid.hex():
continue continue
# Verify amount # Verify amount
if make_int(o['amount']) != int(bid_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']) self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
continue continue

@ -14,11 +14,10 @@ from basicswap.contrib.test_framework.messages import (
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
OP_0, OP_0,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
) )
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
make_int,
TemporaryError, TemporaryError,
) )
from basicswap.util.script import ( from basicswap.util.script import (
@ -27,10 +26,15 @@ from basicswap.util.script import (
getWitnessElementLen, getWitnessElementLen,
) )
from basicswap.util.address import ( from basicswap.util.address import (
toWIF, encodeStealthAddress,
encodeStealthAddress) )
from basicswap.interface.btc import (
BTCInterface,
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
)
from basicswap.chainparams import Coins, chainparams from basicswap.chainparams import Coins, chainparams
from .btc import BTCInterface
class BalanceTypes(IntEnum): class BalanceTypes(IntEnum):
@ -74,6 +78,9 @@ class PARTInterface(BTCInterface):
super().__init__(coin_settings, network, swap_client) super().__init__(coin_settings, network, swap_client)
self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12))) self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12)))
def use_tx_vsize(self) -> bool:
return True
def setAnonTxRingSize(self, value): def setAnonTxRingSize(self, value):
ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value') ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value')
self._anon_tx_ring_size = value self._anon_tx_ring_size = value
@ -93,7 +100,7 @@ class PARTInterface(BTCInterface):
index_info = self.rpc('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo') index_info = self.rpc('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
return index_info['spentindex'] return index_info['spentindex']
def initialiseWallet(self, key): def initialiseWallet(self, key: bytes) -> None:
raise ValueError('TODO') raise ValueError('TODO')
def withdrawCoin(self, value, addr_to, subfee): def withdrawCoin(self, value, addr_to, subfee):
@ -345,21 +352,21 @@ class PARTInterfaceBlind(PARTInterface):
ensure(lock_output_n is not None, 'Output not found in tx') ensure(lock_output_n is not None, 'Output not found in tx')
# Check value # Check value
locked_txo_value = make_int(blinded_info['amount']) locked_txo_value = self.make_int(blinded_info['amount'])
ensure(locked_txo_value == swap_value, 'Bad locked value') ensure(locked_txo_value == swap_value, 'Bad locked value')
# Check script # Check script
lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex']) lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_txo_scriptpk == script_pk, 'Bad output script') ensure(lock_txo_scriptpk == script_pk, 'Bad output script')
A, B = self.extractScriptLockScriptValues(script_out) A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, 'Bad script leader pubkey') ensure(A == Kal, 'Bad script leader pubkey')
ensure(B == Kaf, 'Bad script follower pubkey') ensure(B == Kaf, 'Bad script follower pubkey')
# TODO: Check that inputs are unspent, rangeproofs and commitments sum # TODO: Check that inputs are unspent, rangeproofs and commitments sum
# Verify fee rate # Verify fee rate
vsize = lock_tx_obj['vsize'] vsize = lock_tx_obj['vsize']
fee_paid = make_int(lock_tx_obj['vout'][0]['ct_fee']) fee_paid = self.make_int(lock_tx_obj['vout'][0]['ct_fee'])
fee_rate_paid = fee_paid * 1000 // vsize fee_rate_paid = fee_paid * 1000 // vsize
@ -394,13 +401,13 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce) lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(lock_refund_output_n is not None, 'Output not found in tx') ensure(lock_refund_output_n is not None, 'Output not found in tx')
lock_refund_txo_value = make_int(blinded_info['amount']) lock_refund_txo_value = self.make_int(blinded_info['amount'])
# Check script # Check script
lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex']) lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex'])
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()]) script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script') ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script')
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out) A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, 'Bad script pubkey') ensure(A == Kal, 'Bad script pubkey')
ensure(B == Kaf, 'Bad script pubkey') ensure(B == Kaf, 'Bad script pubkey')
ensure(csv_val == csv_val_expect, 'Bad script csv value') ensure(csv_val == csv_val_expect, 'Bad script csv value')
@ -415,7 +422,7 @@ class PARTInterfaceBlind(PARTInterface):
ensure(rv['inputs_valid'] is True, 'Invalid inputs') ensure(rv['inputs_valid'] is True, 'Invalid inputs')
# Check value # Check value
fee_paid = make_int(lock_refund_tx_obj['vout'][0]['ct_fee']) fee_paid = self.make_int(lock_refund_tx_obj['vout'][0]['ct_fee'])
ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value') ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value')
# Check fee rate # Check fee rate
@ -463,7 +470,7 @@ class PARTInterfaceBlind(PARTInterface):
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script) dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes) vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_paid = make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee']) fee_paid = self.make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee'])
fee_rate_paid = fee_paid * 1000 // vsize fee_rate_paid = fee_paid * 1000 // vsize
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate)) ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
@ -527,7 +534,7 @@ class PARTInterfaceBlind(PARTInterface):
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options]) rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
lock_spend_tx_hex = rv['hex'] lock_spend_tx_hex = rv['hex']
lock_spend_tx_obj = self.rpc('decoderawtransaction', [lock_spend_tx_hex]) lock_spend_tx_obj = self.rpc('decoderawtransaction', [lock_spend_tx_hex])
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee']) pay_fee = self.make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
# lock_spend_tx_hex does not include the dummy witness stack # lock_spend_tx_hex does not include the dummy witness stack
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack) witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
@ -599,8 +606,8 @@ class PARTInterfaceBlind(PARTInterface):
ensure(rv['inputs_valid'] is True, 'Invalid inputs') ensure(rv['inputs_valid'] is True, 'Invalid inputs')
# Check amount # Check amount
fee_paid = make_int(lock_spend_tx_obj['vout'][0]['ct_fee']) fee_paid = self.make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
amount_difference = make_int(input_blinded_info['amount']) - make_int(output_blinded_info['amount']) amount_difference = self.make_int(input_blinded_info['amount']) - self.make_int(output_blinded_info['amount'])
ensure(fee_paid == amount_difference, 'Invalid output amount') ensure(fee_paid == amount_difference, 'Invalid output amount')
# Check fee # Check fee
@ -630,7 +637,7 @@ class PARTInterfaceBlind(PARTInterface):
addr_info = self.rpc_wallet('getaddressinfo', [addr_out]) addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey'] output_pubkey_hex = addr_info['pubkey']
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund) A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance # Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
@ -688,8 +695,7 @@ class PARTInterfaceBlind(PARTInterface):
else: else:
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']: if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()]) self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -703,7 +709,7 @@ class PARTInterfaceBlind(PARTInterface):
assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible
ensure(tx['outputs'][0]['type'] == 'blind', 'Output is not anon') ensure(tx['outputs'][0]['type'] == 'blind', 'Output is not anon')
if make_int(tx['outputs'][0]['amount']) == cb_swap_value: if self.make_int(tx['outputs'][0]['amount']) == cb_swap_value:
height = 0 height = 0
if tx['confirmations'] > 0: if tx['confirmations'] > 0:
chain_height = self.rpc('getblockcount') chain_height = self.rpc('getblockcount')
@ -714,15 +720,14 @@ class PARTInterfaceBlind(PARTInterface):
return -1 return -1
return None return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
Kbv = self.getPubkey(kbv) Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']: if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv) wif_spend_key = self.encodeKey(kbs)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key]) self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr)) self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -741,7 +746,7 @@ class PARTInterfaceBlind(PARTInterface):
raise ValueError('Too many spendable outputs') raise ValueError('Too many spendable outputs')
utxo = utxos[0] utxo = utxos[0]
utxo_sats = make_int(utxo['amount']) utxo_sats = self.make_int(utxo['amount'])
if spend_actual_balance and utxo_sats != cb_swap_value: if spend_actual_balance and utxo_sats != cb_swap_value:
self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value)) self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value))
@ -826,8 +831,7 @@ class PARTInterfaceAnon(PARTInterface):
else: else:
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']: if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()]) self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -841,7 +845,7 @@ class PARTInterfaceAnon(PARTInterface):
assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible
ensure(tx['outputs'][0]['type'] == 'anon', 'Output is not anon') ensure(tx['outputs'][0]['type'] == 'anon', 'Output is not anon')
if make_int(tx['outputs'][0]['amount']) == cb_swap_value: if self.make_int(tx['outputs'][0]['amount']) == cb_swap_value:
height = 0 height = 0
if tx['confirmations'] > 0: if tx['confirmations'] > 0:
chain_height = self.rpc('getblockcount') chain_height = self.rpc('getblockcount')
@ -852,15 +856,14 @@ class PARTInterfaceAnon(PARTInterface):
return -1 return -1
return None return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
Kbv = self.getPubkey(kbv) Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr]) addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']: if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix'] wif_scan_key = self.encodeKey(kbv)
wif_scan_key = toWIF(wif_prefix, kbv) wif_spend_key = self.encodeKey(kbs)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key]) self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr)) self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
@ -874,7 +877,7 @@ class PARTInterfaceAnon(PARTInterface):
raise ValueError('Too many spendable outputs') raise ValueError('Too many spendable outputs')
utxo = autxos[0] utxo = autxos[0]
utxo_sats = make_int(utxo['amount']) utxo_sats = self.make_int(utxo['amount'])
if spend_actual_balance and utxo_sats != cb_swap_value: if spend_actual_balance and utxo_sats != cb_swap_value:
self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value)) self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value))

@ -75,9 +75,11 @@ class PIVXInterface(BTCInterface):
block_rv = { block_rv = {
'hash': block_hash, 'hash': block_hash,
'previousblockhash': block_header['previousblockhash'],
'tx': tx_rv, 'tx': tx_rv,
'confirmations': block_header['confirmations'], 'confirmations': block_header['confirmations'],
'height': block_header['height'], 'height': block_header['height'],
'time': block_header['time'],
'version': block_header['version'], 'version': block_header['version'],
'merkleroot': block_header['merkleroot'], 'merkleroot': block_header['merkleroot'],
} }
@ -105,7 +107,7 @@ class PIVXInterface(BTCInterface):
add_bytes = 107 add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000) 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 return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:

@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from basicswap.chainparams import WOW_COIN, Coins
from .xmr import XMRInterface
class WOWInterface(XMRInterface):
@staticmethod
def coin_type():
return Coins.WOW
@staticmethod
def ticker_str() -> int:
return Coins.WOW.name
@staticmethod
def COIN():
return WOW_COIN
@staticmethod
def exp() -> int:
return 11

@ -24,20 +24,21 @@ from coincurve.dleag import (
verify_ed25519_point, verify_ed25519_point,
) )
from basicswap.interface import ( from basicswap.interface.base import (
Curves) Curves,
)
from basicswap.util import ( from basicswap.util import (
i2b, b2i, b2h, i2b, b2i, b2h,
dumpj, dumpj,
ensure, ensure,
make_int,
TemporaryError) TemporaryError)
from basicswap.util.network import ( from basicswap.util.network import (
is_private_ip_address) is_private_ip_address)
from basicswap.rpc_xmr import ( from basicswap.rpc_xmr import (
make_xmr_rpc_func, make_xmr_rpc_func,
make_xmr_rpc2_func) make_xmr_rpc2_func)
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins from basicswap.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface
class XMRInterface(CoinInterface): class XMRInterface(CoinInterface):
@ -49,6 +50,10 @@ class XMRInterface(CoinInterface):
def coin_type(): def coin_type():
return Coins.XMR return Coins.XMR
@staticmethod
def ticker_str() -> int:
return Coins.XMR.name
@staticmethod @staticmethod
def COIN(): def COIN():
return XMR_COIN return XMR_COIN
@ -128,9 +133,6 @@ class XMRInterface(CoinInterface):
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.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_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 checkWallets(self) -> int:
return 1
def setFeePriority(self, new_priority): 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 self._fee_priority = new_priority
@ -156,7 +158,7 @@ class XMRInterface(CoinInterface):
pass pass
self.rpc_wallet('open_wallet', params) self.rpc_wallet('open_wallet', params)
def initialiseWallet(self, key_view, key_spend, restore_height=None): def initialiseWallet(self, key_view: bytes, key_spend: bytes, restore_height=None) -> None:
with self._mx_wallet: with self._mx_wallet:
try: try:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
@ -212,7 +214,7 @@ class XMRInterface(CoinInterface):
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count'] rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count'] rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
except Exception as e: except Exception as e:
self._log.warning('XMR get_block_count failed with: %s', str(e)) self._log.warning(f'{self.ticker_str()} get_block_count failed with: {e}')
rv['verificationprogress'] = 0.0 rv['verificationprogress'] = 0.0
return rv return rv
@ -240,9 +242,6 @@ class XMRInterface(CoinInterface):
rv['locked'] = False rv['locked'] = False
return rv return rv
def walletRestoreHeight(self):
return self._restore_height
def getMainWalletAddress(self) -> str: def getMainWalletAddress(self) -> str:
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
@ -396,7 +395,7 @@ class XMRInterface(CoinInterface):
try: try:
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height'] current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid) self._log.info(f'findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}')
except Exception as e: except Exception as e:
self._log.info('rpc failed %s', str(e)) self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough current_height = None # If the transfer is available it will be deep enough
@ -411,7 +410,7 @@ class XMRInterface(CoinInterface):
return None return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
''' '''
Notes: Notes:
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
@ -480,9 +479,9 @@ class XMRInterface(CoinInterface):
balance = self.rpc_wallet('get_balance') balance = self.rpc_wallet('get_balance')
if balance['balance'] != balance['unlocked_balance']: if balance['balance'] != balance['unlocked_balance']:
raise ValueError('Balance must be fully confirmed to use sweep all.') raise ValueError('Balance must be fully confirmed to use sweep all.')
self._log.info('XMR {} sweep_all.'.format('estimate fee' if estimate_fee else 'withdraw')) self._log.info('{} {} sweep_all.'.format(self.ticker_str(), 'estimate fee' if estimate_fee else 'withdraw'))
self._log.debug('XMR balance: {}'.format(balance['balance'])) self._log.debug('{} balance: {}'.format(self.ticker_str(), balance['balance']))
params = {'address': addr_to, 'do_not_relay': estimate_fee} params = {'address': addr_to, 'do_not_relay': estimate_fee, 'subaddr_indices_all': True}
if self._fee_priority > 0: if self._fee_priority > 0:
params['priority'] = self._fee_priority params['priority'] = self._fee_priority
rv = self.rpc_wallet('sweep_all', params) rv = self.rpc_wallet('sweep_all', params)
@ -490,7 +489,7 @@ class XMRInterface(CoinInterface):
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 {'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 rv['tx_hash_list'][0]
value_sats: int = make_int(value, self.exp()) 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: if self._fee_priority > 0:
params['priority'] = self._fee_priority params['priority'] = self._fee_priority

@ -54,7 +54,7 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, 'address') address = get_data_entry(post_data, 'address')
if coin_type == Coins.XMR: if coin_type in (Coins.XMR, Coins.WOW):
value = None value = None
sweepall = get_data_entry(post_data, 'sweepall') sweepall = get_data_entry(post_data, 'sweepall')
if not isinstance(sweepall, bool): if not isinstance(sweepall, bool):
@ -74,7 +74,7 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
elif coin_type == Coins.LTC: elif coin_type == Coins.LTC:
type_from = get_data_entry_or(post_data, 'type_from', 'plain') type_from = get_data_entry_or(post_data, 'type_from', 'plain')
txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee) txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee)
elif coin_type == Coins.XMR: elif coin_type in (Coins.XMR, Coins.WOW):
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall) txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
else: else:
txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee) txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee)
@ -228,6 +228,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
'amount_from': ci_from.format_amount(o.amount_from), 'amount_from': ci_from.format_amount(o.amount_from),
'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()), 'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
'rate': ci_to.format_amount(o.rate), 'rate': ci_to.format_amount(o.rate),
'min_bid_amount': ci_from.format_amount(o.min_bid_amount),
} }
if with_extra_info: if with_extra_info:
offer_data['amount_negotiable'] = o.amount_negotiable offer_data['amount_negotiable'] = o.amount_negotiable
@ -428,27 +429,31 @@ def js_smsgaddresses(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
post_data = {} if post_string == '' else getFormData(post_string, is_json) post_data = {} if post_string == '' else getFormData(post_string, is_json)
if len(url_split) > 3: if len(url_split) > 3:
if url_split[3] == 'new': mode: str = url_split[3]
if mode == 'new':
addressnote = get_data_entry_or(post_data, 'addressnote', '') addressnote = get_data_entry_or(post_data, 'addressnote', '')
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote) new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
return bytes(json.dumps({'new_address': new_addr, 'pubkey': pubkey}), 'UTF-8') return bytes(json.dumps({'new_address': new_addr, 'pubkey': pubkey}), 'UTF-8')
if url_split[3] == 'add': if mode == 'add':
addressnote = get_data_entry_or(post_data, 'addressnote', '') addressnote = get_data_entry_or(post_data, 'addressnote', '')
pubkey_hex = get_data_entry(post_data, 'addresspubkey') pubkey_hex = get_data_entry(post_data, 'addresspubkey')
added_address = swap_client.addSMSGAddress(pubkey_hex, addressnote) added_address = swap_client.addSMSGAddress(pubkey_hex, addressnote)
return bytes(json.dumps({'added_address': added_address, 'pubkey': pubkey_hex}), 'UTF-8') return bytes(json.dumps({'added_address': added_address, 'pubkey': pubkey_hex}), 'UTF-8')
elif url_split[3] == 'edit': elif mode == 'edit':
address = get_data_entry(post_data, 'address') address = get_data_entry(post_data, 'address')
activeind = int(get_data_entry(post_data, 'active_ind')) activeind = int(get_data_entry(post_data, 'active_ind'))
addressnote = get_data_entry_or(post_data, 'addressnote', '') addressnote = get_data_entry_or(post_data, 'addressnote', '')
new_addr = swap_client.editSMSGAddress(address, activeind, addressnote) new_addr = swap_client.editSMSGAddress(address, activeind, addressnote)
return bytes(json.dumps({'edited_address': address}), 'UTF-8') return bytes(json.dumps({'edited_address': address}), 'UTF-8')
elif mode == 'disableall':
rv = swap_client.disableAllSMSGAddresses()
return bytes(json.dumps(rv), 'UTF-8')
filters = { filters = {
'exclude_inactive': post_data.get('exclude_inactive', True), 'exclude_inactive': post_data.get('exclude_inactive', True),
} }
return bytes(json.dumps(swap_client.listAllSMSGAddresses(filters)), 'UTF-8') return bytes(json.dumps(swap_client.listAllSMSGAddresses(filters)), 'UTF-8')
@ -680,7 +685,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.') raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.')
ci = swap_client.ci(coin) ci = swap_client.ci(coin)
if coin == Coins.XMR: if coin in (Coins.XMR, Coins.WOW):
key_view = swap_client.getWalletKey(coin, 1, for_ed25519=True) key_view = swap_client.getWalletKey(coin, 1, for_ed25519=True)
key_spend = swap_client.getWalletKey(coin, 2, for_ed25519=True) key_spend = swap_client.getWalletKey(coin, 2, for_ed25519=True)
address = ci.getAddressFromKeys(key_view, key_spend) address = ci.getAddressFromKeys(key_view, key_spend)
@ -688,7 +693,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
seed_key = swap_client.getWalletKey(coin, 1) seed_key = swap_client.getWalletKey(coin, 1)
if coin == Coins.DASH: if coin == Coins.DASH:
return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'mnemonic': ci.seedToMnemonic(seed_key)}), 'UTF-8') return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'mnemonic': ci.entropyToMnemonic(seed_key)}), 'UTF-8')
seed_id = ci.getSeedHash(seed_key) seed_id = ci.getSeedHash(seed_key)
return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'seed_id': seed_id.hex()}), 'UTF-8') return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed_key.hex(), 'seed_id': seed_id.hex()}), 'UTF-8')

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

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

@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: messages.proto
# Protobuf Python Version: 4.25.3
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xc0\x04\n\x0cOfferMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x11\n\tcoin_from\x18\x02 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x03 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x06 \x01(\x04\x12\x12\n\ntime_valid\x18\x07 \x01(\x04\x12\x33\n\tlock_type\x18\x08 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\t \x01(\r\x12\x11\n\tswap_type\x18\n \x01(\r\x12\x15\n\rproof_address\x18\x0b \x01(\t\x12\x17\n\x0fproof_signature\x18\x0c \x01(\t\x12\x15\n\rpkhash_seller\x18\r \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\x0e \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0f \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x10 \x01(\x04\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\x12\x13\n\x0bproof_utxos\x18\x13 \x01(\x0c\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xce\x01\n\nBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x06 \x01(\x0c\x12\x15\n\rproof_address\x18\x07 \x01(\t\x12\x17\n\x0fproof_signature\x18\x08 \x01(\t\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"s\n\x0f\x42idMessage_test\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb7\x01\n\rXmrBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x0c\n\x04pkaf\x18\x06 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x07 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x08 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\t \x01(\x0c\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x03 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x04 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x05 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x07 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\x08 \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\t \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\n \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x81\x01\n\x13\x41\x44SBidIntentMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_OFFERMESSAGE']._serialized_start=30
_globals['_OFFERMESSAGE']._serialized_end=606
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=493
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=606
_globals['_BIDMESSAGE']._serialized_start=609
_globals['_BIDMESSAGE']._serialized_end=815
_globals['_BIDMESSAGE_TEST']._serialized_start=817
_globals['_BIDMESSAGE_TEST']._serialized_end=932
_globals['_BIDACCEPTMESSAGE']._serialized_start=934
_globals['_BIDACCEPTMESSAGE']._serialized_end=1020
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1022
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1083
_globals['_BIDREJECTMESSAGE']._serialized_start=1085
_globals['_BIDREJECTMESSAGE']._serialized_end=1144
_globals['_XMRBIDMESSAGE']._serialized_start=1147
_globals['_XMRBIDMESSAGE']._serialized_end=1330
_globals['_XMRSPLITMESSAGE']._serialized_start=1332
_globals['_XMRSPLITMESSAGE']._serialized_end=1416
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1419
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1675
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1677
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1791
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1793
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1881
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1883
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=1960
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=1963
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2092
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2094
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2206
# @@protoc_insertion_point(module_scope)

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -10,12 +10,15 @@ from basicswap.db import (
from basicswap.util import ( from basicswap.util import (
SerialiseNum, SerialiseNum,
) )
from basicswap.util.script import (
decodeScriptNum,
)
from basicswap.script import ( from basicswap.script import (
OpCodes, OpCodes,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
SwapTypes,
EventLogTypes, EventLogTypes,
SwapTypes,
) )
from . import ProtocolInterface from . import ProtocolInterface
@ -23,13 +26,13 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
ABS_LOCK_TIME_LEEWAY = 10 * 60 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) -> bytearray: 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([ script = bytearray([
OpCodes.OP_IF, OpCodes.OP_IF,
OpCodes.OP_SIZE, OpCodes.OP_SIZE,
0x01, 0x20, # 32 0x01, 0x20, # 32
OpCodes.OP_EQUALVERIFY, OpCodes.OP_EQUALVERIFY,
OpCodes.OP_SHA256, op_hash,
0x20]) \ 0x20]) \
+ secret_hash \ + secret_hash \
+ bytearray([ + bytearray([
@ -54,6 +57,46 @@ def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pk
return script 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:
return False, None, None, None, None
o = 7
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:
return False, script_hash, None, None, None
o += 4
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:
return False, script_hash, pkh_redeem, lock_val, None
o += 5
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:
return False, script_hash, pkh_redeem, lock_val, pkh_refund
return True, script_hash, pkh_redeem, lock_val, pkh_refund
def extractScriptSecretHash(script): def extractScriptSecretHash(script):
return script[7:39] return script[7:39]
@ -62,7 +105,7 @@ def redeemITx(self, bid_id: bytes, session):
bid, offer = self.getBidAndOffer(bid_id, session) bid, offer = self.getBidAndOffer(bid_id, session)
ci_from = self.ci(offer.coin_from) ci_from = self.ci(offer.coin_from)
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate') txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate', session=session)
txid = ci_from.publishTx(bytes.fromhex(txn)) txid = ci_from.publishTx(bytes.fromhex(txn))
bid.initiate_tx.spend_txid = bytes.fromhex(txid) bid.initiate_tx.spend_txid = bytes.fromhex(txid)

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from sqlalchemy.orm import scoped_session
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
) )
from basicswap.interface import Curves from basicswap.interface.base import Curves
from basicswap.chainparams import ( from basicswap.chainparams import (
Coins, Coins,
) )
@ -21,13 +19,17 @@ from basicswap.basicswap_util import (
from . import ProtocolInterface from . import ProtocolInterface
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScriptOp, CScript, CScriptOp,
OP_CHECKMULTISIG) OP_CHECKMULTISIG
)
def addLockRefundSigs(self, xmr_swap, ci): def addLockRefundSigs(self, xmr_swap, ci):
self.log.debug('Setting lock refund tx sigs') self.log.debug('Setting lock refund tx sigs')
witness_stack = [
b'', witness_stack = []
if ci.coin_type() not in (Coins.DCR, ):
witness_stack += [b'', ]
witness_stack += [
xmr_swap.al_lock_refund_tx_sig, xmr_swap.al_lock_refund_tx_sig,
xmr_swap.af_lock_refund_tx_sig, xmr_swap.af_lock_refund_tx_sig,
xmr_swap.a_lock_tx_script, xmr_swap.a_lock_tx_script,
@ -41,7 +43,7 @@ def addLockRefundSigs(self, xmr_swap, ci):
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key): def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
self.log.info('Manually recovering %s', bid_id.hex()) self.log.info('Manually recovering %s', bid_id.hex())
# Manually recover txn if other key is known # Manually recover txn if other key is known
session = scoped_session(self.session_factory) session = self.openSession()
try: try:
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id) bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex())) ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
@ -74,15 +76,15 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
address_to = self.getCachedStealthAddressForCoin(offer.coin_to) address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
amount = bid.amount_to amount = bid.amount_to
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True) 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.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) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), session)
session.commit() session.commit()
return txid return txid
finally: finally:
session.close() self.closeSession(session, commit=False)
session.remove()
def getChainBSplitKey(swap_client, bid, xmr_swap, offer): def getChainBSplitKey(swap_client, bid, xmr_swap, offer):

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os import os
import time
import json import json
import shlex import shlex
import urllib import urllib
import logging
import traceback import traceback
import subprocess import subprocess
from xmlrpc.client import ( from xmlrpc.client import (
@ -20,18 +18,6 @@ from xmlrpc.client import (
from .util import jsonDecimal from .util import jsonDecimal
def waitForRPC(rpc_func, expect_wallet=True, max_tries=7):
for i in range(max_tries + 1):
try:
rpc_func('getwalletinfo' if expect_wallet else 'getblockchaininfo')
return
except Exception as ex:
if i < max_tries:
logging.warning('Can\'t connect to RPC: %s. Retrying in %d second/s.', str(ex), (i + 1))
time.sleep(i + 1)
raise ValueError('waitForRPC failed')
class Jsonrpc(): class Jsonrpc():
# __getattr__ complicates extending ServerProxy # __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False, def __init__(self, uri, transport=None, encoding=None, verbose=False,

@ -26,3 +26,5 @@ class OpCodes(IntEnum):
OP_CHECKSIG = 0xac, OP_CHECKSIG = 0xac,
OP_CHECKLOCKTIMEVERIFY = 0xb1, OP_CHECKLOCKTIMEVERIFY = 0xb1,
OP_CHECKSEQUENCEVERIFY = 0xb2, OP_CHECKSEQUENCEVERIFY = 0xb2,
OP_SHA256_DECRED = 0xc0,

@ -5,14 +5,9 @@
<div class="flex flex-wrap items-center -m-2"> <div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-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>{{ breadcrumb_line_svg | safe }}</li>
<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> <p>Home</p>
</a> </a>
</li> </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=""> <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="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-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> <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">Update your BasicSwap / Wallets password.</p> <p class="font-normal text-coolGray-200 dark:text-white">Change or Set your BasicSwap / Wallets password.</p>
</div> </div>
</div> </div>
</div> </div>
@ -187,4 +182,4 @@ toggles.forEach(function (toggle_id, index) {
}); });
</script> </script>
</body> </body>
</html> </html>

@ -27,13 +27,13 @@
<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.0.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ 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> </div>
<div class="w-full md:w-1/2"> <div class="w-full md:w-1/2">
<div class="flex flex-wrap md:justify-end -mx-5"> <div class="flex flex-wrap md:justify-end -mx-5">
<div class="px-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 }} {{ github_svg | safe }}
</a> </a>
</div> </div>
@ -43,69 +43,67 @@
</div> </div>
</section> </section>
<script> <script>
(function() { var toggleImages = function() {
var toggleImages = function() { var html = document.querySelector('html');
var html = document.querySelector('html'); var darkImages = document.querySelectorAll('.dark-image');
var darkImages = document.querySelectorAll('.dark-image'); var lightImages = document.querySelectorAll('.light-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) { if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block'); toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none'); toggleImageDisplay(lightImages, 'none');
} else { } else {
toggleImageDisplay(darkImages, 'none'); toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block'); toggleImageDisplay(lightImages, 'block');
}
} }
};
var toggleImageDisplay = function(images, display) { var toggleImageDisplay = function(images, display) {
images.forEach(function(img) { images.forEach(function(img) {
img.style.display = display; img.style.display = display;
}); });
} };
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var themeToggle = document.getElementById('theme-toggle'); var themeToggle = document.getElementById('theme-toggle');
if (themeToggle) { if (themeToggle) {
themeToggle.addEventListener('click', function() { themeToggle.addEventListener('click', function() {
toggleImages(); toggleImages();
}); });
} }
toggleImages(); toggleImages();
}); });
})();
</script> </script>
<script> <script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-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)) { if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden'); themeToggleLightIcon.classList.remove('hidden');
} else { } else {
themeToggleDarkIcon.classList.remove('hidden'); themeToggleDarkIcon.classList.remove('hidden');
} }
function setTheme(theme) { function setTheme(theme) {
if (theme === 'light') { if (theme === 'light') {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light'); localStorage.setItem('color-theme', 'light');
} else { } else {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark'); localStorage.setItem('color-theme', 'dark');
} }
} }
document.getElementById('theme-toggle').addEventListener('click', () => { document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') { if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light'); setTheme('light');
} else { } else {
setTheme('dark'); setTheme('dark');
} }
themeToggleDarkIcon.classList.toggle('hidden'); themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden'); themeToggleLightIcon.classList.toggle('hidden');
toggleImages(); toggleImages();
}); });
</script> </script>

@ -1,5 +1,5 @@
<!DOCTYPE html> <!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"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -12,6 +12,7 @@
<link type="text/css" media="all" href="/static/css/style.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/main.js"></script>
<script src="/static/js/libs/flowbite.js"></script> <script src="/static/js/libs/flowbite.js"></script>
<script> <script>
const isDarkMode = const isDarkMode =
localStorage.getItem('color-theme') === 'dark' || localStorage.getItem('color-theme') === 'dark' ||
@ -19,7 +20,7 @@
window.matchMedia('(prefers-color-scheme: dark)').matches); window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) { if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light'); localStorage.setItem('color-theme', 'dark');
} }
document.documentElement.classList.toggle('dark', isDarkMode); document.documentElement.classList.toggle('dark', isDarkMode);
@ -129,6 +130,10 @@
<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> <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> {{ cog_svg | safe }} Settings</a>
</li> </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 %} {% if debug_mode == true %}
<li> <li>
<a href="/rpc" 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">RPC</span> <a href="/rpc" 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">RPC</span>
@ -260,26 +265,25 @@
<!-- dev mode icons on/off --> <!-- dev mode icons on/off -->
<ul class="xl:flex"> <ul class="xl:flex">
<li> <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 }} {{ debug_nerd_svg | safe }}
<span data-tooltip-target="tooltip-DEV" ></span> </div> </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 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> <p><b>Debug mode:</b> Active</p>
{% if debug_ui_mode == true %} {% if debug_ui_mode == true %}
<p><b>Debug UI mode:</b> Active</p> <p><b>Debug UI mode:</b> Active</p>
{% endif %} {% endif %}
</div> </div>
</li> </li>
</ul> </ul>
<!-- dev mode icons on/off --> <!-- dev mode icons on/off -->
{% endif %} {% endif %}
{% if encrypted == true %} {% if encrypted == true %}
<ul class="xl:flex"><li> <ul class="xl:flex"><li>
{% if locked == true %} {% 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"> <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 }} {{ wallet_locked_svg | safe }}</div>
<span data-tooltip-target="tooltip-locked-wallets" ></span> </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"> <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> <p><b>Wallets:</b> Locked </p>
</div> </div>
@ -287,8 +291,7 @@
<a href='/lock'> <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"> <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 }} {{ wallet_unlocked_svg | safe }}
<span data-tooltip-target="tooltip-unlocked-wallets" ></span> </div> </div>
<div class="tooltip-arrow" data-popper-arrow></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"> <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> <p><b>Wallets:</b> Unlocked </p>
</div> </div>
@ -303,7 +306,7 @@
<a href="/tor"> <a href="/tor">
<div data-tooltip-target="tooltip-tor" class="flex items-center text-gray-50 hover:text-gray-100 text-sm"> <div data-tooltip-target="tooltip-tor" class="flex items-center text-gray-50 hover:text-gray-100 text-sm">
{{ tor_purple_svg | safe }} {{ 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 <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 %} {% if tor_established == true %}
<br><b>Tor:</b> Connected <br><b>Tor:</b> Connected
@ -316,7 +319,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"> <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 }} {{ sun_svg | safe }}
{{ moon_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> <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> </button>
</div> </div>
@ -453,6 +455,11 @@
{{ settings_svg | safe }} {{ settings_svg | safe }}
</span><span>Settings</span></a> </span><span>Settings</span></a>
</li> </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 %} {% if debug_mode == true %}
<li> <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"> <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">

@ -10,14 +10,11 @@
<div class="w-auto p-1"> <div class="w-auto p-1">
{{ circular_info_messages_svg | safe }} {{ circular_info_messages_svg | safe }}
</div> </div>
<ul class="ml-4 mt-1"> <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-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> <li class="font-medium text-sm text-green-500 infomsg">{{ m[1] }}</li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="w-auto p-2"> <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> <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>

@ -6,7 +6,8 @@
<div class="md:w-1/2 pl-4"> <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.0.0</span>
<h3 class="mb-6 text-4xl md:text-5xl leading-tight text-coolGray-900 font-bold tracking-tighter dark:text-white">Welcome to BasicSwap DEX</h3> <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.
It’s free, completely trustless, and highly secure.</p>
<div class="flex flex-wrap mb-10 text-center md:text-left"> <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"> <div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
<a href="/wallets"> <a href="/wallets">
@ -69,4 +70,4 @@
</div> </div>
{% include 'footer.html' %} {% include 'footer.html' %}
</body> </body>
</html> </html>

@ -1,5 +1,5 @@
{% include 'header.html' %} {% 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"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
@ -57,19 +57,21 @@
{% if sent_bid_id %} {% if sent_bid_id %}
<section class="py-4" id="messages_send_bid_id" role="alert"> <section class="py-4" id="messages_send_bid_id" role="alert">
<div class="container px-4 mx-auto"> <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 flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2"> <div class="flex-1 p-2">
<div class="flex flex-wrap -m-1"> <div class="flex flex-wrap -m-1">
<div class="w-auto p-1"> <div class="w-auto p-1">
{{ confirm_green_svg | safe }} {{ circular_info_messages_svg | safe }}
</div> </div>
<div class="flex-1 p-1"> <ul class="ml-4 mt-1">
<h3 class="infomsg font-medium text-sm text-green-900">Sent Bid {{ sent_bid_id }}</h3></div> <li class="font-semibold text-sm text-green-500 error_msg"><span class="bold">INFO:</span></li>
</div> <li class="font-medium text-sm text-green-500 infomsg">Sent Bid {{ sent_bid_id }}</li>
</ul>
</div>
</div> </div>
<div class="w-auto p-2"> <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 }} {{ green_cross_close_svg | safe }}
</button> </button>
</div> </div>

@ -113,7 +113,7 @@
</div> </div>
</div> </div>
</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="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="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="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@ -129,7 +129,7 @@
<select class="cursor-not-allowed disabled-select pl-10 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="swap_type" id="swap_type" disabled> <select class="cursor-not-allowed disabled-select pl-10 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="swap_type" id="swap_type" disabled>
{% for a in swap_types %} {% for a in swap_types %}
<option{% if data.swap_type==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[1] }}</option> <option{% if data.swap_type==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[1] }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
@ -145,11 +145,11 @@
<div class="w-full md:flex-1 p-3"> <div class="w-full md:flex-1 p-3">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Send:</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Send:</p>
<div class="custom-select"> <div class="custom-select">
<div class="relative"> <div class="relative">
{{ input_down_arrow_offer_svg | safe }} {{ input_down_arrow_offer_svg | safe }}
<select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_from" name="coin_from" onchange="set_rate('coin_from');" disabled> <select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_from" name="coin_from" disabled>
<option value="-1">Select coin you send</option> <option value="-1">Select coin you send</option>
{% for c in coins_from %} {% for c in coins_from %}
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }} <option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}
@ -162,9 +162,9 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Send</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Send</p>
<div class="relative"> <div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');" readonly> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" readonly>
</div> </div>
</div> </div>
{% if data.swap_style == 'xmr' %} {% if data.swap_style == 'xmr' %}
@ -216,11 +216,11 @@
<div class="w-full md:flex-1 p-3"> <div class="w-full md:flex-1 p-3">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Get</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Get</p>
<div class="custom-select"> <div class="custom-select">
<div class="relative"> <div class="relative">
{{ input_down_arrow_offer_svg | safe }} {{ input_down_arrow_offer_svg | safe }}
<select class="cursor-not-allowed disabled-select select 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-5 focus:ring-0" id="coin_to" name="coin_to" onchange="set_rate('coin_to');" disabled> <select class="cursor-not-allowed disabled-select select 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-5 focus:ring-0" id="coin_to" name="coin_to" disabled>
<option value="-1"></option> <option value="-1"></option>
{% for c in coins %} {% for c in coins %}
<option{% if data.coin_to==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option> <option{% if data.coin_to==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option>
@ -233,9 +233,9 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Get</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Get</p>
<div class="relative"> <div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_to" name="amt_to" value="{{ data.amt_to }}" onchange="set_rate('amt_to');" readonly> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_to" name="amt_to" value="{{ data.amt_to }}" readonly>
</div> </div>
</div> </div>
{% if data.swap_style == 'xmr' and coin_to != '6' %} {% if data.swap_style == 'xmr' and coin_to != '6' %}
@ -301,7 +301,7 @@
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
{{ select_rate_svg | safe }} {{ select_rate_svg | safe }}
</div> </div>
<input class="cursor-not-allowed disabled-input pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');" readonly> <input class="cursor-not-allowed disabled-input pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" readonly>
</div> </div>
</div> </div>
</div> </div>
@ -426,7 +426,7 @@
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="step2" type="submit" value="Back" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600"> <button name="step2" type="submit" value="Back" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
<span>Back</span> <span>Back</span>
</button> </button>
</div> </div>
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<button name="submit_offer" value="Continue" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none"> <button name="submit_offer" value="Continue" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">

@ -233,7 +233,7 @@
<input class="pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');"> <input class="pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');">
</div> </div>
<div class="text-sm mt-2"> <div class="text-sm mt-2">
<a href="" id="get_rate_inferred_button" class="mt-2 dark:text-white bold text-coolGray-800">Get Rate Inferred:<span id="rate_inferred_display"></span></a> <a href="" id="get_rate_inferred_button" class="mt-2 dark:text-white bold text-coolGray-800">Get Rate Inferred:</a><span class="dark:text-white" id="rate_inferred_display"></span>
</div> </div>
<div class="flex form-check form-check-inline mt-5"> <div class="flex form-check form-check-inline mt-5">
<div class="flex items-center h-5"> <input class="form-check-input 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_lock" name="rate_lock" value="rl" checked=checked> </div> <div class="flex items-center h-5"> <input class="form-check-input 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_lock" name="rate_lock" value="rl" checked=checked> </div>
@ -409,7 +409,7 @@
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred); document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
function set_swap_type_enabled(coin_from, coin_to, swap_type) { function set_swap_type_enabled(coin_from, coin_to, swap_type) {
const adaptor_sig_only_coins = ['6' /* XMR */, '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 */];
const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */]; const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */];
let make_hidden = false; let make_hidden = false;
if (adaptor_sig_only_coins.includes(coin_from) || adaptor_sig_only_coins.includes(coin_to)) { if (adaptor_sig_only_coins.includes(coin_from) || adaptor_sig_only_coins.includes(coin_to)) {

@ -153,11 +153,11 @@
<div class="w-full md:flex-1 p-3"> <div class="w-full md:flex-1 p-3">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Send</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Send</p>
<div class="custom-select"> <div class="custom-select">
<div class="relative"> <div class="relative">
{{ input_down_arrow_offer_svg | safe }} {{ input_down_arrow_offer_svg | safe }}
<select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_from" name="coin_from" onchange="set_rate('coin_from');" disabled> <select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_from" name="coin_from" disabled>
<option value="-1">Select coin you send</option> <option value="-1">Select coin you send</option>
{% for c in coins_from %} {% for c in coins_from %}
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option> <option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option>
@ -169,9 +169,9 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Send</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Send</p>
<div class="relative"> <div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input appearance-none pr-10 bg-gray-50 text-gray-900 appearance-none 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-5 focus:ring-0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');" readonly> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input appearance-none pr-10 bg-gray-50 text-gray-900 appearance-none 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-5 focus:ring-0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" readonly>
</div> </div>
</div> </div>
{% if data.swap_style == 'xmr' %} {% if data.swap_style == 'xmr' %}
@ -213,11 +213,11 @@
<div class="w-full md:flex-1 p-3"> <div class="w-full md:flex-1 p-3">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Get</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Coin You Get</p>
<div class="custom-select"> <div class="custom-select">
<div class="relative"> <div class="relative">
{{ input_down_arrow_offer_svg | safe }} {{ input_down_arrow_offer_svg | safe }}
<select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_to" name="coin_to" onchange="set_rate('coin_to');" disabled> <select class="select cursor-not-allowed disabled-select 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-5 focus:ring-0" id="coin_to" name="coin_to" disabled>
<option value="-1"></option> <option value="-1"></option>
{% for c in coins %} {% for c in coins %}
<option{% if data.coin_to==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option> <option{% if data.coin_to==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option>
@ -228,9 +228,9 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Get</p> <p class="mb-1.5 font-medium text-base text-coolGray-800 dark:text-white">Amount You Get</p>
<div class="relative"> <div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_to" name="amt_to" value="{{ data.amt_to }}" onchange="set_rate('amt_to');" readonly> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="cursor-not-allowed disabled-input hover:border-blue-500 pr-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-5 focus:ring-0" type="text" id="amt_to" name="amt_to" value="{{ data.amt_to }}" readonly>
</div> </div>
</div> </div>
{% if data.swap_style == 'xmr' and coin_to != '6' %} {% if data.swap_style == 'xmr' and coin_to != '6' %}
@ -286,7 +286,7 @@
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
{{ select_rate_svg | safe }} {{ select_rate_svg | safe }}
</div> </div>
<input class="cursor-not-allowed disabled-input pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" onchange="set_rate('rate');" readonly> <input class="cursor-not-allowed disabled-input pl-10 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" type="text" id="rate" name="rate" value="{{ data.rate }}" readonly>
</div> </div>
</div> </div>
</div> </div>
@ -321,7 +321,7 @@
<input class="pl-10 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" type="number" name="lockmins" min="10" max="5000" value="{{ data.lockmins }}"> <input class="pl-10 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" type="number" name="lockmins" min="10" max="5000" value="{{ data.lockmins }}">
</div> </div>
{% if data.swap_style != 'xmr' %} {% if data.swap_style != 'xmr' %}
<div class="text-sm text-gray-500 mt-1.5">(Participate txn will be locked for half the time.)</div> <div class="text-sm text-gray-500 mt-1.5">(Participate txn will be locked for half the time.)</div>
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
@ -442,7 +442,7 @@
<input type="hidden" name="rate_var" value="true"> <input type="hidden" name="rate_var" value="true">
{% endif %} {% endif %}
</form> </form>
<script src="static/js/new_offer.js"></script> <script src="static/js/new_offer.js"></script>
</div> </div>
</div> </div>
</div> </div>

@ -65,7 +65,7 @@
</div> </div>
</section> </section>
<section class="py-4 flex overflow-hidden container-to-blur"> <section class="py-4 flex flex-wrap justify-center overflow-hidden container-to-blur">
{% if 'BTC' in enabled_chart_coins %} {% if 'BTC' in enabled_chart_coins %}
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
@ -74,13 +74,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="btc-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="btc-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white active-container" id="btc-active"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white active-container" id="btc-active">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Bitcoin.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Bitcoin"> <img src="/static/images/coins/Bitcoin.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Bitcoin">
<p class="ml-2 text-black text-sm dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Bitcoin (BTC) Bitcoin (BTC)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="btc-price-usd"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="btc-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="btc-price-usd-value"></span> <span id="btc-price-usd-value"></span>
</p> </p>
@ -102,13 +102,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="xmr-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="xmr-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white price-container"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white price-container">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Monero.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Monero"> <img src="/static/images/coins/Monero.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Monero">
<p class="ml-2 text-black text-sm dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Monero (XMR) Monero (XMR)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="xmr-price-usd"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="xmr-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="xmr-price-usd-value"></span> <span id="xmr-price-usd-value"></span>
</p> </p>
@ -135,13 +135,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="part-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="part-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Particl.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Particl"> <img src="/static/images/coins/Particl.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Particl">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Particl (PART) Particl (PART)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="part-price-usd"> <p class="my-2 text-xl font-bold text-leftt monospace text-gray-700 dark:text-gray-100" id="part-price-usd">
<span class="text-sm"> <span class="text-sm">
$ <span id="part-price-usd-value"></span> $ <span id="part-price-usd-value"></span>
</span> </span>
@ -169,13 +169,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="ltc-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="ltc-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Litecoin.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Litecoin"> <img src="/static/images/coins/Litecoin.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Litecoin">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Litecoin (LTC) Litecoin (LTC)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="ltc-price-usd"> <p class="my-2 text-xl font-bold text-leftt monospace text-gray-700 dark:text-gray-100" id="ltc-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="ltc-price-usd-value"></span> <span id="ltc-price-usd-value"></span>
</span> </span>
@ -203,13 +203,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="firo-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="firo-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Firo.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Firo"> <img src="/static/images/coins/Firo.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Firo">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Firo (FIRO) Firo
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="firo-price-usd"> <p class="my-2 text-xl font-bold text-leftt monospace text-gray-700 dark:text-gray-100" id="firo-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="firo-price-usd-value"></span> <span id="firo-price-usd-value"></span>
</span> </span>
@ -237,13 +237,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="pivx-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="pivx-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/PIVX.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="PIVX"> <img src="/static/images/coins/PIVX.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="PIVX">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
PIVX (PIVX) PIVX
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="pivx-price-usd"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="pivx-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="pivx-price-usd-value"></span> <span id="pivx-price-usd-value"></span>
</span> </span>
@ -271,13 +271,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="dash-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="dash-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Dash.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Dash"> <img src="/static/images/coins/Dash.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Dash">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Dash (DASH) Dash
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="dash-price-usd"> <p class="my-2 text-xl font-bold text-leftt monospace text-gray-700 dark:text-gray-100" id="dash-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="dash-price-usd-value"></span> <span id="dash-price-usd-value"></span>
</span> </span>
@ -305,13 +305,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="eth-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="eth-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Ethereum.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Ethereum"> <img src="/static/images/coins/Ethereum.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Ethereum">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Ethereum (ETH) Ethereum (ETH)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="eth-price-usd"> <p class="my-2 text-xl font-bold text-lettt monospace text-gray-700 dark:text-gray-100" id="eth-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="eth-price-usd-value"></span> <span id="eth-price-usd-value"></span>
</span> </span>
@ -339,13 +339,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="doge-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="doge-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Doge.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Dogecoin"> <img src="/static/images/coins/Doge.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Dogecoin">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Dogecoin (DOGE) Dogecoin (DOGE)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="doge-price-usd"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="doge-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="doge-price-usd-value"></span> <span id="doge-price-usd-value"></span>
</span> </span>
@ -373,13 +373,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="dcr-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="dcr-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Decred.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Decred"> <img src="/static/images/coins/Decred.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Decred">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Decred (DCR) Decred (DCR)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="dcr-price-usd"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="dcr-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="dcr-price-usd-value" style="min-width: 80px;"></span> <span id="dcr-price-usd-value" style="min-width: 80px;"></span>
</span> </span>
@ -407,13 +407,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="zano-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="zano-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Zano.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Zano"> <img src="/static/images/coins/Zano.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="Zano">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Zano (ZANO) Zano
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="zano-price-usd-value"> <p class="my-2 text-xl font-bold text-leftt monospace text-gray-700 dark:text-gray-100" id="zano-price-usd">
<span class="text-sm"> <span class="text-sm">
<span id="zano-price-usd-value" style="min-width: 80px;"></span> <span id="zano-price-usd-value" style="min-width: 80px;"></span>
</span> </span>
@ -428,7 +428,7 @@
<div id="zano-volume-24h"> <div id="zano-volume-24h">
</div> </div>
</div> </div>
<div class="flex items-center text-xs text-gray-600 dark:text-gray-300 mt-2 hidden"> <div class="flex items-center text-xs text-gray-600 dark:text-gray-300 mt-2">
<span class="bold mr-2">BTC:</span> <span class="bold mr-2">BTC:</span>
<span id="zano-price-btc"> <span id="zano-price-btc">
</span> </span>
@ -441,13 +441,13 @@
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="wow-container"> <div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="wow-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white"> <div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center"> <div class="flex items-center">
<img src="/static/images/coins/Wownero.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="WOWNERO"> <img src="/static/images/coins/Wownero.png" class="rounded-xl" style="width: 28px; height: 28px; object-fit: contain;" alt="WOWNERO">
<p class="ml-2 text-black text-md dark:text-white"> <p class="ml-1 text-black text-sm dark:text-white">
Wownero (WOW) Wownero (WOW)
</p> </p>
</div> </div>
<div class="flex flex-col justify-start"> <div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="wow-price-usd-value"> <p class="my-2 text-xl font-bold text-left monospace text-gray-700 dark:text-gray-100" id="wow-price-usd-value">
<span class="text-sm"> <span class="text-sm">
<span id="wow-price-usd-value" style="min-width: 80px;"></span> <span id="wow-price-usd-value" style="min-width: 80px;"></span>
</span> </span>
@ -486,22 +486,20 @@ window.addEventListener('load', function() {
if (container != null) { if (container != null) {
container.addEventListener('click', () => { container.addEventListener('click', () => {
setActiveContainer(container_id); setActiveContainer(container_id);
updateChart(coin); updateChart(coin, coinGeckoApiKey);
}); });
} }
if (coin === 'WOW') { if (coin === 'WOW') {
fetchWowneroData(); fetchWowneroData(coinGeckoApiKey);
} else if (coin === 'ZANO') {
fetchZanoData(coinGeckoApiKey);
} else { } else {
fetchCryptoCompareData(coin, api_key); fetchCryptoCompareData(coin, api_key);
} }
}); });
updateChart('BTC'); updateChart('BTC', coinGeckoApiKey);
}); });
function fetchZanoData(coinGeckoApiKey) { function fetchWowneroData(coinGeckoApiKey) {
fetch(`https://api.coingecko.com/api/v3/coins/zano/market_chart?vs_currency=usd&days=30&interval=daily&api_key=${coinGeckoApiKey}`) fetch(`https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=2&api_key={{coingecko_api_key}}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching data. Status: ${response.status}`); throw new Error(`Error fetching data. Status: ${response.status}`);
@ -509,57 +507,41 @@ function fetchZanoData(coinGeckoApiKey) {
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
displayZanoData(data); displayWowneroData(data);
}) // TODO bad responses block all others from displaying properly
.catch(error => { // })
console.error('Fetching Zano data:', error); // .catch(error => {
displayErrorMessage('Unable to fetch data for Zano. Please try again later.'); // console.error('Fetching Wownero data:', error);
// displayErrorMessage('Unable to fetch data for Wownero. Please try again later.');
}); });
} }
function fetchCryptoCompareData(coin, api_key) {
fetch(`https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${api_key}`) function fetchCryptoCompareData(coin, api_key) {
fetch(`https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key={{chart_api_key}}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching data. Status: ${response.status}`); throw new Error(`Error fetching data. Status: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
displayCoinData(coin, data); displayCoinData(coin, data);
}) })
.catch(error => { .catch(error => {
console.error(`Fetching ${coin} data:`, error); console.error(`Fetching ${coin} data:`, error);
displayErrorMessage(`Unable to fetch data. Please verify your API key or try again later.`); displayErrorMessage(`Unable to fetch data. Please verify your API key or try again later.`);
}); });
} }
function fetchWowneroData(coinGeckoApiKey) {
fetch(`https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${coinGeckoApiKey}`)
.then(response => {
if (!response.ok) {
throw new Error(`Error fetching data. Status: ${response.status}`);
}
return response.json();
})
.then(data => {
displayWowneroData(data);
})
.catch(error => {
console.error('Fetching Wownero data:', error);
displayErrorMessage('Unable to fetch data for Wownero. Please try again later.');
});
}
function displayWowneroData(data) { function displayWowneroData(data) {
const prices = data.prices; const prices = data.prices;
const latestPriceUSD = prices[prices.length - 1][1]; const latestPriceUSD = prices[prices.length - 1][1];
const priceChange24h = data.market_caps[data.market_caps.length - 1][1] / data.market_caps[data.market_caps.length - 2][1] - 1; const priceChange24h = prices[prices.length - 1][1] / prices[prices.length - 24][1] - 1;
const volume24h = data.total_volumes[data.total_volumes.length - 1][1]; const volume24h = data.total_volumes[data.total_volumes.length - 1][1];
document.getElementById('wow-price-usd-value').textContent = latestPriceUSD.toFixed(2) + ' $'; document.getElementById('wow-price-usd-value').textContent = '$ ' + latestPriceUSD.toFixed(4);
document.getElementById('wow-price-change-container').textContent = (priceChange24h * 100).toFixed(2) + '%'; document.getElementById('wow-price-change-container').textContent = (priceChange24h * 100).toFixed(2) + '%';
document.getElementById('wow-volume-24h').textContent = volume24h.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' USD'; document.getElementById('wow-volume-24h').textContent = volume24h.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' USD';
@ -575,49 +557,35 @@ function fetchZanoData(coinGeckoApiKey) {
const priceBTC = latestPriceUSD / latestPriceBTC; const priceBTC = latestPriceUSD / latestPriceBTC;
document.getElementById('wow-price-btc').textContent = priceBTC.toFixed(8) + ' BTC'; document.getElementById('wow-price-btc').textContent = priceBTC.toFixed(8) + ' BTC';
}
function displayZanoData(data) {
const prices = data.prices;
const latestPriceUSD = prices[prices.length - 1][1];
const priceChange24h = data.market_caps[data.market_caps.length - 1][1] / data.market_caps[data.market_caps.length - 2][1] - 1;
const volume24h = data.total_volumes[data.total_volumes.length - 1][1];
document.getElementById('zano-price-usd-value').textContent = latestPriceUSD.toFixed(2) + ' $';
document.getElementById('zano-price-change-container').textContent = (priceChange24h * 100).toFixed(2) + '%';
document.getElementById('zano-volume-24h').textContent = volume24h.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' USD';
const priceChangeContainer = document.getElementById('zano-price-change-container');
if (priceChange24h >= 0) {
priceChangeContainer.innerHTML = positivePriceChangeHTML(priceChange24h * 100);
} else {
priceChangeContainer.innerHTML = negativePriceChangeHTML(priceChange24h * 100);
}
const latestPriceBTC = parseFloat(data.prices[data.prices.length - 1][1]);
// Todo fix value USD -> BTC
const priceBTC = latestPriceUSD / latestPriceBTC;
document.getElementById('zano-price-btc').textContent = priceBTC.toFixed(8) + ' BTC';
} }
function displayCoinData(coin, data) { function displayCoinData(coin, data) {
const priceUSD = data.RAW[coin].USD.PRICE; const priceUSD = data.RAW[coin].USD.PRICE;
const priceBTC = data.RAW[coin].BTC.PRICE; const priceBTC = data.RAW[coin].BTC.PRICE;
const priceChange1h = data.RAW[coin].USD.CHANGEPCTHOUR; const priceChange1d = data.RAW[coin].USD.CHANGEPCT24HOUR;
const volume24h = data.RAW[coin].USD.TOTALVOLUME24HTO; const volume24h = data.RAW[coin].USD.TOTALVOLUME24HTO;
document.querySelector(`#${coin.toLowerCase()}-price-usd`).textContent = priceUSD.toFixed(2) + ' $'; const c = coin
if (coin !== 'BTC') {
document.querySelector(`#${coin.toLowerCase()}-price-btc`).textContent = priceBTC.toFixed(8) + ' BTC'; if (c === 'BTC') {
document.querySelector(`#${c.toLowerCase()}-price-usd`).textContent = '$ ' + priceUSD.toFixed(1);
} else if (c === 'ZANO' || c === 'FIRO') {
document.querySelector(`#${c.toLowerCase()}-price-usd`).textContent = '$ ' + priceUSD.toFixed(3);
} else if (c === 'DOGE' || c === 'PIVX' || c === 'PART') {
document.querySelector(`#${c.toLowerCase()}-price-usd`).textContent = '$ ' + priceUSD.toFixed(4);
} else {
document.querySelector(`#${c.toLowerCase()}-price-usd`).textContent = '$ ' + priceUSD.toFixed(2);
}
if (c !== 'BTC') {
document.querySelector(`#${c.toLowerCase()}-price-btc`).textContent = priceBTC.toFixed(8) + ' BTC';
} }
document.querySelector(`#${coin.toLowerCase()}-price-change-container`).textContent = priceChange1h.toFixed(2) + '%'; document.querySelector(`#${c.toLowerCase()}-price-change-container`).textContent = priceChange1d.toFixed(2) + '%';
document.querySelector(`#${coin.toLowerCase()}-volume-24h`).textContent = volume24h.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' USD'; document.querySelector(`#${c.toLowerCase()}-volume-24h`).textContent = volume24h.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' USD';
const priceChangeContainer = document.querySelector(`#${coin.toLowerCase()}-price-change-container`); const priceChangeContainer = document.querySelector(`#${c.toLowerCase()}-price-change-container`);
if (priceChange1h >= 0) { if (priceChange1d >= 0) {
priceChangeContainer.innerHTML = positivePriceChangeHTML(priceChange1h); priceChangeContainer.innerHTML = positivePriceChangeHTML(priceChange1d);
} else { } else {
priceChangeContainer.innerHTML = negativePriceChangeHTML(priceChange1h); priceChangeContainer.innerHTML = negativePriceChangeHTML(priceChange1d);
} }
} }
@ -667,6 +635,8 @@ function negativePriceChangeHTML(value) {
} }
function setActiveContainer(containerId) { function setActiveContainer(containerId) {
const api_key = '{{chart_api_key}}';
const coinGeckoApiKey = '{{coingecko_api_key}}';
const containerIds = ['btc-container', 'xmr-container', 'part-container', 'pivx-container', 'firo-container', 'dash-container', 'ltc-container', 'doge-container', 'eth-container', 'dcr-container', 'zano-container', 'wow-container']; const containerIds = ['btc-container', 'xmr-container', 'part-container', 'pivx-container', 'firo-container', 'dash-container', 'ltc-container', 'doge-container', 'eth-container', 'dcr-container', 'zano-container', 'wow-container'];
const activeClass = 'active-container'; const activeClass = 'active-container';
containerIds.forEach(id => { containerIds.forEach(id => {
@ -691,13 +661,10 @@ const coinOptions = {
} }
}; };
function updateChart(coinSymbol) { function updateChart(coinSymbol, coinGeckoApiKey) {
coin = coinSymbol; coin = coinSymbol;
const api_key = '{{chart_api_key}}';
const coinGeckoApiKey = '{{coingecko_api_key}}';
if (coinSymbol === 'WOW') { if (coinSymbol === 'WOW') {
fetch(`https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=30&interval=daily&api_key=${coinGeckoApiKey}`) fetch(`https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=30&interval=daily&api_key={{coingecko_api_key}}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const chartData = { const chartData = {
@ -716,28 +683,8 @@ function updateChart(coinSymbol) {
chart.update(); chart.update();
}) })
.catch(error => console.error('Error updating chart for Wownero:', error)); .catch(error => console.error('Error updating chart for Wownero:', error));
} else if (coinSymbol === 'ZANO') {
fetch(`https://api.coingecko.com/api/v3/coins/zano/market_chart?vs_currency=usd&days=30&interval=daily&api_key=${coinGeckoApiKey}`)
.then(response => response.json())
.then(data => {
const chartData = {
labels: data.prices.map(entry => formatDate(new Date(entry[0]))),
datasets: [{
label: 'Zano Price (USD)',
data: data.prices.map(entry => entry[1]),
borderColor: 'rgba(77, 132, 240, 1)',
backgroundColor: 'rgba(77, 132, 240, 0.1)',
fill: true
}]
};
chart.data = chartData;
chart.options.scales.y.title.text = 'Price (USD) - Zano 30 DAYS';
chart.update();
})
.catch(error => console.error('Error updating chart for Zano:', error));
} else { } else {
fetch(`https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coinSymbol}&tsym=USD&limit=30&api_key=${api_key}`) fetch(`https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coinSymbol}&tsym=USD&limit=30&api_key={{chart_api_key}}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Check if data is undefined // Check if data is undefined
@ -889,9 +836,9 @@ const chart = new Chart(ctx, {
<div class="flex items-center justify-center pb-4 dark:text-white"> <div class="flex items-center justify-center pb-4 dark:text-white">
<div class="rounded-b-md"> <div class="rounded-b-md">
<div class="w-full md:w-0/12"> <div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-center -m-1.5"> <div class="container flex flex-wrap justify-center">
<div class="w-full md:w-auto p-1.5 hover-container"> <div class="md:w-auto p-1.5 hover-container">
<div class="flex"> <div class="flex">
<button id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled> <button id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled>
</button> </button>
@ -904,8 +851,6 @@ const chart = new Chart(ctx, {
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div>
</div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
@ -913,8 +858,6 @@ const chart = new Chart(ctx, {
</div> </div>
</div> </div>
<div class="w-full md:w-auto p-1.5 hover-container">
<div class="flex">
<button id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled> <button id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled>
</button> </button>
<div class="relative"> <div class="relative">
@ -929,7 +872,7 @@ const chart = new Chart(ctx, {
</div> </div>
</div> </div>
<div class="flex items-center"> <div class="w-full md:w-auto mt-3">
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading bold">Sort By:</p> <p class="text-sm font-heading bold">Sort By:</p>
</div> </div>
@ -1318,10 +1261,11 @@ const coinNameToSymbol = {
'Dash': 'DASH', 'Dash': 'DASH',
'PIVX': 'PIVX', 'PIVX': 'PIVX',
'Decred': 'DCR', 'Decred': 'DCR',
'Zano': 'Zano' 'Zano': 'ZANO'
}; };
const exchangeRateCache = {}; const exchangeRateCache = {};
const coinGeckoApiKey = '{{coingecko_api_key}}';
function updateUsdValue(cryptoCell, coinFullName, isRate = false) { function updateUsdValue(cryptoCell, coinFullName, isRate = false) {
const coinSymbol = coinNameToSymbol[coinFullName]; const coinSymbol = coinNameToSymbol[coinFullName];
@ -1332,68 +1276,52 @@ function updateUsdValue(cryptoCell, coinFullName, isRate = false) {
const cryptoValue = parseFloat(cryptoCell.textContent); const cryptoValue = parseFloat(cryptoCell.textContent);
const usdCell = cryptoCell.nextElementSibling; const usdCell = cryptoCell.nextElementSibling;
if (usdCell) { if (!usdCell) {
// Check if the exchange rate is in the cache and is not expired console.error("USD cell does not exist.");
if (exchangeRateCache[coinSymbol] && !isCacheExpired(coinSymbol)) { return;
console.log(`Using cached exchange rate for ${coinSymbol}`);
const exchangeRate = exchangeRateCache[coinSymbol].rate;
const cryptoValue = parseFloat(cryptoCell.textContent);
const usdValue = cryptoValue * exchangeRate;
usdCell.textContent = `${usdValue.toFixed(2)} USD`;
updateProfitLoss(cryptoCell.closest('tr'));
updateProfitValue(cryptoCell.closest('tr'));
}
} else {
} }
if (isRate) { if (exchangeRateCache[coinSymbol] && !isCacheExpired(coinSymbol)) {
const rateCell = usdCell.nextElementSibling; console.log(`Using cached exchange rate for ${coinSymbol}`);
if (rateCell) { const exchangeRate = exchangeRateCache[coinSymbol].rate;
const usdValue = rateCell.previousElementSibling.textContent * cryptoValue; const usdValue = cryptoValue * exchangeRate;
usdCell.textContent = `${usdValue.toFixed(2)} USD`; usdCell.textContent = `${usdValue.toFixed(2)} USD`;
} else {
console.error("Rate cell does not exist.");
}
} else {
const apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`;
if (exchangeRateCache[coinSymbol] && !isCacheExpired(coinSymbol)) {
// console.log(`Using cached exchange rate for ${coinSymbol}`);
const exchangeRate = exchangeRateCache[coinSymbol].rate;
const usdValue = cryptoValue * exchangeRate;
usdCell.textContent = `${usdValue.toFixed(2)} USD`;
updateProfitLoss(cryptoCell.closest('tr')); updateProfitLoss(cryptoCell.closest('tr'));
updateProfitValue(cryptoCell.closest('tr')); updateProfitValue(cryptoCell.closest('tr'));
} else { return;
// console.log(`Fetching exchange rate from API for ${coinSymbol}`);
fetch(apiUrl)
.then(response => response.json())
.then(data => {
const exchangeRate = data.USD;
if (!isNaN(exchangeRate)) {
// console.log(`Received exchange rate from API for ${coinSymbol}`);
const usdValue = cryptoValue * exchangeRate;
usdCell.textContent = `${usdValue.toFixed(2)} USD`;
exchangeRateCache[coinSymbol] = {
rate: exchangeRate,
timestamp: Date.now(),
ttl: 300000 // 5 minutes
};
updateProfitLoss(cryptoCell.closest('tr'));
updateProfitValue(cryptoCell.closest('tr'));
} else {
console.error('Invalid exchange rate. Response:', data);
usdCell.textContent = 'Invalid exchange rate';
}
})
.catch(error => {
});
}
} }
const apiUrl = coinSymbol === 'WOW'
? `https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd&api_key=${{coingecko_api_key}}`
: `https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`;
fetch(apiUrl)
.then(response => response.json())
.then(data => {
const exchangeRate = coinSymbol === 'WOW' ? data.wownero.usd : data.USD;
if (!isNaN(exchangeRate)) {
console.log(`Received exchange rate from API for ${coinSymbol}`);
const usdValue = cryptoValue * exchangeRate;
usdCell.textContent = `${usdValue.toFixed(2)} USD`;
exchangeRateCache[coinSymbol] = {
rate: exchangeRate,
timestamp: Date.now(),
ttl: 300000 // 5 minutes
};
updateProfitLoss(cryptoCell.closest('tr'));
updateProfitValue(cryptoCell.closest('tr'));
} else {
console.error('Invalid exchange rate. Response:', data);
usdCell.textContent = 'Invalid exchange rate';
}
})
.catch(error => {
console.error(`Fetching ${coinSymbol} data:`, error);
usdCell.textContent = 'Error fetching data';
});
} }
function isCacheExpired(coinSymbol) { function isCacheExpired(coinSymbol) {
@ -1408,15 +1336,17 @@ function updateProfitLoss(row) {
if (!isNaN(sellingUSD) && !isNaN(buyingUSD)) { if (!isNaN(sellingUSD) && !isNaN(buyingUSD)) {
const profitLossPercentage = ((sellingUSD - buyingUSD) / buyingUSD) * 100; const profitLossPercentage = ((sellingUSD - buyingUSD) / buyingUSD) * 100;
profitLossCell.textContent = `${profitLossPercentage.toFixed(2)}%`;
if (profitLossPercentage > 0) { if (profitLossPercentage > 0) {
profitLossCell.classList.add('text-green-500'); // Profit (positive) profitLossCell.textContent = `-${profitLossPercentage.toFixed(2)}%`; // Change from "+" to "-"
profitLossCell.classList.add('text-green-500'); // Profit (negative)
profitLossCell.classList.remove('text-red-500'); profitLossCell.classList.remove('text-red-500');
} else if (profitLossPercentage < 0) { } else if (profitLossPercentage < 0) {
profitLossCell.classList.add('text-red-500'); // Loss (negative) profitLossCell.textContent = `+${Math.abs(profitLossPercentage).toFixed(2)}%`; // Change from "-" to "+"
profitLossCell.classList.add('text-red-500'); // Loss (positive)
profitLossCell.classList.remove('text-green-500'); profitLossCell.classList.remove('text-green-500');
} else { } else {
profitLossCell.textContent = `${profitLossPercentage.toFixed(2)}%`;
profitLossCell.classList.add('text-yellow-500'); // No profit or loss (zero) profitLossCell.classList.add('text-yellow-500'); // No profit or loss (zero)
profitLossCell.classList.remove('text-green-500', 'text-red-500'); profitLossCell.classList.remove('text-green-500', 'text-red-500');
} }
@ -1426,6 +1356,7 @@ function updateProfitLoss(row) {
} }
} }
function updateProfitValue(row) { function updateProfitValue(row) {
const sellingUSD = parseFloat(row.querySelector('.usd-value').textContent); const sellingUSD = parseFloat(row.querySelector('.usd-value').textContent);
const profitValueCell = row.querySelector('.profit-value'); const profitValueCell = row.querySelector('.profit-value');
@ -1460,51 +1391,43 @@ function sortTable(columnIndex) {
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`); const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
let sortOrder = sortIcon.textContent === '↓' ? 1 : -1; let sortOrder = sortIcon.textContent === '↓' ? 1 : -1;
if (sortOrder === 1) { sortIcon.textContent = sortOrder === 1 ? '↑' : '↓';
sortIcon.textContent = '↑';
} else {
sortIcon.textContent = '↓';
}
rows.sort((a, b) => { rows.sort((a, b) => {
const aValue = a.cells[columnIndex].textContent.trim(); const aValue = a.cells[columnIndex].textContent.trim();
const bValue = b.cells[columnIndex].textContent.trim(); const bValue = b.cells[columnIndex].textContent.trim();
if (aValue < bValue) return -1 * sortOrder; return aValue < bValue ? -1 * sortOrder : aValue > bValue ? 1 * sortOrder : 0;
if (aValue > bValue) return 1 * sortOrder;
return 0;
}); });
rows.forEach(row => table.querySelector('tbody').appendChild(row)); rows.forEach(row => table.querySelector('tbody').appendChild(row));
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const coinToSelect = document.getElementById('coin_to'); const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from'); const coinFromSelect = document.getElementById('coin_from');
const coinToButton = document.getElementById('coin_to_button'); const coinToButton = document.getElementById('coin_to_button');
const coinFromButton = document.getElementById('coin_from_button'); const coinFromButton = document.getElementById('coin_from_button');
function updateSelectedImage(selectElement, buttonElement) { function updateSelectedImage(selectElement, buttonElement) {
const selectedOption = selectElement.options[selectElement.selectedIndex]; const selectedOption = selectElement.options[selectElement.selectedIndex];
const imageURL = selectedOption.getAttribute('data-image'); const imageURL = selectedOption.getAttribute('data-image');
if (imageURL) { buttonElement.style.backgroundImage = imageURL ? `url('${imageURL}')` : 'none';
buttonElement.style.backgroundImage = `url('${imageURL}')`;
} else {
buttonElement.style.backgroundImage = 'none';
} }
}
coinToSelect.addEventListener('change', function() { coinToSelect.addEventListener('change', function() {
updateSelectedImage(coinToSelect, coinToButton); updateSelectedImage(coinToSelect, coinToButton);
}); });
coinFromSelect.addEventListener('change', function() { coinFromSelect.addEventListener('change', function() {
updateSelectedImage(coinFromSelect, coinFromButton); updateSelectedImage(coinFromSelect, coinFromButton);
}); });
// Initialize selected images on page load // Initialize selected images on page load
updateSelectedImage(coinToSelect, coinToButton); updateSelectedImage(coinToSelect, coinToButton);
updateSelectedImage(coinFromSelect, coinFromButton); updateSelectedImage(coinFromSelect, coinFromButton);
}); });
</script> </script>
{% include 'footer.html' %} {% include 'footer.html' %}
</body> </body>

@ -1,5 +1,5 @@
{% include 'header.html' %} {% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg %} {% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg %}
<div class="container mx-auto"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2"> <div class="flex flex-wrap items-center -m-2">
@ -62,8 +62,8 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6"> <td class="py-3 px-6">
<div class="relative"> <div class="relative">
{{ input_arrow_down_svg| safe }} {{ input_arrow_down_svg| safe }}
<select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="coin_type"> <select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="coin_type" id="coin_type" onchange="set_coin();">
<option value="-1" {% if coin_type==-1 %} selected{% endif %}>Select Coin</option> <option value="-1" {% if coin_type==-1 %} selected{% endif %}>Select Coin</option>
{% for c in coins %} {% for c in coins %}
<option value="{{ c[0] }}" {% if coin_type==c[0] %} selected{% endif %}>{{ c[1] }}</option> <option value="{{ c[0] }}" {% if coin_type==c[0] %} selected{% endif %}>{{ c[1] }}</option>
@ -72,21 +72,21 @@
</div> </div>
</td> </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" name="cmd"> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" name="cmd" id="cmd" oninput="set_method();">
</td> </td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6"> <td class="py-3 px-6">
<div class="relative"> <div class="relative">
{{ input_arrow_down_svg| safe }} {{ input_arrow_down_svg| safe }}
<select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="call_type"> <select class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="call_type" id="call_type" >
<option value="cli" {% if call_type=="cli" %} selected{% endif %}>CLI</option> <option value="cli" {% if call_type=="cli" %} selected{% endif %}>CLI</option>
<option value="http" {% if call_type=="http" %} selected{% endif %}>HTTP</option> <option value="http" {% if call_type=="http" %} selected{% endif %}>HTTP</option>
</select> </select>
</div> </div>
</td> </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" name="type_map" title="Convert inputs when using http. Example: 'sibj' 1st parameter is a string, 2nd is converted to an int then boolean and json object or array"> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" name="type_map" id="type_map" title="Convert inputs when using http. Example: 'sifbj' 1st parameter is a string, 2nd is converted to an int, 3rd to float then boolean and json object or array">
</td> </td>
</table> </table>
</div> </div>
@ -148,7 +148,7 @@
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
@ -176,4 +176,31 @@
</div> </div>
{% include 'footer.html' %} {% include 'footer.html' %}
</body> </body>
<script>
function set_method() {
const coin_type = document.getElementById('coin_type').value.split(',')[0];
if (coin_type == 4 || coin_type == -6) {
const cmd = document.getElementById('cmd');
const type_map = document.getElementById('type_map');
let method = cmd.value.split(' ')[0];
if (method == 'sendtoaddress') {
type_map.value = 'sf';
}
}
}
function set_coin() {
const coin_type = document.getElementById('coin_type').value.split(',')[0];
let call_type = document.getElementById('call_type');
if (coin_type == '4' || coin_type == '6') {
call_type.disabled = true;
call_type.value = 'http';
} else {
call_type.disabled = false;
}
set_method();
}
document.addEventListener('DOMContentLoaded', () => {
set_coin();
});
</script>
</html> </html>

@ -105,7 +105,7 @@
</tr> </tr>
{% endif %} {% endif %}
{% if c.manage_daemon is defined %} {% if c.manage_daemon is defined %}
{% if c.name == 'monero' %} {% if c.name in ('wownero', 'monero') %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Manage Daemon</td> <td class="py-3 px-6 bold">Manage Daemon</td>
<td class="py-3 px-6"> <td class="py-3 px-6">
@ -175,7 +175,7 @@
</div> </div>
</td> </td>
</tr> </tr>
{% if c.name == 'monero' %} {% if c.name in ('wownero', 'monero') %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Transaction Fee Priority</td> <td class="py-3 px-6 bold">Transaction Fee Priority</td>
<td class="py-3 px-6"> <td class="py-3 px-6">

@ -694,4 +694,12 @@
</g> </g>
</svg> </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' %} {% 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' %}

@ -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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -38,18 +39,15 @@
{% for m in messages %} {% for m in messages %}
<section class="py-4" id="messages_{{ m[0] }}" role="alert"> <section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto"> <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 flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2"> <div class="flex-1 p-2">
<div class="flex flex-wrap -m-1"> <div class="flex flex-wrap -m-1">
<div class="w-auto p-1"> <div class="w-auto p-1"> {{ circular_info_messages_svg | safe }} </div>
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <ul class="ml-4 mt-1">
<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> <li class="font-semibold text-sm text-green-500 error_msg text-left"><span class="bold">ALERT:</span></li>
</svg> <li class="font-medium text-sm text-green-500 infomsg">This will unlock the system for all users!</li>
</div> </ul>
<div class="flex-1 p-1">
<h3 class="font-medium text-sm text-green-900 text-left">{{ m[1] }}</h3>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -60,18 +58,15 @@
{% for m in err_messages %} {% for m in err_messages %}
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert"> <section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto"> <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 flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2"> <div class="flex-1 p-2">
<div class="flex flex-wrap -m-1"> <div class="flex flex-wrap -m-1">
<div class="w-auto p-1"> <div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <ul class="ml-4 mt-1">
<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> <li class="font-semibold text-sm text-red-500 error_msg text-left"><span class="bold">ERROR:</span></li>
</svg> <li class="font-medium text-sm text-red-500 error_msg">{{ m[1] }}</li>
</div> </ul>
<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> </div>
</div> </div>
</div> </div>
@ -103,7 +98,7 @@
<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> <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>
</p> </p>
<p class="text-center"> <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" contenteditable="false">{{ title }}</span>
</p> </p>
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</form> </form>

@ -1,5 +1,5 @@
{% include 'header.html' %} {% include 'header.html' %}
{% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg %} {% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg %}
<script src="/static/js/libs//qrcode.js"></script> <script src="/static/js/libs//qrcode.js"></script>
<div class="container mx-auto"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
@ -27,7 +27,7 @@
</div> </div>
</div> </div>
</section> </section>
{% include 'inc_messages.html' %} {% include 'inc_messages.html' %}
{% if w.updating %} {% if w.updating %}
<section class="py-4" id="messages_updating" role="alert"> <section class="py-4" id="messages_updating" role="alert">
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
@ -42,7 +42,7 @@
<li class="font-semibold text-sm text-blue-500 error_msg"><span class="bold">UPDATING:</span></li> <li class="font-semibold text-sm text-blue-500 error_msg"><span class="bold">UPDATING:</span></li>
<li class="font-medium text-sm text-blue-500">Please wait...</li> <li class="font-medium text-sm text-blue-500">Please wait...</li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="w-auto p-2"> <div class="w-auto p-2">
<button type="button" class="ms-auto bg-blue-50 text-blue-500 rounded-lg focus:ring-0 focus:ring-blue-400 p-1.5 hover:bg-blue-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-blue-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_updating" aria-label="Close"><span class="sr-only">Close</span> <button type="button" class="ms-auto bg-blue-50 text-blue-500 rounded-lg focus:ring-0 focus:ring-blue-400 p-1.5 hover:bg-blue-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-blue-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_updating" aria-label="Close"><span class="sr-only">Close</span>
@ -55,7 +55,7 @@
</section> </section>
{% endif %} {% endif %}
{% if w.havedata %} {% if w.havedata %}
{% if w.error %} {% if w.error %}
<section class="py-4" id="messages_error" role="alert"> <section class="py-4" id="messages_error" role="alert">
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<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="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
@ -113,7 +113,7 @@
</tr> {% if w.cid == '1' %} {# PART #} </tr> {% if w.cid == '1' %} {# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <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"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td>
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} (<span class="usd-value"></span>) <td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
{% if w.blind_unconfirmed %} {% if w.blind_unconfirmed %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
{% endif %} {% endif %}
@ -136,11 +136,11 @@
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} (<span class="usd-value"></span>) <td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
{% if w.mweb_pending %} {% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{# / LTC #} {# / LTC #}
{% if w.locked_utxos %} {% if w.locked_utxos %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <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">Locked Outputs:</td> <td class="py-3 px-6 bold">Locked Outputs:</td>
@ -202,9 +202,11 @@
<div class="container mt-5 mx-auto"> <div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6"> <div class="px-6">
{% if w.cid != '4' %} {# DCR #}
<div class="flex flex-wrap justify-end"> <div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5"> <input class="flex flex-wrap justify-center w-full 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 cursor-pointer" type="submit" name="reseed_{{ w.cid }}" value="Reseed wallet" onclick="return confirmReseed();"> </div> <div class="w-full md:w-auto p-1.5"> <input class="flex flex-wrap justify-center w-full 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 cursor-pointer" type="submit" name="reseed_{{ w.cid }}" value="Reseed wallet" onclick="return confirmReseed();"> </div>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -264,8 +266,8 @@
</div> </div>
{% endif %} {% endif %}
{# / PART #} {# / PART #}
{% if w.cid == '6' %} {% if w.cid in '6, 9' %}
{# XMR #} {# XMR | WOW #}
<div class="w-full md:w-1/2 p-3 flex justify-center items-center"> <div class="w-full md:w-1/2 p-3 flex justify-center items-center">
<div class="h-full"> <div class="h-full">
<div class="flex flex-wrap -m-3"> <div class="flex flex-wrap -m-3">
@ -369,7 +371,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{# / LTC #} {# / LTC #}
</div> </div>
</div> </div>
@ -383,7 +385,7 @@
<script> <script>
// Particl Stealth // Particl Stealth
var stealthAddress = "{{ w.stealth_address }}"; var stealthAddress = "{{ w.stealth_address }}";
var qrCodeStealth = new QRCode(document.getElementById("qrcode-stealth"), { var qrCodeStealth = new QRCode(document.getElementById("qrcode-stealth"), {
text: stealthAddress, text: stealthAddress,
width: 170, width: 170,
@ -399,7 +401,7 @@
<script> <script>
// Litecoin MWEB // Litecoin MWEB
var mwebAddress = "{{ w.mweb_address }}"; var mwebAddress = "{{ w.mweb_address }}";
var qrCodeMWEB = new QRCode(document.getElementById("qrcode-mweb"), { var qrCodeMWEB = new QRCode(document.getElementById("qrcode-mweb"), {
text: mwebAddress, text: mwebAddress,
width: 170, width: 170,
@ -408,14 +410,14 @@
colorLight: "#ffffff", colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L correctLevel: QRCode.CorrectLevel.L
}); });
</script> </script>
{% endif %} {% endif %}
{% if w.cid == '6' %} {% if w.cid in '6, 9' %}
{# XMR #} {# XMR | WOW #}
<script> <script>
// Monero Sub // Monero Sub
var moneroSubAddress = "{{ w.deposit_address }}"; var moneroSubAddress = "{{ w.deposit_address }}";
var qrCodeMoneroSub = new QRCode(document.getElementById("qrcode-monero-sub"), { var qrCodeMoneroSub = new QRCode(document.getElementById("qrcode-monero-sub"), {
text: moneroSubAddress, text: moneroSubAddress,
width: 170, width: 170,
@ -428,7 +430,7 @@
<script> <script>
// Monero Main // Monero Main
var moneroMainAddress = "{{ w.main_address }}"; var moneroMainAddress = "{{ w.main_address }}";
var qrCodeMoneroMain = new QRCode(document.getElementById("qrcode-monero-main"), { var qrCodeMoneroMain = new QRCode(document.getElementById("qrcode-monero-main"), {
text: moneroMainAddress, text: moneroMainAddress,
width: 170, width: 170,
@ -442,7 +444,7 @@
<script> <script>
// Default // Default
var defaultAddress = "{{ w.deposit_address }}"; var defaultAddress = "{{ w.deposit_address }}";
var qrCodeDepost = new QRCode(document.getElementById("qrcode-deposit"), { var qrCodeDepost = new QRCode(document.getElementById("qrcode-deposit"), {
text: defaultAddress, text: defaultAddress,
width: 170, width: 170,
@ -462,48 +464,48 @@
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(el); document.body.removeChild(el);
} }
function copyAndShowMessage(elementId) { function copyAndShowMessage(elementId) {
const addressElement = document.getElementById(elementId); const addressElement = document.getElementById(elementId);
if (!addressElement) return; if (!addressElement) return;
const addressText = addressElement.innerText.trim(); const addressText = addressElement.innerText.trim();
copyToClipboard(addressText); copyToClipboard(addressText);
addressElement.innerText = 'Copied to clipboard'; addressElement.innerText = 'Copied to clipboard';
const originalWidth = addressElement.offsetWidth; const originalWidth = addressElement.offsetWidth;
addressElement.classList.add('copying'); addressElement.classList.add('copying');
addressElement.parentElement.style.width = `${originalWidth}px`; addressElement.parentElement.style.width = `${originalWidth}px`;
setTimeout(function () { setTimeout(function () {
addressElement.innerText = addressText; addressElement.innerText = addressText;
addressElement.classList.remove('copying'); addressElement.classList.remove('copying');
addressElement.parentElement.style.width = ''; addressElement.parentElement.style.width = '';
}, 2000); }, 2000);
} }
const stealthAddressElement = document.getElementById('stealth_address'); const stealthAddressElement = document.getElementById('stealth_address');
if (stealthAddressElement) { if (stealthAddressElement) {
stealthAddressElement.addEventListener('click', function () { stealthAddressElement.addEventListener('click', function () {
copyAndShowMessage('stealth_address'); copyAndShowMessage('stealth_address');
}); });
} }
const mainDepositAddressElement = document.getElementById('main_deposit_address'); const mainDepositAddressElement = document.getElementById('main_deposit_address');
if (mainDepositAddressElement) { if (mainDepositAddressElement) {
mainDepositAddressElement.addEventListener('click', function () { mainDepositAddressElement.addEventListener('click', function () {
copyAndShowMessage('main_deposit_address'); copyAndShowMessage('main_deposit_address');
}); });
} }
const moneroMainAddressElement = document.getElementById('monero_main_address'); const moneroMainAddressElement = document.getElementById('monero_main_address');
if (moneroMainAddressElement) { if (moneroMainAddressElement) {
moneroMainAddressElement.addEventListener('click', function () { moneroMainAddressElement.addEventListener('click', function () {
copyAndShowMessage('monero_main_address'); copyAndShowMessage('monero_main_address');
}); });
} }
const moneroSubAddressElement = document.getElementById('monero_sub_address'); const moneroSubAddressElement = document.getElementById('monero_sub_address');
if (moneroSubAddressElement) { if (moneroSubAddressElement) {
moneroSubAddressElement.addEventListener('click', function () { moneroSubAddressElement.addEventListener('click', function () {
@ -562,124 +564,145 @@
<td class="py-3 px-6"> <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="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"> <div class="ml-2 flex">
{% if w.cid == '1' %} {% if w.cid == '1' %}
{# PART #} {# 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> <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>
<script> <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>
function setAmount(percent, balance, blindBalance, anonBalance) { <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>
var amountInput = document.getElementById('amount'); <script>
var typeSelect = document.getElementById('withdraw_type'); function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var selectedType = typeSelect.value; var amountInput = document.getElementById('amount');
var floatBalance; var typeSelect = document.getElementById('withdraw_type');
var calculatedAmount; var selectedType = typeSelect.value;
var floatBalance;
switch(selectedType) { var calculatedAmount;
case 'plain':
floatBalance = parseFloat(balance); switch(selectedType) {
calculatedAmount = floatBalance * percent; case 'plain':
break; floatBalance = parseFloat(balance);
case 'blind': break;
floatBalance = parseFloat(blindBalance); case 'blind':
calculatedAmount = floatBalance * percent; floatBalance = parseFloat(blindBalance);
break; break;
case 'anon': case 'anon':
floatBalance = parseFloat(anonBalance); floatBalance = parseFloat(anonBalance);
calculatedAmount = floatBalance * percent; break;
break; default:
default: floatBalance = parseFloat(balance);
floatBalance = parseFloat(balance); break;
calculatedAmount = floatBalance * percent; }
break; calculatedAmount = floatBalance * percent;
} amountInput.value = calculatedAmount.toFixed(8);
amountInput.value = calculatedAmount.toFixed(8); var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
} if (subfeeCheckbox) {
</script> subfeeCheckbox.checked = (percent === 1);
{# / PART #} }
{% elif w.cid == '3' %} }
{# LTC #} </script>
<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> {# / PART #}
<script> {% elif w.cid == '3' %}
function setAmount(percent, balance, mwebBalance, selectedType) { {# LTC #}
var amountInput = document.getElementById('amount'); <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>
var typeSelect = document.getElementById('withdraw_type'); <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>
var selectedType = typeSelect.value; <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>
var floatBalance; <script>
var calculatedAmount; function setAmount(percent, balance, cid, mwebBalance) {
var amountInput = document.getElementById('amount');
switch(selectedType) { var typeSelect = document.getElementById('withdraw_type');
case 'plain': var selectedType = typeSelect.value;
floatBalance = parseFloat(balance); var floatBalance;
calculatedAmount = floatBalance * percent; var calculatedAmount;
break;
case 'mweb': switch(selectedType) {
floatBalance = parseFloat(mwebBalance); case 'plain':
calculatedAmount = floatBalance * percent; floatBalance = parseFloat(balance);
break; break;
default: case 'mweb':
floatBalance = parseFloat(balance); floatBalance = parseFloat(mwebBalance);
calculatedAmount = floatBalance * percent; break;
break; default:
} floatBalance = parseFloat(balance);
break;
amountInput.value = calculatedAmount.toFixed(8); }
} calculatedAmount = floatBalance * percent;
</script> amountInput.value = calculatedAmount.toFixed(8);
{# / LTC #}
{% else %} var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
<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> if (subfeeCheckbox) {
<script> subfeeCheckbox.checked = (percent === 1);
function setAmount(percent, balance, cid) { }
var amountInput = document.getElementById('amount'); }
var floatBalance; </script>
var calculatedAmount; {# / LTC #}
{% else %}
floatBalance = parseFloat(balance); <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>
calculatedAmount = floatBalance * percent; <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>
if (cid === '6' && percent === 1) { <script>
amountInput.setAttribute('data-hidden', 'true'); function setAmount(percent, balance, cid) {
amountInput.placeholder = 'Sweep All'; var amountInput = document.getElementById('amount');
amountInput.value = ''; var floatBalance = parseFloat(balance);
amountInput.disabled = true; var calculatedAmount = floatBalance * percent;
} else if (amountInput.getAttribute('data-hidden') === 'true' && percent !== 1) {
amountInput.value = calculatedAmount.toFixed(8); const specialCids = [6, 9];
amountInput.setAttribute('data-hidden', 'false');
amountInput.placeholder = ''; console.log("CID:", cid);
amountInput.disabled = false; console.log("Percent:", percent);
} else { console.log("Balance:", balance);
amountInput.value = calculatedAmount.toFixed(8); console.log("Calculated Amount:", calculatedAmount);
amountInput.placeholder = '';
amountInput.disabled = false; if (specialCids.includes(parseInt(cid)) && percent === 1) {
} amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
if (cid === '6' && percent === 1) { amountInput.value = '';
var sweepAllCheckbox = document.getElementById('sweepall'); amountInput.disabled = true;
if (sweepAllCheckbox) { console.log("Sweep All activated for special CID:", cid);
sweepAllCheckbox.checked = true; } else {
} amountInput.value = calculatedAmount.toFixed(8);
} else { amountInput.setAttribute('data-hidden', 'false');
var sweepAllCheckbox = document.getElementById('sweepall'); amountInput.placeholder = '';
if (sweepAllCheckbox) { amountInput.disabled = false;
sweepAllCheckbox.checked = false; }
}
} let sweepAllCheckbox = document.getElementById('sweepall');
} if (sweepAllCheckbox) {
if (specialCids.includes(parseInt(cid)) && percent === 1) {
</script> sweepAllCheckbox.checked = true;
{% endif %} console.log("Sweep All checkbox checked");
</div> } else {
</td> sweepAllCheckbox.checked = false;
</tr> console.log("Sweep All checkbox unchecked");
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> }
{% if w.cid == '6' %} {# XMR #} }
<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> let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
<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> if (subfeeCheckbox) {
{% endif %} subfeeCheckbox.checked = (percent === 1);
<td> console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
</td> }
</tr> }
</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' %} {% if w.cid == '1' %}
{# PART #} {# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
@ -751,17 +774,17 @@
<div class="pt-6 pb-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl"> <div class="pt-6 pb-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
<div class="px-6"> <div class="px-6">
<div class="flex flex-wrap justify-end"> <div class="flex flex-wrap justify-end">
{% if w.cid != '6' %} {% if w.cid not in '6, 9' %}
{# !XMR #} {# !XMR | WOW #}
{% if w.show_utxo_groups %} {% if w.show_utxo_groups %}
{% else %} {% else %}
<div class="w-full md:w-auto p-1.5"> <button type="submit" class="flex flex-wrap justify-center 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" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div> <div class="w-full md:w-auto p-1.5"> <button type="submit" class="flex flex-wrap justify-center 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" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups"> {{ utxo_groups_svg | safe }} Show UTXO Groups </button> </div>
{% endif %} {% endif %} {% endif %} {% endif %}
{% if w.cid == '6' %} {% if w.cid in '6, 9' %}
{# XMR #} {# XMR | WOW #}
<div class="w-full md:w-auto p-1.5 ml-2"> <button 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" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div> <div class="w-full md:w-auto p-1.5 ml-2"> <button 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" name="estfee_{{ w.cid }}" value="Estimate Fee">Estimate {{ w.ticker }} Fee </button> </div>
{% endif %} {% endif %}
{# / XMR #} <div class="w-full md:w-auto p-1.5 ml-2"> <button 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" name="withdraw_{{ w.cid }}" value="Withdraw" onclick="return confirmWithdrawal();">{{ withdraw_svg | safe }} Withdraw {{ w.ticker }} </div> {# / XMR | WOW #} <div class="w-full md:w-auto p-1.5 ml-2"> <button 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" name="withdraw_{{ w.cid }}" value="Withdraw" onclick="return confirmWithdrawal();">{{ withdraw_svg | safe }} Withdraw {{ w.ticker }} </div>
</div> </div>
</div> </div>
</div> </div>
@ -770,8 +793,8 @@
</div> </div>
</div> </div>
</section> </section>
{% if w.cid != '6' %} {% if w.cid not in '6, 9' %}
{# !XMR #} {# !XMR | WOW #}
{% if w.show_utxo_groups %} {% if w.show_utxo_groups %}
<section class="p-6"> <section class="p-6">
<div class="flex items-center"> <div class="flex items-center">
@ -842,40 +865,56 @@
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</form> </form>
<script> <script>
const coinNameToSymbol = { const coinNameToSymbol = {
'Bitcoin': 'BTC', 'Bitcoin': 'BTC',
'Particl': 'PART', 'Particl': 'PART',
'Particl Blind': 'PART', 'Particl Blind': 'PART',
'Particl Anon': 'PART', 'Particl Anon': 'PART',
'Monero': 'XMR', 'Monero': 'XMR',
'Litecoin': 'LTC', 'Wownero': 'WOW',
'Firo': 'FIRO', 'Litecoin': 'LTC',
'Dash': 'DASH', 'Firo': 'FIRO',
'PIVX': 'PIVX', 'Dash': 'DASH',
'DECRED': 'DCR', 'PIVX': 'PIVX',
'WOWNERO': 'WOW' 'Decred': 'DCR',
}; 'Zano': 'ZANO',
};
const getUsdValue = (cryptoValue, coinSymbol) => fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
.then(response => response.json()) const getUsdValue = (cryptoValue, coinSymbol) => {
.then(data => { if (coinSymbol === 'WOW') {
const exchangeRate = data.USD; return fetch(`https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`)
if (!isNaN(exchangeRate)) { .then(response => response.json())
return cryptoValue * exchangeRate; .then(data => {
} else { const exchangeRate = data.wownero.usd;
throw new Error(`Invalid exchange rate for ${coinSymbol}`); 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 updateUsdValue = async (cryptoCell, coinFullName, usdValueSpan) => {
const coinSymbol = coinNameToSymbol[coinFullName] || ''; const coinSymbol = coinNameToSymbol[coinFullName] || '';
if (!coinSymbol) { if (!coinSymbol) {
console.error(`Coin symbol not found for full name: ${coinFullName}`); console.error(`Coin symbol not found for full name: ${coinFullName}`);
return; return;
} }
const cryptoValue = parseFloat(cryptoCell.textContent); const cryptoValue = parseFloat(cryptoCell.textContent);
if (!isNaN(cryptoValue) && cryptoValue !== 0) { if (!isNaN(cryptoValue) && cryptoValue !== 0) {
try { try {
const usdValue = await getUsdValue(cryptoValue, coinSymbol); const usdValue = await getUsdValue(cryptoValue, coinSymbol);
@ -894,19 +933,19 @@
} }
} }
}; };
const calculateTotalUsdValue = async () => { const calculateTotalUsdValue = async () => {
const coinNameValues = document.querySelectorAll('.coinname-value'); const coinNameValues = document.querySelectorAll('.coinname-value');
let totalUsdValue = 0; let totalUsdValue = 0;
for (const coinNameValue of coinNameValues) { for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname'); const coinFullName = coinNameValue.getAttribute('data-coinname');
const cryptoValue = parseFloat(coinNameValue.textContent); const cryptoValue = parseFloat(coinNameValue.textContent);
const coinSymbol = coinNameToSymbol[coinFullName]; const coinSymbol = coinNameToSymbol[coinFullName];
if (coinSymbol) { if (coinSymbol) {
const usdValueSpan = coinNameValue.querySelector('.usd-value'); const usdValueSpan = coinNameValue.querySelector('.usd-value');
if (!isNaN(cryptoValue) && cryptoValue !== 0) { if (!isNaN(cryptoValue) && cryptoValue !== 0) {
try { try {
const usdValue = await getUsdValue(cryptoValue, coinSymbol); const usdValue = await getUsdValue(cryptoValue, coinSymbol);
@ -926,24 +965,24 @@
console.error(`Coin symbol not found for full name: ${coinFullName}`); console.error(`Coin symbol not found for full name: ${coinFullName}`);
} }
} }
const totalUsdValueElement = document.getElementById('total-usd-value'); const totalUsdValueElement = document.getElementById('total-usd-value');
if (totalUsdValueElement) { if (totalUsdValueElement) {
totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`; totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`;
} }
}; };
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const coinNameValues = document.querySelectorAll('.coinname-value'); const coinNameValues = document.querySelectorAll('.coinname-value');
for (const coinNameValue of coinNameValues) { for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname'); const coinFullName = coinNameValue.getAttribute('data-coinname');
const usdValueSpan = coinNameValue.querySelector('.usd-value'); const usdValueSpan = coinNameValue.querySelector('.usd-value');
updateUsdValue(coinNameValue, coinFullName, usdValueSpan); updateUsdValue(coinNameValue, coinFullName, usdValueSpan);
} }
calculateTotalUsdValue(); calculateTotalUsdValue();
function set_sweep_all(element) { function set_sweep_all(element) {
let input = document.getElementById('amount'); let input = document.getElementById('amount');
if (element.checked) { if (element.checked) {
@ -952,7 +991,7 @@
input.disabled = false; input.disabled = false;
} }
} }
let cb_sweepall = document.getElementById('sweepall'); let cb_sweepall = document.getElementById('sweepall');
if (cb_sweepall) { if (cb_sweepall) {
set_sweep_all(cb_sweepall); set_sweep_all(cb_sweepall);
@ -960,22 +999,21 @@
set_sweep_all(event.currentTarget); set_sweep_all(event.currentTarget);
}); });
} }
}); });
</script>
{% include 'footer.html' %}
<script>
function confirmReseed() { function confirmReseed() {
return confirm("Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets."); return confirm("Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.");
} }
function confirmWithdrawal() { function confirmWithdrawal() {
return confirm("Are you sure?"); return confirm("Are you sure?");
} }
function confirmUTXOResize() { function confirmUTXOResize() {
return confirm("Are you sure?"); return confirm("Are you sure?");
} }
</script> </script>
{% include 'footer.html' %}
</body> </body>
</html> </html>

@ -35,7 +35,7 @@
<div id="total-btc-value" class="text-sm text-white mt-2"></div> <div id="total-btc-value" class="text-sm text-white mt-2"></div>
</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"> <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> <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>
</div> </div>
@ -213,27 +213,47 @@ const coinNameToSymbol = {
'Particl Blind': 'PART', 'Particl Blind': 'PART',
'Particl Anon': 'PART', 'Particl Anon': 'PART',
'Monero': 'XMR', 'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC', 'Litecoin': 'LTC',
'Firo': 'FIRO', 'Firo': 'FIRO',
'Dash': 'DASH', 'Dash': 'DASH',
'PIVX': 'PIVX', 'PIVX': 'PIVX',
'Wownero': 'WOW',
'Decred': 'DCR', 'Decred': 'DCR',
'Zano': 'ZANO', 'Zano': 'ZANO',
}; };
const getUsdValue = (cryptoValue, coinSymbol) => { const getUsdValue = (cryptoValue, coinSymbol) => {
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`) let apiUrl;
if (coinSymbol === 'WOW') {
apiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`;
} else {
apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`;
}
return fetch(apiUrl)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const exchangeRate = data.USD; if (coinSymbol === 'WOW') {
if (!isNaN(exchangeRate)) { const exchangeRate = data.wownero.usd;
return { if (!isNaN(exchangeRate)) {
usdValue: cryptoValue * exchangeRate, return {
btcValue: cryptoValue / exchangeRate usdValue: cryptoValue * exchangeRate,
}; btcValue: cryptoValue / exchangeRate
};
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
}
} else { } else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`); const exchangeRate = data.USD;
if (!isNaN(exchangeRate)) {
return {
usdValue: cryptoValue * exchangeRate,
btcValue: cryptoValue / exchangeRate
};
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
}
} }
}); });
}; };

@ -89,7 +89,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
page_data['coin_from'] = getCoinType(get_data_entry(form_data, 'coin_from')) page_data['coin_from'] = getCoinType(get_data_entry(form_data, 'coin_from'))
coin_from = Coins(page_data['coin_from']) coin_from = Coins(page_data['coin_from'])
ci_from = swap_client.ci(coin_from) ci_from = swap_client.ci(coin_from)
if coin_from != Coins.XMR: if coin_from not in (Coins.XMR, Coins.WOW):
page_data['fee_from_conf'] = ci_from._conf_target # Set default value page_data['fee_from_conf'] = ci_from._conf_target # Set default value
parsed_data['coin_from'] = coin_from parsed_data['coin_from'] = coin_from
except Exception: except Exception:
@ -99,7 +99,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
page_data['coin_to'] = getCoinType(get_data_entry(form_data, 'coin_to')) page_data['coin_to'] = getCoinType(get_data_entry(form_data, 'coin_to'))
coin_to = Coins(page_data['coin_to']) coin_to = Coins(page_data['coin_to'])
ci_to = swap_client.ci(coin_to) ci_to = swap_client.ci(coin_to)
if coin_to != Coins.XMR: if coin_to not in (Coins.XMR, Coins.WOW):
page_data['fee_to_conf'] = ci_to._conf_target # Set default value page_data['fee_to_conf'] = ci_to._conf_target # Set default value
parsed_data['coin_to'] = coin_to parsed_data['coin_to'] = coin_to
except Exception: except Exception:
@ -115,7 +115,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
errors.append('Amount From') errors.append('Amount From')
try: try:
if 'amt_bid_min' not in page_data: if have_data_entry(form_data, 'amt_bid_min') is False:
if options.get('add_min_bid_amt', False) is True: if options.get('add_min_bid_amt', False) is True:
parsed_data['amt_bid_min'] = ci_from.chainparams_network()['min_amount'] parsed_data['amt_bid_min'] = ci_from.chainparams_network()['min_amount']
else: else:
@ -161,7 +161,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
page_data['swap_type'] = get_data_entry(form_data, 'swap_type') page_data['swap_type'] = get_data_entry(form_data, 'swap_type')
parsed_data['swap_type'] = page_data['swap_type'] parsed_data['swap_type'] = page_data['swap_type']
swap_type = swap_type_from_string(parsed_data['swap_type']) swap_type = swap_type_from_string(parsed_data['swap_type'])
elif parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON): elif parsed_data['coin_to'] in (Coins.XMR, Coins.WOW, Coins.PART_ANON):
parsed_data['swap_type'] = strSwapType(SwapTypes.XMR_SWAP) parsed_data['swap_type'] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP swap_type = SwapTypes.XMR_SWAP
else: else:
@ -243,7 +243,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN()) page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN())
page_data['tla_from'] = ci_from.ticker() page_data['tla_from'] = ci_from.ticker()
if ci_to == Coins.XMR: if ci_to in (Coins.XMR, Coins.WOW):
if have_data_entry(form_data, 'fee_rate_to'): if have_data_entry(form_data, 'fee_rate_to'):
page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to') page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to')
parsed_data['to_fee_override'] = page_data['to_fee_override'] parsed_data['to_fee_override'] = page_data['to_fee_override']
@ -267,7 +267,7 @@ def postNewOfferFromParsed(swap_client, parsed_data):
if 'swap_type' in parsed_data: if 'swap_type' in parsed_data:
str_swap_type = parsed_data['swap_type'].lower() str_swap_type = parsed_data['swap_type'].lower()
swap_type = swap_type_from_string(str_swap_type) swap_type = swap_type_from_string(str_swap_type)
elif parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON): elif parsed_data['coin_to'] in (Coins.XMR, Coins.WOW, Coins.PART_ANON):
swap_type = SwapTypes.XMR_SWAP swap_type = SwapTypes.XMR_SWAP
if swap_type == SwapTypes.XMR_SWAP: if swap_type == SwapTypes.XMR_SWAP:
@ -472,8 +472,8 @@ def page_newoffer(self, url_split, post_string):
'err_messages': err_messages, 'err_messages': err_messages,
'coins_from': coins_from, 'coins_from': coins_from,
'coins': coins_to, 'coins': coins_to,
'addrs': swap_client.listSmsgAddresses('offer_send_from'), 'addrs': swap_client.listSMSGAddresses('offer_send_from'),
'addrs_to': swap_client.listSmsgAddresses('offer_send_to'), 'addrs_to': swap_client.listSMSGAddresses('offer_send_to'),
'data': page_data, 'data': page_data,
'automation_strategies': automation_strategies, 'automation_strategies': automation_strategies,
'summary': summary, 'summary': summary,
@ -602,7 +602,7 @@ def page_offer(self, url_split, post_string):
'created_at': offer.created_at, 'created_at': offer.created_at,
'expired_at': offer.expire_at, 'expired_at': offer.expire_at,
'sent': offer.was_sent, 'sent': offer.was_sent,
'was_revoked': 'True' if offer.active_ind == 2 else 'False', 'was_revoked': True if offer.active_ind == 2 else False,
'show_bid_form': show_bid_form, 'show_bid_form': show_bid_form,
'show_edit_form': show_edit_form, 'show_edit_form': show_edit_form,
'amount_negotiable': offer.amount_negotiable, 'amount_negotiable': offer.amount_negotiable,
@ -677,7 +677,7 @@ def page_offer(self, url_split, post_string):
'err_messages': err_messages, 'err_messages': err_messages,
'data': data, 'data': data,
'bids': formatted_bids, 'bids': formatted_bids,
'addrs': None if show_bid_form is None else swap_client.listSmsgAddresses('bid'), 'addrs': None if show_bid_form is None else swap_client.listSMSGAddresses('bid'),
'summary': summary, 'summary': summary,
}) })

@ -57,7 +57,7 @@ def page_settings(self, url_split, post_string):
for name, c in swap_client.settings['chainclients'].items(): for name, c in swap_client.settings['chainclients'].items():
if have_data_entry(form_data, 'apply_' + name): if have_data_entry(form_data, 'apply_' + name):
data = {'lookups': get_data_entry(form_data, 'lookups_' + name)} data = {'lookups': get_data_entry(form_data, 'lookups_' + name)}
if name == 'monero': if name in ('monero', 'wownero'):
data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name)) 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['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name) data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
@ -104,7 +104,7 @@ def page_settings(self, url_split, post_string):
'manage_daemon': c.get('manage_daemon', 'Unknown'), 'manage_daemon': c.get('manage_daemon', 'Unknown'),
'connection_type': c.get('connection_type', 'Unknown'), 'connection_type': c.get('connection_type', 'Unknown'),
}) })
if name == 'monero': if name in ('monero', 'wownero'):
chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0) 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]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown')
chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost') chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost')

@ -176,7 +176,7 @@ def page_wallet(self, url_split, post_string):
if estimate_fee and withdraw: 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, ): if estimate_fee and coin_id not in (Coins.XMR, Coins.WOW):
ci = swap_client.ci(coin_id) ci = swap_client.ci(coin_id)
ticker: str = ci.ticker() ticker: str = ci.ticker()
err_messages.append(f'Estimate fee unavailable for {ticker}.') err_messages.append(f'Estimate fee unavailable for {ticker}.')
@ -206,7 +206,7 @@ def page_wallet(self, url_split, post_string):
elif coin_id == Coins.LTC: elif coin_id == Coins.LTC:
txid = swap_client.withdrawLTC(type_from, value, address, subfee) 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)) messages.append('Withdrew {} {} (from {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, address, txid))
elif coin_id == Coins.XMR: elif coin_id in (Coins.XMR, Coins.WOW):
if estimate_fee: if estimate_fee:
fee_estimate = ci.estimateFee(value, address, sweepall) fee_estimate = ci.estimateFee(value, address, sweepall)
suffix = 's' if fee_estimate['num_txns'] > 1 else '' suffix = 's' if fee_estimate['num_txns'] > 1 else ''
@ -281,7 +281,7 @@ def page_wallet(self, url_split, post_string):
wallet_data['est_fee'] = 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())) 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['deposit_address'] = w.get('deposit_address', 'Refresh necessary')
if k == Coins.XMR: 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: elif k == Coins.LTC:
wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary') wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary')

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
from basicswap.util.crypto import ripemd160 from basicswap.util.crypto import ripemd160, sha256
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
@ -68,7 +67,7 @@ def encodeStealthAddress(prefix_byte: int, scan_pubkey: bytes, spend_pubkey: byt
data += bytes((0x00,)) # num prefix bits data += bytes((0x00,)) # num prefix bits
b = bytes((prefix_byte,)) + data b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4] b += sha256(sha256(b))[:4]
return b58encode(b) return b58encode(b)
@ -83,16 +82,15 @@ def toWIF(prefix_byte: int, b: bytes, compressed: bool = True) -> str:
b = bytes((prefix_byte,)) + b b = bytes((prefix_byte,)) + b
if compressed: if compressed:
b += bytes((0x01,)) b += bytes((0x01,))
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4] b += sha256(sha256(b))[:4]
return b58encode(b) return b58encode(b)
def getKeyID(key_data: bytes) -> bytes: def getKeyID(key_data: bytes) -> bytes:
sha256_hash = hashlib.sha256(key_data).digest() return ripemd160(sha256(key_data))
return ripemd160(sha256_hash)
def bech32Decode(hrp, addr): def bech32Decode(hrp: str, addr: str) -> bytes:
hrpgot, data = bech32_decode(addr) hrpgot, data = bech32_decode(addr)
if hrpgot != hrp: if hrpgot != hrp:
return None return None
@ -102,25 +100,26 @@ def bech32Decode(hrp, addr):
return bytes(decoded) return bytes(decoded)
def bech32Encode(hrp, data): def bech32Encode(hrp: str, data: bytes) -> str:
ret = bech32_encode(hrp, convertbits(data, 8, 5)) ret = bech32_encode(hrp, convertbits(data, 8, 5))
if bech32Decode(hrp, ret) is None: if bech32Decode(hrp, ret) is None:
return None return None
return ret return ret
def decodeAddress(address_str: str): def decodeAddress(address: str) -> bytes:
b58_addr = b58decode(address_str) addr_data = b58decode(address)
if b58_addr is not None: if addr_data is None:
address = b58_addr[:-4] return None
checksum = b58_addr[-4:] prefixed_data = addr_data[:-4]
assert (hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch' checksum = addr_data[-4:]
return b58_addr[:-4] if sha256(sha256(prefixed_data))[:4] != checksum:
return None raise ValueError('Checksum mismatch')
return prefixed_data
def encodeAddress(address: bytes) -> str: def encodeAddress(address: bytes) -> str:
checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest() checksum = sha256(sha256(address))
return b58encode(address + checksum[0:4]) return b58encode(address + checksum[0:4])

@ -1,23 +1,41 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2024 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from Crypto.Hash import RIPEMD160, SHA256 # pycryptodome from basicswap.contrib.blake256.blake256 import blake_hash
from Crypto.Hash import HMAC, RIPEMD160, SHA256, SHA512 # pycryptodome
def sha256(data):
def sha256(data: bytes) -> bytes:
h = SHA256.new() h = SHA256.new()
h.update(data) h.update(data)
return h.digest() return h.digest()
def ripemd160(data): def sha512(data: bytes) -> bytes:
h = SHA512.new()
h.update(data)
return h.digest()
def ripemd160(data: bytes) -> bytes:
h = RIPEMD160.new() h = RIPEMD160.new()
h.update(data) h.update(data)
return h.digest() return h.digest()
def hash160(s): def blake256(data: bytes) -> bytes:
return ripemd160(sha256(s)) return blake_hash(data)
def hash160(data: bytes) -> bytes:
return ripemd160(sha256(data))
def hmac_sha512(secret: bytes, data: bytes) -> bytes:
h = HMAC.new(secret, digestmod=SHA512)
h.update(data)
return h.digest()

@ -0,0 +1,116 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from .crypto import blake256, hash160, hmac_sha512, ripemd160
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'))
def hash160_dcr(data: bytes) -> bytes:
return ripemd160(blake256(data))
class ExtKeyPair():
__slots__ = ('_depth', '_fingerprint', '_child_no', '_chaincode', '_key', '_pubkey', 'hash_func')
def __init__(self, coin_type=1):
if coin_type == 4:
self.hash_func = hash160_dcr
else:
self.hash_func = hash160
def set_seed(self, seed: bytes) -> None:
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
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')
self._pubkey = PublicKey.from_secret(self._key).format()
self._key = None
def derive(self, child_no: int):
out = ExtKeyPair()
out._depth = self._depth + 1
out._child_no = child_no
if (child_no >> 31) == 0:
if self._key:
K = PublicKey.from_secret(self._key)
k_encoded = K.format()
else:
K = PublicKey(self._pubkey)
k_encoded = self._pubkey
out._fingerprint = self.hash_func(k_encoded)[:4]
new_hash = BIP32Hash(self._chaincode, child_no, k_encoded[0], k_encoded[1:])
out._chaincode = new_hash[32:]
if self._key:
k = PrivateKey(self._key)
k.add(new_hash[:32], update=True)
out._key = k.secret
out._pubkey = None
else:
K.add(new_hash[:32], update=True)
out._key = None
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]
new_hash = BIP32Hash(self._chaincode, child_no, 0, self._key)
out._chaincode = new_hash[32:]
k.add(new_hash[:32], update=True)
out._key = k.secret
out._pubkey = None
out.hash_func = self.hash_func
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
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
def decode(self, data: bytes) -> None:
if len(data) != 74:
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._chaincode = data[9:41]
if data[41] == 0:
self._key = data[42:]
self._pubkey = None
else:
self._key = None
self._pubkey = data[41:]

@ -5,13 +5,41 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
def decode_varint(b: bytes) -> int: def decode_compactsize(b: bytes, offset: int = 0) -> (int, int):
i = 0 i = b[offset]
shift = 0 if i < 0xfd:
for c in b: return i, 1
i += (c & 0x7F) << shift offset += 1
shift += 7 if i == 0xfd:
return i 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
def encode_compactsize(i: int) -> bytes:
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')
def decode_varint(b: bytes, offset: int = 0) -> (int, int):
i: int = 0
num_bytes: int = 0
while True:
c = b[offset + num_bytes]
i += (c & 0x7F) << (num_bytes * 7)
num_bytes += 1
if not c & 0x80:
break
if num_bytes > 8:
raise ValueError('Too many bytes')
return i, num_bytes
def encode_varint(i: int) -> bytes: def encode_varint(i: int) -> bytes:

@ -5,37 +5,40 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os import contextlib
import sys import gnupg
import hashlib
import json import json
import time import logging
import mmap import mmap
import stat import os
import gnupg import platform
import socks import random
import shutil import shutil
import signal import signal
import socket import socket
import hashlib import socks
import stat
import sys
import tarfile import tarfile
import zipfile import threading
import logging import time
import platform
import contextlib
import urllib.parse import urllib.parse
import zipfile
from urllib.error import ContentTooShortError from urllib.error import ContentTooShortError
from urllib.request import Request, urlopen
from urllib.parse import _splittype from urllib.parse import _splittype
from urllib.request import Request, urlopen
import basicswap.config as cfg import basicswap.config as cfg
from basicswap import __version__
from basicswap.base import getaddrinfo_tor from basicswap.base import getaddrinfo_tor
from basicswap.basicswap import BasicSwap from basicswap.basicswap import BasicSwap
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap import __version__
from basicswap.ui.util import getCoinName from basicswap.ui.util import getCoinName
from basicswap.util import toBool from basicswap.util import toBool
from basicswap.util.rfc2440 import rfc2440_hash_password from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from bin.basicswap_run import startDaemon, startXmrWalletDaemon from bin.basicswap_run import startDaemon, startXmrWalletDaemon
PARTICL_VERSION = os.getenv('PARTICL_VERSION', '23.2.7.0') PARTICL_VERSION = os.getenv('PARTICL_VERSION', '23.2.7.0')
@ -52,18 +55,25 @@ MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.3')
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')
XMR_SITE_COMMIT = 'd00169a6decd9470ebdf6a75e3351df4ebcd260a' # Lock hashes.txt to monero version XMR_SITE_COMMIT = 'd00169a6decd9470ebdf6a75e3351df4ebcd260a' # Lock hashes.txt to monero version
WOWNERO_VERSION = os.getenv('WOWNERO_VERSION', '0.11.1.0')
WOWNERO_VERSION_TAG = os.getenv('WOWNERO_VERSION_TAG', '')
WOW_SITE_COMMIT = '97e100e1605e9f59bc8ca82a5b237d5562c8a21c' # todo
PIVX_VERSION = os.getenv('PIVX_VERSION', '5.6.1') PIVX_VERSION = os.getenv('PIVX_VERSION', '5.6.1')
PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '') PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '')
DASH_VERSION = os.getenv('DASH_VERSION', '20.0.2') DASH_VERSION = os.getenv('DASH_VERSION', '20.0.2')
DASH_VERSION_TAG = os.getenv('DASH_VERSION_TAG', '') DASH_VERSION_TAG = os.getenv('DASH_VERSION_TAG', '')
FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.13.2') FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.13.3')
FIRO_VERSION_TAG = os.getenv('FIRO_VERSION_TAG', '') FIRO_VERSION_TAG = os.getenv('FIRO_VERSION_TAG', '')
NAV_VERSION = os.getenv('NAV_VERSION', '7.0.3') NAV_VERSION = os.getenv('NAV_VERSION', '7.0.3')
NAV_VERSION_TAG = os.getenv('NAV_VERSION_TAG', '') NAV_VERSION_TAG = os.getenv('NAV_VERSION_TAG', '')
DCR_VERSION = os.getenv('DCR_VERSION', '1.8.1')
DCR_VERSION_TAG = os.getenv('DCR_VERSION_TAG', '')
GUIX_SSL_CERT_DIR = None GUIX_SSL_CERT_DIR = None
ADD_PUBKEY_URL = os.getenv('ADD_PUBKEY_URL', '') ADD_PUBKEY_URL = os.getenv('ADD_PUBKEY_URL', '')
@ -75,10 +85,12 @@ SKIP_GPG_VALIDATION = toBool(os.getenv('SKIP_GPG_VALIDATION', 'false'))
known_coins = { known_coins = {
'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)), 'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)),
'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('davidburkett38',)),
'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)), 'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)),
'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('davidburkett38',)),
'decred': (DCR_VERSION, DCR_VERSION_TAG, ('decred_release',)),
'namecoin': ('0.18.0', '', ('JeremyRand',)), 'namecoin': ('0.18.0', '', ('JeremyRand',)),
'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)), 'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)),
'wownero': (WOWNERO_VERSION, WOWNERO_VERSION_TAG, ('wowario',)),
'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('fuzzbawls',)), 'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('fuzzbawls',)),
'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)), 'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)),
'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)), 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)),
@ -87,6 +99,7 @@ known_coins = {
disabled_coins = [ disabled_coins = [
'navcoin', 'navcoin',
'namecoin', # Needs update
] ]
expected_key_ids = { expected_key_ids = {
@ -95,11 +108,13 @@ expected_key_ids = {
'laanwj': ('1E4AED62986CD25D',), 'laanwj': ('1E4AED62986CD25D',),
'JeremyRand': ('2DBE339E29F6294C',), 'JeremyRand': ('2DBE339E29F6294C',),
'binaryfate': ('F0AF4D462A0BDF92',), 'binaryfate': ('F0AF4D462A0BDF92',),
'wowario': ('793504B449C69220',),
'davidburkett38': ('3620E9D387E55666',), 'davidburkett38': ('3620E9D387E55666',),
'fuzzbawls': ('C1ABA64407731FD9',), 'fuzzbawls': ('C1ABA64407731FD9',),
'pasta': ('52527BEDABE87984',), 'pasta': ('52527BEDABE87984',),
'reuben': ('1290A1D0FA7EE109',), 'reuben': ('1290A1D0FA7EE109',),
'nav_builder': ('2782262BF6E7FADB',), 'nav_builder': ('2782262BF6E7FADB',),
'decred_release': ('6D897EDF518A031D',),
} }
USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system()) USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system())
@ -148,6 +163,17 @@ XMR_RPC_USER = os.getenv('XMR_RPC_USER', '')
XMR_RPC_PWD = os.getenv('XMR_RPC_PWD', '') XMR_RPC_PWD = os.getenv('XMR_RPC_PWD', '')
DEFAULT_XMR_RESTORE_HEIGHT = int(os.getenv('DEFAULT_XMR_RESTORE_HEIGHT', 2245107)) DEFAULT_XMR_RESTORE_HEIGHT = int(os.getenv('DEFAULT_XMR_RESTORE_HEIGHT', 2245107))
WOW_RPC_HOST = os.getenv('WOW_RPC_HOST', '127.0.0.1')
BASE_WOW_RPC_PORT = int(os.getenv('BASE_WOW_RPC_PORT', 34598))
BASE_WOW_ZMQ_PORT = int(os.getenv('BASE_WOW_ZMQ_PORT', 34698))
BASE_WOW_WALLET_PORT = int(os.getenv('BASE_WOW_WALLET_PORT', 34798))
WOW_WALLET_RPC_HOST = os.getenv('WOW_WALLET_RPC_HOST', '127.0.0.1')
WOW_WALLET_RPC_USER = os.getenv('WOW_WALLET_RPC_USER', 'wow_wallet_user')
WOW_WALLET_RPC_PWD = os.getenv('WOW_WALLET_RPC_PWD', 'wow_wallet_pwd')
WOW_RPC_USER = os.getenv('WOW_RPC_USER', '')
WOW_RPC_PWD = os.getenv('WOW_RPC_PWD', '')
DEFAULT_WOW_RESTORE_HEIGHT = int(os.getenv('DEFAULT_WOW_RESTORE_HEIGHT', 450000))
LTC_RPC_HOST = os.getenv('LTC_RPC_HOST', '127.0.0.1') LTC_RPC_HOST = os.getenv('LTC_RPC_HOST', '127.0.0.1')
LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19895)) LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19895))
LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333))
@ -160,6 +186,14 @@ BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334))
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '') BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '') BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
DCR_RPC_HOST = os.getenv('DCR_RPC_HOST', '127.0.0.1')
DCR_RPC_PORT = int(os.getenv('DCR_RPC_PORT', 9109))
DCR_WALLET_RPC_HOST = os.getenv('DCR_WALLET_RPC_HOST', '127.0.0.1')
DCR_WALLET_RPC_PORT = int(os.getenv('DCR_WALLET_RPC_PORT', 9209))
DCR_WALLET_PWD = os.getenv('DCR_WALLET_PWD', random.randbytes(random.randint(14, 18)).hex())
DCR_RPC_USER = os.getenv('DCR_RPC_USER', 'user')
DCR_RPC_PWD = os.getenv('DCR_RPC_PWD', random.randbytes(random.randint(14, 18)).hex())
NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1') NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1')
NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19698)) NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19698))
@ -209,12 +243,28 @@ monerod_proxy_config = [
'hide-my-port=1', # Don't share the p2p port 'hide-my-port=1', # Don't share the p2p port
'p2p-bind-ip=127.0.0.1', # Don't broadcast ip 'p2p-bind-ip=127.0.0.1', # Don't broadcast ip
'in-peers=0', # Changes "error" in log to "incoming connections disabled" 'in-peers=0', # Changes "error" in log to "incoming connections disabled"
'out-peers=24',
f'tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16' # Outgoing tx relay to onion
] ]
monero_wallet_rpc_proxy_config = [ monero_wallet_rpc_proxy_config = [
'daemon-ssl-allow-any-cert=1', # 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
wownerod_proxy_config = [
f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}',
'proxy-allow-dns-leaks=0',
'no-igd=1', # Disable UPnP port mapping
'hide-my-port=1', # Don't share the p2p port
'p2p-bind-ip=127.0.0.1', # Don't broadcast ip
'in-peers=0', # Changes "error" in log to "incoming connections disabled"
'out-peers=24',
f'tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16' # Outgoing tx relay to onion
] ]
wownero_wallet_rpc_proxy_config = [
# 'daemon-ssl-allow-any-cert=1', moved to startup flag
]
default_socket = socket.socket default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout() default_socket_timeout = socket.getdefaulttimeout()
@ -355,16 +405,16 @@ def setConnectionParameters(timeout: int = 5, allow_set_tor: bool = True):
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
def popConnectionParameters(): def popConnectionParameters() -> None:
if use_tor_proxy: if use_tor_proxy:
socket.socket = default_socket socket.socket = default_socket
socket.getaddrinfo = default_socket_getaddrinfo socket.getaddrinfo = default_socket_getaddrinfo
socket.setdefaulttimeout(default_socket_timeout) socket.setdefaulttimeout(default_socket_timeout)
def downloadFile(url, path, timeout=5, resume_from=0): def downloadFile(url: str, path: str, timeout=5, resume_from=0) -> None:
logger.info('Downloading file %s', url) logger.info(f'Downloading file {url}')
logger.info('To %s', path) logger.info(f'To {path}')
try: try:
setConnectionParameters(timeout=timeout) setConnectionParameters(timeout=timeout)
urlretrieve(url, path, make_reporthook(resume_from), resume_from=resume_from) urlretrieve(url, path, make_reporthook(resume_from), resume_from=resume_from)
@ -385,13 +435,12 @@ def importPubkeyFromUrls(gpg, pubkeyurls):
try: try:
logger.info('Importing public key from url: ' + url) logger.info('Importing public key from url: ' + url)
rv = gpg.import_keys(downloadBytes(url)) rv = gpg.import_keys(downloadBytes(url))
for key in rv.fingerprints:
gpg.trust_keys(key, 'TRUST_FULLY')
break break
except Exception as e: except Exception as e:
logging.warning('Import from url failed: %s', str(e)) logging.warning('Import from url failed: %s', str(e))
for key in rv.fingerprints:
gpg.trust_keys(key, 'TRUST_FULLY')
def testTorConnection(): def testTorConnection():
test_url = 'https://check.torproject.org/' test_url = 'https://check.torproject.org/'
@ -467,9 +516,9 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
logger.info('extractCore %s v%s%s', coin, version, version_tag) logger.info('extractCore %s v%s%s', coin, version, version_tag)
extract_core_overwrite = extra_opts.get('extract_core_overwrite', True) extract_core_overwrite = extra_opts.get('extract_core_overwrite', True)
if coin in ('monero', 'firo'): if coin in ('monero', 'firo', 'wownero'):
if coin == 'monero': if coin in ('monero', 'wownero'):
bins = ['monerod', 'monero-wallet-rpc'] bins = [coin + 'd', coin + '-wallet-rpc']
elif coin == 'firo': elif coin == 'firo':
bins = [coin + 'd', coin + '-cli', coin + '-tx'] bins = [coin + 'd', coin + '-cli', coin + '-tx']
else: else:
@ -519,11 +568,25 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path) logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
return return
bins = [coin + 'd', coin + '-cli', coin + '-tx']
versions = version.split('.')
dir_name = 'dashcore' if coin == 'dash' else coin dir_name = 'dashcore' if coin == 'dash' else coin
if int(versions[0]) >= 22 or int(versions[1]) >= 19: if coin == 'decred':
bins.append(coin + '-wallet') bins = ['dcrd', 'dcrwallet']
else:
bins = [coin + 'd', coin + '-cli', coin + '-tx']
versions = version.split('.')
if int(versions[0]) >= 22 or int(versions[1]) >= 19:
bins.append(coin + '-wallet')
def get_archive_path(b):
if coin == 'pivx':
return '{}-{}/bin/{}'.format(dir_name, version, b)
elif coin == 'particl' and '_nousb-' in release_path:
return '{}-{}_nousb/bin/{}'.format(dir_name, version + version_tag, b)
elif coin == 'decred':
return '{}-{}-v{}/{}'.format(dir_name, extra_opts['arch_name'], version, b)
else:
return '{}-{}/bin/{}'.format(dir_name, version + version_tag, b)
if 'win32' in BIN_ARCH or 'win64' in BIN_ARCH: if 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
with zipfile.ZipFile(release_path) as fz: with zipfile.ZipFile(release_path) as fz:
for b in bins: for b in bins:
@ -531,7 +594,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
out_path = os.path.join(bin_dir, b) out_path = os.path.join(bin_dir, b)
if (not os.path.exists(out_path)) or extract_core_overwrite: if (not os.path.exists(out_path)) or extract_core_overwrite:
with open(out_path, 'wb') as fout: with open(out_path, 'wb') as fout:
fout.write(fz.read('{}-{}/bin/{}'.format(dir_name, version, b))) fout.write(fz.read(get_archive_path(b)))
try: try:
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH) os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
except Exception as e: except Exception as e:
@ -541,15 +604,7 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
for b in bins: for b in bins:
out_path = os.path.join(bin_dir, b) out_path = os.path.join(bin_dir, b)
if not os.path.exists(out_path) or extract_core_overwrite: if not os.path.exists(out_path) or extract_core_overwrite:
with open(out_path, 'wb') as fout, ft.extractfile(get_archive_path(b)) as fi:
if coin == 'pivx':
filename = '{}-{}/bin/{}'.format(dir_name, version, b)
elif coin == 'particl' and '_nousb-' in release_path:
filename = '{}-{}_nousb/bin/{}'.format(dir_name, version + version_tag, b)
else:
filename = '{}-{}/bin/{}'.format(dir_name, version + version_tag, b)
with open(out_path, 'wb') as fout, ft.extractfile(filename) as fi:
fout.write(fi.read()) fout.write(fi.read())
try: try:
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH) os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
@ -602,6 +657,70 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_path = os.path.join(bin_dir, assert_filename) assert_path = os.path.join(bin_dir, assert_filename)
if not os.path.exists(assert_path): if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path) downloadFile(assert_url, assert_path)
elif coin == 'wownero':
use_file_ext = 'tar.bz2' if FILE_EXT == 'tar.gz' else FILE_EXT
release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, use_file_ext)
if os_name == 'osx':
os_name = 'mac'
architecture = 'x64'
release_url = 'https://git.wownero.com/attachments/280753b0-3af0-4a78-a248-8b925e8f4593'
if 'aarch64' in BIN_ARCH:
architecture = 'armv8'
release_url = 'https://git.wownero.com/attachments/0869ffe3-eeff-4240-a185-168ca80fa1e3'
elif 'arm' in BIN_ARCH:
architecture = 'armv7' # 32bit doesn't work
release_url = 'https://git.wownero.com/attachments/ff0c4886-3865-4670-9bc6-63dd60ded0e3'
elif 'osx64' in BIN_ARCH:
release_url = 'https://git.wownero.com/attachments/7e3fd17c-1bcd-442c-b82d-92a00cccffb8'
elif 'win64' in BIN_ARCH:
release_url = 'https://git.wownero.com/attachments/a1cf8611-1727-4b49-a8e5-1c66fe4f72a3'
elif 'win32' in BIN_ARCH:
release_url = 'https://git.wownero.com/attachments/007d606d-56e0-4c8a-92c1-d0974a781e80'
release_path = os.path.join(bin_dir, release_filename)
if not os.path.exists(release_path):
downloadFile(release_url, release_path)
assert_filename = 'wownero-{}-hashes.txt'.format(version)
assert_url = 'https://git.wownero.com/wownero/wownero.org-website/raw/commit/{}/hashes.txt'.format(WOW_SITE_COMMIT)
assert_path = os.path.join(bin_dir, assert_filename)
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
elif coin == 'decred':
arch_name = BIN_ARCH
if USE_PLATFORM == 'Darwin':
arch_name = 'darwin-amd64'
elif USE_PLATFORM == 'Windows':
arch_name = 'windows-amd64'
else:
machine: str = platform.machine()
if 'arm' in machine:
arch_name = 'linux-arm'
else:
arch_name = 'linux-amd64'
extra_opts['arch_name'] = arch_name
release_filename = '{}-{}-{}.{}'.format(coin, version, arch_name, FILE_EXT)
release_page_url = 'https://github.com/decred/decred-binaries/releases/download/v{}'.format(version)
release_url = release_page_url + '/' + 'decred-{}-v{}.{}'.format(arch_name, version, FILE_EXT)
release_path = os.path.join(bin_dir, release_filename)
if not os.path.exists(release_path):
downloadFile(release_url, release_path)
assert_filename = 'decred-v{}-manifest.txt'.format(version)
assert_url = release_page_url + '/' + assert_filename
assert_path = os.path.join(bin_dir, assert_filename)
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
assert_sig_filename = assert_filename + '.asc'
assert_sig_url = assert_url + '.asc'
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
if not os.path.exists(assert_sig_path):
downloadFile(assert_sig_url, assert_sig_path)
else: else:
major_version = int(version.split('.')[0]) major_version = int(version.split('.')[0])
@ -722,23 +841,27 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
if coin in ('navcoin', ): if coin in ('navcoin', ):
pubkey_filename = '{}_builder.pgp'.format(coin) pubkey_filename = '{}_builder.pgp'.format(coin)
elif coin in ('decred', ):
pubkey_filename = '{}_release.pgp'.format(coin)
else: else:
pubkey_filename = '{}_{}.pgp'.format(coin, signing_key_name) pubkey_filename = '{}_{}.pgp'.format(coin, signing_key_name)
pubkeyurls = [ pubkeyurls = [
'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp/keys/' + pubkey_filename, 'https://raw.githubusercontent.com/basicswap/basicswap/master/pgp/keys/' + pubkey_filename,
'https://gitlab.com/particl/basicswap/-/raw/master/pgp/keys/' + pubkey_filename, 'https://gitlab.com/particl/basicswap/-/raw/master/pgp/keys/' + pubkey_filename,
] ]
if coin == 'dash': if coin == 'dash':
pubkeyurls.append('https://raw.githubusercontent.com/dashpay/dash/master/contrib/gitian-keys/pasta.pgp') pubkeyurls.append('https://raw.githubusercontent.com/dashpay/dash/master/contrib/gitian-keys/pasta.pgp')
if coin == 'monero': if coin == 'monero':
pubkeyurls.append('https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc') pubkeyurls.append('https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc')
if coin == 'wownero':
pubkeyurls.append('https://git.wownero.com/wownero/wownero/raw/branch/master/utils/gpg_keys/wowario.asc')
if coin == 'firo': if coin == 'firo':
pubkeyurls.append('https://firo.org/reuben.asc') pubkeyurls.append('https://firo.org/reuben.asc')
if ADD_PUBKEY_URL != '': if ADD_PUBKEY_URL != '':
pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename) pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename)
if coin in ('monero', 'firo'): if coin in ('monero', 'wownero', 'firo'):
with open(assert_path, 'rb') as fp: with open(assert_path, 'rb') as fp:
verified = gpg.verify_file(fp) verified = gpg.verify_file(fp)
@ -778,12 +901,15 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
def writeTorSettings(fp, coin, coin_settings, tor_control_password): def writeTorSettings(fp, coin, coin_settings, tor_control_password):
onionport = coin_settings['onionport']
''' '''
TOR_PROXY_HOST must be an ip address. TOR_PROXY_HOST must be an ip address.
BTC versions >21 and Particl with lookuptorcontrolhost=any can accept hostnames, XMR and LTC cannot BTC versions >21 and Particl with lookuptorcontrolhost=any can accept hostnames, XMR and LTC cannot
''' '''
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n') fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
if coin in ('decred',):
return
onionport = coin_settings['onionport']
fp.write(f'torpassword={tor_control_password}\n') fp.write(f'torpassword={tor_control_password}\n')
fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n') fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n')
@ -802,7 +928,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if not os.path.exists(data_dir): if not os.path.exists(data_dir):
os.makedirs(data_dir) os.makedirs(data_dir)
if coin == 'monero': if coin in ('wownero', 'monero'):
core_conf_path = os.path.join(data_dir, coin + 'd.conf') core_conf_path = os.path.join(data_dir, coin + 'd.conf')
if os.path.exists(core_conf_path): if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path)) exitWithError('{} exists'.format(core_conf_path))
@ -826,18 +952,28 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP)) fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
fp.write('prune-blockchain=1\n') fp.write('prune-blockchain=1\n')
if tor_control_password is not None: if coin == 'monero':
for opt_line in monerod_proxy_config: if XMR_RPC_USER != '':
fp.write(opt_line + '\n') fp.write(f'rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n')
if tor_control_password is not None:
for opt_line in monerod_proxy_config:
fp.write(opt_line + '\n')
if XMR_RPC_USER != '': if coin == 'wownero':
fp.write(f'rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n') if WOW_RPC_USER != '':
fp.write(f'rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n')
if tor_control_password is not None:
for opt_line in wownerod_proxy_config:
fp.write(opt_line + '\n')
if coin in ('wownero', 'monero'):
wallets_dir = core_settings.get('walletsdir', data_dir) wallets_dir = core_settings.get('walletsdir', data_dir)
if not os.path.exists(wallets_dir): if not os.path.exists(wallets_dir):
os.makedirs(wallets_dir) os.makedirs(wallets_dir)
wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf') wallet_conf_path = os.path.join(wallets_dir, coin + '-wallet-rpc.conf')
if coin == 'monero':
wallet_conf_path = os.path.join(wallets_dir, 'monero_wallet.conf')
if os.path.exists(wallet_conf_path): if os.path.exists(wallet_conf_path):
exitWithError('{} exists'.format(wallet_conf_path)) exitWithError('{} exists'.format(wallet_conf_path))
with open(wallet_conf_path, 'w') as fp: with open(wallet_conf_path, 'w') as fp:
@ -851,9 +987,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP)) fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
fp.write(f'wallet-dir={config_datadir}\n') fp.write(f'wallet-dir={config_datadir}\n')
fp.write('log-file={}\n'.format(os.path.join(config_datadir, 'wallet.log'))) fp.write('log-file={}\n'.format(os.path.join(config_datadir, 'wallet.log')))
fp.write('shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb')))
fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword'])) fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword']))
if coin == 'monero':
fp.write('shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb')))
elif coin == 'wownero':
fp.write('wow-shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb')))
if chain == 'regtest': if chain == 'regtest':
fp.write('allow-mismatched-daemon-version=1\n') fp.write('allow-mismatched-daemon-version=1\n')
@ -863,6 +1001,43 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write(opt_line + '\n') fp.write(opt_line + '\n')
return return
if coin == 'decred':
chainname = 'simnet' if chain == 'regtest' else chain
core_conf_path = os.path.join(data_dir, 'dcrd.conf')
if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path))
with open(core_conf_path, 'w') as fp:
if chain != 'mainnet':
fp.write(chainname + '=1\n')
fp.write('debuglevel=info\n')
fp.write('notls=1\n')
fp.write('rpclisten={}:{}\n'.format(core_settings['rpchost'], core_settings['rpcport']))
fp.write('rpcuser={}\n'.format(core_settings['rpcuser']))
fp.write('rpcpass={}\n'.format(core_settings['rpcpassword']))
if tor_control_password is not None:
writeTorSettings(fp, coin, core_settings, tor_control_password)
wallet_conf_path = os.path.join(data_dir, 'dcrwallet.conf')
if os.path.exists(wallet_conf_path):
exitWithError('{} exists'.format(wallet_conf_path))
with open(wallet_conf_path, 'w') as fp:
if chain != 'mainnet':
fp.write(chainname + '=1\n')
fp.write('debuglevel=info\n')
fp.write('noservertls=1\n')
fp.write('noclienttls=1\n')
fp.write('rpcconnect={}:{}\n'.format(core_settings['rpchost'], core_settings['rpcport']))
fp.write('rpclisten={}:{}\n'.format(core_settings['walletrpchost'], core_settings['walletrpcport']))
fp.write('username={}\n'.format(core_settings['rpcuser']))
fp.write('password={}\n'.format(core_settings['rpcpassword']))
return
core_conf_path = os.path.join(data_dir, coin + '.conf') core_conf_path = os.path.join(data_dir, coin + '.conf')
if os.path.exists(core_conf_path): if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path)) exitWithError('{} exists'.format(core_conf_path))
@ -990,12 +1165,15 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
coin_settings = settings['chainclients'][coin] coin_settings = settings['chainclients'][coin]
data_dir = coin_settings['datadir'] data_dir = coin_settings['datadir']
if coin == 'monero': if coin in ('monero', 'wownero'):
core_conf_path = os.path.join(data_dir, coin + 'd.conf') core_conf_path = os.path.join(data_dir, coin + 'd.conf')
if not os.path.exists(core_conf_path): if not os.path.exists(core_conf_path):
exitWithError('{} does not exist'.format(core_conf_path)) exitWithError('{} does not exist'.format(core_conf_path))
wallets_dir = coin_settings.get('walletsdir', data_dir) wallets_dir = coin_settings.get('walletsdir', data_dir)
wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf') wallet_conf_path = os.path.join(wallets_dir, coin + '-wallet-rpc.conf')
if coin == 'monero':
wallet_conf_path = os.path.join(wallets_dir, 'monero_wallet.conf')
if not os.path.exists(wallet_conf_path): if not os.path.exists(wallet_conf_path):
exitWithError('{} does not exist'.format(wallet_conf_path)) exitWithError('{} does not exist'.format(wallet_conf_path))
@ -1008,16 +1186,27 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
# Disable tor first # Disable tor first
for line in fp_in: for line in fp_in:
skip_line: bool = False skip_line: bool = False
for opt_line in monerod_proxy_config: if coin == 'monero':
setting: str = opt_line[0: opt_line.find('=') + 1] for opt_line in monerod_proxy_config:
if line.startswith(setting): setting: str = opt_line[0: opt_line.find('=') + 1]
skip_line = True if line.startswith(setting):
break skip_line = True
break
if coin == 'wownero':
for opt_line in wownerod_proxy_config:
setting: str = opt_line[0: opt_line.find('=') + 1]
if line.startswith(setting):
skip_line = True
break
if not skip_line: if not skip_line:
fp.write(line) fp.write(line)
if enable: if enable:
for opt_line in monerod_proxy_config: if coin == 'monero':
fp.write(opt_line + '\n') for opt_line in monerod_proxy_config:
fp.write(opt_line + '\n')
if coin == 'wownero':
for opt_line in wownerod_proxy_config:
fp.write(opt_line + '\n')
with open(wallet_conf_path, 'w') as fp: with open(wallet_conf_path, 'w') as fp:
with open(wallet_conf_path + '.last') as fp_in: with open(wallet_conf_path + '.last') as fp_in:
@ -1039,7 +1228,11 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
coin_settings['trusted_daemon'] = extra_opts.get('trust_remote_node', 'auto') coin_settings['trusted_daemon'] = extra_opts.get('trust_remote_node', 'auto')
return return
config_path = os.path.join(data_dir, coin + '.conf') if coin == 'decred':
config_path = os.path.join(data_dir, 'dcrd.conf')
else:
config_path = os.path.join(data_dir, coin + '.conf')
if not os.path.exists(config_path): if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path)) exitWithError('{} does not exist'.format(config_path))
@ -1051,9 +1244,12 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
default_onionport = PART_ONION_PORT default_onionport = PART_ONION_PORT
elif coin == 'litecoin': elif coin == 'litecoin':
default_onionport = LTC_ONION_PORT default_onionport = LTC_ONION_PORT
elif coin in ('decred',):
pass
else: else:
exitWithError('Unknown default onion listening port for {}'.format(coin)) exitWithError('Unknown default onion listening port for {}'.format(coin))
coin_settings['onionport'] = default_onionport if default_onionport > 0:
coin_settings['onionport'] = default_onionport
# Backup # Backup
shutil.copyfile(config_path, config_path + '.last') shutil.copyfile(config_path, config_path + '.last')
@ -1106,6 +1302,7 @@ def printHelp():
print('--htmlhost= Interface to host html server on, default:127.0.0.1.') print('--htmlhost= Interface to host html server on, default:127.0.0.1.')
print('--wshost= Interface to host websocket server on, disable by setting to "none", default\'s to --htmlhost.') print('--wshost= Interface to host websocket server on, disable by setting to "none", default\'s to --htmlhost.')
print('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT)) print('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
print('--wowrestoreheight=n Block height to restore Wownero wallet from, default:{}.'.format(DEFAULT_WOW_RESTORE_HEIGHT))
print('--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false') print('--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false')
print('--noextractover Prevent extracting cores if files exist. Speeds up tests') print('--noextractover Prevent extracting cores if files exist. Speeds up tests')
print('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.') print('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.')
@ -1126,13 +1323,13 @@ def printHelp():
def finalise_daemon(d): def finalise_daemon(d):
logging.info('Interrupting {}'.format(d.pid)) logging.info('Interrupting {}'.format(d.handle.pid))
try: try:
d.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT) d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT)
d.wait(timeout=120) d.handle.wait(timeout=120)
except Exception as e: except Exception as e:
logging.info(f'Error {e} for process {d.pid}') logging.info(f'Error {e} for process {d.handle.pid}')
for fp in (d.stdout, d.stderr, d.stdin): for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp: if fp:
fp.close() fp.close()
@ -1153,7 +1350,7 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
if coin_settings['manage_daemon']: if coin_settings['manage_daemon']:
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args)) daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args))
swap_client.setDaemonPID(c, daemons[-1].pid) swap_client.setDaemonPID(c, daemons[-1].handle.pid)
swap_client.setCoinRunParams(c) swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c) swap_client.createCoinInterface(c)
swap_client.waitForDaemonRPC(c, with_wallet=True) swap_client.waitForDaemonRPC(c, with_wallet=True)
@ -1181,6 +1378,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
swap_client = None swap_client = None
daemons = [] daemons = []
daemon_args = ['-noconnect', '-nodnsseed'] daemon_args = ['-noconnect', '-nodnsseed']
generated_mnemonic: bool = False
coins_failed_to_initialise = [] coins_failed_to_initialise = []
@ -1190,7 +1388,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
if not swap_client.use_tor_proxy: if not swap_client.use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0 # Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten') daemon_args.append('-nolisten')
coins_to_create_wallets_for = (Coins.PART, Coins.BTC, Coins.LTC, Coins.DASH) coins_to_create_wallets_for = (Coins.PART, Coins.BTC, Coins.LTC, Coins.DCR, Coins.DASH)
# Always start Particl, it must be running to initialise a wallet in addcoin mode # Always start Particl, it must be running to initialise a wallet in addcoin mode
# Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic # Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic
start_daemons = ['particl', ] + [c for c in with_coins if c != 'particl'] start_daemons = ['particl', ] + [c for c in with_coins if c != 'particl']
@ -1200,8 +1398,14 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
if c == Coins.XMR: if c == Coins.XMR:
if coin_settings['manage_wallet_daemon']: if coin_settings['manage_wallet_daemon']:
filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '') filename = coin_name + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename)) daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename))
elif c == Coins.WOW:
if coin_settings['manage_wallet_daemon']:
filename = coin_name + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename))
elif c == Coins.DCR:
pass
else: else:
if coin_settings['manage_daemon']: if coin_settings['manage_daemon']:
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
@ -1211,11 +1415,26 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())] coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())]
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args)) daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args))
swap_client.setDaemonPID(c, daemons[-1].pid) swap_client.setDaemonPID(c, daemons[-1].handle.pid)
swap_client.setCoinRunParams(c) swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c) swap_client.createCoinInterface(c)
if c in coins_to_create_wallets_for: if c in coins_to_create_wallets_for:
if c == Coins.DCR:
if coin_settings['manage_wallet_daemon'] is False:
continue
from basicswap.interface.dcr.util import createDCRWallet
dcr_password = coin_settings['wallet_pwd'] if WALLET_ENCRYPTION_PWD == '' else WALLET_ENCRYPTION_PWD
extra_opts = ['--appdata="{}"'.format(coin_settings['datadir']),
'--pass={}'.format(dcr_password),
]
filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '')
args = [os.path.join(coin_settings['bindir'], filename), '--create'] + extra_opts
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
createDCRWallet(args, hex_seed, logger, threading.Event())
continue
swap_client.waitForDaemonRPC(c, with_wallet=False) swap_client.waitForDaemonRPC(c, with_wallet=False)
# Create wallet if it doesn't exist yet # Create wallet if it doesn't exist yet
wallets = swap_client.callcoinrpc(c, 'listwallets') wallets = swap_client.callcoinrpc(c, 'listwallets')
@ -1240,6 +1459,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
logger.info('Loading Particl mnemonic') logger.info('Loading Particl mnemonic')
if particl_wallet_mnemonic is None: if particl_wallet_mnemonic is None:
particl_wallet_mnemonic = swap_client.callcoinrpc(Coins.PART, 'mnemonic', ['new'])['mnemonic'] particl_wallet_mnemonic = swap_client.callcoinrpc(Coins.PART, 'mnemonic', ['new'])['mnemonic']
generated_mnemonic = True
swap_client.callcoinrpc(Coins.PART, 'extkeyimportmaster', [particl_wallet_mnemonic]) swap_client.callcoinrpc(Coins.PART, 'extkeyimportmaster', [particl_wallet_mnemonic])
# Particl wallet must be unlocked to call getWalletKey # Particl wallet must be unlocked to call getWalletKey
if WALLET_ENCRYPTION_PWD != '': if WALLET_ENCRYPTION_PWD != '':
@ -1249,7 +1469,9 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
c = swap_client.getCoinIdFromName(coin_name) c = swap_client.getCoinIdFromName(coin_name)
if c in (Coins.PART, ): if c in (Coins.PART, ):
continue continue
swap_client.waitForDaemonRPC(c) if c not in (Coins.DCR, ):
# initialiseWallet only sets main_wallet_seedid_
swap_client.waitForDaemonRPC(c)
try: try:
swap_client.initialiseWallet(c, raise_errors=True) swap_client.initialiseWallet(c, raise_errors=True)
except Exception as e: except Exception as e:
@ -1269,14 +1491,17 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
print('') print('')
for pair in coins_failed_to_initialise: for pair in coins_failed_to_initialise:
c, _ = pair c, e = pair
if c in (Coins.PIVX, ): if c in (Coins.PIVX, ):
print(f'NOTE - Unable to initialise wallet for {getCoinName(c)}. To complete setup click \'Reseed Wallet\' from the ui page once chain is synced.') print(f'NOTE - Unable to initialise wallet for {getCoinName(c)}. To complete setup click \'Reseed Wallet\' from the ui page once chain is synced.')
else: else:
print(f'WARNING - Failed to initialise wallet for {getCoinName(c)}') print(f'WARNING - Failed to initialise wallet for {getCoinName(c)}: {e}')
if 'decred' in with_coins and WALLET_ENCRYPTION_PWD != '':
print('WARNING - dcrwallet requires the password to be entered at the first startup when encrypted.\nPlease use basicswap-run with --startonlycoin=decred and the WALLET_ENCRYPTION_PWD environment var set for the initial sync.')
if particl_wallet_mnemonic is not None: if particl_wallet_mnemonic is not None:
if particl_wallet_mnemonic: if generated_mnemonic:
# Print directly to stdout for tests # Print directly to stdout for tests
print('IMPORTANT - Save your particl wallet recovery phrase:\n{}\n'.format(particl_wallet_mnemonic)) print('IMPORTANT - Save your particl wallet recovery phrase:\n{}\n'.format(particl_wallet_mnemonic))
@ -1293,7 +1518,7 @@ def signal_handler(sig, frame):
def check_btc_fastsync_data(base_dir, sync_file_path): def check_btc_fastsync_data(base_dir, sync_file_path):
github_pgp_url = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp' github_pgp_url = 'https://raw.githubusercontent.com/basicswap/basicswap/master/pgp'
gitlab_pgp_url = 'https://gitlab.com/particl/basicswap/-/raw/master/pgp' gitlab_pgp_url = 'https://gitlab.com/particl/basicswap/-/raw/master/pgp'
asc_filename = BITCOIN_FASTSYNC_FILE + '.asc' asc_filename = BITCOIN_FASTSYNC_FILE + '.asc'
asc_file_path = os.path.join(base_dir, asc_filename) asc_file_path = os.path.join(base_dir, asc_filename)
@ -1342,6 +1567,7 @@ def main():
coins_changed = False coins_changed = False
htmlhost = '127.0.0.1' htmlhost = '127.0.0.1'
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
wow_restore_height = DEFAULT_WOW_RESTORE_HEIGHT
prepare_bin_only = False prepare_bin_only = False
no_cores = False no_cores = False
enable_tor = False enable_tor = False
@ -1428,24 +1654,24 @@ def main():
particl_wallet_mnemonic = s[1].strip('"') particl_wallet_mnemonic = s[1].strip('"')
continue continue
if name in ('withcoin', 'withcoins'): if name in ('withcoin', 'withcoins'):
for coin in [s.lower() for s in s[1].split(',')]: for coin in [s.strip().lower() for s in s[1].split(',')]:
ensure_coin_valid(coin) ensure_coin_valid(coin)
with_coins.add(coin) with_coins.add(coin)
coins_changed = True coins_changed = True
continue continue
if name in ('withoutcoin', 'withoutcoins'): if name in ('withoutcoin', 'withoutcoins'):
for coin in [s.lower() for s in s[1].split(',')]: for coin in [s.strip().lower() for s in s[1].split(',')]:
ensure_coin_valid(coin, test_disabled=False) ensure_coin_valid(coin, test_disabled=False)
with_coins.discard(coin) with_coins.discard(coin)
coins_changed = True coins_changed = True
continue continue
if name == 'addcoin': if name == 'addcoin':
add_coin = s[1].lower() add_coin = s[1].strip().lower()
ensure_coin_valid(add_coin) ensure_coin_valid(add_coin)
with_coins = {add_coin, } with_coins = {add_coin, }
continue continue
if name == 'disablecoin': if name == 'disablecoin':
disable_coin = s[1].lower() disable_coin = s[1].strip().lower()
ensure_coin_valid(disable_coin, test_disabled=False) ensure_coin_valid(disable_coin, test_disabled=False)
continue continue
if name == 'htmlhost': if name == 'htmlhost':
@ -1457,6 +1683,9 @@ def main():
if name == 'xmrrestoreheight': if name == 'xmrrestoreheight':
xmr_restore_height = int(s[1]) xmr_restore_height = int(s[1])
continue continue
if name == 'wowrestoreheight':
wow_restore_height = int(s[1])
continue
if name == 'keysdirpath': if name == 'keysdirpath':
extra_opts['keysdirpath'] = os.path.expanduser(s[1].strip('"')) extra_opts['keysdirpath'] = os.path.expanduser(s[1].strip('"'))
continue continue
@ -1488,7 +1717,7 @@ def main():
if use_tor_proxy and extra_opts.get('no_tor_proxy', False): if use_tor_proxy and extra_opts.get('no_tor_proxy', False):
exitWithError('Can\'t use --usetorproxy and --notorproxy together') exitWithError('Can\'t use --usetorproxy and --notorproxy together')
# Automatically enable tor for certain commands if it's set in basicswap config # Automatically enable usetorproxy for certain commands if it's set in basicswap config
if not (initwalletsonly or enable_tor or disable_tor or disable_coin) and \ if not (initwalletsonly or enable_tor or disable_tor or disable_coin) and \
not use_tor_proxy and os.path.exists(config_path): not use_tor_proxy and os.path.exists(config_path):
settings = load_config(config_path) settings = load_config(config_path)
@ -1555,7 +1784,19 @@ def main():
'override_feerate': 0.002, 'override_feerate': 0.002,
'conf_target': 2, 'conf_target': 2,
'core_version_group': 21, 'core_version_group': 21,
'chain_lookups': 'local', },
'bitcoin': {
'connection_type': 'rpc' if 'bitcoin' in with_coins else 'none',
'manage_daemon': True if ('bitcoin' in with_coins and BTC_RPC_HOST == '127.0.0.1') else False,
'rpchost': BTC_RPC_HOST,
'rpcport': BTC_RPC_PORT + port_offset,
'onionport': BTC_ONION_PORT + port_offset,
'datadir': os.getenv('BTC_DATA_DIR', os.path.join(data_dir, 'bitcoin')),
'bindir': os.path.join(bin_dir, 'bitcoin'),
'use_segwit': True,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 22,
}, },
'litecoin': { 'litecoin': {
'connection_type': 'rpc' if 'litecoin' in with_coins else 'none', 'connection_type': 'rpc' if 'litecoin' in with_coins else 'none',
@ -1570,21 +1811,26 @@ def main():
'conf_target': 2, 'conf_target': 2,
'core_version_group': 21, 'core_version_group': 21,
'min_relay_fee': 0.00001, 'min_relay_fee': 0.00001,
'chain_lookups': 'local',
}, },
'bitcoin': { 'decred': {
'connection_type': 'rpc' if 'bitcoin' in with_coins else 'none', 'connection_type': 'rpc' if 'decred' in with_coins else 'none',
'manage_daemon': True if ('bitcoin' in with_coins and BTC_RPC_HOST == '127.0.0.1') else False, 'manage_daemon': True if ('decred' in with_coins and DCR_RPC_HOST == '127.0.0.1') else False,
'rpchost': BTC_RPC_HOST, 'manage_wallet_daemon': True if ('decred' in with_coins and DCR_WALLET_RPC_HOST == '127.0.0.1') else False,
'rpcport': BTC_RPC_PORT + port_offset, 'wallet_pwd': DCR_WALLET_PWD if WALLET_ENCRYPTION_PWD == '' else '',
'onionport': BTC_ONION_PORT + port_offset, 'rpchost': DCR_RPC_HOST,
'datadir': os.getenv('BTC_DATA_DIR', os.path.join(data_dir, 'bitcoin')), 'rpcport': DCR_RPC_PORT + port_offset,
'bindir': os.path.join(bin_dir, 'bitcoin'), 'walletrpchost': DCR_WALLET_RPC_HOST,
'walletrpcport': DCR_WALLET_RPC_PORT + port_offset,
'rpcuser': DCR_RPC_USER,
'rpcpassword': DCR_RPC_PWD,
'datadir': os.getenv('DCR_DATA_DIR', os.path.join(data_dir, 'decred')),
'bindir': os.path.join(bin_dir, 'decred'),
'use_csv': True,
'use_segwit': True, 'use_segwit': True,
'blocks_confirmed': 1, 'blocks_confirmed': 2,
'conf_target': 2, 'conf_target': 2,
'core_version_group': 22, 'core_type_group': 'dcr',
'chain_lookups': 'local', 'min_relay_fee': 0.00001,
}, },
'namecoin': { 'namecoin': {
'connection_type': 'rpc' if 'namecoin' in with_coins else 'none', 'connection_type': 'rpc' if 'namecoin' in with_coins else 'none',
@ -1620,6 +1866,7 @@ def main():
'rpctimeout': 60, 'rpctimeout': 60,
'walletrpctimeout': 120, 'walletrpctimeout': 120,
'walletrpctimeoutlong': 600, 'walletrpctimeoutlong': 600,
'core_type_group': 'xmr',
}, },
'pivx': { 'pivx': {
'connection_type': 'rpc' if 'pivx' in with_coins else 'none', 'connection_type': 'rpc' if 'pivx' in with_coins else 'none',
@ -1634,7 +1881,6 @@ def main():
'blocks_confirmed': 1, 'blocks_confirmed': 1,
'conf_target': 2, 'conf_target': 2,
'core_version_group': 17, 'core_version_group': 17,
'chain_lookups': 'local',
}, },
'dash': { 'dash': {
'connection_type': 'rpc' if 'dash' in with_coins else 'none', 'connection_type': 'rpc' if 'dash' in with_coins else 'none',
@ -1649,7 +1895,6 @@ def main():
'blocks_confirmed': 1, 'blocks_confirmed': 1,
'conf_target': 2, 'conf_target': 2,
'core_version_group': 18, 'core_version_group': 18,
'chain_lookups': 'local',
}, },
'firo': { 'firo': {
'connection_type': 'rpc' if 'firo' in with_coins else 'none', 'connection_type': 'rpc' if 'firo' in with_coins else 'none',
@ -1665,7 +1910,6 @@ def main():
'conf_target': 2, 'conf_target': 2,
'core_version_group': 14, 'core_version_group': 14,
'min_relay_fee': 0.00001, 'min_relay_fee': 0.00001,
'chain_lookups': 'local',
}, },
'navcoin': { 'navcoin': {
'connection_type': 'rpc' if 'navcoin' in with_coins else 'none', 'connection_type': 'rpc' if 'navcoin' in with_coins else 'none',
@ -1682,6 +1926,28 @@ def main():
'core_version_group': 18, 'core_version_group': 18,
'chain_lookups': 'local', 'chain_lookups': 'local',
'startup_tries': 40, 'startup_tries': 40,
},
'wownero': {
'connection_type': 'rpc' if 'wownero' in with_coins else 'none',
'manage_daemon': True if ('wownero' in with_coins and WOW_RPC_HOST == '127.0.0.1') else False,
'manage_wallet_daemon': True if ('wownero' in with_coins and WOW_WALLET_RPC_HOST == '127.0.0.1') else False,
'rpcport': BASE_WOW_RPC_PORT + port_offset,
'zmqport': BASE_WOW_ZMQ_PORT + port_offset,
'walletrpcport': BASE_WOW_WALLET_PORT + port_offset,
'rpchost': WOW_RPC_HOST,
'trusted_daemon': extra_opts.get('trust_remote_node', 'auto'),
'walletrpchost': WOW_WALLET_RPC_HOST,
'walletrpcuser': WOW_WALLET_RPC_USER,
'walletrpcpassword': WOW_WALLET_RPC_PWD,
'walletfile': 'swap_wallet',
'datadir': os.getenv('WOW_DATA_DIR', os.path.join(data_dir, 'wownero')),
'bindir': os.path.join(bin_dir, 'wownero'),
'restore_height': wow_restore_height,
'blocks_confirmed': 2,
'rpctimeout': 60,
'walletrpctimeout': 120,
'walletrpctimeoutlong': 300,
'core_type_group': 'xmr',
} }
} }
@ -1697,6 +1963,9 @@ def main():
if XMR_RPC_USER != '': if XMR_RPC_USER != '':
chainclients['monero']['rpcuser'] = XMR_RPC_USER chainclients['monero']['rpcuser'] = XMR_RPC_USER
chainclients['monero']['rpcpassword'] = XMR_RPC_PWD chainclients['monero']['rpcpassword'] = XMR_RPC_PWD
if WOW_RPC_USER != '':
chainclients['wownero']['rpcuser'] = WOW_RPC_USER
chainclients['wownero']['rpcpassword'] = WOW_RPC_PWD
if PIVX_RPC_USER != '': if PIVX_RPC_USER != '':
chainclients['pivx']['rpcuser'] = PIVX_RPC_USER chainclients['pivx']['rpcuser'] = PIVX_RPC_USER
chainclients['pivx']['rpcpassword'] = PIVX_RPC_PWD chainclients['pivx']['rpcpassword'] = PIVX_RPC_PWD
@ -1711,6 +1980,7 @@ def main():
chainclients['nav']['rpcpassword'] = NAV_RPC_PWD chainclients['nav']['rpcpassword'] = NAV_RPC_PWD
chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir']) chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir'])
chainclients['wownero']['walletsdir'] = os.getenv('WOW_WALLETS_DIR', chainclients['wownero']['datadir'])
if initwalletsonly: if initwalletsonly:
logger.info('Initialising wallets') logger.info('Initialising wallets')
@ -1784,6 +2054,8 @@ def main():
if add_coin != '': if add_coin != '':
logger.info('Adding coin: %s', add_coin) logger.info('Adding coin: %s', add_coin)
settings = load_config(config_path) settings = load_config(config_path)
if tor_control_password is None and settings.get('use_tor', False):
extra_opts['tor_control_password'] = settings.get('tor_control_password', None)
if particl_wallet_mnemonic != 'none': if particl_wallet_mnemonic != 'none':
# Ensure Particl wallet is unencrypted or correct password is supplied # Ensure Particl wallet is unencrypted or correct password is supplied

@ -18,6 +18,7 @@ import basicswap.config as cfg
from basicswap import __version__ from basicswap import __version__
from basicswap.ui.util import getCoinName from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams
from basicswap.http_server import HttpThread from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer from basicswap.contrib.websocket_server import WebsocketServer
@ -28,18 +29,21 @@ if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
swap_client = None swap_client = None
# TODO: deduplicate
known_coins = [
'particl', class Daemon:
'litecoin', __slots__ = ('handle', 'files')
'bitcoin',
'namecoin', def __init__(self, handle, files):
'monero', self.handle = handle
'pivx', self.files = files
'dash',
'firo',
'navcoin', 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): def signal_handler(sig, frame):
@ -49,7 +53,7 @@ def signal_handler(sig, frame):
swap_client.stopRunning() swap_client.stopRunning()
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[]): def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir) datadir_path = os.path.expanduser(node_dir)
@ -71,41 +75,64 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
for line in config_to_add: for line in config_to_add:
fp.write(line + '\n') fp.write(line + '\n')
args = [daemon_bin, '-datadir=' + datadir_path] + opts args = [daemon_bin, ]
logging.info('Starting node ' + daemon_bin + ' ' + '-datadir=' + node_dir) add_datadir: bool = extra_config.get('add_datadir', True)
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=datadir_path) 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=[]): def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir) datadir_path = os.path.expanduser(node_dir)
args = [daemon_bin, '--non-interactive', '--config-file=' + os.path.join(datadir_path, 'monerod.conf')] + opts config_filename = 'wownerod.conf' if daemon_bin.startswith('wow') else 'monerod.conf'
logging.info('Starting node {} --data-dir={}'.format(daemon_bin, node_dir)) 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) # 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_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w')
file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w') file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w')
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=file_stdout, stderr=file_stderr, cwd=datadir_path) 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=[]): def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin)) daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_bin, '--non-interactive']
data_dir = os.path.expanduser(node_dir)
config_path = os.path.join(data_dir, 'monero_wallet.conf')
args = [daemon_bin, '--non-interactive', '--config-file=' + config_path] + opts
# Remove old config
needs_rewrite: bool = False needs_rewrite: bool = False
config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy='] config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy=']
with open(config_path) as fp:
for line in fp: data_dir = os.path.expanduser(node_dir)
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())) wallet_config_filename = 'wownero-wallet-rpc.conf' if wallet_bin.startswith('wow') else 'monero_wallet.conf'
needs_rewrite = True 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: if needs_rewrite:
logging.info('Rewriting monero_wallet.conf') logging.info('Rewriting wallet config')
shutil.copyfile(config_path, config_path + '.last') shutil.copyfile(config_path, config_path + '.last')
with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to: with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to:
for line in fp_from: for line in fp_from:
@ -117,7 +144,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_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_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w')
wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w') wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w')
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir) 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): def ws_new_client(client, server):
@ -148,7 +175,11 @@ def runClient(fp, data_dir, chain, start_only_coins):
pids_path = os.path.join(data_dir, '.pids') pids_path = os.path.join(data_dir, '.pids')
if os.getenv('WALLET_ENCRYPTION_PWD', '') != '': if os.getenv('WALLET_ENCRYPTION_PWD', '') != '':
raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.') 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): if not os.path.exists(settings_path):
raise ValueError('Settings file not found: ' + str(settings_path)) raise ValueError('Settings file not found: ' + str(settings_path))
@ -172,21 +203,20 @@ def runClient(fp, data_dir, chain, start_only_coins):
try: try:
# Try start daemons # Try start daemons
for c, v in settings['chainclients'].items(): for c, v in settings['chainclients'].items():
if len(start_only_coins) > 0 and c not in start_only_coins: if len(start_only_coins) > 0 and c not in start_only_coins:
continue continue
try: try:
coin_id = swap_client.getCoinIdFromName(c) coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id) display_name = getCoinName(coin_id)
except Exception as e: except Exception as e:
logger.warning('Error getting coin display name for {}: {}'.format(c, str(e))) logger.warning('Not starting unknown coin: {}'.format(c))
display_name = 'Unknown' continue
if c == 'monero': if c in ('monero', 'wownero'):
if v['manage_daemon'] is True: if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon') swap_client.log.info(f'Starting {display_name} daemon')
filename = 'monerod' + ('.exe' if os.name == 'nt' else '') filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename)) daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
pid = daemons[-1].pid pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid)) swap_client.log.info('Started {} {}'.format(filename, pid))
if v['manage_wallet_daemon'] is True: if v['manage_wallet_daemon'] is True:
@ -199,7 +229,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost']) proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost'])
if proxy_host: if proxy_host:
proxy_log_str = ' through proxy' proxy_log_str = ' through proxy'
opts += ['--proxy', f'{proxy_host}:{proxy_port}', ] opts += ['--proxy', f'{proxy_host}:{proxy_port}', '--daemon-ssl-allow-any-cert', ]
swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str)) swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str))
@ -210,18 +240,49 @@ def runClient(fp, data_dir, chain, start_only_coins):
opts.append(daemon_rpcuser + ':' + daemon_rpcpass) opts.append(daemon_rpcuser + ':' + daemon_rpcpass)
opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon') opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '') filename = c + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts)) daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
pid = daemons[-1].pid pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid)) swap_client.log.info('Started {} {}'.format(filename, pid))
continue 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: if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon') swap_client.log.info(f'Starting {display_name} daemon')
filename = c + 'd' + ('.exe' if os.name == 'nt' else '') filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(v['datadir'], v['bindir'], filename)) daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
pid = daemons[-1].pid pid = daemons[-1].handle.pid
pids.append((c, pid)) pids.append((c, pid))
swap_client.setDaemonPID(c, pid) swap_client.setDaemonPID(c, pid)
swap_client.log.info('Started {} {}'.format(filename, pid)) swap_client.log.info('Started {} {}'.format(filename, pid))
@ -281,18 +342,18 @@ def runClient(fp, data_dir, chain, start_only_coins):
closed_pids = [] closed_pids = []
for d in daemons: for d in daemons:
swap_client.log.info('Interrupting {}'.format(d.pid)) swap_client.log.info('Interrupting {}'.format(d.handle.pid))
try: try:
d.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT) d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT)
except Exception as e: except Exception as e:
swap_client.log.info('Interrupting %d, error %s', d.pid, str(e)) swap_client.log.info('Interrupting %d, error %s', d.handle.pid, str(e))
for d in daemons: for d in daemons:
try: try:
d.wait(timeout=120) d.handle.wait(timeout=120)
for fp in (d.stdout, d.stderr, d.stdin): for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
if fp: if fp:
fp.close() fp.close()
closed_pids.append(d.pid) closed_pids.append(d.handle.pid)
except Exception as ex: except Exception as ex:
swap_client.log.error('Error: {}'.format(ex)) swap_client.log.error('Error: {}'.format(ex))
@ -359,13 +420,16 @@ def main():
continue continue
if name == 'startonlycoin': if name == 'startonlycoin':
for coin in [s.lower() for s in s[1].split(',')]: for coin in [s.lower() for s in s[1].split(',')]:
if coin not in known_coins: if is_known_coin(coin) is False:
raise ValueError(f'Unknown coin: {coin}') raise ValueError(f'Unknown coin: {coin}')
start_only_coins.add(coin) start_only_coins.add(coin)
continue continue
logger.warning('Unknown argument %s', v) logger.warning('Unknown argument %s', v)
if os.name == 'nt':
logger.warning('Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead.')
if data_dir is None: if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR)) data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
logger.info('Using datadir: %s', data_dir) logger.info('Using datadir: %s', data_dir)

@ -1,7 +1,7 @@
## Source code ## Source code
git clone https://github.com/tecnovert/basicswap.git git clone https://github.com/basicswap/basicswap.git
## Run Using Docker ## Run Using Docker
@ -125,7 +125,7 @@ Install Git:
Download the BasicSwap code: Download the BasicSwap code:
git clone https://github.com/tecnovert/basicswap.git git clone https://github.com/basicswap/basicswap.git
cd basicswap/docker/ cd basicswap/docker/
@ -140,7 +140,7 @@ Continue from the [Run Using Docker](#run-using-docker) section.
### Ubuntu Setup: ### Ubuntu Setup:
apt-get install -y wget git python3-venv python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config curl jq apt-get install -y wget git python3-venv python3-pip gnupg unzip automake libtool pkg-config curl jq
### OSX Setup: ### OSX Setup:
@ -170,7 +170,7 @@ Close the terminal and open a new one to update the python symlinks.
cd $SWAP_DATADIR cd $SWAP_DATADIR
git clone https://github.com/tecnovert/basicswap.git git clone https://github.com/basicswap/basicswap.git
cd $SWAP_DATADIR/basicswap cd $SWAP_DATADIR/basicswap
@ -182,7 +182,6 @@ From https://pypi.org/project/certifi/
Continue installing Basicswap Continue installing Basicswap
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto
pip3 install . pip3 install .

@ -99,7 +99,7 @@ Install coincurve
Install basicswap Install basicswap
git clone https://github.com/tecnovert/basicswap.git git clone https://github.com/basicswap/basicswap.git
cd basicswap cd basicswap
pip3 install . pip3 install .

@ -1,3 +1,15 @@
0.13.2
==============
- Remove protobuf and protoc dependencies.
- Include mnemonic dependency directly.
0.13.1
==============
- coins: Add Decred.
0.13.0 0.13.0
============== ==============

@ -22,7 +22,7 @@ Docker will create directories instead of files if these don't exist.
#### For a new install #### For a new install
Use the `--usetorproxy` argument to download the coin binaries over tor, then enable tor with `--enabletor`. Use the `--usetorproxy` argument to download the coin binaries over tor, then enable tor with `--enabletor`.
Note that some download links, notably for Litecoin, are unreachable when using tor. Note that some download links may be unreachable when using tor.
docker compose -f docker-compose_with_tor.yml run --rm swapclient \ docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl

@ -0,0 +1,16 @@
monero_wallet:
image: i_decred_wallet
build:
context: decred_wallet
dockerfile: Dockerfile
container_name: decred_wallet
volumes:
- ${DATA_PATH}/decred_wallet:/data
expose:
- ${DCR_WALLET_RPC_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

@ -0,0 +1,16 @@
wownero_wallet:
image: i_wownero_wallet
build:
context: wownero_wallet
dockerfile: Dockerfile
container_name: wownero_wallet
volumes:
- ${DATA_PATH}/wownero_wallet:/data
expose:
- ${BASE_WOW_WALLET_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

@ -0,0 +1,16 @@
monero_daemon:
image: i_decred_daemon
build:
context: decred_daemon
dockerfile: Dockerfile
container_name: decred_daemon
volumes:
- ${DATA_PATH}/decred_daemon:/data
expose:
- ${DCR_RPC_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

@ -0,0 +1,16 @@
wownero_daemon:
image: i_wownero_daemon
build:
context: wownero_daemon
dockerfile: Dockerfile
container_name: wownero_daemon
volumes:
- ${DATA_PATH}/wownero_daemon:/data
expose:
- ${BASE_WOW_RPC_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

@ -8,6 +8,8 @@
- ${DATA_PATH}/swapclient:/data/swapclient - ${DATA_PATH}/swapclient:/data/swapclient
- ${DATA_PATH}/monero_daemon:/data/monero_daemon - ${DATA_PATH}/monero_daemon:/data/monero_daemon
- ${DATA_PATH}/monero_wallet:/data/monero_wallet - ${DATA_PATH}/monero_wallet:/data/monero_wallet
- ${DATA_PATH}/wownero_daemon:/data/wownero_daemon
- ${DATA_PATH}/wownero_wallet:/data/wownero_wallet
- ${DATA_PATH}/particl:/data/particl - ${DATA_PATH}/particl:/data/particl
- ${DATA_PATH}/bitcoin:/data/bitcoin - ${DATA_PATH}/bitcoin:/data/bitcoin
- ${DATA_PATH}/litecoin:/data/litecoin - ${DATA_PATH}/litecoin:/data/litecoin
@ -45,6 +47,16 @@
- XMR_WALLET_RPC_USER - XMR_WALLET_RPC_USER
- XMR_WALLET_RPC_PWD - XMR_WALLET_RPC_PWD
- DEFAULT_XMR_RESTORE_HEIGHT - DEFAULT_XMR_RESTORE_HEIGHT
- WOW_DATA_DIR
- WOW_RPC_HOST
- BASE_WOW_RPC_PORT
- BASE_WOW_ZMQ_PORT
- WOW_WALLETS_DIR
- WOW_WALLET_RPC_HOST
- BASE_WOW_WALLET_PORT
- WOW_WALLET_RPC_USER
- WOW_WALLET_RPC_PWD
- DEFAULT_WOW_RESTORE_HEIGHT
- PIVX_DATA_DIR - PIVX_DATA_DIR
- PIVX_RPC_HOST - PIVX_RPC_HOST
- PIVX_RPC_PORT - PIVX_RPC_PORT

@ -0,0 +1,25 @@
FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=decred --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
COPY --from=install_stage /coin_bin .
ENV DCR_DATA /data
RUN groupadd -r decred && useradd -r -m -g decred decred \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir "$DCR_DATA" \
&& chown -R decred:decred "$DCR_DATA" \
&& ln -sfn "$DECRED_DATA" /home/decred/decred \
&& chown -h decred:decred /home/decred/decred
VOLUME /data
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 9108 9109
CMD ["/decred/dcrd", "--datadir=/data"]

@ -0,0 +1,11 @@
#!/bin/bash
set -e
if [[ "$1" == "dcrctl" || "$1" == "dcrd" || "$1" == "dcrwallet" ]]; then
mkdir -p "$DECRED_DATA"
chown -h decred:decred /home/decred/decred
exec gosu decred "$@"
else
exec "$@"
fi

@ -0,0 +1,19 @@
FROM i_decred_daemon
ENV DCR_DATA /data
RUN groupadd -r decred && useradd -r -m -g decred decred \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir "$DCR_DATA" \
&& chown -R decred:decred "$DCR_DATA" \
&& ln -sfn "$DECRED_DATA" /home/decred/decred \
&& chown -h decred:decred /home/decred/decred
VOLUME /data
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 9209
CMD ["/decred/dcrwallet", "--datadir=/data"]

@ -0,0 +1,11 @@
#!/bin/bash
set -e
if [[ "$1" == "dcrctl" || "$1" == "dcrd" || "$1" == "dcrwallet" ]]; then
mkdir -p "$DECRED_DATA"
chown -h decred:decred /home/decred/decred
exec gosu decred "$@"
else
exec "$@"
fi

@ -14,17 +14,25 @@ PART_ZMQ_PORT=20792
PART_RPC_USER=particl_user PART_RPC_USER=particl_user
PART_RPC_PWD=particl_pwd PART_RPC_PWD=particl_pwd
BTC_DATA_DIR=/data/bitcoin
BTC_RPC_HOST=bitcoin_core
BTC_RPC_PORT=19796
BTC_RPC_USER=bitcoin_user
BTC_RPC_PWD=bitcoin_pwd
LTC_DATA_DIR=/data/litecoin LTC_DATA_DIR=/data/litecoin
LTC_RPC_HOST=litecoin_core LTC_RPC_HOST=litecoin_core
LTC_RPC_PORT=19795 LTC_RPC_PORT=19795
LTC_RPC_USER=litecoin_user LTC_RPC_USER=litecoin_user
LTC_RPC_PWD=litecoin_pwd LTC_RPC_PWD=litecoin_pwd
BTC_DATA_DIR=/data/bitcoin DCR_DATA_DIR=/data/decred
BTC_RPC_HOST=bitcoin_core DCR_RPC_HOST=decred_daemon
BTC_RPC_PORT=19796 DCR_RPC_PORT=DCR_RPC_PORT
BTC_RPC_USER=bitcoin_user DCR_WALLET_RPC_HOST=decred_wallet
BTC_RPC_PWD=bitcoin_pwd DCR_WALLET_RPC_PORT=9209
DCR_RPC_USER=decred_user
DCR_RPC_PWD=decred_pass
XMR_DATA_DIR=/data/monero_daemon XMR_DATA_DIR=/data/monero_daemon
XMR_RPC_HOST=monero_daemon XMR_RPC_HOST=monero_daemon
@ -36,6 +44,16 @@ BASE_XMR_WALLET_PORT=29998
XMR_WALLET_RPC_USER=xmr_wallet_user XMR_WALLET_RPC_USER=xmr_wallet_user
XMR_WALLET_RPC_PWD=xmr_wallet_pwd XMR_WALLET_RPC_PWD=xmr_wallet_pwd
WOW_DATA_DIR=/data/wownero_daemon
WOW_RPC_HOST=wownero_daemon
BASE_WOW_RPC_PORT=34598
WOW_WALLETS_DIR=/data/wownero_wallet
WOW_WALLET_RPC_HOST=wownero_wallet
BASE_WOW_WALLET_PORT=34798
WOW_WALLET_RPC_USER=wow_wallet_user
WOW_WALLET_RPC_PWD=wow_wallet_pwd
PIVX_DATA_DIR=/data/pivx PIVX_DATA_DIR=/data/pivx
PIVX_RPC_HOST=pivx_core PIVX_RPC_HOST=pivx_core
PIVX_RPC_PORT=51473 PIVX_RPC_PORT=51473

@ -3,6 +3,13 @@
This will setup Basicswap so that each coin runs in it's own container. This will setup Basicswap so that each coin runs in it's own container.
### Coin notes
- Decred is only partially supported, the wallet will need to be initialised manually.
## Setup
Install dependencies: Install dependencies:
sudo apt install basez docker-compose sudo apt install basez docker-compose
@ -33,13 +40,14 @@ Create docker-compose config:
# Using the helper script: # Using the helper script:
./scripts/build_yml_files.py -c bitcoin monero ./scripts/build_yml_files.py -c bitcoin monero
# Or # Or manually:
cat compose-fragments/0_start.yml > docker-compose.yml cat compose-fragments/0_start.yml > docker-compose.yml
# Add the relevant coin fragments # Add the relevant coin fragments
cat compose-fragments/1_bitcoin.yml >> docker-compose.yml cat compose-fragments/1_bitcoin.yml >> docker-compose.yml
cat compose-fragments/1_litecoin.yml >> docker-compose.yml cat compose-fragments/1_litecoin.yml >> docker-compose.yml
cat compose-fragments/1_decred-wallet.yml >> docker-compose.yml
cat compose-fragments/1_monero-wallet.yml >> docker-compose.yml cat compose-fragments/1_monero-wallet.yml >> docker-compose.yml
cat compose-fragments/1_pivx.yml >> docker-compose.yml cat compose-fragments/1_pivx.yml >> docker-compose.yml
cat compose-fragments/1_dash.yml >> docker-compose.yml cat compose-fragments/1_dash.yml >> docker-compose.yml
@ -121,6 +129,7 @@ Start BasicSwap:
popd popd
docker-compose build monero_daemon docker-compose build monero_daemon
docker-compose build decred_daemon
docker-compose build docker-compose build
docker-compose build --no-cache swapclient docker-compose build --no-cache swapclient
@ -178,4 +187,4 @@ Prepare wallet:
# Notes # Notes
Switch Basicswap repo: Switch Basicswap repo:
docker-compose build swapclient --build-arg BASICSWAP_URL=https://github.com/tecnovert/basicswap/archive/refs/heads/dev.zip --build-arg BASICSWAP_DIR=basicswap-dev docker-compose build swapclient --build-arg BASICSWAP_URL=https://github.com/basicswap/basicswap/archive/refs/heads/dev.zip --build-arg BASICSWAP_DIR=basicswap-dev

@ -57,12 +57,21 @@ def main():
if coin_name == 'particl': if coin_name == 'particl':
# Nothing to do # Nothing to do
continue continue
if coin_name == 'monero': if coin_name in ('monero', 'wownero'):
with open(os.path.join(fragments_dir, '1_monero-wallet.yml'), 'rb') as fp_in: with open(os.path.join(fragments_dir, '1_{coin_name}-wallet.yml'), 'rb') as fp_in:
for line in fp_in: for line in fp_in:
fp.write(line) fp.write(line)
fpp.write(line) fpp.write(line)
with open(os.path.join(fragments_dir, '8_monero-daemon.yml'), 'rb') as fp_in: with open(os.path.join(fragments_dir, '8_{coin_name}-daemon.yml'), 'rb') as fp_in:
for line in fp_in:
fp.write(line)
continue
if coin_name == 'decred':
with open(os.path.join(fragments_dir, '1_decred-wallet.yml'), 'rb') as fp_in:
for line in fp_in:
fp.write(line)
fpp.write(line)
with open(os.path.join(fragments_dir, '8_decred-daemon.yml'), 'rb') as fp_in:
for line in fp_in: for line in fp_in:
fp.write(line) fp.write(line)
continue continue

@ -7,14 +7,6 @@ ENV LANG=C.UTF-8 \
RUN apt-get update; \ RUN apt-get update; \
apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata; apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata;
# Must install protoc directly as latest package is not up to date
RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/releases/download/v21.1/protobuf-python-4.21.1.tar.gz && \
tar xvf protobuf_src.tar.gz && \
cd protobuf-3.21.1 && \
./configure --prefix=/usr && \
make -j$(nproc) install && \
ldconfig
ARG COINCURVE_VERSION=v0.2 ARG COINCURVE_VERSION=v0.2
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \ RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
unzip coincurve-anonswap.zip && \ unzip coincurve-anonswap.zip && \
@ -22,18 +14,16 @@ RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archiv
cd coincurve-anonswap && \ cd coincurve-anonswap && \
python3 setup.py install --force python3 setup.py install --force
ARG BASICSWAP_URL=https://github.com/tecnovert/basicswap/archive/master.zip ARG BASICSWAP_URL=https://github.com/basicswap/basicswap/archive/master.zip
ARG BASICSWAP_DIR=basicswap-master ARG BASICSWAP_DIR=basicswap-master
RUN wget -O basicswap-repo.zip $BASICSWAP_URL; \ RUN wget -O basicswap-repo.zip $BASICSWAP_URL; \
unzip basicswap-repo.zip; \ unzip basicswap-repo.zip; \
mv $BASICSWAP_DIR /basicswap; \ mv $BASICSWAP_DIR /basicswap; \
cd /basicswap; \ cd /basicswap; \
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
pip3 install .; pip3 install .;
#COPY ./test_code basicswap #COPY ./test_code basicswap
#RUN cd basicswap; \ #RUN cd basicswap; \
# protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
# pip3 install .; # pip3 install .;
RUN groupadd -r swap_user && useradd -g swap_user -ms /bin/bash swap_user && \ RUN groupadd -r swap_user && useradd -g swap_user -ms /bin/bash swap_user && \

@ -0,0 +1,24 @@
FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=wownero --withoutcoins=particl
FROM debian:bullseye-slim
COPY --from=install_stage /coin_bin .
ENV WOWNERO_DATA /data
RUN groupadd -r wownero && useradd -r -m -g wownero wownero \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p "$WOWNERO_DATA" \
&& chown -R wownero:wownero "$WOWNERO_DATA" \
&& ln -sfn "$WOWNERO_DATA" /home/wownero/.wownero \
&& chown -h wownero:wownero /home/wownero/.wownero
VOLUME $WOWNERO_DATA
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/wownero/wownerod", "--non-interactive", "--config-file=/home/wownero/.wownero/wownerod.conf", "--confirm-external-bind"]

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

Loading…
Cancel
Save