mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-06 02:38:11 +01:00
Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f61c7d26d | ||
|
|
b4a08ce15e | ||
|
|
744ad7988a | ||
|
|
56cd6da556 | ||
|
|
42955af42c | ||
|
|
f20a9fd75b | ||
|
|
4942f23de6 | ||
|
|
037851a002 | ||
|
|
cf92c5635d | ||
|
|
5f7abbb2eb | ||
|
|
fdb02d10d6 | ||
|
|
0dc55fc449 | ||
|
|
790a550e7f | ||
|
|
d19a7538fd | ||
|
|
913cdfa984 | ||
|
|
289b2a53db | ||
|
|
5941f2952e | ||
|
|
414947cbb5 | ||
|
|
435e74f83a | ||
|
|
0ca5aefc12 | ||
|
|
938c641736 | ||
|
|
68b066a2d1 | ||
|
|
c5508fe9be | ||
|
|
418e863d26 | ||
|
|
1bb721edf5 | ||
|
|
801006fa70 | ||
|
|
1763dec981 | ||
|
|
128291a36a | ||
|
|
31ead537c9 | ||
|
|
25cfcc7cee | ||
|
|
e7a70f1e26 | ||
|
|
ad43ce4095 | ||
|
|
b1b00b5342 | ||
|
|
a4dc9af301 | ||
|
|
eefaab1752 | ||
|
|
c16dd1bba3 | ||
|
|
08df0ceae0 | ||
|
|
26de907185 | ||
|
|
e90800884a | ||
|
|
bebbba49ff | ||
|
|
d417a46e67 | ||
|
|
1b36154142 | ||
|
|
f4f64423a4 | ||
|
|
3ba2145cc9 | ||
|
|
9386544b3d | ||
|
|
3e3a83e6d4 | ||
|
|
33105a832f | ||
|
|
01f6a1d877 | ||
|
|
4143f1a8ce | ||
|
|
ea41c4b41a | ||
|
|
bd571702cb | ||
|
|
fa8764342e | ||
|
|
757f8f2762 | ||
|
|
0bd626d659 | ||
|
|
5db8d6ccbe | ||
|
|
eb30ef22fc | ||
|
|
2e4be0274a | ||
|
|
28af80873a | ||
|
|
8795ecc231 | ||
|
|
5bf20370eb | ||
|
|
893fc87b28 | ||
|
|
6e0f6dabe4 | ||
|
|
2983238ef5 | ||
|
|
3b86985ae3 | ||
|
|
732c87b013 | ||
|
|
6be9a14335 | ||
|
|
f93fae6696 | ||
|
|
373525b364 | ||
|
|
7b03ce4769 | ||
|
|
b484827c15 | ||
|
|
b5f6eb6526 | ||
|
|
e28d41ed0c | ||
|
|
aefb094694 | ||
|
|
cf1811cec3 | ||
|
|
273da833db | ||
|
|
f6916f093b | ||
|
|
51c1179326 | ||
|
|
6f0123e13e | ||
|
|
ca5b9e5e00 | ||
|
|
283cfc7c59 | ||
|
|
e05aaeba26 | ||
|
|
8d96ea7fcf | ||
|
|
b400669919 | ||
|
|
fc17fa41ff | ||
|
|
a214866b08 | ||
|
|
62e6978be1 | ||
|
|
00d70f8cc7 | ||
|
|
60cca03d31 | ||
|
|
52ae633c21 | ||
|
|
f02920721f | ||
|
|
765fadb0ed | ||
|
|
ccc90ccb67 | ||
|
|
bff3c45976 | ||
|
|
17308f9a66 | ||
|
|
ed80caf532 | ||
|
|
465f910812 | ||
|
|
a7c2fbba1f | ||
|
|
b7da4f2096 | ||
|
|
b73907bb84 | ||
|
|
499b086b57 | ||
|
|
b83f289013 | ||
|
|
3ea832bd03 | ||
|
|
1b43806d51 | ||
|
|
745d1460e5 | ||
|
|
15e7a6efda | ||
|
|
602682a2f4 | ||
|
|
2296198b44 | ||
|
|
cc3ef1c065 | ||
|
|
1d5d6004bc | ||
|
|
3345d56f5b | ||
|
|
e23216df07 | ||
|
|
3cab753398 | ||
|
|
5e71367c21 | ||
|
|
1eca1b60ab | ||
|
|
01c8130535 | ||
|
|
062cc6dbdc | ||
|
|
72bfcd3521 | ||
|
|
33cf81a76d | ||
|
|
445aa116ad | ||
|
|
bdc173187d | ||
|
|
87ad321987 | ||
|
|
19c6cff7d3 | ||
|
|
996c67beea | ||
|
|
e2fe0697ee | ||
|
|
0a5680da13 | ||
|
|
264f4d209f | ||
|
|
3ee69ea11a | ||
|
|
c523754516 | ||
|
|
13015e3da9 | ||
|
|
f7141dd0c9 | ||
|
|
3430776ffc | ||
|
|
48a46aea47 | ||
|
|
eb7f3b54ec | ||
|
|
c43d46c7e8 | ||
|
|
7d77d46fa2 | ||
|
|
934e809ac3 | ||
|
|
8081f22e92 | ||
|
|
c9b99dd67a | ||
|
|
fe83736ec7 | ||
|
|
817d2c1e9c | ||
|
|
c0d9b7c161 | ||
|
|
014ee22b79 | ||
|
|
cbd0898eb1 | ||
|
|
63d27b4a6f | ||
|
|
fdfa03eaaf | ||
|
|
c9d1129e93 | ||
|
|
376b485261 | ||
|
|
75fa008f0a | ||
|
|
54983913e1 | ||
|
|
c6f3c684a8 | ||
|
|
39aad231cd | ||
|
|
bd06c435e9 | ||
|
|
fb1caea4de | ||
|
|
d8430f4ca9 | ||
|
|
f7315d405d | ||
|
|
bcd251c4df | ||
|
|
3f963f3329 | ||
|
|
4117c461bb | ||
|
|
8c6ea947ba | ||
|
|
f2a3fc1da1 | ||
|
|
771ad2586a | ||
|
|
60a3956c07 | ||
|
|
d097846756 | ||
|
|
1209d1b269 | ||
|
|
209dea52b3 | ||
|
|
f954822d74 | ||
|
|
dbdb89cd10 | ||
|
|
c53e426989 | ||
|
|
484ad0ca38 | ||
|
|
ac7f24daff | ||
|
|
c1f724ac5e | ||
|
|
d5f643aab9 | ||
|
|
72fc065928 | ||
|
|
25b479fdb6 | ||
|
|
4b18ddfe79 | ||
|
|
bdea7de27e | ||
|
|
bcd9d5c9af | ||
|
|
fe976810e3 | ||
|
|
033167a451 | ||
|
|
cdfb9132ad | ||
|
|
b6d29a33d2 | ||
|
|
003d7b85ab | ||
|
|
1b585ea5c9 | ||
|
|
47a80dc603 | ||
|
|
b29d37a8be | ||
|
|
60369fc2a4 | ||
|
|
3927d823c0 | ||
|
|
1d58dfdc94 | ||
|
|
7ac75f7344 | ||
|
|
80c43056cc | ||
|
|
1564655777 | ||
|
|
2d243fc310 | ||
|
|
75ad5a5b4d | ||
|
|
5e69bf172c | ||
|
|
f307332409 | ||
|
|
56378d168b | ||
|
|
274be9d716 | ||
|
|
eec0760e44 | ||
|
|
6ed108e741 | ||
|
|
7429dc5b2d | ||
|
|
9a900a5bac | ||
|
|
ba4796c763 | ||
|
|
57f238d48e | ||
|
|
d93a73c29e | ||
|
|
94303cff93 | ||
|
|
a977cfe857 | ||
|
|
00912b277a | ||
|
|
40eff0ce0f | ||
|
|
9835d33d12 | ||
|
|
64165e387e | ||
|
|
34d94760a3 | ||
|
|
713990f57b |
17
.cirrus.yml
17
.cirrus.yml
@@ -3,11 +3,10 @@ container:
|
||||
|
||||
lint_task:
|
||||
setup_script:
|
||||
- pip install flake8
|
||||
- pip install codespell
|
||||
- pip install flake8 codespell
|
||||
script:
|
||||
- flake8 --version
|
||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
||||
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
|
||||
test_task:
|
||||
@@ -17,25 +16,21 @@ test_task:
|
||||
- BIN_DIR: /tmp/cached_bin
|
||||
- PARTICL_BINDIR: ${BIN_DIR}/particl
|
||||
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
|
||||
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
|
||||
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
|
||||
- XMR_BINDIR: ${BIN_DIR}/monero
|
||||
setup_script:
|
||||
- apt-get update
|
||||
- apt-get install -y wget python3-pip gnupg unzip automake libtool pkg-config
|
||||
- apt-get install -y python3-pip pkg-config
|
||||
- pip install tox pytest
|
||||
- python3 setup.py install
|
||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||
- cd coincurve-anonswap
|
||||
- python3 setup.py install --force
|
||||
- pip install .
|
||||
bins_cache:
|
||||
folder: /tmp/cached_bin
|
||||
reupload_on_changes: false
|
||||
fingerprint_script:
|
||||
- basicswap-prepare -v
|
||||
populate_script:
|
||||
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
||||
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
|
||||
script:
|
||||
- cd "${CIRRUS_WORKING_DIR}"
|
||||
- export DATADIRS="${TEST_DIR}"
|
||||
|
||||
62
.github/workflows/ci.yml
vendored
Normal file
62
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
BIN_DIR: /tmp/cached_bin
|
||||
TEST_RELOAD_PATH: /tmp/test_basicswap
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 codespell pytest
|
||||
pip install -r requirements.txt --require-hashes
|
||||
- name: Install
|
||||
run: |
|
||||
pip install .
|
||||
- name: Running flake8
|
||||
run: |
|
||||
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||
- name: Running codespell
|
||||
run: |
|
||||
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
- name: Running test_other
|
||||
run: |
|
||||
pytest tests/basicswap/test_other.py
|
||||
- name: Cache coin cores
|
||||
id: cache-cores
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cores
|
||||
CACHE_KEY: $(printf $(python bin/basicswap-prepare.py --version --withcoins=bitcoin) | sha256sum | head -c 64)
|
||||
with:
|
||||
path: $BIN_DIR
|
||||
key: $CACHE_KEY
|
||||
|
||||
- if: ${{ steps.cache-yarn.outputs.cache-hit != 'true' }}
|
||||
name: Running basicswap-prepare
|
||||
run: |
|
||||
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
|
||||
- name: Running test_encrypted_xmr_reload
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export TEST_PATH=${TEST_RELOAD_PATH}
|
||||
mkdir -p ${TEST_PATH}/bin
|
||||
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
|
||||
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,3 +13,6 @@ __pycache__
|
||||
# geckodriver.log
|
||||
*.log
|
||||
docker/.env
|
||||
|
||||
# vscode dev container settings
|
||||
compose-dev.yaml
|
||||
60
.travis.yml
60
.travis.yml
@@ -1,60 +0,0 @@
|
||||
dist: bionic
|
||||
os: linux
|
||||
language: python
|
||||
python: '3.7'
|
||||
stages:
|
||||
- lint
|
||||
- test
|
||||
env:
|
||||
global:
|
||||
- TEST_DIR=${HOME}/test_basicswap2
|
||||
- TEST_RELOAD_PATH=~/test_basicswap1
|
||||
- BIN_DIR=~/cached_bin
|
||||
- PARTICL_BINDIR=${BIN_DIR}/particl
|
||||
- BITCOIN_BINDIR=${BIN_DIR}/bitcoin
|
||||
- LITECOIN_BINDIR=${BIN_DIR}/litecoin
|
||||
- XMR_BINDIR=${BIN_DIR}/monero
|
||||
cache:
|
||||
directories:
|
||||
- "$BIN_DIR"
|
||||
before_install:
|
||||
- sudo apt-get install -y wget python3-pip gnupg unzip automake libtool pkg-config
|
||||
install:
|
||||
- travis_retry pip install tox pytest
|
||||
before_script:
|
||||
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
|
||||
- unzip -d coincurve-anonswap coincurve-anonswap.zip
|
||||
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
|
||||
- cd coincurve-anonswap
|
||||
- python3 setup.py install --force
|
||||
script:
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- python3 setup.py install
|
||||
- basicswap-prepare --bindir=${BIN_DIR} --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
|
||||
- export DATADIRS="${TEST_DIR}"
|
||||
- mkdir -p "${DATADIRS}/bin"
|
||||
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
|
||||
- mkdir -p "${TEST_RELOAD_PATH}/bin"
|
||||
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
|
||||
- # tox
|
||||
- pytest tests/basicswap/test_xmr.py
|
||||
- pytest tests/basicswap/test_xmr_reload.py
|
||||
- pytest tests/basicswap/test_xmr_bids_offline.py
|
||||
after_success:
|
||||
- echo "End test"
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
env:
|
||||
cache: false
|
||||
install:
|
||||
- travis_retry pip install flake8==3.7.0
|
||||
- travis_retry pip install codespell==1.15.0
|
||||
before_script:
|
||||
script:
|
||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
|
||||
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
|
||||
after_success:
|
||||
- echo "End lint"
|
||||
- stage: test
|
||||
env:
|
||||
12
Dockerfile
12
Dockerfile
@@ -5,18 +5,12 @@ ENV LANG=C.UTF-8 \
|
||||
DATADIRS="/coindata"
|
||||
|
||||
RUN apt-get update; \
|
||||
apt-get install -y wget python3-pip gnupg unzip make g++ autoconf automake libtool pkg-config gosu tzdata;
|
||||
|
||||
ARG COINCURVE_VERSION=v0.2
|
||||
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
|
||||
unzip coincurve-anonswap.zip && \
|
||||
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \
|
||||
cd coincurve-anonswap && \
|
||||
python3 setup.py install --force
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata;
|
||||
|
||||
# Install requirements first so as to skip in subsequent rebuilds
|
||||
COPY ./requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
RUN pip3 install -r requirements.txt --require-hashes
|
||||
|
||||
COPY . basicswap-master
|
||||
RUN cd basicswap-master; \
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
include *.md LICENSE
|
||||
|
||||
recursive-include doc *
|
||||
recursive-include basicswap/templates *
|
||||
recursive-include basicswap/static *
|
||||
@@ -35,9 +35,9 @@ Built as a low-friction, highly secure solution to the frequent losses of funds
|
||||
|
||||
## Under the Hood
|
||||
|
||||
**BasicSwap** can be best understood as the decentralized version of the SWIFT messaging network; providing a decentralized messaging protocol that allows for peers to connect directly with each other with the purpose of executing atomic swaps without central points of failure and using official core wallets (Bitcoin Core, Litecoin Core, etc).
|
||||
**BasicSwap** can be best understood as the decentralized version of the SWIFT messaging network; providing a decentralized messaging protocol that allows for peers to connect directly with each other with the purpose of executing atomic swaps without central points of failure and using official core wallets (Bitcoin Core, Litecoin Core, etc).
|
||||
|
||||
**BasicSwap** does not process, initiate, or execute swaps; it merely enables peers to communicate with each other and exchange the required information to simplify the process of using atomic swaps on the respective blockchains of the coins being swapped.
|
||||
**BasicSwap** does not process, initiate, or execute swaps; it merely enables peers to communicate with each other and exchange the required information to simplify the process of using atomic swaps on the respective blockchains of the coins being swapped.
|
||||
|
||||
In essence, **BasicSwap** operates merely as a decentralized messaging protocol supplemented by a user-friendly interface.
|
||||
|
||||
@@ -122,7 +122,7 @@ If 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.
|
||||
|
||||
* [Download BasicSwapDEX](https://github.com/tecnovert/basicswap/tree/master/doc)
|
||||
* [Download BasicSwapDEX](https://github.com/basicswap/basicswap/tree/master/doc)
|
||||
|
||||
#### Community chat support
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.13.2"
|
||||
__version__ = "0.14.2"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -18,6 +19,9 @@ import subprocess
|
||||
|
||||
from sockshandler import SocksiPyHandler
|
||||
|
||||
from .db import (
|
||||
DBMethods,
|
||||
)
|
||||
from .rpc import (
|
||||
callrpc,
|
||||
)
|
||||
@@ -34,8 +38,8 @@ def getaddrinfo_tor(*args):
|
||||
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
|
||||
|
||||
|
||||
class BaseApp:
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
|
||||
class BaseApp(DBMethods):
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
self.log_name = log_name
|
||||
self.fp = fp
|
||||
self.fail_code = 0
|
||||
@@ -47,19 +51,19 @@ class BaseApp:
|
||||
self.coin_clients = {}
|
||||
self.coin_interfaces = {}
|
||||
self.mxDB = threading.Lock()
|
||||
self.debug = self.settings.get('debug', False)
|
||||
self.debug = self.settings.get("debug", False)
|
||||
self.delay_event = threading.Event()
|
||||
self.chainstate_delay_event = threading.Event()
|
||||
|
||||
self._network = None
|
||||
self.prepareLogging()
|
||||
self.log.info('Network: {}'.format(self.chain))
|
||||
self.log.info("Network: {}".format(self.chain))
|
||||
|
||||
self.use_tor_proxy = self.settings.get('use_tor', False)
|
||||
self.tor_proxy_host = self.settings.get('tor_proxy_host', '127.0.0.1')
|
||||
self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050)
|
||||
self.tor_control_password = self.settings.get('tor_control_password', None)
|
||||
self.tor_control_port = self.settings.get('tor_control_port', 9051)
|
||||
self.use_tor_proxy = self.settings.get("use_tor", False)
|
||||
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
|
||||
self.tor_proxy_port = self.settings.get("tor_proxy_port", 9050)
|
||||
self.tor_control_password = self.settings.get("tor_control_password", None)
|
||||
self.tor_control_port = self.settings.get("tor_control_port", 9051)
|
||||
self.default_socket = socket.socket
|
||||
self.default_socket_timeout = socket.getdefaulttimeout()
|
||||
self.default_socket_getaddrinfo = socket.getaddrinfo
|
||||
@@ -77,10 +81,17 @@ class BaseApp:
|
||||
# Remove any existing handlers
|
||||
self.log.handlers = []
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S')
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
stream_stdout = logging.StreamHandler()
|
||||
if self.log_name != 'BasicSwap':
|
||||
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S'))
|
||||
if self.log_name != "BasicSwap":
|
||||
stream_stdout.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s %(name)s %(levelname)s : %(message)s",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
)
|
||||
else:
|
||||
stream_stdout.setFormatter(formatter)
|
||||
stream_fp = logging.StreamHandler(self.fp)
|
||||
@@ -92,67 +103,91 @@ class BaseApp:
|
||||
|
||||
def getChainClientSettings(self, coin):
|
||||
try:
|
||||
return self.settings['chainclients'][chainparams[coin]['name']]
|
||||
return self.settings["chainclients"][chainparams[coin]["name"]]
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def setDaemonPID(self, name, pid) -> None:
|
||||
if isinstance(name, Coins):
|
||||
self.coin_clients[name]['pid'] = pid
|
||||
self.coin_clients[name]["pid"] = pid
|
||||
return
|
||||
for c, v in self.coin_clients.items():
|
||||
if v['name'] == name:
|
||||
v['pid'] = pid
|
||||
if v["name"] == name:
|
||||
v["pid"] = pid
|
||||
|
||||
def getChainDatadirPath(self, coin) -> str:
|
||||
datadir = self.coin_clients[coin]['datadir']
|
||||
testnet_name = '' if self.chain == 'mainnet' else chainparams[coin][self.chain].get('name', self.chain)
|
||||
datadir = self.coin_clients[coin]["datadir"]
|
||||
testnet_name = (
|
||||
""
|
||||
if self.chain == "mainnet"
|
||||
else chainparams[coin][self.chain].get("name", self.chain)
|
||||
)
|
||||
return os.path.join(datadir, testnet_name)
|
||||
|
||||
def getCoinIdFromName(self, coin_name: str):
|
||||
for c, params in chainparams.items():
|
||||
if coin_name.lower() == params['name'].lower():
|
||||
if coin_name.lower() == params["name"].lower():
|
||||
return c
|
||||
raise ValueError('Unknown coin: {}'.format(coin_name))
|
||||
raise ValueError("Unknown coin: {}".format(coin_name))
|
||||
|
||||
def callrpc(self, method, params=[], wallet=None):
|
||||
cc = self.coin_clients[Coins.PART]
|
||||
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
|
||||
return callrpc(
|
||||
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
|
||||
)
|
||||
|
||||
def callcoinrpc(self, coin, method, params=[], wallet=None):
|
||||
cc = self.coin_clients[coin]
|
||||
return callrpc(cc['rpcport'], cc['rpcauth'], method, params, wallet, cc['rpchost'])
|
||||
return callrpc(
|
||||
cc["rpcport"], cc["rpcauth"], method, params, wallet, cc["rpchost"]
|
||||
)
|
||||
|
||||
def callcoincli(self, coin_type, params, wallet=None, timeout=None):
|
||||
bindir = self.coin_clients[coin_type]['bindir']
|
||||
datadir = self.coin_clients[coin_type]['datadir']
|
||||
command_cli = os.path.join(bindir, chainparams[coin_type]['name'] + '-cli' + ('.exe' if os.name == 'nt' else ''))
|
||||
args = [command_cli, ]
|
||||
if self.chain != 'mainnet':
|
||||
args.append('-' + self.chain)
|
||||
args.append('-datadir=' + datadir)
|
||||
bindir = self.coin_clients[coin_type]["bindir"]
|
||||
datadir = self.coin_clients[coin_type]["datadir"]
|
||||
cli_bin: str = chainparams[coin_type].get(
|
||||
"cli_binname", chainparams[coin_type]["name"] + "-cli"
|
||||
)
|
||||
command_cli = os.path.join(
|
||||
bindir, cli_bin + (".exe" if os.name == "nt" else "")
|
||||
)
|
||||
args = [
|
||||
command_cli,
|
||||
]
|
||||
if self.chain != "mainnet":
|
||||
args.append("-" + self.chain)
|
||||
args.append("-datadir=" + datadir)
|
||||
args += shlex.split(params)
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
out = p.communicate(timeout=timeout)
|
||||
if len(out[1]) > 0:
|
||||
raise ValueError('CLI error ' + str(out[1]))
|
||||
return out[0].decode('utf-8').strip()
|
||||
raise ValueError("CLI error " + str(out[1]))
|
||||
return out[0].decode("utf-8").strip()
|
||||
|
||||
def is_transient_error(self, ex) -> bool:
|
||||
if isinstance(ex, TemporaryError):
|
||||
return True
|
||||
str_error = str(ex).lower()
|
||||
return 'read timed out' in str_error or 'no connection to daemon' in str_error
|
||||
return "read timed out" in str_error or "no connection to daemon" in str_error
|
||||
|
||||
def setConnectionParameters(self, timeout=120):
|
||||
opener = urllib.request.build_opener()
|
||||
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
||||
opener.addheaders = [("User-agent", "Mozilla/5.0")]
|
||||
urllib.request.install_opener(opener)
|
||||
|
||||
if self.use_tor_proxy:
|
||||
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port, rdns=True)
|
||||
socks.setdefaultproxy(
|
||||
socks.PROXY_TYPE_SOCKS5,
|
||||
self.tor_proxy_host,
|
||||
self.tor_proxy_port,
|
||||
rdns=True,
|
||||
)
|
||||
socket.socket = socks.socksocket
|
||||
socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail
|
||||
socket.getaddrinfo = (
|
||||
getaddrinfo_tor # Without this accessing .onion links would fail
|
||||
)
|
||||
|
||||
socket.setdefaulttimeout(timeout)
|
||||
|
||||
@@ -162,12 +197,19 @@ class BaseApp:
|
||||
socket.getaddrinfo = self.default_socket_getaddrinfo
|
||||
socket.setdefaulttimeout(self.default_socket_timeout)
|
||||
|
||||
def readURL(self, url: str, timeout: int = 120, headers=None) -> bytes:
|
||||
def readURL(self, url: str, timeout: int = 120, headers={}) -> bytes:
|
||||
open_handler = None
|
||||
if self.use_tor_proxy:
|
||||
open_handler = SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port)
|
||||
opener = urllib.request.build_opener(open_handler) if self.use_tor_proxy else urllib.request.build_opener()
|
||||
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
||||
open_handler = SocksiPyHandler(
|
||||
socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port
|
||||
)
|
||||
opener = (
|
||||
urllib.request.build_opener(open_handler)
|
||||
if self.use_tor_proxy
|
||||
else urllib.request.build_opener()
|
||||
)
|
||||
if headers is None:
|
||||
opener.addheaders = [("User-agent", "Mozilla/5.0")]
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
return opener.open(request, timeout=timeout).read()
|
||||
|
||||
@@ -178,7 +220,9 @@ class BaseApp:
|
||||
|
||||
def torControl(self, query):
|
||||
try:
|
||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(self.tor_control_password, query).encode('utf-8')
|
||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
|
||||
self.tor_control_password, query
|
||||
).encode("utf-8")
|
||||
c = socket.create_connection((self.tor_proxy_host, self.tor_control_port))
|
||||
c.send(command)
|
||||
response = bytearray()
|
||||
@@ -190,23 +234,23 @@ class BaseApp:
|
||||
c.close()
|
||||
return response
|
||||
except Exception as e:
|
||||
self.log.error(f'torControl {e}')
|
||||
self.log.error(f"torControl {e}")
|
||||
return
|
||||
|
||||
def getTime(self) -> int:
|
||||
return int(time.time()) + self.mock_time_offset
|
||||
|
||||
def setMockTimeOffset(self, new_offset: int) -> None:
|
||||
self.log.warning(f'Setting mocktime to {new_offset}')
|
||||
self.log.warning(f"Setting mocktime to {new_offset}")
|
||||
self.mock_time_offset = new_offset
|
||||
|
||||
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
|
||||
value: int = self.settings.get(name, default_v)
|
||||
if value < min_v:
|
||||
self.log.warning(f'Setting {name} to {min_v}')
|
||||
self.log.warning(f"Setting {name} to {min_v}")
|
||||
value = min_v
|
||||
if value > max_v:
|
||||
self.log.warning(f'Setting {name} to {max_v}')
|
||||
self.log.warning(f"Setting {name} to {max_v}")
|
||||
value = max_v
|
||||
return value
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -64,6 +65,7 @@ class SwapTypes(IntEnum):
|
||||
SELLER_FIRST_2MSG = auto()
|
||||
BUYER_FIRST_2MSG = auto()
|
||||
XMR_SWAP = auto()
|
||||
XMR_BCH_SWAP = auto()
|
||||
|
||||
|
||||
class OfferStates(IntEnum):
|
||||
@@ -75,13 +77,13 @@ class OfferStates(IntEnum):
|
||||
|
||||
class BidStates(IntEnum):
|
||||
BID_SENT = 1
|
||||
BID_RECEIVING = 2 # Partially received
|
||||
BID_RECEIVING = 2 # Partially received
|
||||
BID_RECEIVED = 3
|
||||
BID_RECEIVING_ACC = 4 # Partially received accept message
|
||||
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
|
||||
SWAP_INITIATED = 6 # Initiate txn validated
|
||||
SWAP_PARTICIPATING = 7 # Participate txn validated
|
||||
SWAP_COMPLETED = 8 # All swap txns spent
|
||||
BID_RECEIVING_ACC = 4 # Partially received accept message
|
||||
BID_ACCEPTED = 5 # BidAcceptMessage received/sent
|
||||
SWAP_INITIATED = 6 # Initiate txn validated
|
||||
SWAP_PARTICIPATING = 7 # Participate txn validated
|
||||
SWAP_COMPLETED = 8 # All swap txns spent
|
||||
XMR_SWAP_SCRIPT_COIN_LOCKED = 9
|
||||
XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX = 10
|
||||
XMR_SWAP_NOSCRIPT_COIN_LOCKED = 11
|
||||
@@ -95,16 +97,18 @@ class BidStates(IntEnum):
|
||||
XMR_SWAP_FAILED = 19
|
||||
SWAP_DELAYING = 20
|
||||
SWAP_TIMEDOUT = 21
|
||||
BID_ABANDONED = 22 # Bid will no longer be processed
|
||||
BID_ERROR = 23 # An error occurred
|
||||
BID_ABANDONED = 22 # Bid will no longer be processed
|
||||
BID_ERROR = 23 # An error occurred
|
||||
BID_STALLED_FOR_TEST = 24
|
||||
BID_REJECTED = 25
|
||||
BID_STATE_UNKNOWN = 26
|
||||
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
|
||||
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
|
||||
XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS = 27 # XmrBidLockTxSigsMessage
|
||||
XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage
|
||||
BID_REQUEST_SENT = 29
|
||||
BID_REQUEST_ACCEPTED = 30
|
||||
BID_EXPIRED = 31
|
||||
BID_AACCEPT_DELAY = 32
|
||||
BID_AACCEPT_FAIL = 33
|
||||
|
||||
|
||||
class TxStates(IntEnum):
|
||||
@@ -136,6 +140,8 @@ class TxTypes(IntEnum):
|
||||
|
||||
ITX_PRE_FUNDED = auto()
|
||||
|
||||
BCH_MERCY = auto()
|
||||
|
||||
|
||||
class ActionTypes(IntEnum):
|
||||
ACCEPT_BID = auto()
|
||||
@@ -183,6 +189,8 @@ class EventLogTypes(IntEnum):
|
||||
PTX_REDEEM_PUBLISHED = auto()
|
||||
PTX_REFUND_PUBLISHED = auto()
|
||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||
BCH_MERCY_TX_PUBLISHED = auto()
|
||||
BCH_MERCY_TX_FOUND = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -194,6 +202,7 @@ class DebugTypes(IntEnum):
|
||||
NONE = 0
|
||||
BID_STOP_AFTER_COIN_A_LOCK = auto()
|
||||
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
|
||||
BID_DONT_SPEND_COIN_A_LOCK_REFUND2 = auto() # continues
|
||||
CREATE_INVALID_COIN_B_LOCK = auto()
|
||||
BUYER_STOP_AFTER_ITX = auto()
|
||||
MAKE_INVALID_PTX = auto()
|
||||
@@ -204,6 +213,10 @@ class DebugTypes(IntEnum):
|
||||
DUPLICATE_ACTIONS = auto()
|
||||
DONT_CONFIRM_PTX = auto()
|
||||
OFFER_LOCK_2_VALUE_INC = auto()
|
||||
BID_STOP_AFTER_COIN_B_LOCK = auto()
|
||||
BID_DONT_SPEND_COIN_B_LOCK = auto()
|
||||
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
|
||||
BID_DONT_SPEND_COIN_A_LOCK = auto()
|
||||
|
||||
|
||||
class NotificationTypes(IntEnum):
|
||||
@@ -221,12 +234,12 @@ class AutomationOverrideOptions(IntEnum):
|
||||
|
||||
def strAutomationOverrideOption(option):
|
||||
if option == AutomationOverrideOptions.DEFAULT:
|
||||
return 'Default'
|
||||
return "Default"
|
||||
if option == AutomationOverrideOptions.ALWAYS_ACCEPT:
|
||||
return 'Always Accept'
|
||||
return "Always Accept"
|
||||
if option == AutomationOverrideOptions.NEVER_ACCEPT:
|
||||
return 'Never Accept'
|
||||
return 'Unknown'
|
||||
return "Never Accept"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
class VisibilityOverrideOptions(IntEnum):
|
||||
@@ -237,244 +250,257 @@ class VisibilityOverrideOptions(IntEnum):
|
||||
|
||||
def strVisibilityOverrideOption(option):
|
||||
if option == VisibilityOverrideOptions.DEFAULT:
|
||||
return 'Default'
|
||||
return "Default"
|
||||
if option == VisibilityOverrideOptions.HIDE:
|
||||
return 'Hide'
|
||||
return "Hide"
|
||||
if option == VisibilityOverrideOptions.BLOCK:
|
||||
return 'Block'
|
||||
return 'Unknown'
|
||||
return "Block"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def strOfferState(state):
|
||||
if state == OfferStates.OFFER_SENT:
|
||||
return 'Sent'
|
||||
return "Sent"
|
||||
if state == OfferStates.OFFER_RECEIVED:
|
||||
return 'Received'
|
||||
return "Received"
|
||||
if state == OfferStates.OFFER_ABANDONED:
|
||||
return 'Abandoned'
|
||||
return "Abandoned"
|
||||
if state == OfferStates.OFFER_EXPIRED:
|
||||
return 'Expired'
|
||||
return 'Unknown'
|
||||
return "Expired"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def strBidState(state):
|
||||
if state == BidStates.BID_SENT:
|
||||
return 'Sent'
|
||||
return "Sent"
|
||||
if state == BidStates.BID_RECEIVING:
|
||||
return 'Receiving'
|
||||
return "Receiving"
|
||||
if state == BidStates.BID_RECEIVING_ACC:
|
||||
return 'Receiving accept'
|
||||
return "Receiving accept"
|
||||
if state == BidStates.BID_RECEIVED:
|
||||
return 'Received'
|
||||
return "Received"
|
||||
if state == BidStates.BID_ACCEPTED:
|
||||
return 'Accepted'
|
||||
return "Accepted"
|
||||
if state == BidStates.SWAP_INITIATED:
|
||||
return 'Initiated'
|
||||
return "Initiated"
|
||||
if state == BidStates.SWAP_PARTICIPATING:
|
||||
return 'Participating'
|
||||
return "Participating"
|
||||
if state == BidStates.SWAP_COMPLETED:
|
||||
return 'Completed'
|
||||
return "Completed"
|
||||
if state == BidStates.SWAP_TIMEDOUT:
|
||||
return 'Timed-out'
|
||||
return "Timed-out"
|
||||
if state == BidStates.BID_ABANDONED:
|
||||
return 'Abandoned'
|
||||
return "Abandoned"
|
||||
if state == BidStates.BID_STALLED_FOR_TEST:
|
||||
return 'Stalled (debug)'
|
||||
return "Stalled (debug)"
|
||||
if state == BidStates.BID_ERROR:
|
||||
return 'Error'
|
||||
return "Error"
|
||||
if state == BidStates.BID_REJECTED:
|
||||
return 'Rejected'
|
||||
return "Rejected"
|
||||
if state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
|
||||
return 'Script coin locked'
|
||||
return "Script coin locked"
|
||||
if state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
|
||||
return 'Script coin spend tx valid'
|
||||
return "Script coin spend tx valid"
|
||||
if state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
|
||||
return 'Scriptless coin locked'
|
||||
return "Scriptless coin locked"
|
||||
if state == BidStates.XMR_SWAP_LOCK_RELEASED:
|
||||
return 'Script coin lock released'
|
||||
return "Script coin lock released"
|
||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
||||
return 'Script tx redeemed'
|
||||
return "Script tx redeemed"
|
||||
if state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
||||
return 'Script pre-refund tx in chain'
|
||||
return "Script pre-refund tx in chain"
|
||||
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
|
||||
return 'Scriptless tx redeemed'
|
||||
return "Scriptless tx redeemed"
|
||||
if state == BidStates.XMR_SWAP_NOSCRIPT_TX_RECOVERED:
|
||||
return 'Scriptless tx recovered'
|
||||
return "Scriptless tx recovered"
|
||||
if state == BidStates.XMR_SWAP_FAILED_REFUNDED:
|
||||
return 'Failed, refunded'
|
||||
return "Failed, refunded"
|
||||
if state == BidStates.XMR_SWAP_FAILED_SWIPED:
|
||||
return 'Failed, swiped'
|
||||
return "Failed, swiped"
|
||||
if state == BidStates.XMR_SWAP_FAILED:
|
||||
return 'Failed'
|
||||
return "Failed"
|
||||
if state == BidStates.SWAP_DELAYING:
|
||||
return 'Delaying'
|
||||
return "Delaying"
|
||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS:
|
||||
return 'Exchanged script lock tx sigs msg'
|
||||
return "Exchanged script lock tx sigs msg"
|
||||
if state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX:
|
||||
return 'Exchanged script lock spend tx msg'
|
||||
return "Exchanged script lock spend tx msg"
|
||||
if state == BidStates.BID_REQUEST_SENT:
|
||||
return 'Request sent'
|
||||
return "Request sent"
|
||||
if state == BidStates.BID_REQUEST_ACCEPTED:
|
||||
return 'Request accepted'
|
||||
return "Request accepted"
|
||||
if state == BidStates.BID_STATE_UNKNOWN:
|
||||
return 'Unknown bid state'
|
||||
return "Unknown bid state"
|
||||
if state == BidStates.BID_EXPIRED:
|
||||
return 'Expired'
|
||||
return 'Unknown' + ' ' + str(state)
|
||||
return "Expired"
|
||||
if state == BidStates.BID_AACCEPT_DELAY:
|
||||
return "Auto accept delay"
|
||||
if state == BidStates.BID_AACCEPT_FAIL:
|
||||
return "Auto accept failed"
|
||||
return "Unknown" + " " + str(state)
|
||||
|
||||
|
||||
def strTxState(state):
|
||||
if state == TxStates.TX_NONE:
|
||||
return 'None'
|
||||
return "None"
|
||||
if state == TxStates.TX_SENT:
|
||||
return 'Sent'
|
||||
return "Sent"
|
||||
if state == TxStates.TX_CONFIRMED:
|
||||
return 'Confirmed'
|
||||
return "Confirmed"
|
||||
if state == TxStates.TX_REDEEMED:
|
||||
return 'Redeemed'
|
||||
return "Redeemed"
|
||||
if state == TxStates.TX_REFUNDED:
|
||||
return 'Refunded'
|
||||
return "Refunded"
|
||||
if state == TxStates.TX_IN_MEMPOOL:
|
||||
return 'In Mempool'
|
||||
return "In Mempool"
|
||||
if state == TxStates.TX_IN_CHAIN:
|
||||
return 'In Chain'
|
||||
return 'Unknown'
|
||||
return "In Chain"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def strTxType(tx_type):
|
||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK:
|
||||
return 'Chain A Lock Tx'
|
||||
return "Chain A Lock Tx"
|
||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND:
|
||||
return 'Chain A Lock Spend Tx'
|
||||
return "Chain A Lock Spend Tx"
|
||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND:
|
||||
return 'Chain A Lock Refund Tx'
|
||||
return "Chain A Lock Refund Tx"
|
||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND:
|
||||
return 'Chain A Lock Refund Spend Tx'
|
||||
return "Chain A Lock Refund Spend Tx"
|
||||
if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE:
|
||||
return 'Chain A Lock Refund Swipe Tx'
|
||||
return "Chain A Lock Refund Swipe Tx"
|
||||
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
|
||||
return 'Chain B Lock Tx'
|
||||
return "Chain B Lock Tx"
|
||||
if tx_type == TxTypes.ITX_PRE_FUNDED:
|
||||
return 'Funded mock initiate tx'
|
||||
return 'Unknown'
|
||||
return "Funded mock initiate Tx"
|
||||
if tx_type == TxTypes.BCH_MERCY:
|
||||
return "BCH Mercy Tx"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def strAddressType(addr_type):
|
||||
if addr_type == AddressTypes.OFFER:
|
||||
return 'Offer'
|
||||
return "Offer"
|
||||
if addr_type == AddressTypes.BID:
|
||||
return 'Bid'
|
||||
return "Bid"
|
||||
if addr_type == AddressTypes.RECV_OFFER:
|
||||
return 'Offer recv'
|
||||
return "Offer recv"
|
||||
if addr_type == AddressTypes.SEND_OFFER:
|
||||
return 'Offer send'
|
||||
return 'Unknown'
|
||||
return "Offer send"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def getLockName(lock_type):
|
||||
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
|
||||
return 'Sequence lock, blocks'
|
||||
return "Sequence lock, blocks"
|
||||
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
|
||||
return 'Sequence lock, time'
|
||||
return "Sequence lock, time"
|
||||
if lock_type == TxLockTypes.ABS_LOCK_BLOCKS:
|
||||
return 'blocks'
|
||||
return "blocks"
|
||||
if lock_type == TxLockTypes.ABS_LOCK_TIME:
|
||||
return 'time'
|
||||
return "time"
|
||||
|
||||
|
||||
def describeEventEntry(event_type, event_msg):
|
||||
if event_type == EventLogTypes.FAILED_TX_B_LOCK_PUBLISH:
|
||||
return 'Failed to publish lock tx B'
|
||||
return "Failed to publish lock tx B"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_PUBLISHED:
|
||||
return 'Lock tx A published'
|
||||
return "Lock tx A published"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_PUBLISHED:
|
||||
return 'Lock tx B published'
|
||||
return "Lock tx B published"
|
||||
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
||||
return 'Failed to publish lock tx B spend: ' + event_msg
|
||||
return "Failed to publish lock tx B spend: " + event_msg
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
||||
return 'Lock tx A seen in chain'
|
||||
return "Lock tx A seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
||||
return 'Lock tx A confirmed in chain'
|
||||
return "Lock tx A confirmed in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_SEEN:
|
||||
return 'Lock tx B seen in chain'
|
||||
return "Lock tx B seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
|
||||
return 'Lock tx B confirmed in chain'
|
||||
return "Lock tx B confirmed in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_IN_MEMPOOL:
|
||||
return 'Lock tx B seen in mempool'
|
||||
return "Lock tx B seen in mempool"
|
||||
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
|
||||
return 'Debug tweak applied ' + event_msg
|
||||
return "Debug tweak applied " + event_msg
|
||||
if event_type == EventLogTypes.FAILED_TX_B_REFUND:
|
||||
return 'Failed to publish lock tx B refund'
|
||||
return "Failed to publish lock tx B refund"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_INVALID:
|
||||
return 'Detected invalid lock Tx B'
|
||||
return "Detected invalid lock Tx B"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_PUBLISHED:
|
||||
return 'Lock tx A refund tx published'
|
||||
return "Lock tx A refund tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_PUBLISHED:
|
||||
return 'Lock tx A refund spend tx published'
|
||||
return "Lock tx A refund spend tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SWIPE_TX_PUBLISHED:
|
||||
return 'Lock tx A refund swipe tx published'
|
||||
return "Lock tx A refund swipe tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED:
|
||||
return 'Lock tx B refund tx published'
|
||||
return "Lock tx B refund tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SPEND_TX_PUBLISHED:
|
||||
return 'Lock tx A spend tx published'
|
||||
return "Lock tx A spend tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED:
|
||||
return 'Lock tx B spend tx published'
|
||||
return "Lock tx B spend tx published"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_TX_SEEN:
|
||||
return 'Lock tx A refund tx seen in chain'
|
||||
return "Lock tx A refund tx seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_REFUND_SPEND_TX_SEEN:
|
||||
return 'Lock tx A refund spend tx seen in chain'
|
||||
return "Lock tx A refund spend tx seen in chain"
|
||||
if event_type == EventLogTypes.SYSTEM_WARNING:
|
||||
return 'Warning: ' + event_msg
|
||||
return "Warning: " + event_msg
|
||||
if event_type == EventLogTypes.ERROR:
|
||||
return 'Error: ' + event_msg
|
||||
return "Error: " + event_msg
|
||||
if event_type == EventLogTypes.AUTOMATION_CONSTRAINT:
|
||||
return 'Failed auto accepting'
|
||||
return "Failed auto accepting"
|
||||
if event_type == EventLogTypes.AUTOMATION_ACCEPTING_BID:
|
||||
return 'Auto accepting'
|
||||
return "Auto accepting"
|
||||
if event_type == EventLogTypes.ITX_PUBLISHED:
|
||||
return 'Initiate tx published'
|
||||
return "Initiate tx published"
|
||||
if event_type == EventLogTypes.ITX_REDEEM_PUBLISHED:
|
||||
return 'Initiate tx redeem tx published'
|
||||
return "Initiate tx redeem tx published"
|
||||
if event_type == EventLogTypes.ITX_REFUND_PUBLISHED:
|
||||
return 'Initiate tx refund tx published'
|
||||
return "Initiate tx refund tx published"
|
||||
if event_type == EventLogTypes.PTX_PUBLISHED:
|
||||
return 'Participate tx published'
|
||||
return "Participate tx published"
|
||||
if event_type == EventLogTypes.PTX_REDEEM_PUBLISHED:
|
||||
return 'Participate tx redeem tx published'
|
||||
return "Participate tx redeem tx published"
|
||||
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
|
||||
return 'Participate tx refund tx published'
|
||||
return "Participate tx refund tx published"
|
||||
if event_type == EventLogTypes.BCH_MERCY_TX_FOUND:
|
||||
return "BCH mercy tx found"
|
||||
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
|
||||
return "Lock tx B mercy tx published"
|
||||
|
||||
|
||||
def getVoutByAddress(txjs, p2sh):
|
||||
for o in txjs['vout']:
|
||||
for o in txjs["vout"]:
|
||||
try:
|
||||
if 'address' in o['scriptPubKey'] and o['scriptPubKey']['address'] == p2sh:
|
||||
return o['n']
|
||||
if p2sh in o['scriptPubKey']['addresses']:
|
||||
return o['n']
|
||||
if "address" in o["scriptPubKey"] and o["scriptPubKey"]["address"] == p2sh:
|
||||
return o["n"]
|
||||
if p2sh in o["scriptPubKey"]["addresses"]:
|
||||
return o["n"]
|
||||
except Exception:
|
||||
pass
|
||||
raise ValueError('Address output not found in txn')
|
||||
raise ValueError("Address output not found in txn")
|
||||
|
||||
|
||||
def getVoutByScriptPubKey(txjs, scriptPubKey_hex: str) -> int:
|
||||
for o in txjs['vout']:
|
||||
for o in txjs["vout"]:
|
||||
try:
|
||||
if scriptPubKey_hex == o['scriptPubKey']['hex']:
|
||||
return o['n']
|
||||
if scriptPubKey_hex == o["scriptPubKey"]["hex"]:
|
||||
return o["n"]
|
||||
except Exception:
|
||||
pass
|
||||
raise ValueError('scriptPubKey output not found in txn')
|
||||
raise ValueError("scriptPubKey output not found in txn")
|
||||
|
||||
|
||||
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type='pubkey_address'):
|
||||
return encodeAddress(bytes((chainparams[coin_type][chain_name][addr_type],)) + decodeAddress(addr)[1:])
|
||||
def replaceAddrPrefix(addr, coin_type, chain_name, addr_type="pubkey_address"):
|
||||
return encodeAddress(
|
||||
bytes((chainparams[coin_type][chain_name][addr_type],))
|
||||
+ decodeAddress(addr)[1:]
|
||||
)
|
||||
|
||||
|
||||
def getOfferProofOfFundsHash(offer_msg, offer_addr):
|
||||
# TODO: Hash must not include proof_of_funds sig if it exists in offer_msg
|
||||
h = hashlib.sha256()
|
||||
h.update(offer_addr.encode('utf-8'))
|
||||
h.update(offer_addr.encode("utf-8"))
|
||||
offer_bytes = offer_msg.to_bytes()
|
||||
h.update(offer_bytes)
|
||||
return h.digest()
|
||||
@@ -484,33 +510,48 @@ def getLastBidState(packed_states):
|
||||
num_states = len(packed_states) // 12
|
||||
if num_states < 2:
|
||||
return BidStates.BID_STATE_UNKNOWN
|
||||
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
|
||||
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
|
||||
try:
|
||||
num_states = len(packed_states) // 12
|
||||
if num_states < 2:
|
||||
return BidStates.BID_STATE_UNKNOWN
|
||||
return struct.unpack_from('<i', packed_states[(num_states - 2) * 12:])[0]
|
||||
return struct.unpack_from("<i", packed_states[(num_states - 2) * 12 :])[0]
|
||||
except Exception:
|
||||
return BidStates.BID_STATE_UNKNOWN
|
||||
|
||||
|
||||
def strSwapType(swap_type):
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return 'seller_first'
|
||||
return "seller_first"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
return 'xmr_swap'
|
||||
return "xmr_swap"
|
||||
return None
|
||||
|
||||
|
||||
def strSwapDesc(swap_type):
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return 'Secret Hash'
|
||||
return "Secret Hash"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
return 'Adaptor Sig'
|
||||
return "Adaptor Sig"
|
||||
return None
|
||||
|
||||
|
||||
inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED]
|
||||
inactive_states = [
|
||||
BidStates.SWAP_COMPLETED,
|
||||
BidStates.BID_ERROR,
|
||||
BidStates.BID_REJECTED,
|
||||
BidStates.SWAP_TIMEDOUT,
|
||||
BidStates.BID_ABANDONED,
|
||||
BidStates.BID_EXPIRED,
|
||||
]
|
||||
|
||||
|
||||
def canAcceptBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_RECEIVED,
|
||||
BidStates.BID_AACCEPT_DELAY,
|
||||
BidStates.BID_AACCEPT_FAIL,
|
||||
)
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
|
||||
1
basicswap/bin/__init__.py
Normal file
1
basicswap/bin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
name = "bin"
|
||||
2659
basicswap/bin/prepare.py
Normal file
2659
basicswap/bin/prepare.py
Normal file
File diff suppressed because it is too large
Load Diff
626
basicswap/bin/run.py
Executable file
626
basicswap/bin/run.py
Executable file
@@ -0,0 +1,626 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import signal
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.chainparams import chainparams
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.level = logging.DEBUG
|
||||
if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
swap_client = None
|
||||
|
||||
|
||||
class Daemon:
|
||||
__slots__ = ("handle", "files")
|
||||
|
||||
def __init__(self, handle, files):
|
||||
self.handle = handle
|
||||
self.files = files
|
||||
|
||||
|
||||
def is_known_coin(coin_name: str) -> bool:
|
||||
for k, v in chainparams.items():
|
||||
if coin_name == v["name"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global swap_client
|
||||
logger.info("Signal %d detected, ending program." % (sig))
|
||||
if swap_client is not None:
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
|
||||
# Rewrite litecoin.conf for 0.21.3
|
||||
# TODO: Remove
|
||||
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
|
||||
if os.path.exists(ltc_conf_path):
|
||||
config_to_add = ["blockfilterindex=0", "peerblockfilters=0"]
|
||||
with open(ltc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line in config_to_add:
|
||||
config_to_add.remove(line)
|
||||
|
||||
if len(config_to_add) > 0:
|
||||
logger.info("Rewriting litecoin.conf")
|
||||
shutil.copyfile(ltc_conf_path, ltc_conf_path + ".last")
|
||||
with open(ltc_conf_path, "a") as fp:
|
||||
for line in config_to_add:
|
||||
fp.write(line + "\n")
|
||||
|
||||
args = [
|
||||
daemon_bin,
|
||||
]
|
||||
add_datadir: bool = extra_config.get("add_datadir", True)
|
||||
if add_datadir:
|
||||
args.append("-datadir=" + datadir_path)
|
||||
args += opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
opened_files = []
|
||||
if extra_config.get("stdout_to_file", False):
|
||||
stdout_dest = open(
|
||||
os.path.join(
|
||||
datadir_path, extra_config.get("stdout_filename", "core_stdout.log")
|
||||
),
|
||||
"w",
|
||||
)
|
||||
opened_files.append(stdout_dest)
|
||||
stderr_dest = stdout_dest
|
||||
else:
|
||||
stdout_dest = subprocess.PIPE
|
||||
stderr_dest = subprocess.PIPE
|
||||
|
||||
shell: bool = False
|
||||
if extra_config.get("use_shell", False):
|
||||
args = " ".join(args)
|
||||
shell = True
|
||||
return Daemon(
|
||||
subprocess.Popen(
|
||||
args,
|
||||
shell=shell,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=stdout_dest,
|
||||
stderr=stderr_dest,
|
||||
cwd=datadir_path,
|
||||
),
|
||||
opened_files,
|
||||
)
|
||||
|
||||
|
||||
def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
config_filename = (
|
||||
"wownerod.conf" if daemon_bin.startswith("wow") else "monerod.conf"
|
||||
)
|
||||
args = [
|
||||
daemon_path,
|
||||
"--non-interactive",
|
||||
"--config-file=" + os.path.join(datadir_path, config_filename),
|
||||
] + opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
file_stdout = open(os.path.join(datadir_path, "core_stdout.log"), "w")
|
||||
file_stderr = open(os.path.join(datadir_path, "core_stderr.log"), "w")
|
||||
return Daemon(
|
||||
subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=file_stdout,
|
||||
stderr=file_stderr,
|
||||
cwd=datadir_path,
|
||||
),
|
||||
[file_stdout, file_stderr],
|
||||
)
|
||||
|
||||
|
||||
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
|
||||
args = [daemon_path, "--non-interactive"]
|
||||
|
||||
needs_rewrite: bool = False
|
||||
config_to_remove = [
|
||||
"daemon-address=",
|
||||
"untrusted-daemon=",
|
||||
"trusted-daemon=",
|
||||
"proxy=",
|
||||
]
|
||||
|
||||
data_dir = os.path.expanduser(node_dir)
|
||||
|
||||
wallet_config_filename = (
|
||||
"wownero-wallet-rpc.conf"
|
||||
if wallet_bin.startswith("wow")
|
||||
else "monero_wallet.conf"
|
||||
)
|
||||
config_path = os.path.join(data_dir, wallet_config_filename)
|
||||
if os.path.exists(config_path):
|
||||
args += ["--config-file=" + config_path]
|
||||
with open(config_path) as fp:
|
||||
for line in fp:
|
||||
if any(
|
||||
line.startswith(config_line) for config_line in config_to_remove
|
||||
):
|
||||
logger.warning(
|
||||
"Found old config in monero_wallet.conf: {}".format(
|
||||
line.strip()
|
||||
)
|
||||
)
|
||||
needs_rewrite = True
|
||||
args += opts
|
||||
|
||||
if needs_rewrite:
|
||||
logger.info("Rewriting wallet config")
|
||||
shutil.copyfile(config_path, config_path + ".last")
|
||||
with open(config_path + ".last") as fp_from, open(config_path, "w") as fp_to:
|
||||
for line in fp_from:
|
||||
if not any(
|
||||
line.startswith(config_line) for config_line in config_to_remove
|
||||
):
|
||||
fp_to.write(line)
|
||||
|
||||
logger.info("Starting wallet daemon {}".format(wallet_bin))
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
|
||||
wallet_stdout = open(os.path.join(data_dir, "wallet_stdout.log"), "w")
|
||||
wallet_stderr = open(os.path.join(data_dir, "wallet_stderr.log"), "w")
|
||||
return Daemon(
|
||||
subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=wallet_stdout,
|
||||
stderr=wallet_stderr,
|
||||
cwd=data_dir,
|
||||
),
|
||||
[wallet_stdout, wallet_stderr],
|
||||
)
|
||||
|
||||
|
||||
def ws_new_client(client, server):
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_new_client {client["id"]}')
|
||||
|
||||
|
||||
def ws_client_left(client, server):
|
||||
if client is None:
|
||||
return
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_client_left {client["id"]}')
|
||||
|
||||
|
||||
def ws_message_received(client, server, message):
|
||||
if len(message) > 200:
|
||||
message = message[:200] + ".."
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
|
||||
|
||||
|
||||
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
|
||||
return coin_settings.get(
|
||||
"core_binname", chainparams[coin_id].get("core_binname", default_name)
|
||||
) + (".exe" if os.name == "nt" else "")
|
||||
|
||||
|
||||
def getWalletBinName(coin_id: int, coin_settings, default_name: str) -> str:
|
||||
return coin_settings.get(
|
||||
"wallet_binname", chainparams[coin_id].get("wallet_binname", default_name)
|
||||
) + (".exe" if os.name == "nt" else "")
|
||||
|
||||
|
||||
def getCoreBinArgs(coin_id: int, coin_settings):
|
||||
extra_args = []
|
||||
if "config_filename" in coin_settings:
|
||||
extra_args.append("--conf=" + coin_settings["config_filename"])
|
||||
if "port" in coin_settings:
|
||||
extra_args.append("--port=" + str(int(coin_settings["port"])))
|
||||
return extra_args
|
||||
|
||||
|
||||
def runClient(fp, data_dir, chain, start_only_coins):
|
||||
global swap_client, logger
|
||||
daemons = []
|
||||
pids = []
|
||||
threads = []
|
||||
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||
pids_path = os.path.join(data_dir, ".pids")
|
||||
|
||||
if os.getenv("WALLET_ENCRYPTION_PWD", "") != "":
|
||||
if "decred" in start_only_coins:
|
||||
# Workaround for dcrwallet requiring password for initial startup
|
||||
logger.warning(
|
||||
"Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Please unset the WALLET_ENCRYPTION_PWD environment variable."
|
||||
)
|
||||
|
||||
if not os.path.exists(settings_path):
|
||||
raise ValueError("Settings file not found: " + str(settings_path))
|
||||
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
||||
logger = swap_client.log
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
for ln in fd:
|
||||
# TODO: try close
|
||||
logger.warning("Found pid for daemon {} ".format(ln.strip()))
|
||||
|
||||
# Ensure daemons are stopped
|
||||
swap_client.stopDaemons()
|
||||
|
||||
# Settings may have been modified
|
||||
settings = swap_client.settings
|
||||
try:
|
||||
# Try start daemons
|
||||
for c, v in settings["chainclients"].items():
|
||||
if len(start_only_coins) > 0 and c not in start_only_coins:
|
||||
continue
|
||||
try:
|
||||
coin_id = swap_client.getCoinIdFromName(c)
|
||||
display_name = getCoinName(coin_id)
|
||||
except Exception as e: # noqa: F841
|
||||
logger.warning("Not starting unknown coin: {}".format(c))
|
||||
continue
|
||||
if c in ("monero", "wownero"):
|
||||
if v["manage_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||
|
||||
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
daemon_addr = "{}:{}".format(v["rpchost"], v["rpcport"])
|
||||
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
|
||||
coin_id, v["rpchost"]
|
||||
)
|
||||
opts = [
|
||||
"--daemon-address",
|
||||
daemon_addr,
|
||||
]
|
||||
|
||||
proxy_log_str = ""
|
||||
proxy_host, proxy_port = swap_client.getXMRWalletProxy(
|
||||
coin_id, v["rpchost"]
|
||||
)
|
||||
if proxy_host:
|
||||
proxy_log_str = " through proxy"
|
||||
opts += [
|
||||
"--proxy",
|
||||
f"{proxy_host}:{proxy_port}",
|
||||
"--daemon-ssl-allow-any-cert",
|
||||
]
|
||||
|
||||
swap_client.log.info(
|
||||
"daemon-address: {} ({}){}".format(
|
||||
daemon_addr,
|
||||
"trusted" if trusted_daemon else "untrusted",
|
||||
proxy_log_str,
|
||||
)
|
||||
)
|
||||
|
||||
daemon_rpcuser = v.get("rpcuser", "")
|
||||
daemon_rpcpass = v.get("rpcpassword", "")
|
||||
if daemon_rpcuser != "":
|
||||
opts.append("--daemon-login")
|
||||
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
|
||||
|
||||
opts.append(
|
||||
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
|
||||
)
|
||||
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
|
||||
|
||||
daemons.append(
|
||||
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
|
||||
continue # /monero
|
||||
|
||||
if c == "decred":
|
||||
appdata = v["datadir"]
|
||||
extra_opts = [
|
||||
f'--appdata="{appdata}"',
|
||||
]
|
||||
use_shell: bool = True if os.name == "nt" else False
|
||||
if v["manage_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
filename: str = getCoreBinName(coin_id, v, "dcrd")
|
||||
|
||||
extra_config = {
|
||||
"add_datadir": False,
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrd_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
appdata,
|
||||
v["bindir"],
|
||||
filename,
|
||||
opts=extra_opts,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
|
||||
|
||||
wallet_pwd = v["wallet_pwd"]
|
||||
if wallet_pwd == "":
|
||||
# Only set when in startonlycoin mode
|
||||
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
|
||||
if wallet_pwd != "":
|
||||
extra_opts.append(f'--pass="{wallet_pwd}"')
|
||||
extra_config = {
|
||||
"add_datadir": False,
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrwallet_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
appdata,
|
||||
v["bindir"],
|
||||
filename,
|
||||
opts=extra_opts,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
|
||||
continue # /decred
|
||||
|
||||
if v["manage_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
|
||||
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||
extra_opts = getCoreBinArgs(coin_id, v)
|
||||
daemons.append(
|
||||
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
pids.append((c, pid))
|
||||
swap_client.setDaemonPID(c, pid)
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
if len(pids) > 0:
|
||||
with open(pids_path, "w") as fd:
|
||||
for p in pids:
|
||||
fd.write("{}:{}\n".format(*p))
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
if len(start_only_coins) > 0:
|
||||
logger.info(
|
||||
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
|
||||
)
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
pass
|
||||
else:
|
||||
swap_client.start()
|
||||
if "htmlhost" in settings:
|
||||
swap_client.log.info(
|
||||
"Starting http server at http://%s:%d."
|
||||
% (settings["htmlhost"], settings["htmlport"])
|
||||
)
|
||||
allow_cors = (
|
||||
settings["allowcors"]
|
||||
if "allowcors" in settings
|
||||
else cfg.DEFAULT_ALLOW_CORS
|
||||
)
|
||||
thread_http = HttpThread(
|
||||
fp,
|
||||
settings["htmlhost"],
|
||||
settings["htmlport"],
|
||||
allow_cors,
|
||||
swap_client,
|
||||
)
|
||||
threads.append(thread_http)
|
||||
thread_http.start()
|
||||
|
||||
if "wshost" in settings:
|
||||
ws_url = "ws://{}:{}".format(settings["wshost"], settings["wsport"])
|
||||
swap_client.log.info(f"Starting ws server at {ws_url}.")
|
||||
|
||||
swap_client.ws_server = WebsocketServer(
|
||||
host=settings["wshost"], port=settings["wsport"]
|
||||
)
|
||||
swap_client.ws_server.client_port = settings.get(
|
||||
"wsclientport", settings["wsport"]
|
||||
)
|
||||
swap_client.ws_server.set_fn_new_client(ws_new_client)
|
||||
swap_client.ws_server.set_fn_client_left(ws_client_left)
|
||||
swap_client.ws_server.set_fn_message_received(ws_message_received)
|
||||
swap_client.ws_server.run_forever(threaded=True)
|
||||
|
||||
logger.info("Exit with Ctrl + c.")
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
swap_client.update()
|
||||
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
|
||||
if swap_client.ws_server:
|
||||
try:
|
||||
swap_client.log.info("Stopping websocket server.")
|
||||
swap_client.ws_server.shutdown_gracefully()
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.finalise()
|
||||
swap_client.log.info("Stopping HTTP threads.")
|
||||
for t in threads:
|
||||
try:
|
||||
t.stop()
|
||||
t.join()
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
swap_client.log.info("Interrupting {}".format(d.handle.pid))
|
||||
try:
|
||||
d.handle.send_signal(
|
||||
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||
)
|
||||
except Exception as e:
|
||||
swap_client.log.info("Interrupting %d, error %s", d.handle.pid, str(e))
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
|
||||
if fp:
|
||||
fp.close()
|
||||
closed_pids.append(d.handle.pid)
|
||||
except Exception as ex:
|
||||
swap_client.log.error("Error: {}".format(ex))
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
lines = fd.read().split("\n")
|
||||
still_running = ""
|
||||
for ln in lines:
|
||||
try:
|
||||
if int(ln.split(":")[1]) not in closed_pids:
|
||||
still_running += ln + "\n"
|
||||
except Exception:
|
||||
pass
|
||||
with open(pids_path, "w") as fd:
|
||||
fd.write(still_running)
|
||||
|
||||
|
||||
def printVersion():
|
||||
logger.info("Basicswap version: %s", __version__)
|
||||
|
||||
|
||||
def printHelp():
|
||||
print("Usage: basicswap-run ")
|
||||
print("\n--help, -h Print help.")
|
||||
print("--version, -v Print version.")
|
||||
print(
|
||||
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
|
||||
cfg.BASICSWAP_DATADIR
|
||||
)
|
||||
)
|
||||
print("--mainnet Run in mainnet mode.")
|
||||
print("--testnet Run in testnet mode.")
|
||||
print("--regtest Run in regtest mode.")
|
||||
print(
|
||||
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
data_dir = None
|
||||
chain = "mainnet"
|
||||
start_only_coins = set()
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != "-":
|
||||
logger.warning("Unknown argument %s", v)
|
||||
continue
|
||||
|
||||
s = v.split("=")
|
||||
name = s[0].strip()
|
||||
|
||||
for i in range(2):
|
||||
if name[0] == "-":
|
||||
name = name[1:]
|
||||
|
||||
if name == "v" or name == "version":
|
||||
printVersion()
|
||||
return 0
|
||||
if name == "h" or name == "help":
|
||||
printHelp()
|
||||
return 0
|
||||
|
||||
if name in ("mainnet", "testnet", "regtest"):
|
||||
chain = name
|
||||
continue
|
||||
|
||||
if len(s) == 2:
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
continue
|
||||
if name == "startonlycoin":
|
||||
for coin in [s.lower() for s in s[1].split(",")]:
|
||||
if is_known_coin(coin) is False:
|
||||
raise ValueError(f"Unknown coin: {coin}")
|
||||
start_only_coins.add(coin)
|
||||
continue
|
||||
|
||||
logger.warning("Unknown argument %s", v)
|
||||
|
||||
if os.name == "nt":
|
||||
logger.warning(
|
||||
"Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead."
|
||||
)
|
||||
|
||||
if data_dir is None:
|
||||
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
|
||||
logger.info("Using datadir: %s", data_dir)
|
||||
logger.info("Chain: %s", chain)
|
||||
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
|
||||
logger.info(
|
||||
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
|
||||
)
|
||||
runClient(fp, data_dir, chain, start_only_coins)
|
||||
|
||||
print("Done.")
|
||||
return swap_client.fail_code if swap_client is not None else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,8 +9,8 @@ from .util import (
|
||||
COIN,
|
||||
)
|
||||
|
||||
XMR_COIN = 10 ** 12
|
||||
WOW_COIN = 10 ** 11
|
||||
XMR_COIN = 10**12
|
||||
WOW_COIN = 10**11
|
||||
|
||||
|
||||
class Coins(IntEnum):
|
||||
@@ -30,419 +30,464 @@ class Coins(IntEnum):
|
||||
NAV = 14
|
||||
LTC_MWEB = 15
|
||||
# ZANO = 16
|
||||
BCH = 17
|
||||
|
||||
|
||||
chainparams = {
|
||||
Coins.PART: {
|
||||
'name': 'particl',
|
||||
'ticker': 'PART',
|
||||
'message_magic': 'Bitcoin Signed Message:\n',
|
||||
'blocks_target': 60 * 2,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 51735,
|
||||
'pubkey_address': 0x38,
|
||||
'script_address': 0x3c,
|
||||
'key_prefix': 0x6c,
|
||||
'stealth_key_prefix': 0x14,
|
||||
'hrp': 'pw',
|
||||
'bip44': 44,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "particl",
|
||||
"ticker": "PART",
|
||||
"message_magic": "Bitcoin Signed Message:\n",
|
||||
"blocks_target": 60 * 2,
|
||||
"decimal_places": 8,
|
||||
"mainnet": {
|
||||
"rpcport": 51735,
|
||||
"pubkey_address": 0x38,
|
||||
"script_address": 0x3C,
|
||||
"key_prefix": 0x6C,
|
||||
"stealth_key_prefix": 0x14,
|
||||
"hrp": "pw",
|
||||
"bip44": 44,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 51935,
|
||||
'pubkey_address': 0x76,
|
||||
'script_address': 0x7a,
|
||||
'key_prefix': 0x2e,
|
||||
'stealth_key_prefix': 0x15,
|
||||
'hrp': 'tpw',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"testnet": {
|
||||
"rpcport": 51935,
|
||||
"pubkey_address": 0x76,
|
||||
"script_address": 0x7A,
|
||||
"key_prefix": 0x2E,
|
||||
"stealth_key_prefix": 0x15,
|
||||
"hrp": "tpw",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 51936,
|
||||
"pubkey_address": 0x76,
|
||||
"script_address": 0x7A,
|
||||
"key_prefix": 0x2E,
|
||||
"stealth_key_prefix": 0x15,
|
||||
"hrp": "rtpw",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 51936,
|
||||
'pubkey_address': 0x76,
|
||||
'script_address': 0x7a,
|
||||
'key_prefix': 0x2e,
|
||||
'stealth_key_prefix': 0x15,
|
||||
'hrp': 'rtpw',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.BTC: {
|
||||
'name': 'bitcoin',
|
||||
'ticker': 'BTC',
|
||||
'message_magic': 'Bitcoin Signed Message:\n',
|
||||
'blocks_target': 60 * 10,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 8332,
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'key_prefix': 128,
|
||||
'hrp': 'bc',
|
||||
'bip44': 0,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "bitcoin",
|
||||
"ticker": "BTC",
|
||||
"message_magic": "Bitcoin Signed Message:\n",
|
||||
"blocks_target": 60 * 10,
|
||||
"decimal_places": 8,
|
||||
"mainnet": {
|
||||
"rpcport": 8332,
|
||||
"pubkey_address": 0,
|
||||
"script_address": 5,
|
||||
"key_prefix": 128,
|
||||
"hrp": "bc",
|
||||
"bip44": 0,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 18332,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'tb',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet3',
|
||||
"testnet": {
|
||||
"rpcport": 18332,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "tb",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "bcrt",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'bcrt',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.LTC: {
|
||||
'name': 'litecoin',
|
||||
'ticker': 'LTC',
|
||||
'message_magic': 'Litecoin Signed Message:\n',
|
||||
'blocks_target': 60 * 1,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 9332,
|
||||
'pubkey_address': 48,
|
||||
'script_address': 5,
|
||||
'script_address2': 50,
|
||||
'key_prefix': 176,
|
||||
'hrp': 'ltc',
|
||||
'bip44': 2,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "litecoin",
|
||||
"ticker": "LTC",
|
||||
"message_magic": "Litecoin Signed Message:\n",
|
||||
"blocks_target": 60 * 1,
|
||||
"decimal_places": 8,
|
||||
"mainnet": {
|
||||
"rpcport": 9332,
|
||||
"pubkey_address": 48,
|
||||
"script_address": 5,
|
||||
"script_address2": 50,
|
||||
"key_prefix": 176,
|
||||
"hrp": "ltc",
|
||||
"bip44": 2,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 19332,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'script_address2': 58,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'tltc',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet4',
|
||||
"testnet": {
|
||||
"rpcport": 19332,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"script_address2": 58,
|
||||
"key_prefix": 239,
|
||||
"hrp": "tltc",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet4",
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 19443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"script_address2": 58,
|
||||
"key_prefix": 239,
|
||||
"hrp": "rltc",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 19443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'script_address2': 58,
|
||||
'key_prefix': 239,
|
||||
'hrp': 'rltc',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.DCR: {
|
||||
'name': 'decred',
|
||||
'ticker': 'DCR',
|
||||
'message_magic': 'Decred Signed Message:\n',
|
||||
'blocks_target': 60 * 5,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 9109,
|
||||
'pubkey_address': 0x073f,
|
||||
'script_address': 0x071a,
|
||||
'key_prefix': 0x22de,
|
||||
'bip44': 42,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "decred",
|
||||
"ticker": "DCR",
|
||||
"message_magic": "Decred Signed Message:\n",
|
||||
"blocks_target": 60 * 5,
|
||||
"decimal_places": 8,
|
||||
"mainnet": {
|
||||
"rpcport": 9109,
|
||||
"pubkey_address": 0x073F,
|
||||
"script_address": 0x071A,
|
||||
"key_prefix": 0x22DE,
|
||||
"bip44": 42,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 19109,
|
||||
'pubkey_address': 0x0f21,
|
||||
'script_address': 0x0efc,
|
||||
'key_prefix': 0x230e,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet3',
|
||||
"testnet": {
|
||||
"rpcport": 19109,
|
||||
"pubkey_address": 0x0F21,
|
||||
"script_address": 0x0EFC,
|
||||
"key_prefix": 0x230E,
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
},
|
||||
"regtest": { # simnet
|
||||
"rpcport": 18656,
|
||||
"pubkey_address": 0x0E91,
|
||||
"script_address": 0x0E6C,
|
||||
"key_prefix": 0x2307,
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': { # simnet
|
||||
'rpcport': 18656,
|
||||
'pubkey_address': 0x0e91,
|
||||
'script_address': 0x0e6c,
|
||||
'key_prefix': 0x2307,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.NMC: {
|
||||
'name': 'namecoin',
|
||||
'ticker': 'NMC',
|
||||
'message_magic': 'Namecoin Signed Message:\n',
|
||||
'blocks_target': 60 * 10,
|
||||
'decimal_places': 8,
|
||||
'mainnet': {
|
||||
'rpcport': 8336,
|
||||
'pubkey_address': 52,
|
||||
'script_address': 13,
|
||||
'hrp': 'nc',
|
||||
'bip44': 7,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "namecoin",
|
||||
"ticker": "NMC",
|
||||
"message_magic": "Namecoin Signed Message:\n",
|
||||
"blocks_target": 60 * 10,
|
||||
"decimal_places": 8,
|
||||
"mainnet": {
|
||||
"rpcport": 8336,
|
||||
"pubkey_address": 52,
|
||||
"script_address": 13,
|
||||
"hrp": "nc",
|
||||
"bip44": 7,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 18336,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'hrp': 'tn',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet3',
|
||||
"testnet": {
|
||||
"rpcport": 18336,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"hrp": "tn",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"hrp": "ncrt",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'hrp': 'ncrt',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.XMR: {
|
||||
'name': 'monero',
|
||||
'ticker': 'XMR',
|
||||
'client': 'xmr',
|
||||
'decimal_places': 12,
|
||||
'mainnet': {
|
||||
'rpcport': 18081,
|
||||
'walletrpcport': 18082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
"name": "monero",
|
||||
"ticker": "XMR",
|
||||
"client": "xmr",
|
||||
"decimal_places": 12,
|
||||
"mainnet": {
|
||||
"rpcport": 18081,
|
||||
"walletrpcport": 18082,
|
||||
"min_amount": 1000000000,
|
||||
"max_amount": 10000000 * XMR_COIN,
|
||||
"address_prefix": 18,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 28081,
|
||||
'walletrpcport': 28082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
"testnet": {
|
||||
"rpcport": 28081,
|
||||
"walletrpcport": 28082,
|
||||
"min_amount": 1000000000,
|
||||
"max_amount": 10000000 * XMR_COIN,
|
||||
"address_prefix": 18,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18081,
|
||||
"walletrpcport": 18082,
|
||||
"min_amount": 1000000000,
|
||||
"max_amount": 10000000 * XMR_COIN,
|
||||
"address_prefix": 18,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18081,
|
||||
'walletrpcport': 18082,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * XMR_COIN,
|
||||
'address_prefix': 18,
|
||||
}
|
||||
},
|
||||
Coins.WOW: {
|
||||
'name': 'wownero',
|
||||
'ticker': 'WOW',
|
||||
'client': 'wow',
|
||||
'decimal_places': 11,
|
||||
'mainnet': {
|
||||
'rpcport': 34568,
|
||||
'walletrpcport': 34572, # todo
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
"name": "wownero",
|
||||
"ticker": "WOW",
|
||||
"client": "wow",
|
||||
"decimal_places": 11,
|
||||
"mainnet": {
|
||||
"rpcport": 34568,
|
||||
"walletrpcport": 34572, # todo
|
||||
"min_amount": 100000000,
|
||||
"max_amount": 10000000 * WOW_COIN,
|
||||
"address_prefix": 4146,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 44568,
|
||||
'walletrpcport': 44572,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
"testnet": {
|
||||
"rpcport": 44568,
|
||||
"walletrpcport": 44572,
|
||||
"min_amount": 100000000,
|
||||
"max_amount": 10000000 * WOW_COIN,
|
||||
"address_prefix": 4146,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 54568,
|
||||
"walletrpcport": 54572,
|
||||
"min_amount": 100000000,
|
||||
"max_amount": 10000000 * WOW_COIN,
|
||||
"address_prefix": 4146,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 54568,
|
||||
'walletrpcport': 54572,
|
||||
'min_amount': 100000,
|
||||
'max_amount': 10000 * WOW_COIN,
|
||||
'address_prefix': 4146,
|
||||
}
|
||||
},
|
||||
Coins.PIVX: {
|
||||
'name': 'pivx',
|
||||
'ticker': 'PIVX',
|
||||
'message_magic': 'DarkNet Signed Message:\n',
|
||||
'blocks_target': 60 * 1,
|
||||
'decimal_places': 8,
|
||||
'has_cltv': True,
|
||||
'has_csv': False,
|
||||
'has_segwit': False,
|
||||
'use_ticker_as_name': True,
|
||||
'mainnet': {
|
||||
'rpcport': 51473,
|
||||
'pubkey_address': 30,
|
||||
'script_address': 13,
|
||||
'key_prefix': 212,
|
||||
'bip44': 119,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "pivx",
|
||||
"ticker": "PIVX",
|
||||
"display_name": "PIVX",
|
||||
"message_magic": "DarkNet Signed Message:\n",
|
||||
"blocks_target": 60 * 1,
|
||||
"decimal_places": 8,
|
||||
"has_cltv": True,
|
||||
"has_csv": False,
|
||||
"has_segwit": False,
|
||||
"mainnet": {
|
||||
"rpcport": 51473,
|
||||
"pubkey_address": 30,
|
||||
"script_address": 13,
|
||||
"key_prefix": 212,
|
||||
"bip44": 119,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 51475,
|
||||
'pubkey_address': 139,
|
||||
'script_address': 19,
|
||||
'key_prefix': 239,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
'name': 'testnet4',
|
||||
"testnet": {
|
||||
"rpcport": 51475,
|
||||
"pubkey_address": 139,
|
||||
"script_address": 19,
|
||||
"key_prefix": 239,
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet4",
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 51477,
|
||||
"pubkey_address": 139,
|
||||
"script_address": 19,
|
||||
"key_prefix": 239,
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 51477,
|
||||
'pubkey_address': 139,
|
||||
'script_address': 19,
|
||||
'key_prefix': 239,
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.DASH: {
|
||||
'name': 'dash',
|
||||
'ticker': 'DASH',
|
||||
'message_magic': 'DarkCoin Signed Message:\n',
|
||||
'blocks_target': 60 * 2.5,
|
||||
'decimal_places': 8,
|
||||
'has_csv': True,
|
||||
'has_segwit': False,
|
||||
'mainnet': {
|
||||
'rpcport': 9998,
|
||||
'pubkey_address': 76,
|
||||
'script_address': 16,
|
||||
'key_prefix': 204,
|
||||
'hrp': '',
|
||||
'bip44': 5,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "dash",
|
||||
"ticker": "DASH",
|
||||
"message_magic": "DarkCoin Signed Message:\n",
|
||||
"blocks_target": 60 * 2.5,
|
||||
"decimal_places": 8,
|
||||
"has_csv": True,
|
||||
"has_segwit": False,
|
||||
"mainnet": {
|
||||
"rpcport": 9998,
|
||||
"pubkey_address": 76,
|
||||
"script_address": 16,
|
||||
"key_prefix": 204,
|
||||
"hrp": "",
|
||||
"bip44": 5,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 19998,
|
||||
'pubkey_address': 140,
|
||||
'script_address': 19,
|
||||
'key_prefix': 239,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"testnet": {
|
||||
"rpcport": 19998,
|
||||
"pubkey_address": 140,
|
||||
"script_address": 19,
|
||||
"key_prefix": 239,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18332,
|
||||
"pubkey_address": 140,
|
||||
"script_address": 19,
|
||||
"key_prefix": 239,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18332,
|
||||
'pubkey_address': 140,
|
||||
'script_address': 19,
|
||||
'key_prefix': 239,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.FIRO: {
|
||||
'name': 'firo',
|
||||
'ticker': 'FIRO',
|
||||
'message_magic': 'Zcoin Signed Message:\n',
|
||||
'blocks_target': 60 * 10,
|
||||
'decimal_places': 8,
|
||||
'has_cltv': False,
|
||||
'has_csv': False,
|
||||
'has_segwit': False,
|
||||
'mainnet': {
|
||||
'rpcport': 8888,
|
||||
'pubkey_address': 82,
|
||||
'script_address': 7,
|
||||
'key_prefix': 210,
|
||||
'hrp': '',
|
||||
'bip44': 136,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "firo",
|
||||
"ticker": "FIRO",
|
||||
"message_magic": "Zcoin Signed Message:\n",
|
||||
"blocks_target": 60 * 10,
|
||||
"decimal_places": 8,
|
||||
"has_cltv": False,
|
||||
"has_csv": False,
|
||||
"has_segwit": False,
|
||||
"mainnet": {
|
||||
"rpcport": 8888,
|
||||
"pubkey_address": 82,
|
||||
"script_address": 7,
|
||||
"key_prefix": 210,
|
||||
"hrp": "",
|
||||
"bip44": 136,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 18888,
|
||||
'pubkey_address': 65,
|
||||
'script_address': 178,
|
||||
'key_prefix': 185,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"testnet": {
|
||||
"rpcport": 18888,
|
||||
"pubkey_address": 65,
|
||||
"script_address": 178,
|
||||
"key_prefix": 185,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 28888,
|
||||
"pubkey_address": 65,
|
||||
"script_address": 178,
|
||||
"key_prefix": 239,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 28888,
|
||||
'pubkey_address': 65,
|
||||
'script_address': 178,
|
||||
'key_prefix': 239,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.NAV: {
|
||||
'name': 'navcoin',
|
||||
'ticker': 'NAV',
|
||||
'message_magic': 'Navcoin Signed Message:\n',
|
||||
'blocks_target': 30,
|
||||
'decimal_places': 8,
|
||||
'has_csv': True,
|
||||
'has_segwit': True,
|
||||
'mainnet': {
|
||||
'rpcport': 44444,
|
||||
'pubkey_address': 53,
|
||||
'script_address': 85,
|
||||
'key_prefix': 150,
|
||||
'hrp': '',
|
||||
'bip44': 130,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"name": "navcoin",
|
||||
"ticker": "NAV",
|
||||
"message_magic": "Navcoin Signed Message:\n",
|
||||
"blocks_target": 30,
|
||||
"decimal_places": 8,
|
||||
"has_csv": True,
|
||||
"has_segwit": True,
|
||||
"mainnet": {
|
||||
"rpcport": 44444,
|
||||
"pubkey_address": 53,
|
||||
"script_address": 85,
|
||||
"key_prefix": 150,
|
||||
"hrp": "",
|
||||
"bip44": 130,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 44445,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
"testnet": {
|
||||
"rpcport": 44445,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 44446,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'key_prefix': 239,
|
||||
'hrp': '',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
}
|
||||
"regtest": {
|
||||
"rpcport": 44446,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
},
|
||||
Coins.BCH: {
|
||||
"name": "bitcoincash",
|
||||
"ticker": "BCH",
|
||||
"display_name": "Bitcoin Cash",
|
||||
"message_magic": "Bitcoin Signed Message:\n",
|
||||
"blocks_target": 60 * 2,
|
||||
"decimal_places": 8,
|
||||
"has_cltv": True,
|
||||
"has_csv": True,
|
||||
"has_segwit": False,
|
||||
"cli_binname": "bitcoin-cli",
|
||||
"core_binname": "bitcoind",
|
||||
"mainnet": {
|
||||
"rpcport": 8332,
|
||||
"pubkey_address": 0,
|
||||
"script_address": 5,
|
||||
"key_prefix": 128,
|
||||
"hrp": "bitcoincash",
|
||||
"bip44": 0,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 18332,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "bchtest",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "bchreg",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
},
|
||||
},
|
||||
}
|
||||
ticker_map = {}
|
||||
|
||||
|
||||
for c, params in chainparams.items():
|
||||
ticker_map[params['ticker'].lower()] = c
|
||||
ticker_map[params["ticker"].lower()] = c
|
||||
|
||||
|
||||
def getCoinIdFromTicker(ticker):
|
||||
def getCoinIdFromTicker(ticker: str) -> str:
|
||||
try:
|
||||
return ticker_map[ticker.lower()]
|
||||
except Exception:
|
||||
raise ValueError('Unknown coin')
|
||||
raise ValueError("Unknown coin")
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2019-2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
|
||||
CONFIG_FILENAME = 'basicswap.json'
|
||||
BASICSWAP_DATADIR = os.getenv('BASICSWAP_DATADIR', os.path.join('~', '.basicswap'))
|
||||
CONFIG_FILENAME = "basicswap.json"
|
||||
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
||||
DEFAULT_ALLOW_CORS = False
|
||||
TEST_DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
|
||||
DEFAULT_TEST_BINDIR = os.path.expanduser(os.getenv('DEFAULT_TEST_BINDIR', os.path.join('~', '.basicswap', 'bin')))
|
||||
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
|
||||
DEFAULT_TEST_BINDIR = os.path.expanduser(
|
||||
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
|
||||
)
|
||||
|
||||
bin_suffix = ('.exe' if os.name == 'nt' else '')
|
||||
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'particl')))
|
||||
PARTICLD = os.getenv('PARTICLD', 'particld' + bin_suffix)
|
||||
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + bin_suffix)
|
||||
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + bin_suffix)
|
||||
bin_suffix = ".exe" if os.name == "nt" else ""
|
||||
PARTICL_BINDIR = os.path.expanduser(
|
||||
os.getenv("PARTICL_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "particl"))
|
||||
)
|
||||
PARTICLD = os.getenv("PARTICLD", "particld" + bin_suffix)
|
||||
PARTICL_CLI = os.getenv("PARTICL_CLI", "particl-cli" + bin_suffix)
|
||||
PARTICL_TX = os.getenv("PARTICL_TX", "particl-tx" + bin_suffix)
|
||||
|
||||
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoin')))
|
||||
BITCOIND = os.getenv('BITCOIND', 'bitcoind' + bin_suffix)
|
||||
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + bin_suffix)
|
||||
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + bin_suffix)
|
||||
BITCOIN_BINDIR = os.path.expanduser(
|
||||
os.getenv("BITCOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "bitcoin"))
|
||||
)
|
||||
BITCOIND = os.getenv("BITCOIND", "bitcoind" + bin_suffix)
|
||||
BITCOIN_CLI = os.getenv("BITCOIN_CLI", "bitcoin-cli" + bin_suffix)
|
||||
BITCOIN_TX = os.getenv("BITCOIN_TX", "bitcoin-tx" + bin_suffix)
|
||||
|
||||
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'litecoin')))
|
||||
LITECOIND = os.getenv('LITECOIND', 'litecoind' + bin_suffix)
|
||||
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + bin_suffix)
|
||||
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + bin_suffix)
|
||||
LITECOIN_BINDIR = os.path.expanduser(
|
||||
os.getenv("LITECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "litecoin"))
|
||||
)
|
||||
LITECOIND = os.getenv("LITECOIND", "litecoind" + bin_suffix)
|
||||
LITECOIN_CLI = os.getenv("LITECOIN_CLI", "litecoin-cli" + bin_suffix)
|
||||
LITECOIN_TX = os.getenv("LITECOIN_TX", "litecoin-tx" + bin_suffix)
|
||||
|
||||
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'namecoin')))
|
||||
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + bin_suffix)
|
||||
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + bin_suffix)
|
||||
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
|
||||
NAMECOIN_BINDIR = os.path.expanduser(
|
||||
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
|
||||
)
|
||||
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
|
||||
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
|
||||
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
|
||||
|
||||
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
|
||||
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
|
||||
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)
|
||||
XMR_BINDIR = os.path.expanduser(
|
||||
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
|
||||
)
|
||||
XMRD = os.getenv("XMRD", "monerod" + bin_suffix)
|
||||
XMR_WALLET_RPC = os.getenv("XMR_WALLET_RPC", "monero-wallet-rpc" + bin_suffix)
|
||||
|
||||
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.
|
||||
|
||||
1148
basicswap/db.py
1148
basicswap/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,326 +1,427 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from .db import (
|
||||
AutomationStrategy,
|
||||
BidState,
|
||||
Concepts,
|
||||
AutomationStrategy,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
CURRENT_DB_VERSION,
|
||||
CURRENT_DB_DATA_VERSION)
|
||||
)
|
||||
|
||||
from .basicswap_util import (
|
||||
BidStates,
|
||||
strBidState,
|
||||
canAcceptBidState,
|
||||
isActiveBidState,
|
||||
isErrorBidState,
|
||||
isFailingBidState,
|
||||
isFinalBidState,
|
||||
strBidState,
|
||||
)
|
||||
|
||||
|
||||
def addBidState(self, state, now, cursor):
|
||||
self.add(
|
||||
BidState(
|
||||
active_ind=1,
|
||||
state_id=int(state),
|
||||
in_progress=isActiveBidState(state),
|
||||
in_error=isErrorBidState(state),
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
can_accept=canAcceptBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now,
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
|
||||
|
||||
def upgradeDatabaseData(self, data_version):
|
||||
if data_version >= CURRENT_DB_DATA_VERSION:
|
||||
return
|
||||
|
||||
self.log.info('Upgrading database records from version %d to %d.', data_version, CURRENT_DB_DATA_VERSION)
|
||||
with self.mxDB:
|
||||
try:
|
||||
session = scoped_session(self.session_factory)
|
||||
self.log.info(
|
||||
"Upgrading database records from version %d to %d.",
|
||||
data_version,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
)
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
now = int(time.time())
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
if data_version < 1:
|
||||
session.add(AutomationStrategy(
|
||||
if data_version < 1:
|
||||
self.add(
|
||||
AutomationStrategy(
|
||||
active_ind=1,
|
||||
label='Accept All',
|
||||
label="Accept All",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps({'exact_rate_only': True,
|
||||
'max_concurrent_bids': 5}).encode('utf-8'),
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=False,
|
||||
created_at=now))
|
||||
session.add(AutomationStrategy(
|
||||
created_at=now,
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
self.add(
|
||||
AutomationStrategy(
|
||||
active_ind=1,
|
||||
label='Accept Known',
|
||||
label="Accept Known",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps({'exact_rate_only': True,
|
||||
'max_concurrent_bids': 5}).encode('utf-8'),
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=True,
|
||||
note='Accept bids from identities with previously successful swaps only',
|
||||
created_at=now))
|
||||
note="Accept bids from identities with previously successful swaps only",
|
||||
created_at=now,
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
|
||||
for state in BidStates:
|
||||
session.add(BidState(
|
||||
active_ind=1,
|
||||
state_id=int(state),
|
||||
in_progress=isActiveBidState(state),
|
||||
in_error=isErrorBidState(state),
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now))
|
||||
for state in BidStates:
|
||||
addBidState(self, state, now, cursor)
|
||||
|
||||
if data_version > 0 and data_version < 2:
|
||||
for state in (BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX):
|
||||
session.add(BidState(
|
||||
if data_version > 0 and data_version < 2:
|
||||
for state in (
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||
):
|
||||
self.add(
|
||||
BidState(
|
||||
active_ind=1,
|
||||
state_id=int(state),
|
||||
in_progress=isActiveBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now))
|
||||
if data_version > 0 and data_version < 3:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
session.execute('UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id', {'in_error': in_error, 'swap_failed': swap_failed, 'swap_ended': swap_ended, 'state_id': int(state)})
|
||||
if data_version > 0 and data_version < 4:
|
||||
for state in (BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_ACCEPTED):
|
||||
session.add(BidState(
|
||||
active_ind=1,
|
||||
state_id=int(state),
|
||||
in_progress=isActiveBidState(state),
|
||||
in_error=isErrorBidState(state),
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now))
|
||||
created_at=now,
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
if data_version > 0 and data_version < 3:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
cursor.execute(
|
||||
"UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
{
|
||||
"in_error": in_error,
|
||||
"swap_failed": swap_failed,
|
||||
"swap_ended": swap_ended,
|
||||
"state_id": int(state),
|
||||
},
|
||||
)
|
||||
if data_version > 0 and data_version < 4:
|
||||
for state in (
|
||||
BidStates.BID_REQUEST_SENT,
|
||||
BidStates.BID_REQUEST_ACCEPTED,
|
||||
):
|
||||
addBidState(self, state, now, cursor)
|
||||
|
||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||
self.setIntKV('db_data_version', self.db_data_version, session)
|
||||
session.commit()
|
||||
self.log.info('Upgraded database records to version {}'.format(self.db_data_version))
|
||||
finally:
|
||||
session.close()
|
||||
session.remove()
|
||||
if data_version > 0 and data_version < 5:
|
||||
for state in (
|
||||
BidStates.BID_EXPIRED,
|
||||
BidStates.BID_AACCEPT_DELAY,
|
||||
BidStates.BID_AACCEPT_FAIL,
|
||||
):
|
||||
addBidState(self, state, now, cursor)
|
||||
|
||||
self.db_data_version = CURRENT_DB_DATA_VERSION
|
||||
self.setIntKV("db_data_version", self.db_data_version, cursor)
|
||||
self.commitDB()
|
||||
self.log.info(
|
||||
"Upgraded database records to version {}".format(self.db_data_version)
|
||||
)
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
|
||||
def upgradeDatabase(self, db_version):
|
||||
if db_version >= CURRENT_DB_VERSION:
|
||||
return
|
||||
|
||||
self.log.info('Upgrading database from version %d to %d.', db_version, CURRENT_DB_VERSION)
|
||||
self.log.info(
|
||||
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
|
||||
)
|
||||
|
||||
while True:
|
||||
session = scoped_session(self.session_factory)
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
|
||||
current_version = db_version
|
||||
if current_version == 6:
|
||||
session.execute('ALTER TABLE bids ADD COLUMN security_token BLOB')
|
||||
session.execute('ALTER TABLE offers ADD COLUMN security_token BLOB')
|
||||
db_version += 1
|
||||
elif current_version == 7:
|
||||
session.execute('ALTER TABLE transactions ADD COLUMN block_hash BLOB')
|
||||
session.execute('ALTER TABLE transactions ADD COLUMN block_height INTEGER')
|
||||
session.execute('ALTER TABLE transactions ADD COLUMN block_time INTEGER')
|
||||
db_version += 1
|
||||
elif current_version == 8:
|
||||
session.execute('''
|
||||
CREATE TABLE wallets (
|
||||
record_id INTEGER NOT NULL,
|
||||
coin_id INTEGER,
|
||||
wallet_name VARCHAR,
|
||||
wallet_data VARCHAR,
|
||||
balance_type INTEGER,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
db_version += 1
|
||||
elif current_version == 9:
|
||||
session.execute('ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR')
|
||||
db_version += 1
|
||||
elif current_version == 10:
|
||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER')
|
||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER')
|
||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR')
|
||||
session.execute('ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR')
|
||||
session.execute('UPDATE smsgaddresses SET active_ind = 1, created_at = 1')
|
||||
current_version = db_version
|
||||
if current_version == 6:
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN security_token BLOB")
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN security_token BLOB")
|
||||
db_version += 1
|
||||
elif current_version == 7:
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN block_hash BLOB")
|
||||
cursor.execute(
|
||||
"ALTER TABLE transactions ADD COLUMN block_height INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN block_time INTEGER")
|
||||
db_version += 1
|
||||
elif current_version == 8:
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE wallets (
|
||||
record_id INTEGER NOT NULL,
|
||||
coin_id INTEGER,
|
||||
wallet_name VARCHAR,
|
||||
wallet_data VARCHAR,
|
||||
balance_type INTEGER,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
db_version += 1
|
||||
elif current_version == 9:
|
||||
cursor.execute("ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR")
|
||||
db_version += 1
|
||||
elif current_version == 10:
|
||||
cursor.execute(
|
||||
"ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR")
|
||||
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR")
|
||||
cursor.execute(
|
||||
"UPDATE smsgaddresses SET active_ind = 1, created_at = 1"
|
||||
)
|
||||
|
||||
session.execute('ALTER TABLE offers ADD COLUMN addr_to VARCHAR')
|
||||
session.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
|
||||
db_version += 1
|
||||
elif current_version == 11:
|
||||
session.execute('ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN protocol_version INTEGER')
|
||||
session.execute('ALTER TABLE offers ADD COLUMN protocol_version INTEGER')
|
||||
session.execute('ALTER TABLE transactions ADD COLUMN tx_data BLOB')
|
||||
db_version += 1
|
||||
elif current_version == 12:
|
||||
session.execute('''
|
||||
CREATE TABLE knownidentities (
|
||||
record_id INTEGER NOT NULL,
|
||||
address VARCHAR,
|
||||
label VARCHAR,
|
||||
publickey BLOB,
|
||||
num_sent_bids_successful INTEGER,
|
||||
num_recv_bids_successful INTEGER,
|
||||
num_sent_bids_rejected INTEGER,
|
||||
num_recv_bids_rejected INTEGER,
|
||||
num_sent_bids_failed INTEGER,
|
||||
num_recv_bids_failed INTEGER,
|
||||
note VARCHAR,
|
||||
updated_at BIGINT,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN reject_code INTEGER')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN rate INTEGER')
|
||||
session.execute('ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER')
|
||||
session.execute('ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER')
|
||||
db_version += 1
|
||||
elif current_version == 13:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE automationstrategies (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
label VARCHAR,
|
||||
type_ind INTEGER,
|
||||
only_known_identities INTEGER,
|
||||
num_concurrent INTEGER,
|
||||
data BLOB,
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN addr_to VARCHAR")
|
||||
cursor.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
|
||||
db_version += 1
|
||||
elif current_version == 11:
|
||||
cursor.execute(
|
||||
"ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN protocol_version INTEGER")
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN protocol_version INTEGER")
|
||||
cursor.execute("ALTER TABLE transactions ADD COLUMN tx_data BLOB")
|
||||
db_version += 1
|
||||
elif current_version == 12:
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE knownidentities (
|
||||
record_id INTEGER NOT NULL,
|
||||
address VARCHAR,
|
||||
label VARCHAR,
|
||||
publickey BLOB,
|
||||
num_sent_bids_successful INTEGER,
|
||||
num_recv_bids_successful INTEGER,
|
||||
num_sent_bids_rejected INTEGER,
|
||||
num_recv_bids_rejected INTEGER,
|
||||
num_sent_bids_failed INTEGER,
|
||||
num_recv_bids_failed INTEGER,
|
||||
note VARCHAR,
|
||||
updated_at BIGINT,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN reject_code INTEGER")
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN rate INTEGER")
|
||||
cursor.execute(
|
||||
"ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER")
|
||||
db_version += 1
|
||||
elif current_version == 13:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE automationstrategies (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
label VARCHAR,
|
||||
type_ind INTEGER,
|
||||
only_known_identities INTEGER,
|
||||
num_concurrent INTEGER,
|
||||
data BLOB,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
session.execute('''
|
||||
CREATE TABLE automationlinks (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE automationlinks (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
strategy_id INTEGER,
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
strategy_id INTEGER,
|
||||
|
||||
data BLOB,
|
||||
repeat_limit INTEGER,
|
||||
repeat_count INTEGER,
|
||||
data BLOB,
|
||||
repeat_limit INTEGER,
|
||||
repeat_count INTEGER,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
session.execute('''
|
||||
CREATE TABLE history (
|
||||
record_id INTEGER NOT NULL,
|
||||
concept_type INTEGER,
|
||||
concept_id INTEGER,
|
||||
changed_data BLOB,
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE history (
|
||||
record_id INTEGER NOT NULL,
|
||||
concept_type INTEGER,
|
||||
concept_id INTEGER,
|
||||
changed_data BLOB,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
session.execute('''
|
||||
CREATE TABLE bidstates (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
state_id INTEGER,
|
||||
label VARCHAR,
|
||||
in_progress INTEGER,
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE bidstates (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
state_id INTEGER,
|
||||
label VARCHAR,
|
||||
in_progress INTEGER,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
session.execute('ALTER TABLE wallets ADD COLUMN active_ind INTEGER')
|
||||
session.execute('ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER')
|
||||
session.execute('ALTER TABLE eventqueue RENAME TO actions')
|
||||
session.execute('ALTER TABLE actions RENAME COLUMN event_id TO action_id')
|
||||
session.execute('ALTER TABLE actions RENAME COLUMN event_type TO action_type')
|
||||
session.execute('ALTER TABLE actions RENAME COLUMN event_data TO action_data')
|
||||
elif current_version == 14:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB')
|
||||
session.execute('ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id')
|
||||
elif current_version == 15:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE notifications (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
event_type INTEGER,
|
||||
event_data BLOB,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))''')
|
||||
elif current_version == 16:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE prefunded_transactions (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
tx_type INTEGER,
|
||||
tx_data BLOB,
|
||||
used_by BLOB,
|
||||
PRIMARY KEY (record_id))''')
|
||||
elif current_version == 17:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER')
|
||||
session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER')
|
||||
session.execute('ALTER TABLE knownidentities ADD COLUMN data BLOB')
|
||||
session.execute('UPDATE knownidentities SET active_ind = 1')
|
||||
elif current_version == 18:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING')
|
||||
session.execute('ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING')
|
||||
elif current_version == 19:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE bidstates ADD COLUMN in_error INTEGER')
|
||||
session.execute('ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER')
|
||||
session.execute('ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER')
|
||||
elif current_version == 20:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE message_links (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
cursor.execute("ALTER TABLE wallets ADD COLUMN active_ind INTEGER")
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE eventqueue RENAME TO actions")
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_id TO action_id"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_type TO action_type"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE actions RENAME COLUMN event_data TO action_data"
|
||||
)
|
||||
elif current_version == 14:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id"
|
||||
)
|
||||
elif current_version == 15:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE notifications (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
event_type INTEGER,
|
||||
event_data BLOB,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
elif current_version == 16:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE prefunded_transactions (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
tx_type INTEGER,
|
||||
tx_data BLOB,
|
||||
used_by BLOB,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
elif current_version == 17:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER"
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER"
|
||||
)
|
||||
cursor.execute("ALTER TABLE knownidentities ADD COLUMN data BLOB")
|
||||
cursor.execute("UPDATE knownidentities SET active_ind = 1")
|
||||
elif current_version == 18:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING")
|
||||
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING")
|
||||
elif current_version == 19:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN in_error INTEGER")
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER")
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER")
|
||||
elif current_version == 20:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE message_links (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
created_at BIGINT,
|
||||
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
|
||||
msg_type INTEGER,
|
||||
msg_sequence INTEGER,
|
||||
msg_id BLOB,
|
||||
PRIMARY KEY (record_id))''')
|
||||
session.execute('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER')
|
||||
elif current_version == 21:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
|
||||
elif current_version == 22:
|
||||
db_version += 1
|
||||
session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
|
||||
elif current_version == 23:
|
||||
db_version += 1
|
||||
session.execute('''
|
||||
CREATE TABLE checkedblocks (
|
||||
record_id INTEGER NOT NULL,
|
||||
created_at BIGINT,
|
||||
coin_type INTEGER,
|
||||
block_height INTEGER,
|
||||
block_hash BLOB,
|
||||
block_time INTEGER,
|
||||
PRIMARY KEY (record_id))''')
|
||||
session.execute('ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB')
|
||||
if current_version != db_version:
|
||||
self.db_version = db_version
|
||||
self.setIntKV('db_version', db_version, session)
|
||||
session.commit()
|
||||
session.close()
|
||||
session.remove()
|
||||
self.log.info('Upgraded database to version {}'.format(self.db_version))
|
||||
continue
|
||||
msg_type INTEGER,
|
||||
msg_sequence INTEGER,
|
||||
msg_id BLOB,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
|
||||
elif current_version == 21:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
|
||||
elif current_version == 22:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
|
||||
elif current_version == 23:
|
||||
db_version += 1
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE checkedblocks (
|
||||
record_id INTEGER NOT NULL,
|
||||
created_at BIGINT,
|
||||
coin_type INTEGER,
|
||||
block_height INTEGER,
|
||||
block_hash BLOB,
|
||||
block_time INTEGER,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
|
||||
elif current_version == 24:
|
||||
db_version += 1
|
||||
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
|
||||
if current_version != db_version:
|
||||
self.db_version = db_version
|
||||
self.setIntKV("db_version", db_version, cursor)
|
||||
self.commitDB()
|
||||
self.log.info("Upgraded database to version {}".format(self.db_version))
|
||||
continue
|
||||
except Exception as e:
|
||||
self.log.error("Upgrade failed {}".format(e))
|
||||
self.rollbackDB()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
break
|
||||
|
||||
if db_version != CURRENT_DB_VERSION:
|
||||
raise ValueError('Unable to upgrade database.')
|
||||
raise ValueError("Unable to upgrade database.")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023 The BSX Developers
|
||||
# Copyright (c) 2023-2024 The Basicswap Developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -12,47 +12,122 @@ from .db import (
|
||||
def remove_expired_data(self, time_offset: int = 0):
|
||||
now: int = self.getTime()
|
||||
try:
|
||||
session = self.openSession()
|
||||
cursor = self.openDB()
|
||||
|
||||
active_bids_insert = self.activeBidsQueryStr(now, '', 'b2')
|
||||
query_str = f'''
|
||||
active_bids_insert: str = self.activeBidsQueryStr("", "b2")
|
||||
query_str = f"""
|
||||
SELECT o.offer_id FROM offers o
|
||||
WHERE o.expire_at <= :expired_at AND 0 = (SELECT COUNT(*) FROM bids b2 WHERE b2.offer_id = o.offer_id AND {active_bids_insert})
|
||||
'''
|
||||
"""
|
||||
num_offers = 0
|
||||
num_bids = 0
|
||||
offer_rows = session.execute(query_str, {'expired_at': now - time_offset})
|
||||
offer_rows = cursor.execute(
|
||||
query_str, {"now": now, "expired_at": now - time_offset}
|
||||
)
|
||||
for offer_row in offer_rows:
|
||||
num_offers += 1
|
||||
bid_rows = session.execute('SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||
bid_rows = cursor.execute(
|
||||
"SELECT bids.bid_id FROM bids WHERE bids.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
for bid_row in bid_rows:
|
||||
num_bids += 1
|
||||
session.execute('DELETE FROM transactions WHERE transactions.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id', {'type_ind': int(Concepts.BID), 'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM actions WHERE actions.linked_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM bids WHERE bids.bid_id = :bid_id', {'bid_id': bid_row[0]})
|
||||
session.execute('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id', {'type_ind': int(Concepts.BID), 'linked_id': bid_row[0]})
|
||||
cursor.execute(
|
||||
"DELETE FROM transactions WHERE transactions.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :bid_id",
|
||||
{"type_ind": int(Concepts.BID), "bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_swaps WHERE xmr_swaps.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM actions WHERE actions.linked_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM addresspool WHERE addresspool.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_split_data WHERE xmr_split_data.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM bids WHERE bids.bid_id = :bid_id",
|
||||
{"bid_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||
)
|
||||
|
||||
session.execute('DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM actions WHERE actions.linked_id = :offer_id', {'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM offers WHERE offers.offer_id = :offer_id', {'offer_id': offer_row[0]})
|
||||
session.execute('DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id', {'type_ind': int(Concepts.OFFER), 'offer_id': offer_row[0]})
|
||||
cursor.execute(
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM automationlinks WHERE automationlinks.linked_type = :type_ind AND automationlinks.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM prefunded_transactions WHERE prefunded_transactions.linked_type = :type_ind AND prefunded_transactions.linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM history WHERE history.concept_type = :type_ind AND history.concept_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM xmr_offers WHERE xmr_offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM sentoffers WHERE sentoffers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM actions WHERE actions.linked_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM offers WHERE offers.offer_id = :offer_id",
|
||||
{"offer_id": offer_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
{"type_ind": int(Concepts.OFFER), "offer_id": offer_row[0]},
|
||||
)
|
||||
|
||||
if num_offers > 0 or num_bids > 0:
|
||||
self.log.info('Removed data for {} expired offer{} and {} bid{}.'.format(num_offers, 's' if num_offers != 1 else '', num_bids, 's' if num_bids != 1 else ''))
|
||||
self.log.info(
|
||||
"Removed data for {} expired offer{} and {} bid{}.".format(
|
||||
num_offers,
|
||||
"s" if num_offers != 1 else "",
|
||||
num_bids,
|
||||
"s" if num_bids != 1 else "",
|
||||
)
|
||||
)
|
||||
|
||||
session.execute('DELETE FROM checkedblocks WHERE created_at <= :expired_at', {'expired_at': now - time_offset})
|
||||
cursor.execute(
|
||||
"DELETE FROM checkedblocks WHERE created_at <= :expired_at",
|
||||
{"expired_at": now - time_offset},
|
||||
)
|
||||
|
||||
finally:
|
||||
self.closeSession(session)
|
||||
self.closeDB(cursor)
|
||||
|
||||
@@ -13,8 +13,8 @@ def encodepoint(P):
|
||||
zi = edf.inv(P[2])
|
||||
x = (P[0] * zi) % edf.q
|
||||
y = (P[1] * zi) % edf.q
|
||||
y += ((x & 1) << 255)
|
||||
return y.to_bytes(32, byteorder='little')
|
||||
y += (x & 1) << 255
|
||||
return y.to_bytes(32, byteorder="little")
|
||||
|
||||
|
||||
def hashToEd25519(bytes_in):
|
||||
@@ -22,8 +22,8 @@ def hashToEd25519(bytes_in):
|
||||
for i in range(1000):
|
||||
h255 = bytearray(hashed)
|
||||
x_sign = 0 if h255[31] & 0x80 == 0 else 1
|
||||
h255[31] &= 0x7f # Clear top bit
|
||||
y = int.from_bytes(h255, byteorder='little')
|
||||
h255[31] &= 0x7F # Clear top bit
|
||||
y = int.from_bytes(h255, byteorder="little")
|
||||
x = edf.xrecover(y, x_sign)
|
||||
if x == 0 and y == 1: # Skip infinity point
|
||||
continue
|
||||
@@ -33,4 +33,4 @@ def hashToEd25519(bytes_in):
|
||||
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
||||
return P
|
||||
hashed = hashlib.sha256(hashed).digest()
|
||||
raise ValueError('hashToEd25519 failed')
|
||||
raise ValueError("hashToEd25519 failed")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import json
|
||||
|
||||
|
||||
class Explorer():
|
||||
class Explorer:
|
||||
def __init__(self, swapclient, coin_type, base_url):
|
||||
self.swapclient = swapclient
|
||||
self.coin_type = coin_type
|
||||
@@ -15,82 +15,94 @@ class Explorer():
|
||||
self.log = self.swapclient.log
|
||||
|
||||
def readURL(self, url):
|
||||
self.log.debug('Explorer url: {}'.format(url))
|
||||
self.log.debug("Explorer url: {}".format(url))
|
||||
return self.swapclient.readURL(url)
|
||||
|
||||
|
||||
class ExplorerInsight(Explorer):
|
||||
def getChainHeight(self):
|
||||
return json.loads(self.readURL(self.base_url + '/sync'))['blockChainHeight']
|
||||
return json.loads(self.readURL(self.base_url + "/sync"))["blockChainHeight"]
|
||||
|
||||
def getBlock(self, block_hash):
|
||||
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
|
||||
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
|
||||
return data
|
||||
|
||||
def getTransaction(self, txid):
|
||||
data = json.loads(self.readURL(self.base_url + '/tx/{}'.format(txid)))
|
||||
data = json.loads(self.readURL(self.base_url + "/tx/{}".format(txid)))
|
||||
return data
|
||||
|
||||
def getBalance(self, address):
|
||||
data = json.loads(self.readURL(self.base_url + '/addr/{}/balance'.format(address)))
|
||||
data = json.loads(
|
||||
self.readURL(self.base_url + "/addr/{}/balance".format(address))
|
||||
)
|
||||
return data
|
||||
|
||||
def lookupUnspentByAddress(self, address):
|
||||
data = json.loads(self.readURL(self.base_url + '/addr/{}/utxo'.format(address)))
|
||||
data = json.loads(self.readURL(self.base_url + "/addr/{}/utxo".format(address)))
|
||||
rv = []
|
||||
for utxo in data:
|
||||
rv.append({
|
||||
'txid': utxo['txid'],
|
||||
'index': utxo['vout'],
|
||||
'height': utxo['height'],
|
||||
'n_conf': utxo['confirmations'],
|
||||
'value': utxo['satoshis'],
|
||||
})
|
||||
rv.append(
|
||||
{
|
||||
"txid": utxo["txid"],
|
||||
"index": utxo["vout"],
|
||||
"height": utxo["height"],
|
||||
"n_conf": utxo["confirmations"],
|
||||
"value": utxo["satoshis"],
|
||||
}
|
||||
)
|
||||
return rv
|
||||
|
||||
|
||||
class ExplorerBitAps(Explorer):
|
||||
def getChainHeight(self):
|
||||
return json.loads(self.readURL(self.base_url + '/block/last'))['data']['block']['height']
|
||||
return json.loads(self.readURL(self.base_url + "/block/last"))["data"]["block"][
|
||||
"height"
|
||||
]
|
||||
|
||||
def getBlock(self, block_hash):
|
||||
data = json.loads(self.readURL(self.base_url + '/block/{}'.format(block_hash)))
|
||||
data = json.loads(self.readURL(self.base_url + "/block/{}".format(block_hash)))
|
||||
return data
|
||||
|
||||
def getTransaction(self, txid):
|
||||
data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(txid)))
|
||||
data = json.loads(self.readURL(self.base_url + "/transaction/{}".format(txid)))
|
||||
return data
|
||||
|
||||
def getBalance(self, address):
|
||||
data = json.loads(self.readURL(self.base_url + '/address/state/' + address))
|
||||
return data['data']['balance']
|
||||
data = json.loads(self.readURL(self.base_url + "/address/state/" + address))
|
||||
return data["data"]["balance"]
|
||||
|
||||
def lookupUnspentByAddress(self, address):
|
||||
# Can't get unspents return only if exactly one transaction exists
|
||||
data = json.loads(self.readURL(self.base_url + '/address/transactions/' + address))
|
||||
data = json.loads(
|
||||
self.readURL(self.base_url + "/address/transactions/" + address)
|
||||
)
|
||||
try:
|
||||
assert data['data']['list'] == 1
|
||||
assert data["data"]["list"] == 1
|
||||
except Exception as ex:
|
||||
self.log.debug('Explorer error: {}'.format(str(ex)))
|
||||
self.log.debug("Explorer error: {}".format(str(ex)))
|
||||
return None
|
||||
tx = data['data']['list'][0]
|
||||
tx_data = json.loads(self.readURL(self.base_url + '/transaction/{}'.format(tx['txId'])))['data']
|
||||
tx = data["data"]["list"][0]
|
||||
tx_data = json.loads(
|
||||
self.readURL(self.base_url + "/transaction/{}".format(tx["txId"]))
|
||||
)["data"]
|
||||
|
||||
for i, vout in tx_data['vOut'].items():
|
||||
if vout['address'] == address:
|
||||
return [{
|
||||
'txid': tx_data['txId'],
|
||||
'index': int(i),
|
||||
'height': tx_data['blockHeight'],
|
||||
'n_conf': tx_data['confirmations'],
|
||||
'value': vout['value'],
|
||||
}]
|
||||
for i, vout in tx_data["vOut"].items():
|
||||
if vout["address"] == address:
|
||||
return [
|
||||
{
|
||||
"txid": tx_data["txId"],
|
||||
"index": int(i),
|
||||
"height": tx_data["blockHeight"],
|
||||
"n_conf": tx_data["confirmations"],
|
||||
"value": vout["value"],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class ExplorerChainz(Explorer):
|
||||
def getChainHeight(self):
|
||||
return int(self.readURL(self.base_url + '?q=getblockcount'))
|
||||
return int(self.readURL(self.base_url + "?q=getblockcount"))
|
||||
|
||||
def lookupUnspentByAddress(self, address):
|
||||
chain_height = self.getChainHeight()
|
||||
self.log.debug('[rm] chain_height %d', chain_height)
|
||||
self.log.debug("[rm] chain_height %d", chain_height)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -56,12 +57,12 @@ from .ui.page_identity import page_identity
|
||||
from .ui.page_smsgaddresses import page_smsgaddresses
|
||||
from .ui.page_debug import page_debug
|
||||
|
||||
env = Environment(loader=PackageLoader('basicswap', 'templates'))
|
||||
env.filters['formatts'] = format_timestamp
|
||||
env = Environment(loader=PackageLoader("basicswap", "templates"))
|
||||
env.filters["formatts"] = format_timestamp
|
||||
|
||||
|
||||
def extractDomain(url):
|
||||
return url.split('://', 1)[1].split('/', 1)[0]
|
||||
return url.split("://", 1)[1].split("/", 1)[0]
|
||||
|
||||
|
||||
def listAvailableExplorers(swap_client):
|
||||
@@ -69,40 +70,47 @@ def listAvailableExplorers(swap_client):
|
||||
for c in Coins:
|
||||
if c not in chainparams:
|
||||
continue
|
||||
for i, e in enumerate(swap_client.coin_clients[c]['explorers']):
|
||||
explorers.append(('{}_{}'.format(int(c), i), getCoinName(c) + ' - ' + extractDomain(e.base_url)))
|
||||
for i, e in enumerate(swap_client.coin_clients[c]["explorers"]):
|
||||
explorers.append(
|
||||
(
|
||||
"{}_{}".format(int(c), i),
|
||||
getCoinName(c) + " - " + extractDomain(e.base_url),
|
||||
)
|
||||
)
|
||||
return explorers
|
||||
|
||||
|
||||
def listExplorerActions(swap_client):
|
||||
actions = [('height', 'Chain Height'),
|
||||
('block', 'Get Block'),
|
||||
('tx', 'Get Transaction'),
|
||||
('balance', 'Address Balance'),
|
||||
('unspent', 'List Unspent')]
|
||||
actions = [
|
||||
("height", "Chain Height"),
|
||||
("block", "Get Block"),
|
||||
("tx", "Get Transaction"),
|
||||
("balance", "Address Balance"),
|
||||
("unspent", "List Unspent"),
|
||||
]
|
||||
return actions
|
||||
|
||||
|
||||
def parse_cmd(cmd: str, type_map: str):
|
||||
params = shlex.split(cmd)
|
||||
if len(params) < 1:
|
||||
return '', []
|
||||
return "", []
|
||||
method = params[0]
|
||||
typed_params = []
|
||||
params = params[1:]
|
||||
|
||||
for i, param in enumerate(params):
|
||||
if i >= len(type_map):
|
||||
type_ind = 's'
|
||||
type_ind = "s"
|
||||
else:
|
||||
type_ind = type_map[i]
|
||||
if type_ind == 'i':
|
||||
if type_ind == "i":
|
||||
typed_params.append(int(param))
|
||||
elif type_ind == 'f':
|
||||
elif type_ind == "f":
|
||||
typed_params.append(float(param))
|
||||
elif type_ind == 'b':
|
||||
elif type_ind == "b":
|
||||
typed_params.append(toBool(param))
|
||||
elif type_ind == 'j':
|
||||
elif type_ind == "j":
|
||||
typed_params.append(json.loads(param))
|
||||
else:
|
||||
typed_params.append(param)
|
||||
@@ -111,7 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def log_error(self, format, *args):
|
||||
super().log_message(format, *args)
|
||||
|
||||
@@ -123,96 +130,112 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return os.urandom(8).hex()
|
||||
|
||||
def checkForm(self, post_string, name, messages):
|
||||
if post_string == '':
|
||||
if post_string == "":
|
||||
return None
|
||||
form_data = parse.parse_qs(post_string)
|
||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
||||
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||
if self.server.last_form_id.get(name, None) == form_id:
|
||||
messages.append('Prevented double submit for form {}.'.format(form_id))
|
||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
return form_data
|
||||
|
||||
def render_template(self, template, args_dict, status_code=200, version=__version__):
|
||||
def render_template(
|
||||
self, template, args_dict, status_code=200, version=__version__
|
||||
):
|
||||
swap_client = self.server.swap_client
|
||||
if swap_client.ws_server:
|
||||
args_dict['ws_url'] = swap_client.ws_server.url
|
||||
args_dict["ws_port"] = swap_client.ws_server.client_port
|
||||
if swap_client.debug:
|
||||
args_dict['debug_mode'] = True
|
||||
args_dict["debug_mode"] = True
|
||||
if swap_client.debug_ui:
|
||||
args_dict['debug_ui_mode'] = True
|
||||
args_dict["debug_ui_mode"] = True
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict['use_tor_proxy'] = True
|
||||
args_dict["use_tor_proxy"] = True
|
||||
# TODO: Cache value?
|
||||
try:
|
||||
args_dict['tor_established'] = True if get_tor_established_state(swap_client) == '1' else False
|
||||
except Exception:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
except Exception as e:
|
||||
args_dict["tor_established"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.error(f"Error getting Tor state: {str(e)}")
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
if swap_client._show_notifications:
|
||||
args_dict['notifications'] = swap_client.getNotifications()
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
|
||||
if 'messages' in args_dict:
|
||||
if "messages" in args_dict:
|
||||
messages_with_ids = []
|
||||
for msg in args_dict['messages']:
|
||||
for msg in args_dict["messages"]:
|
||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
args_dict['messages'] = messages_with_ids
|
||||
if 'err_messages' in args_dict:
|
||||
args_dict["messages"] = messages_with_ids
|
||||
if "err_messages" in args_dict:
|
||||
err_messages_with_ids = []
|
||||
for msg in args_dict['err_messages']:
|
||||
for msg in args_dict["err_messages"]:
|
||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
args_dict['err_messages'] = err_messages_with_ids
|
||||
args_dict["err_messages"] = err_messages_with_ids
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
self.server.session_tokens['shutdown'] = shutdown_token
|
||||
args_dict['shutdown_token'] = shutdown_token
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
args_dict["shutdown_token"] = shutdown_token
|
||||
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict['encrypted'] = encrypted
|
||||
args_dict['locked'] = locked
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
|
||||
args_dict['version'] = version
|
||||
args_dict["version"] = version
|
||||
|
||||
self.putHeaders(status_code, 'text/html')
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
form_id=self.generate_form_id(),
|
||||
**args_dict,
|
||||
), 'UTF-8')
|
||||
self.putHeaders(status_code, "text/html")
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
h2=self.server.title,
|
||||
form_id=self.generate_form_id(),
|
||||
**args_dict,
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
def render_simple_template(self, template, args_dict):
|
||||
swap_client = self.server.swap_client
|
||||
return bytes(template.render(
|
||||
title=self.server.title,
|
||||
**args_dict,
|
||||
), 'UTF-8')
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
**args_dict,
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
def page_info(self, info_str, post_string=None):
|
||||
template = env.get_template('info.html')
|
||||
template = env.get_template("info.html")
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
return self.render_template(template, {
|
||||
'title_str': 'BasicSwap Info',
|
||||
'message_str': info_str,
|
||||
'summary': summary,
|
||||
})
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"title_str": "BasicSwap Info",
|
||||
"message_str": info_str,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_error(self, error_str, post_string=None):
|
||||
template = env.get_template('error.html')
|
||||
template = env.get_template("error.html")
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
return self.render_template(template, {
|
||||
'title_str': 'BasicSwap Error',
|
||||
'message_str': error_str,
|
||||
'summary': summary,
|
||||
})
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"title_str": "BasicSwap Error",
|
||||
"message_str": error_str,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_explorers(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -224,42 +247,49 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
action = -1
|
||||
messages = []
|
||||
err_messages = []
|
||||
form_data = self.checkForm(post_string, 'explorers', err_messages)
|
||||
form_data = self.checkForm(post_string, "explorers", err_messages)
|
||||
if form_data:
|
||||
|
||||
explorer = form_data[b'explorer'][0].decode('utf-8')
|
||||
action = form_data[b'action'][0].decode('utf-8')
|
||||
explorer = form_data[b"explorer"][0].decode("utf-8")
|
||||
action = form_data[b"action"][0].decode("utf-8")
|
||||
|
||||
args = '' if b'args' not in form_data else form_data[b'args'][0].decode('utf-8')
|
||||
args = (
|
||||
""
|
||||
if b"args" not in form_data
|
||||
else form_data[b"args"][0].decode("utf-8")
|
||||
)
|
||||
try:
|
||||
c, e = explorer.split('_')
|
||||
exp = swap_client.coin_clients[Coins(int(c))]['explorers'][int(e)]
|
||||
if action == 'height':
|
||||
c, e = explorer.split("_")
|
||||
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
|
||||
if action == "height":
|
||||
result = str(exp.getChainHeight())
|
||||
elif action == 'block':
|
||||
elif action == "block":
|
||||
result = dumpj(exp.getBlock(args))
|
||||
elif action == 'tx':
|
||||
elif action == "tx":
|
||||
result = dumpj(exp.getTransaction(args))
|
||||
elif action == 'balance':
|
||||
elif action == "balance":
|
||||
result = dumpj(exp.getBalance(args))
|
||||
elif action == 'unspent':
|
||||
elif action == "unspent":
|
||||
result = dumpj(exp.lookupUnspentByAddress(args))
|
||||
else:
|
||||
result = 'Unknown action'
|
||||
result = "Unknown action"
|
||||
except Exception as ex:
|
||||
result = str(ex)
|
||||
|
||||
template = env.get_template('explorers.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'explorers': listAvailableExplorers(swap_client),
|
||||
'explorer': explorer,
|
||||
'actions': listExplorerActions(swap_client),
|
||||
'action': action,
|
||||
'result': result,
|
||||
'summary': summary,
|
||||
})
|
||||
template = env.get_template("explorers.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"explorers": listAvailableExplorers(swap_client),
|
||||
"explorer": explorer,
|
||||
"actions": listExplorerActions(swap_client),
|
||||
"action": action,
|
||||
"result": result,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_rpc(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -267,34 +297,33 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
result = None
|
||||
cmd = ''
|
||||
cmd = ""
|
||||
coin_type_selected = -1
|
||||
coin_type = -1
|
||||
coin_id = -1
|
||||
call_type = 'cli'
|
||||
type_map = ''
|
||||
call_type = "cli"
|
||||
type_map = ""
|
||||
messages = []
|
||||
err_messages = []
|
||||
form_data = self.checkForm(post_string, 'rpc', err_messages)
|
||||
form_data = self.checkForm(post_string, "rpc", err_messages)
|
||||
if form_data:
|
||||
try:
|
||||
call_type = get_data_entry_or(form_data, 'call_type', 'cli')
|
||||
type_map = get_data_entry_or(form_data, 'type_map', '')
|
||||
call_type = get_data_entry_or(form_data, "call_type", "cli")
|
||||
type_map = get_data_entry_or(form_data, "type_map", "")
|
||||
try:
|
||||
coin_type_selected = get_data_entry(form_data, 'coin_type')
|
||||
coin_type_split = coin_type_selected.split(',')
|
||||
coin_type_selected = get_data_entry(form_data, "coin_type")
|
||||
coin_type_split = coin_type_selected.split(",")
|
||||
coin_type = Coins(int(coin_type_split[0]))
|
||||
coin_variant = int(coin_type_split[1])
|
||||
except Exception:
|
||||
raise ValueError('Unknown Coin Type')
|
||||
raise ValueError("Unknown Coin Type")
|
||||
|
||||
if coin_type in (Coins.DCR,):
|
||||
call_type = 'http'
|
||||
call_type = "http"
|
||||
|
||||
try:
|
||||
cmd = get_data_entry(form_data, 'cmd')
|
||||
cmd = get_data_entry(form_data, "cmd")
|
||||
except Exception:
|
||||
raise ValueError('Invalid command')
|
||||
raise ValueError("Invalid command")
|
||||
if coin_type in (Coins.XMR, Coins.WOW):
|
||||
ci = swap_client.ci(coin_type)
|
||||
arr = cmd.split(None, 1)
|
||||
@@ -309,10 +338,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
params = None
|
||||
rv = ci.rpc2(method, params)
|
||||
else:
|
||||
raise ValueError('Unknown RPC variant')
|
||||
raise ValueError("Unknown RPC variant")
|
||||
result = json.dumps(rv, indent=4)
|
||||
else:
|
||||
if call_type == 'http':
|
||||
if call_type == "http":
|
||||
ci = swap_client.ci(coin_type)
|
||||
method, params = parse_cmd(cmd, type_map)
|
||||
if coin_variant == 1:
|
||||
@@ -320,50 +349,56 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
elif coin_variant == 2:
|
||||
rv = ci.rpc_wallet_mweb(method, params)
|
||||
else:
|
||||
if coin_type in (Coins.DCR, ):
|
||||
if coin_type in (Coins.DCR,):
|
||||
rv = ci.rpc(method, params)
|
||||
else:
|
||||
rv = ci.rpc_wallet(method, params)
|
||||
if not isinstance(rv, str):
|
||||
rv = json.dumps(rv, indent=4)
|
||||
result = cmd + '\n' + rv
|
||||
result = cmd + "\n" + rv
|
||||
else:
|
||||
result = cmd + '\n' + swap_client.callcoincli(coin_type, cmd)
|
||||
result = cmd + "\n" + swap_client.callcoincli(coin_type, cmd)
|
||||
except Exception as ex:
|
||||
result = cmd + '\n' + str(ex)
|
||||
result = cmd + "\n" + str(ex)
|
||||
if self.server.swap_client.debug is True:
|
||||
self.server.swap_client.log.error(traceback.format_exc())
|
||||
|
||||
template = env.get_template('rpc.html')
|
||||
template = env.get_template("rpc.html")
|
||||
|
||||
coin_available = listAvailableCoins(swap_client, with_variants=False)
|
||||
with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available)
|
||||
with_wow: bool = any(c[0] == Coins.WOW for c in coin_available)
|
||||
coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, Coins.WOW)]
|
||||
coins = [
|
||||
(str(c[0]) + ",0", c[1])
|
||||
for c in coin_available
|
||||
if c[0] not in (Coins.XMR, Coins.WOW)
|
||||
]
|
||||
|
||||
if any(c[0] == Coins.DCR for c in coin_available):
|
||||
coins.append((str(int(Coins.DCR)) + ',1', 'Decred Wallet'))
|
||||
coins.append((str(int(Coins.DCR)) + ",1", "Decred Wallet"))
|
||||
if any(c[0] == Coins.LTC for c in coin_available):
|
||||
coins.append((str(int(Coins.LTC)) + ',2', 'Litecoin MWEB Wallet'))
|
||||
coins.append((str(int(Coins.LTC)) + ",2", "Litecoin MWEB Wallet"))
|
||||
if with_xmr:
|
||||
coins.append((str(int(Coins.XMR)) + ',0', 'Monero'))
|
||||
coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON'))
|
||||
coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet'))
|
||||
coins.append((str(int(Coins.XMR)) + ",0", "Monero"))
|
||||
coins.append((str(int(Coins.XMR)) + ",1", "Monero JSON"))
|
||||
coins.append((str(int(Coins.XMR)) + ",2", "Monero Wallet"))
|
||||
if with_wow:
|
||||
coins.append((str(int(Coins.WOW)) + ',0', 'Wownero'))
|
||||
coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON'))
|
||||
coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet'))
|
||||
coins.append((str(int(Coins.WOW)) + ",0", "Wownero"))
|
||||
coins.append((str(int(Coins.WOW)) + ",1", "Wownero JSON"))
|
||||
coins.append((str(int(Coins.WOW)) + ",2", "Wownero Wallet"))
|
||||
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'coins': coins,
|
||||
'coin_type': coin_type_selected,
|
||||
'call_type': call_type,
|
||||
'result': result,
|
||||
'messages': messages,
|
||||
'summary': summary,
|
||||
})
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"coins": coins,
|
||||
"coin_type": coin_type_selected,
|
||||
"call_type": call_type,
|
||||
"result": result,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_active(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -371,12 +406,24 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
active_swaps = swap_client.listSwapsInProgress()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
template = env.get_template('active.html')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'active_swaps': [(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps],
|
||||
'summary': summary,
|
||||
})
|
||||
template = env.get_template("active.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"refresh": 30,
|
||||
"active_swaps": [
|
||||
(
|
||||
s[0].hex(),
|
||||
s[1],
|
||||
strBidState(s[2]),
|
||||
strTxState(s[3]),
|
||||
strTxState(s[4]),
|
||||
)
|
||||
for s in active_swaps
|
||||
],
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_watched(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -384,62 +431,68 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
template = env.get_template('watched.html')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'last_scanned': [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
||||
'watched_outputs': [(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs],
|
||||
'summary': summary,
|
||||
})
|
||||
template = env.get_template("watched.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"refresh": 30,
|
||||
"last_scanned": [(getCoinName(ls[0]), ls[1]) for ls in last_scanned],
|
||||
"watched_outputs": [
|
||||
(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4]))
|
||||
for wo in watched_outputs
|
||||
],
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_shutdown(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
if len(url_split) > 2:
|
||||
token = url_split[2]
|
||||
expect_token = self.server.session_tokens.get('shutdown', None)
|
||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||
if token != expect_token:
|
||||
return self.page_info('Unexpected token, still running.')
|
||||
return self.page_info("Unexpected token, still running.")
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
return self.page_info('Shutting down')
|
||||
return self.page_info("Shutting down")
|
||||
|
||||
def page_index(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
template = env.get_template('index.html')
|
||||
return self.render_template(template, {
|
||||
'refresh': 30,
|
||||
'summary': summary,
|
||||
'use_tor_proxy': swap_client.use_tor_proxy
|
||||
})
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/offers")
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
def page_404(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
template = env.get_template('404.html')
|
||||
return self.render_template(template, {
|
||||
'summary': summary,
|
||||
})
|
||||
template = env.get_template("404.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def putHeaders(self, status_code, content_type):
|
||||
self.send_response(status_code)
|
||||
if self.server.allow_cors:
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string='', is_json=False):
|
||||
def handle_http(self, status_code, path, post_string="", is_json=False):
|
||||
swap_client = self.server.swap_client
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split('/')
|
||||
if post_string == '' and len(parsed.query) > 0:
|
||||
url_split = parsed.path.split("/")
|
||||
if post_string == "" and len(parsed.query) > 0:
|
||||
post_string = parsed.query
|
||||
if len(url_split) > 1 and url_split[1] == 'json':
|
||||
if len(url_split) > 1 and url_split[1] == "json":
|
||||
try:
|
||||
self.putHeaders(status_code, 'text/plain')
|
||||
self.putHeaders(status_code, "text/plain")
|
||||
func = js_url_to_function(url_split)
|
||||
return func(self, url_split, post_string, is_json)
|
||||
except Exception as ex:
|
||||
@@ -447,37 +500,42 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
return js_error(self, str(ex))
|
||||
|
||||
if len(url_split) > 1 and url_split[1] == 'static':
|
||||
if len(url_split) > 1 and url_split[1] == "static":
|
||||
try:
|
||||
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
||||
if len(url_split) > 3 and url_split[2] == 'sequence_diagrams':
|
||||
with open(os.path.join(static_path, 'sequence_diagrams', url_split[3]), 'rb') as fp:
|
||||
self.putHeaders(status_code, 'image/svg+xml')
|
||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||
with open(
|
||||
os.path.join(static_path, "sequence_diagrams", url_split[3]),
|
||||
"rb",
|
||||
) as fp:
|
||||
self.putHeaders(status_code, "image/svg+xml")
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == 'images':
|
||||
elif len(url_split) > 3 and url_split[2] == "images":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
_, extension = os.path.splitext(filename)
|
||||
mime_type = {
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
}.get(extension, '')
|
||||
if mime_type == '':
|
||||
raise ValueError('Unknown file type ' + filename)
|
||||
with open(os.path.join(static_path, 'images', filename), 'rb') as fp:
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".ico": "image/x-icon",
|
||||
}.get(extension, "")
|
||||
if mime_type == "":
|
||||
raise ValueError("Unknown file type " + filename)
|
||||
with open(
|
||||
os.path.join(static_path, "images", filename), "rb"
|
||||
) as fp:
|
||||
self.putHeaders(status_code, mime_type)
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == 'css':
|
||||
elif len(url_split) > 3 and url_split[2] == "css":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
with open(os.path.join(static_path, 'css', filename), 'rb') as fp:
|
||||
self.putHeaders(status_code, 'text/css; charset=utf-8')
|
||||
with open(os.path.join(static_path, "css", filename), "rb") as fp:
|
||||
self.putHeaders(status_code, "text/css; charset=utf-8")
|
||||
return fp.read()
|
||||
elif len(url_split) > 3 and url_split[2] == 'js':
|
||||
elif len(url_split) > 3 and url_split[2] == "js":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
with open(os.path.join(static_path, 'js', filename), 'rb') as fp:
|
||||
self.putHeaders(status_code, 'application/javascript')
|
||||
with open(os.path.join(static_path, "js", filename), "rb") as fp:
|
||||
self.putHeaders(status_code, "application/javascript")
|
||||
return fp.read()
|
||||
else:
|
||||
return self.page_404(url_split)
|
||||
@@ -492,63 +550,63 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if len(url_split) > 1:
|
||||
page = url_split[1]
|
||||
|
||||
if page == 'active':
|
||||
if page == "active":
|
||||
return self.page_active(url_split, post_string)
|
||||
if page == 'wallets':
|
||||
if page == "wallets":
|
||||
return page_wallets(self, url_split, post_string)
|
||||
if page == 'wallet':
|
||||
if page == "wallet":
|
||||
return page_wallet(self, url_split, post_string)
|
||||
if page == 'settings':
|
||||
if page == "settings":
|
||||
return page_settings(self, url_split, post_string)
|
||||
if page == 'error':
|
||||
if page == "error":
|
||||
return self.page_error(url_split, post_string)
|
||||
if page == 'info':
|
||||
if page == "info":
|
||||
return self.page_info(url_split, post_string)
|
||||
if page == 'rpc':
|
||||
if page == "rpc":
|
||||
return self.page_rpc(url_split, post_string)
|
||||
if page == 'debug':
|
||||
if page == "debug":
|
||||
return page_debug(self, url_split, post_string)
|
||||
if page == 'explorers':
|
||||
if page == "explorers":
|
||||
return self.page_explorers(url_split, post_string)
|
||||
if page == 'offer':
|
||||
if page == "offer":
|
||||
return page_offer(self, url_split, post_string)
|
||||
if page == 'offers':
|
||||
if page == "offers":
|
||||
return page_offers(self, url_split, post_string)
|
||||
if page == 'newoffer':
|
||||
if page == "newoffer":
|
||||
return page_newoffer(self, url_split, post_string)
|
||||
if page == 'sentoffers':
|
||||
if page == "sentoffers":
|
||||
return page_offers(self, url_split, post_string, sent=True)
|
||||
if page == 'bid':
|
||||
if page == "bid":
|
||||
return page_bid(self, url_split, post_string)
|
||||
if page == 'receivedbids':
|
||||
if page == "receivedbids":
|
||||
return page_bids(self, url_split, post_string, received=True)
|
||||
if page == 'sentbids':
|
||||
if page == "sentbids":
|
||||
return page_bids(self, url_split, post_string, sent=True)
|
||||
if page == 'availablebids':
|
||||
if page == "availablebids":
|
||||
return page_bids(self, url_split, post_string, available=True)
|
||||
if page == 'watched':
|
||||
if page == "watched":
|
||||
return self.page_watched(url_split, post_string)
|
||||
if page == 'smsgaddresses':
|
||||
if page == "smsgaddresses":
|
||||
return page_smsgaddresses(self, url_split, post_string)
|
||||
if page == 'identity':
|
||||
if page == "identity":
|
||||
return page_identity(self, url_split, post_string)
|
||||
if page == 'tor':
|
||||
if page == "tor":
|
||||
return page_tor(self, url_split, post_string)
|
||||
if page == 'automation':
|
||||
if page == "automation":
|
||||
return page_automation_strategies(self, url_split, post_string)
|
||||
if page == 'automationstrategy':
|
||||
if page == "automationstrategy":
|
||||
return page_automation_strategy(self, url_split, post_string)
|
||||
if page == 'newautomationstrategy':
|
||||
if page == "newautomationstrategy":
|
||||
return page_automation_strategy_new(self, url_split, post_string)
|
||||
if page == 'shutdown':
|
||||
if page == "shutdown":
|
||||
return self.page_shutdown(url_split, post_string)
|
||||
if page == 'changepassword':
|
||||
if page == "changepassword":
|
||||
return page_changepassword(self, url_split, post_string)
|
||||
if page == 'unlock':
|
||||
if page == "unlock":
|
||||
return page_unlock(self, url_split, post_string)
|
||||
if page == 'lock':
|
||||
if page == "lock":
|
||||
return page_lock(self, url_split, post_string)
|
||||
if page != '':
|
||||
if page != "":
|
||||
return self.page_404(url_split)
|
||||
return self.page_index(url_split)
|
||||
except LockedCoinError:
|
||||
@@ -563,20 +621,20 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_POST(self):
|
||||
post_string = self.rfile.read(int(self.headers.get('Content-Length')))
|
||||
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
|
||||
|
||||
is_json = True if 'json' in self.headers.get('Content-Type', '') else False
|
||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, 'text/html')
|
||||
self.putHeaders(200, "text/html")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200, 'ok')
|
||||
self.send_response(200, "ok")
|
||||
if self.server.allow_cors:
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Headers', '*')
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Headers", "*")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
@@ -590,7 +648,7 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
self.port_no = port_no
|
||||
self.allow_cors = allow_cors
|
||||
self.swap_client = swap_client
|
||||
self.title = 'BasicSwap - ' + __version__
|
||||
self.title = "BasicSwap - " + __version__
|
||||
self.last_form_id = dict()
|
||||
self.session_tokens = dict()
|
||||
self.env = env
|
||||
@@ -605,9 +663,9 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
# Send fake request
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
||||
conn.connect()
|
||||
conn.request('GET', '/none')
|
||||
conn.request("GET", "/none")
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
_ = response.read()
|
||||
conn.close()
|
||||
|
||||
def serve_forever(self):
|
||||
|
||||
@@ -14,7 +14,8 @@ from basicswap.chainparams import (
|
||||
)
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
i2b, b2i,
|
||||
i2b,
|
||||
b2i,
|
||||
make_int,
|
||||
format_amount,
|
||||
TemporaryError,
|
||||
@@ -26,9 +27,7 @@ from basicswap.util.ecc import (
|
||||
ep,
|
||||
getSecretInt,
|
||||
)
|
||||
from coincurve.dleag import (
|
||||
verify_secp256k1_point
|
||||
)
|
||||
from coincurve.dleag import verify_secp256k1_point
|
||||
from coincurve.keys import (
|
||||
PublicKey,
|
||||
)
|
||||
@@ -52,6 +51,7 @@ class CoinInterface:
|
||||
self.setDefaults()
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
self._altruistic = True
|
||||
|
||||
def setDefaults(self):
|
||||
self._unknown_wallet_seed = True
|
||||
@@ -66,33 +66,33 @@ class CoinInterface:
|
||||
|
||||
def coin_name(self) -> str:
|
||||
coin_chainparams = chainparams[self.coin_type()]
|
||||
if coin_chainparams.get('use_ticker_as_name', False):
|
||||
return coin_chainparams['ticker']
|
||||
return coin_chainparams['name'].capitalize()
|
||||
if "display_name" in coin_chainparams:
|
||||
return coin_chainparams["display_name"]
|
||||
return coin_chainparams["name"].capitalize()
|
||||
|
||||
def ticker(self) -> str:
|
||||
ticker = chainparams[self.coin_type()]['ticker']
|
||||
if self._network == 'testnet':
|
||||
ticker = 't' + ticker
|
||||
elif self._network == 'regtest':
|
||||
ticker = 'rt' + ticker
|
||||
ticker = chainparams[self.coin_type()]["ticker"]
|
||||
if self._network == "testnet":
|
||||
ticker = "t" + ticker
|
||||
elif self._network == "regtest":
|
||||
ticker = "rt" + ticker
|
||||
return ticker
|
||||
|
||||
def getExchangeTicker(self, exchange_name: str) -> str:
|
||||
return chainparams[self.coin_type()]['ticker']
|
||||
return chainparams[self.coin_type()]["ticker"]
|
||||
|
||||
def getExchangeName(self, exchange_name: str) -> str:
|
||||
return chainparams[self.coin_type()]['name']
|
||||
return chainparams[self.coin_type()]["name"]
|
||||
|
||||
def ticker_mainnet(self) -> str:
|
||||
ticker = chainparams[self.coin_type()]['ticker']
|
||||
ticker = chainparams[self.coin_type()]["ticker"]
|
||||
return ticker
|
||||
|
||||
def min_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['min_amount']
|
||||
return chainparams[self.coin_type()][self._network]["min_amount"]
|
||||
|
||||
def max_amount(self) -> int:
|
||||
return chainparams[self.coin_type()][self._network]['max_amount']
|
||||
return chainparams[self.coin_type()][self._network]["max_amount"]
|
||||
|
||||
def setWalletSeedWarning(self, value: bool) -> None:
|
||||
self._unknown_wallet_seed = value
|
||||
@@ -110,7 +110,7 @@ class CoinInterface:
|
||||
return chainparams[self.coin_type()][self._network]
|
||||
|
||||
def has_segwit(self) -> bool:
|
||||
return chainparams[self.coin_type()].get('has_segwit', True)
|
||||
return chainparams[self.coin_type()].get("has_segwit", True)
|
||||
|
||||
def use_p2shp2wsh(self) -> bool:
|
||||
# p2sh-p2wsh
|
||||
@@ -120,24 +120,26 @@ class CoinInterface:
|
||||
if isinstance(ex, TemporaryError):
|
||||
return True
|
||||
str_error: str = str(ex).lower()
|
||||
if 'not enough unlocked money' in str_error:
|
||||
if "not enough unlocked money" in str_error:
|
||||
return True
|
||||
if 'no unlocked balance' in str_error:
|
||||
if "no unlocked balance" in str_error:
|
||||
return True
|
||||
if 'transaction was rejected by daemon' in str_error:
|
||||
if "transaction was rejected by daemon" in str_error:
|
||||
return True
|
||||
if 'invalid unlocked_balance' in str_error:
|
||||
if "invalid unlocked_balance" in str_error:
|
||||
return True
|
||||
if 'daemon is busy' in str_error:
|
||||
if "daemon is busy" in str_error:
|
||||
return True
|
||||
if 'timed out' in str_error:
|
||||
if "timed out" in str_error:
|
||||
return True
|
||||
if 'request-sent' in str_error:
|
||||
if "request-sent" in str_error:
|
||||
return True
|
||||
return False
|
||||
|
||||
def setConfTarget(self, new_conf_target: int) -> None:
|
||||
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
|
||||
ensure(
|
||||
new_conf_target >= 1 and new_conf_target < 33, "Invalid conf_target value"
|
||||
)
|
||||
self._conf_target = new_conf_target
|
||||
|
||||
def walletRestoreHeight(self) -> int:
|
||||
@@ -166,31 +168,19 @@ class CoinInterface:
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def altruistic(self) -> bool:
|
||||
return self._altruistic
|
||||
|
||||
class AdaptorSigInterface():
|
||||
|
||||
class AdaptorSigInterface:
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
b'',
|
||||
bytes(72),
|
||||
bytes(72),
|
||||
bytes(len(script))
|
||||
]
|
||||
return [b"", bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
b'',
|
||||
bytes(72),
|
||||
bytes(72),
|
||||
bytes((1,)),
|
||||
bytes(len(script))
|
||||
]
|
||||
return [b"", bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes):
|
||||
return [
|
||||
bytes(72),
|
||||
b'',
|
||||
bytes(len(script))
|
||||
]
|
||||
return [bytes(72), b"", bytes(len(script))]
|
||||
|
||||
|
||||
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
||||
@@ -209,7 +199,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
|
||||
|
||||
def verifyKey(self, k: bytes) -> bool:
|
||||
i = b2i(k)
|
||||
return (i < ep.o and i > 0)
|
||||
return i < ep.o and i > 0
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
|
||||
return verify_secp256k1_point(pubkey_bytes)
|
||||
|
||||
1126
basicswap/interface/bch.py
Normal file
1126
basicswap/interface/bch.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
247
basicswap/interface/contrib/bch_test_framework/cashaddress.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import unittest
|
||||
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
def polymod(values):
|
||||
chk = 1
|
||||
generator = [
|
||||
(0x01, 0x98F2BC8E61),
|
||||
(0x02, 0x79B76D99E2),
|
||||
(0x04, 0xF33E5FB3C4),
|
||||
(0x08, 0xAE2EABE2A8),
|
||||
(0x10, 0x1E4F43E470),
|
||||
]
|
||||
for value in values:
|
||||
top = chk >> 35
|
||||
chk = ((chk & 0x07FFFFFFFF) << 5) ^ value
|
||||
for i in generator:
|
||||
if top & i[0] != 0:
|
||||
chk ^= i[1]
|
||||
return chk ^ 1
|
||||
|
||||
|
||||
def calculate_checksum(prefix, payload):
|
||||
poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
|
||||
out = list()
|
||||
for i in range(8):
|
||||
out.append((poly >> 5 * (7 - i)) & 0x1F)
|
||||
return out
|
||||
|
||||
|
||||
def verify_checksum(prefix, payload):
|
||||
return polymod(prefix_expand(prefix) + payload) == 0
|
||||
|
||||
|
||||
def b32decode(inputs):
|
||||
out = list()
|
||||
for letter in inputs:
|
||||
out.append(CHARSET.find(letter))
|
||||
return out
|
||||
|
||||
|
||||
def b32encode(inputs):
|
||||
out = ""
|
||||
for char_code in inputs:
|
||||
out += CHARSET[char_code]
|
||||
return out
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def prefix_expand(prefix):
|
||||
return [ord(x) & 0x1F for x in prefix] + [0]
|
||||
|
||||
|
||||
class Address:
|
||||
"""
|
||||
Class to handle CashAddr.
|
||||
|
||||
:param version: Version of CashAddr
|
||||
:type version: ``str``
|
||||
:param payload: Payload of CashAddr as int list of the bytearray
|
||||
:type payload: ``list`` of ``int``
|
||||
"""
|
||||
|
||||
VERSIONS = {
|
||||
"P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"},
|
||||
"P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"},
|
||||
"P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"},
|
||||
"P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"},
|
||||
"P2SH32-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 11,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"},
|
||||
"P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"},
|
||||
"P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"},
|
||||
"P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"},
|
||||
"P2SH20-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 24,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2SH32-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 27,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2PKH-CATKN": {
|
||||
"prefix": "bitcoincash",
|
||||
"version_bit": 16,
|
||||
"network": "mainnet",
|
||||
},
|
||||
"P2SH20-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 24,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2SH32-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 27,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2PKH-CATKN-TESTNET": {
|
||||
"prefix": "bchtest",
|
||||
"version_bit": 16,
|
||||
"network": "testnet",
|
||||
},
|
||||
"P2SH20-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 24,
|
||||
"network": "regtest",
|
||||
},
|
||||
"P2SH32-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 27,
|
||||
"network": "regtest",
|
||||
},
|
||||
"P2PKH-CATKN-REGTEST": {
|
||||
"prefix": "bchreg",
|
||||
"version_bit": 16,
|
||||
"network": "regtest",
|
||||
},
|
||||
}
|
||||
|
||||
VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"}
|
||||
|
||||
ADDRESS_TYPES = {
|
||||
0: "P2PKH",
|
||||
8: "P2SH20",
|
||||
11: "P2SH32",
|
||||
16: "P2PKH-CATKN",
|
||||
24: "P2SH20-CATKN",
|
||||
27: "P2SH32-CATKN",
|
||||
}
|
||||
|
||||
def __init__(self, version, payload):
|
||||
if version not in Address.VERSIONS:
|
||||
raise ValueError("Invalid address version provided")
|
||||
|
||||
self.version = version
|
||||
self.payload = payload
|
||||
self.prefix = Address.VERSIONS[self.version]["prefix"]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Address('{self.cash_address()}')"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
return self.cash_address() == other
|
||||
elif isinstance(other, Address):
|
||||
return self.cash_address() == other.cash_address()
|
||||
else:
|
||||
raise ValueError(
|
||||
"Address can be compared to a string address"
|
||||
" or an instance of Address"
|
||||
)
|
||||
|
||||
def cash_address(self):
|
||||
"""
|
||||
Generate CashAddr of the Address
|
||||
|
||||
:rtype: ``str``
|
||||
"""
|
||||
version_bit = Address.VERSIONS[self.version]["version_bit"]
|
||||
payload = [version_bit] + list(self.payload)
|
||||
payload = convertbits(payload, 8, 5)
|
||||
checksum = calculate_checksum(self.prefix, payload)
|
||||
return self.prefix + ":" + b32encode(payload + checksum)
|
||||
|
||||
@staticmethod
|
||||
def from_string(address):
|
||||
"""
|
||||
Generate Address from a cashadress string
|
||||
|
||||
:param scriptcode: The cashaddress string
|
||||
:type scriptcode: ``str``
|
||||
:returns: Instance of :class:~bitcash.cashaddress.Address
|
||||
"""
|
||||
try:
|
||||
address = str(address)
|
||||
except Exception:
|
||||
raise ValueError("Expected string as input")
|
||||
|
||||
if address.upper() != address and address.lower() != address:
|
||||
raise ValueError(
|
||||
"Cash address contains uppercase and lowercase characters: " + address
|
||||
)
|
||||
|
||||
address = address.lower()
|
||||
colon_count = address.count(":")
|
||||
if colon_count == 0:
|
||||
raise ValueError("Cash address is missing prefix")
|
||||
if colon_count > 1:
|
||||
raise ValueError("Cash address contains more than one colon character")
|
||||
|
||||
prefix, base32string = address.split(":")
|
||||
decoded = b32decode(base32string)
|
||||
|
||||
if not verify_checksum(prefix, decoded):
|
||||
raise ValueError(
|
||||
"Bad cash address checksum for address {}".format(address)
|
||||
)
|
||||
converted = convertbits(decoded, 5, 8)
|
||||
|
||||
try:
|
||||
version = Address.ADDRESS_TYPES[converted[0]]
|
||||
except Exception:
|
||||
raise ValueError("Could not determine address version")
|
||||
|
||||
version += Address.VERSION_SUFFIXES[prefix]
|
||||
|
||||
payload = converted[1:-6]
|
||||
return Address(version, payload)
|
||||
|
||||
class TestFrameworkScript(unittest.TestCase):
|
||||
def test_base58encodedecode(self):
|
||||
def check_cashaddress(address: str):
|
||||
self.assertEqual(Address.from_string(address).cash_address(), address)
|
||||
|
||||
check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6")
|
||||
43
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
43
basicswap/interface/contrib/bch_test_framework/script.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
from basicswap.contrib.test_framework.script import CScriptOp
|
||||
|
||||
|
||||
OP_TXINPUTCOUNT = CScriptOp(0xc3)
|
||||
OP_1 = CScriptOp(0x51)
|
||||
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
|
||||
OP_TXOUTPUTCOUNT = CScriptOp(0xc4)
|
||||
OP_0 = CScriptOp(0x00)
|
||||
OP_UTXOVALUE = CScriptOp(0xc6)
|
||||
OP_OUTPUTVALUE = CScriptOp(0xcc)
|
||||
OP_SUB = CScriptOp(0x94)
|
||||
OP_UTXOTOKENCATEGORY = CScriptOp(0xce)
|
||||
OP_OUTPUTTOKENCATEGORY = CScriptOp(0xd1)
|
||||
OP_EQUALVERIFY = CScriptOp(0x88)
|
||||
OP_UTXOTOKENCOMMITMENT = CScriptOp(0xcf)
|
||||
OP_OUTPUTTOKENCOMMITMENT = CScriptOp(0xd2)
|
||||
OP_UTXOTOKENAMOUNT = CScriptOp(0xd0)
|
||||
OP_OUTPUTTOKENAMOUNT = CScriptOp(0xd3)
|
||||
OP_INPUTSEQUENCENUMBER = CScriptOp(0xcb)
|
||||
OP_NOTIF = CScriptOp(0x64)
|
||||
OP_OUTPUTBYTECODE = CScriptOp(0xcd)
|
||||
OP_OVER = CScriptOp(0x78)
|
||||
OP_CHECKDATASIG = CScriptOp(0xba)
|
||||
OP_CHECKDATASIGVERIFY = CScriptOp(0xbb)
|
||||
OP_ELSE = CScriptOp(0x67)
|
||||
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
|
||||
OP_DROP = CScriptOp(0x75)
|
||||
OP_EQUAL = CScriptOp(0x87)
|
||||
OP_ENDIF = CScriptOp(0x68)
|
||||
OP_HASH256 = CScriptOp(0xaa)
|
||||
OP_PUSHBYTES_32 = CScriptOp(0x20)
|
||||
OP_DUP = CScriptOp(0x76)
|
||||
OP_HASH160 = CScriptOp(0xa9)
|
||||
OP_CHECKSIG = CScriptOp(0xac)
|
||||
OP_SHA256 = CScriptOp(0xa8)
|
||||
OP_VERIFY = CScriptOp(0x69)
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -11,7 +12,10 @@ from basicswap.util.address import decodeAddress
|
||||
from basicswap.contrib.mnemonic import Mnemonic
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||
OP_DUP,
|
||||
OP_HASH160,
|
||||
OP_EQUALVERIFY,
|
||||
OP_CHECKSIG,
|
||||
)
|
||||
|
||||
|
||||
@@ -22,41 +26,62 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(coin_settings, network, swap_client)
|
||||
self._wallet_passphrase = ''
|
||||
self._wallet_passphrase = ""
|
||||
self._have_checked_seed = False
|
||||
|
||||
def entropyToMnemonic(self, key: bytes) -> str:
|
||||
return Mnemonic('english').to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key: bytes):
|
||||
words = self.entropyToMnemonic(key)
|
||||
|
||||
mnemonic_passphrase = ''
|
||||
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_passphrase != '':
|
||||
self.unlockWallet(self._wallet_passphrase)
|
||||
self._wallet_v20_compatible = (
|
||||
False
|
||||
if not swap_client
|
||||
else swap_client.getChainClientSettings(self.coin_type()).get(
|
||||
"wallet_v20_compatible", False
|
||||
)
|
||||
)
|
||||
|
||||
def decodeAddress(self, address: str) -> bytes:
|
||||
return decodeAddress(address)[1:]
|
||||
|
||||
def checkExpectedSeed(self, key_hash: str):
|
||||
try:
|
||||
rv = self.rpc_wallet('dumphdinfo')
|
||||
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
|
||||
def getWalletSeedID(self) -> str:
|
||||
hdseed: str = self.rpc_wallet("dumphdinfo")["hdseed"]
|
||||
return self.getSeedHash(bytes.fromhex(hdseed)).hex()
|
||||
|
||||
def entropyToMnemonic(self, key: bytes) -> None:
|
||||
return Mnemonic("english").to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_v20_compatible:
|
||||
self._log.warning("Generating wallet compatible with v20 seed.")
|
||||
words = self.entropyToMnemonic(key_bytes)
|
||||
mnemonic_passphrase = ""
|
||||
self.rpc_wallet(
|
||||
"upgradetohd", [words, mnemonic_passphrase, self._wallet_passphrase]
|
||||
)
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_passphrase != "":
|
||||
self.unlockWallet(self._wallet_passphrase)
|
||||
return
|
||||
|
||||
key_wif = self.encodeKey(key_bytes)
|
||||
self.rpc_wallet("sethdseed", [True, key_wif])
|
||||
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
self._expect_seedid_hex = expect_seedid
|
||||
rv = self.rpc_wallet("dumphdinfo")
|
||||
if rv["mnemonic"] != "":
|
||||
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
self._have_checked_seed = True
|
||||
return entropy_hash == key_hash
|
||||
except Exception as e:
|
||||
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
||||
return False
|
||||
have_expected_seed: bool = expect_seedid == entropy_hash
|
||||
else:
|
||||
have_expected_seed: bool = expect_seedid == self.getWalletSeedID()
|
||||
self._have_checked_seed = True
|
||||
return have_expected_seed
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
|
||||
return self.rpc_wallet('sendtoaddress', params)
|
||||
params = [addr_to, value, "", "", subfee, False, False, self._conf_target]
|
||||
return self.rpc_wallet("sendtoaddress", params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc_wallet("getwalletinfo")["balance"])
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||
# Return P2PKH
|
||||
@@ -66,29 +91,38 @@ class DASHInterface(BTCInterface):
|
||||
add_bytes = 107
|
||||
size = len(tx.serialize_with_witness()) + add_bytes
|
||||
pay_fee = round(fee_rate * size / 1000)
|
||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
||||
self._log.info(
|
||||
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||
)
|
||||
return pay_fee
|
||||
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# Only works for wallet txns
|
||||
try:
|
||||
rv = self.rpc_wallet('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
rv = self.rpc_wallet("gettransaction", [txid_hex])
|
||||
except Exception as e: # noqa: F841
|
||||
self._log.debug(
|
||||
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||
)
|
||||
return None
|
||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||
|
||||
return None
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
super().unlockWallet(password)
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if self._wallet_v20_compatible:
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if not self._have_checked_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
try:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
except Exception as ex:
|
||||
# dumphdinfo can fail if the wallet is not initialised
|
||||
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
|
||||
|
||||
def lockWallet(self):
|
||||
super().lockWallet()
|
||||
self._wallet_passphrase = ''
|
||||
self._wallet_passphrase = ""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
from .dcr import DCRInterface
|
||||
|
||||
__all__ = ['DCRInterface',]
|
||||
__all__ = [
|
||||
"DCRInterface",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ class SigHashType(IntEnum):
|
||||
SigHashSingle = 0x3
|
||||
SigHashAnyOneCanPay = 0x80
|
||||
|
||||
SigHashMask = 0x1f
|
||||
SigHashMask = 0x1F
|
||||
|
||||
|
||||
class SignatureType(IntEnum):
|
||||
@@ -33,7 +33,7 @@ class SignatureType(IntEnum):
|
||||
|
||||
|
||||
class COutPoint:
|
||||
__slots__ = ('hash', 'n', 'tree')
|
||||
__slots__ = ("hash", "n", "tree")
|
||||
|
||||
def __init__(self, hash=0, n=0, tree=0):
|
||||
self.hash = hash
|
||||
@@ -41,24 +41,30 @@ class COutPoint:
|
||||
self.tree = tree
|
||||
|
||||
def get_hash(self) -> bytes:
|
||||
return self.hash.to_bytes(32, 'big')
|
||||
return self.hash.to_bytes(32, "big")
|
||||
|
||||
|
||||
class CTxIn:
|
||||
__slots__ = ('prevout', 'sequence',
|
||||
'value_in', 'block_height', 'block_index', 'signature_script') # Witness
|
||||
__slots__ = (
|
||||
"prevout",
|
||||
"sequence",
|
||||
"value_in",
|
||||
"block_height",
|
||||
"block_index",
|
||||
"signature_script",
|
||||
) # Witness
|
||||
|
||||
def __init__(self, prevout=COutPoint(), sequence=0):
|
||||
self.prevout = prevout
|
||||
self.sequence = sequence
|
||||
self.value_in = -1
|
||||
self.block_height = 0
|
||||
self.block_index = 0xffffffff
|
||||
self.block_index = 0xFFFFFFFF
|
||||
self.signature_script = bytes()
|
||||
|
||||
|
||||
class CTxOut:
|
||||
__slots__ = ('value', 'version', 'script_pubkey')
|
||||
__slots__ = ("value", "version", "script_pubkey")
|
||||
|
||||
def __init__(self, value=0, script_pubkey=bytes()):
|
||||
self.value = value
|
||||
@@ -67,7 +73,7 @@ class CTxOut:
|
||||
|
||||
|
||||
class CTransaction:
|
||||
__slots__ = ('hash', 'version', 'vin', 'vout', 'locktime', 'expiry')
|
||||
__slots__ = ("hash", "version", "vin", "vout", "locktime", "expiry")
|
||||
|
||||
def __init__(self, tx=None):
|
||||
if tx is None:
|
||||
@@ -85,8 +91,8 @@ class CTransaction:
|
||||
|
||||
def deserialize(self, data: bytes) -> None:
|
||||
|
||||
version = int.from_bytes(data[:4], 'little')
|
||||
self.version = version & 0xffff
|
||||
version = int.from_bytes(data[:4], "little")
|
||||
self.version = version & 0xFFFF
|
||||
ser_type: int = version >> 16
|
||||
o = 4
|
||||
|
||||
@@ -97,13 +103,13 @@ class CTransaction:
|
||||
for i in range(num_txin):
|
||||
txi = CTxIn()
|
||||
txi.prevout = COutPoint()
|
||||
txi.prevout.hash = int.from_bytes(data[o:o + 32], 'little')
|
||||
txi.prevout.hash = int.from_bytes(data[o : o + 32], "little")
|
||||
o += 32
|
||||
txi.prevout.n = int.from_bytes(data[o:o + 4], 'little')
|
||||
txi.prevout.n = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
txi.prevout.tree = data[o]
|
||||
o += 1
|
||||
txi.sequence = int.from_bytes(data[o:o + 4], 'little')
|
||||
txi.sequence = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
self.vin.append(txi)
|
||||
|
||||
@@ -112,19 +118,19 @@ class CTransaction:
|
||||
|
||||
for i in range(num_txout):
|
||||
txo = CTxOut()
|
||||
txo.value = int.from_bytes(data[o:o + 8], 'little')
|
||||
txo.value = int.from_bytes(data[o : o + 8], "little")
|
||||
o += 8
|
||||
txo.version = int.from_bytes(data[o:o + 2], 'little')
|
||||
txo.version = int.from_bytes(data[o : o + 2], "little")
|
||||
o += 2
|
||||
script_bytes, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
txo.script_pubkey = data[o:o + script_bytes]
|
||||
txo.script_pubkey = data[o : o + script_bytes]
|
||||
o += script_bytes
|
||||
self.vout.append(txo)
|
||||
|
||||
self.locktime = int.from_bytes(data[o:o + 4], 'little')
|
||||
self.locktime = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
self.expiry = int.from_bytes(data[o:o + 4], 'little')
|
||||
self.expiry = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
|
||||
if ser_type == TxSerializeType.NoWitness:
|
||||
@@ -137,51 +143,53 @@ class CTransaction:
|
||||
self.vin = [CTxIn() for _ in range(num_wit_scripts)]
|
||||
else:
|
||||
if num_wit_scripts != len(self.vin):
|
||||
raise ValueError('non equal witness and prefix txin quantities')
|
||||
raise ValueError("non equal witness and prefix txin quantities")
|
||||
|
||||
for i in range(num_wit_scripts):
|
||||
txi = self.vin[i]
|
||||
txi.value_in = int.from_bytes(data[o:o + 8], 'little')
|
||||
txi.value_in = int.from_bytes(data[o : o + 8], "little")
|
||||
o += 8
|
||||
txi.block_height = int.from_bytes(data[o:o + 4], 'little')
|
||||
txi.block_height = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
txi.block_index = int.from_bytes(data[o:o + 4], 'little')
|
||||
txi.block_index = int.from_bytes(data[o : o + 4], "little")
|
||||
o += 4
|
||||
script_bytes, nb = decode_compactsize(data, o)
|
||||
o += nb
|
||||
txi.signature_script = data[o:o + script_bytes]
|
||||
txi.signature_script = data[o : o + script_bytes]
|
||||
o += script_bytes
|
||||
|
||||
def serialize(self, ser_type=TxSerializeType.Full) -> bytes:
|
||||
data = bytes()
|
||||
version = (self.version & 0xffff) | (ser_type << 16)
|
||||
data += version.to_bytes(4, 'little')
|
||||
version = (self.version & 0xFFFF) | (ser_type << 16)
|
||||
data += version.to_bytes(4, "little")
|
||||
|
||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.NoWitness:
|
||||
data += encode_compactsize(len(self.vin))
|
||||
for txi in self.vin:
|
||||
data += txi.prevout.hash.to_bytes(32, 'little')
|
||||
data += txi.prevout.n.to_bytes(4, 'little')
|
||||
data += txi.prevout.tree.to_bytes(1, 'little')
|
||||
data += txi.sequence.to_bytes(4, 'little')
|
||||
data += txi.prevout.hash.to_bytes(32, "little")
|
||||
data += txi.prevout.n.to_bytes(4, "little")
|
||||
data += txi.prevout.tree.to_bytes(1, "little")
|
||||
data += txi.sequence.to_bytes(4, "little")
|
||||
|
||||
data += encode_compactsize(len(self.vout))
|
||||
for txo in self.vout:
|
||||
data += txo.value.to_bytes(8, 'little')
|
||||
data += txo.version.to_bytes(2, 'little')
|
||||
data += txo.value.to_bytes(8, "little")
|
||||
data += txo.version.to_bytes(2, "little")
|
||||
data += encode_compactsize(len(txo.script_pubkey))
|
||||
data += txo.script_pubkey
|
||||
|
||||
data += self.locktime.to_bytes(4, 'little')
|
||||
data += self.expiry.to_bytes(4, 'little')
|
||||
data += self.locktime.to_bytes(4, "little")
|
||||
data += self.expiry.to_bytes(4, "little")
|
||||
|
||||
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
|
||||
data += encode_compactsize(len(self.vin))
|
||||
for txi in self.vin:
|
||||
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
|
||||
data += tc_value_in.to_bytes(8, 'little')
|
||||
data += txi.block_height.to_bytes(4, 'little')
|
||||
data += txi.block_index.to_bytes(4, 'little')
|
||||
tc_value_in = (
|
||||
txi.value_in & 0xFFFFFFFFFFFFFFFF
|
||||
) # Convert negative values
|
||||
data += tc_value_in.to_bytes(8, "little")
|
||||
data += txi.block_height.to_bytes(4, "little")
|
||||
data += txi.block_index.to_bytes(4, "little")
|
||||
data += encode_compactsize(len(txi.signature_script))
|
||||
data += txi.signature_script
|
||||
|
||||
@@ -191,10 +199,10 @@ class CTransaction:
|
||||
return blake256(self.serialize(TxSerializeType.NoWitness))[::-1]
|
||||
|
||||
def TxHashWitness(self) -> bytes:
|
||||
raise ValueError('todo')
|
||||
raise ValueError("todo")
|
||||
|
||||
def TxHashFull(self) -> bytes:
|
||||
raise ValueError('todo')
|
||||
raise ValueError("todo")
|
||||
|
||||
|
||||
def findOutput(tx, script_pk: bytes):
|
||||
|
||||
@@ -9,34 +9,34 @@ import traceback
|
||||
from basicswap.rpc import Jsonrpc
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], host='127.0.0.1'):
|
||||
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
x = Jsonrpc(url)
|
||||
x.__handler = None
|
||||
v = x.json_request(method, params)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
|
||||
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||
|
||||
if 'error' in r and r['error'] is not None:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
|
||||
return r['result']
|
||||
return r["result"]
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, host='127.0.0.1'):
|
||||
def openrpc(rpc_port, auth, host="127.0.0.1"):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
return Jsonrpc(url)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC error ' + str(ex))
|
||||
raise ValueError("RPC error " + str(ex))
|
||||
|
||||
|
||||
def make_rpc_func(port, auth, host='127.0.0.1'):
|
||||
def make_rpc_func(port, auth, host="127.0.0.1"):
|
||||
port = port
|
||||
auth = auth
|
||||
host = host
|
||||
@@ -44,4 +44,5 @@ def make_rpc_func(port, auth, host='127.0.0.1'):
|
||||
def rpc_func(method, params=None):
|
||||
nonlocal port, auth, host
|
||||
return callrpc(port, auth, method, params, host)
|
||||
|
||||
return rpc_func
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
OP_0 = 0x00
|
||||
OP_DATA_1 = 0x01
|
||||
OP_1NEGATE = 0x4f
|
||||
OP_1NEGATE = 0x4F
|
||||
OP_1 = 0x51
|
||||
OP_IF = 0x63
|
||||
OP_ELSE = 0x67
|
||||
@@ -16,13 +16,13 @@ OP_DROP = 0x75
|
||||
OP_DUP = 0x76
|
||||
OP_EQUAL = 0x87
|
||||
OP_EQUALVERIFY = 0x88
|
||||
OP_PUSHDATA1 = 0x4c
|
||||
OP_PUSHDATA2 = 0x4d
|
||||
OP_PUSHDATA4 = 0x4e
|
||||
OP_HASH160 = 0xa9
|
||||
OP_CHECKSIG = 0xac
|
||||
OP_CHECKMULTISIG = 0xae
|
||||
OP_CHECKSEQUENCEVERIFY = 0xb2
|
||||
OP_PUSHDATA1 = 0x4C
|
||||
OP_PUSHDATA2 = 0x4D
|
||||
OP_PUSHDATA4 = 0x4E
|
||||
OP_HASH160 = 0xA9
|
||||
OP_CHECKSIG = 0xAC
|
||||
OP_CHECKMULTISIG = 0xAE
|
||||
OP_CHECKSEQUENCEVERIFY = 0xB2
|
||||
|
||||
|
||||
def push_script_data(data_array: bytearray, data: bytes) -> None:
|
||||
@@ -39,12 +39,12 @@ def push_script_data(data_array: bytearray, data: bytes) -> None:
|
||||
return
|
||||
|
||||
if len_data < OP_PUSHDATA1:
|
||||
data_array += len_data.to_bytes(1, 'little')
|
||||
elif len_data <= 0xff:
|
||||
data_array += len_data.to_bytes(1, "little")
|
||||
elif len_data <= 0xFF:
|
||||
data_array += bytes((OP_PUSHDATA1, len_data))
|
||||
elif len_data <= 0xffff:
|
||||
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
|
||||
elif len_data <= 0xFFFF:
|
||||
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, "little")
|
||||
else:
|
||||
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
|
||||
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, "little")
|
||||
|
||||
data_array += data
|
||||
|
||||
@@ -10,46 +10,48 @@ import subprocess
|
||||
|
||||
|
||||
def createDCRWallet(args, hex_seed, logging, delay_event):
|
||||
logging.info('Creating DCR wallet')
|
||||
logging.info("Creating DCR wallet")
|
||||
|
||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||
|
||||
if os.name == 'nt':
|
||||
str_args = ' '.join(args)
|
||||
p = subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
if os.name == "nt":
|
||||
str_args = " ".join(args)
|
||||
p = subprocess.Popen(
|
||||
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
|
||||
)
|
||||
else:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
|
||||
def readOutput():
|
||||
buf = os.read(pipe_r, 1024).decode('utf-8')
|
||||
buf = os.read(pipe_r, 1024).decode("utf-8")
|
||||
response = None
|
||||
if 'Opened wallet' in buf:
|
||||
if "Opened wallet" in buf:
|
||||
pass
|
||||
elif 'Use the existing configured private passphrase' in buf:
|
||||
response = b'y\n'
|
||||
elif 'Do you want to add an additional layer of encryption' in buf:
|
||||
response = b'n\n'
|
||||
elif 'Do you have an existing wallet seed' in buf:
|
||||
response = b'y\n'
|
||||
elif 'Enter existing wallet seed' in buf:
|
||||
response = (hex_seed + '\n').encode('utf-8')
|
||||
elif 'Seed input successful' in buf:
|
||||
elif "Use the existing configured private passphrase" in buf:
|
||||
response = b"y\n"
|
||||
elif "Do you want to add an additional layer of encryption" in buf:
|
||||
response = b"n\n"
|
||||
elif "Do you have an existing wallet seed" in buf:
|
||||
response = b"y\n"
|
||||
elif "Enter existing wallet seed" in buf:
|
||||
response = (hex_seed + "\n").encode("utf-8")
|
||||
elif "Seed input successful" in buf:
|
||||
pass
|
||||
elif 'Upgrading database from version' in buf:
|
||||
elif "Upgrading database from version" in buf:
|
||||
pass
|
||||
elif 'Ticket commitments db upgrade done' in buf:
|
||||
elif "Ticket commitments db upgrade done" in buf:
|
||||
pass
|
||||
elif 'The wallet has been created successfully' in buf:
|
||||
elif "The wallet has been created successfully" in buf:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f'Unexpected output: {buf}')
|
||||
raise ValueError(f"Unexpected output: {buf}")
|
||||
if response is not None:
|
||||
p.stdin.write(response)
|
||||
p.stdin.flush()
|
||||
|
||||
try:
|
||||
while p.poll() is None:
|
||||
if os.name == 'nt':
|
||||
if os.name == "nt":
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
continue
|
||||
@@ -57,7 +59,7 @@ def createDCRWallet(args, hex_seed, logging, delay_event):
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
except Exception as e:
|
||||
logging.error(f'dcrwallet --create failed: {e}')
|
||||
logging.error(f"dcrwallet --create failed: {e}")
|
||||
finally:
|
||||
if p.poll() is None:
|
||||
p.terminate()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -40,10 +41,12 @@ class FIROInterface(BTCInterface):
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
|
||||
# No multiwallet support
|
||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
|
||||
def getExchangeName(self, exchange_name):
|
||||
return 'zcoin'
|
||||
def getExchangeName(self, exchange_name: str) -> str:
|
||||
return "zcoin"
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
# load with -hdseed= parameter
|
||||
@@ -52,8 +55,8 @@ class FIROInterface(BTCInterface):
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
||||
return self.rpc('getnewaddress', [label])
|
||||
def getNewAddress(self, use_segwit, label="swap_receive"):
|
||||
return self.rpc("getnewaddress", [label])
|
||||
# addr_plain = self.rpc('getnewaddress', [label])
|
||||
# return self.rpc('addwitnessaddress', [addr_plain])
|
||||
|
||||
@@ -61,20 +64,20 @@ class FIROInterface(BTCInterface):
|
||||
return decodeAddress(address)[1:]
|
||||
|
||||
def encodeSegwitAddress(self, script):
|
||||
raise ValueError('TODO')
|
||||
raise ValueError("TODO")
|
||||
|
||||
def decodeSegwitAddress(self, addr):
|
||||
raise ValueError('TODO')
|
||||
raise ValueError("TODO")
|
||||
|
||||
def isWatchOnlyAddress(self, address):
|
||||
addr_info = self.rpc('validateaddress', [address])
|
||||
return addr_info['iswatchonly']
|
||||
addr_info = self.rpc("validateaddress", [address])
|
||||
return addr_info["iswatchonly"]
|
||||
|
||||
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
||||
addr_info = self.rpc('validateaddress', [address])
|
||||
addr_info = self.rpc("validateaddress", [address])
|
||||
if not or_watch_only:
|
||||
return addr_info['ismine']
|
||||
return addr_info['ismine'] or addr_info['iswatchonly']
|
||||
return addr_info["ismine"]
|
||||
return addr_info["ismine"] or addr_info["iswatchonly"]
|
||||
|
||||
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||
lock_tx_dest = self.getScriptDest(lock_script)
|
||||
@@ -82,58 +85,83 @@ class FIROInterface(BTCInterface):
|
||||
|
||||
if not self.isAddressMine(address, or_watch_only=True):
|
||||
# Expects P2WSH nested in BIP16_P2SH
|
||||
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
|
||||
addr_info = self.rpc('validateaddress', [address])
|
||||
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True])
|
||||
|
||||
return address
|
||||
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
||||
def getLockTxHeight(
|
||||
self,
|
||||
txid,
|
||||
dest_address,
|
||||
bid_amount,
|
||||
rescan_from,
|
||||
find_index: bool = False,
|
||||
vout: int = -1,
|
||||
):
|
||||
# Add watchonly address and rescan if required
|
||||
|
||||
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||
self.importWatchOnlyAddress(dest_address, 'bid')
|
||||
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
||||
self.importWatchOnlyAddress(dest_address, "bid")
|
||||
self._log.info("Imported watch-only addr: {}".format(dest_address))
|
||||
self._log.info(
|
||||
"Rescanning {} chain from height: {}".format(
|
||||
self.coin_name(), rescan_from
|
||||
)
|
||||
)
|
||||
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||
|
||||
return_txid = True if txid is None else False
|
||||
if txid is None:
|
||||
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||
txns = self.rpc(
|
||||
"listunspent",
|
||||
[
|
||||
0,
|
||||
9999999,
|
||||
[
|
||||
dest_address,
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
for tx in txns:
|
||||
if self.make_int(tx['amount']) == bid_amount:
|
||||
txid = bytes.fromhex(tx['txid'])
|
||||
if self.make_int(tx["amount"]) == bid_amount:
|
||||
txid = bytes.fromhex(tx["txid"])
|
||||
break
|
||||
|
||||
if txid is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
tx = self.rpc('gettransaction', [txid.hex()])
|
||||
tx = self.rpc("gettransaction", [txid.hex()])
|
||||
|
||||
block_height = 0
|
||||
if 'blockhash' in tx:
|
||||
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||
block_height = block_header['height']
|
||||
if "blockhash" in tx:
|
||||
block_header = self.rpc("getblockheader", [tx["blockhash"]])
|
||||
block_height = block_header["height"]
|
||||
|
||||
rv = {
|
||||
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
||||
'height': block_height}
|
||||
"depth": 0 if "confirmations" not in tx else tx["confirmations"],
|
||||
"height": block_height,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
||||
self._log.debug(
|
||||
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
if find_index:
|
||||
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
|
||||
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
|
||||
if return_txid:
|
||||
rv['txid'] = txid.hex()
|
||||
rv["txid"] = txid.hex()
|
||||
|
||||
return rv
|
||||
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||
def createSCLockTx(
|
||||
self, value: int, script: bytearray, vkbv: bytes = None
|
||||
) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
@@ -144,24 +172,36 @@ class FIROInterface(BTCInterface):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
|
||||
def signTxWithWallet(self, tx):
|
||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||
return bytes.fromhex(rv['hex'])
|
||||
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||
return bytes.fromhex(rv["hex"])
|
||||
|
||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
||||
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
def createRawFundedTransaction(
|
||||
self,
|
||||
addr_to: str,
|
||||
amount: int,
|
||||
sub_fee: bool = False,
|
||||
lock_unspents: bool = True,
|
||||
) -> str:
|
||||
txn = self.rpc(
|
||||
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
|
||||
)
|
||||
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
||||
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||
self._log.debug(
|
||||
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
|
||||
)
|
||||
options = {
|
||||
'lockUnspents': lock_unspents,
|
||||
'feeRate': fee_rate,
|
||||
"lockUnspents": lock_unspents,
|
||||
"feeRate": fee_rate,
|
||||
}
|
||||
if sub_fee:
|
||||
options['subtractFeeFromOutputs'] = [0,]
|
||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||
options["subtractFeeFromOutputs"] = [
|
||||
0,
|
||||
]
|
||||
return self.rpc("fundrawtransaction", [txn, options])["hex"]
|
||||
|
||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||
return self.rpc("signrawtransaction", [txn_funded])["hex"]
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
|
||||
# Return P2PKH
|
||||
@@ -188,60 +228,75 @@ class FIROInterface(BTCInterface):
|
||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee]
|
||||
return self.rpc('sendtoaddress', params)
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc("sendtoaddress", params)
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc("getwalletinfo")["balance"])
|
||||
|
||||
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||
add_bytes = 107
|
||||
size = len(tx.serialize_with_witness()) + add_bytes
|
||||
pay_fee = round(fee_rate * size / 1000)
|
||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
||||
self._log.info(
|
||||
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||
)
|
||||
return pay_fee
|
||||
|
||||
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||
key_wif = self.encodeKey(key)
|
||||
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||
return bytes.fromhex(rv['hex'])
|
||||
rv = self.rpc(
|
||||
"signrawtransaction",
|
||||
[
|
||||
tx.hex(),
|
||||
[],
|
||||
[
|
||||
key_wif,
|
||||
],
|
||||
],
|
||||
)
|
||||
return bytes.fromhex(rv["hex"])
|
||||
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# Only works for wallet txns
|
||||
try:
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
rv = self.rpc("gettransaction", [txid_hex])
|
||||
except Exception as e: # noqa: F841
|
||||
self._log.debug(
|
||||
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||
)
|
||||
return None
|
||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||
return None
|
||||
|
||||
def getProofOfFunds(self, amount_for, extra_commit_bytes):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
|
||||
unspents_by_addr = dict()
|
||||
unspents = self.rpc('listunspent')
|
||||
unspents = self.rpc("listunspent")
|
||||
for u in unspents:
|
||||
if u['spendable'] is not True:
|
||||
if u["spendable"] is not True:
|
||||
continue
|
||||
if u['address'] not in unspents_by_addr:
|
||||
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
|
||||
utxo_amount: int = self.make_int(u['amount'], r=1)
|
||||
unspents_by_addr[u['address']]['total'] += utxo_amount
|
||||
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
|
||||
if u["address"] not in unspents_by_addr:
|
||||
unspents_by_addr[u["address"]] = {"total": 0, "utxos": []}
|
||||
utxo_amount: int = self.make_int(u["amount"], r=1)
|
||||
unspents_by_addr[u["address"]]["total"] += utxo_amount
|
||||
unspents_by_addr[u["address"]]["utxos"].append(
|
||||
(utxo_amount, u["txid"], u["vout"])
|
||||
)
|
||||
|
||||
max_utxos: int = 4
|
||||
|
||||
viable_addrs = []
|
||||
for addr, data in unspents_by_addr.items():
|
||||
if data['total'] >= amount_for:
|
||||
if data["total"] >= amount_for:
|
||||
# Sort from largest to smallest amount
|
||||
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
|
||||
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
|
||||
|
||||
# Max outputs required to reach amount_for
|
||||
utxos_req: int = 0
|
||||
@@ -256,13 +311,17 @@ class FIROInterface(BTCInterface):
|
||||
viable_addrs.append(addr)
|
||||
continue
|
||||
|
||||
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
|
||||
ensure(
|
||||
len(viable_addrs) > 0, "Could not find address with enough funds for proof"
|
||||
)
|
||||
|
||||
sign_for_addr: str = random.choice(viable_addrs)
|
||||
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||
self._log.debug("sign_for_addr %s", sign_for_addr)
|
||||
|
||||
prove_utxos = []
|
||||
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
|
||||
sorted_utxos = sorted(
|
||||
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
|
||||
)
|
||||
|
||||
hasher = hashlib.sha256()
|
||||
|
||||
@@ -272,18 +331,29 @@ class FIROInterface(BTCInterface):
|
||||
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
||||
prove_utxos.append(outpoint)
|
||||
hasher.update(outpoint[0])
|
||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||
if sum_value >= amount_for:
|
||||
break
|
||||
utxos_hash = hasher.digest()
|
||||
|
||||
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
if (
|
||||
self.using_segwit()
|
||||
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
# 'Address does not refer to key' for non p2pkh
|
||||
pkh = self.decodeAddress(sign_for_addr)
|
||||
sign_for_addr = self.pkh_to_address(pkh)
|
||||
self._log.debug('sign_for_addr converted %s', sign_for_addr)
|
||||
self._log.debug("sign_for_addr converted %s", sign_for_addr)
|
||||
|
||||
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
|
||||
signature = self.rpc(
|
||||
"signmessage",
|
||||
[
|
||||
sign_for_addr,
|
||||
sign_for_addr
|
||||
+ "_swap_proof_"
|
||||
+ utxos_hash.hex()
|
||||
+ extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
|
||||
return (sign_for_addr, signature, prove_utxos)
|
||||
|
||||
@@ -292,19 +362,23 @@ class FIROInterface(BTCInterface):
|
||||
sum_value: int = 0
|
||||
for outpoint in utxos:
|
||||
hasher.update(outpoint[0])
|
||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||
utxos_hash = hasher.digest()
|
||||
|
||||
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
|
||||
ensure(passed is True, 'Proof of funds signature invalid')
|
||||
passed = self.verifyMessage(
|
||||
address,
|
||||
address + "_swap_proof_" + utxos_hash.hex() + extra_commit_bytes.hex(),
|
||||
signature,
|
||||
)
|
||||
ensure(passed is True, "Proof of funds signature invalid")
|
||||
|
||||
if self.using_segwit():
|
||||
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
|
||||
|
||||
sum_value: int = 0
|
||||
for outpoint in utxos:
|
||||
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
|
||||
sum_value += self.make_int(txout['value'])
|
||||
txout = self.rpc("gettxout", [outpoint[0].hex(), outpoint[1]])
|
||||
sum_value += self.make_int(txout["value"])
|
||||
|
||||
return sum_value
|
||||
|
||||
@@ -314,15 +388,15 @@ class FIROInterface(BTCInterface):
|
||||
chain_blocks: int = self.getChainHeight()
|
||||
|
||||
current_height: int = chain_blocks
|
||||
block_hash = self.rpc('getblockhash', [current_height])
|
||||
block_hash = self.rpc("getblockhash", [current_height])
|
||||
|
||||
script_hash: bytes = self.decodeAddress(addr_find)
|
||||
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
||||
|
||||
while current_height > height_start:
|
||||
block_hash = self.rpc('getblockhash', [current_height])
|
||||
block_hash = self.rpc("getblockhash", [current_height])
|
||||
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
block = self.rpc("getblock", [block_hash, False])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
for tx in decoded_block.vtx:
|
||||
@@ -330,38 +404,46 @@ class FIROInterface(BTCInterface):
|
||||
if txo.scriptPubKey == find_scriptPubKey:
|
||||
tx.rehash()
|
||||
txid = i2b(tx.sha256)
|
||||
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
|
||||
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
|
||||
self.rpc('invalidateblock', [block_hash])
|
||||
self.rpc('reconsiderblock', [block_hash])
|
||||
self._log.info(
|
||||
"Found output to addr: {} in tx {} in block {}".format(
|
||||
addr_find, txid.hex(), block_hash
|
||||
)
|
||||
)
|
||||
self._log.info(
|
||||
"rescanblockchain hack invalidateblock {}".format(
|
||||
block_hash
|
||||
)
|
||||
)
|
||||
self.rpc("invalidateblock", [block_hash])
|
||||
self.rpc("reconsiderblock", [block_hash])
|
||||
return
|
||||
current_height -= 1
|
||||
|
||||
def getBlockWithTxns(self, block_hash: str):
|
||||
# TODO: Bypass decoderawtransaction and getblockheader
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
block_header = self.rpc('getblockheader', [block_hash])
|
||||
block = self.rpc("getblock", [block_hash, False])
|
||||
block_header = self.rpc("getblockheader", [block_hash])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
|
||||
tx_rv = []
|
||||
for tx in decoded_block.vtx:
|
||||
tx_hex = tx.serialize_with_witness().hex()
|
||||
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
||||
if 'hex' not in tx_dec:
|
||||
tx_dec['hex'] = tx_hex
|
||||
tx_dec = self.rpc("decoderawtransaction", [tx_hex])
|
||||
if "hex" not in tx_dec:
|
||||
tx_dec["hex"] = tx_hex
|
||||
|
||||
tx_rv.append(tx_dec)
|
||||
|
||||
block_rv = {
|
||||
'hash': block_hash,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
"hash": block_hash,
|
||||
"previousblockhash": block_header["previousblockhash"],
|
||||
"tx": tx_rv,
|
||||
"confirmations": block_header["confirmations"],
|
||||
"height": block_header["height"],
|
||||
"time": block_header["time"],
|
||||
"version": block_header["version"],
|
||||
"merkleroot": block_header["merkleroot"],
|
||||
}
|
||||
|
||||
return block_rv
|
||||
|
||||
@@ -17,63 +17,73 @@ class LTCInterface(BTCInterface):
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
|
||||
self._rpc_wallet_mweb = 'mweb'
|
||||
self.rpc_wallet_mweb = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet_mweb)
|
||||
self._rpc_wallet_mweb = "mweb"
|
||||
self.rpc_wallet_mweb = make_rpc_func(
|
||||
self._rpcport,
|
||||
self._rpcauth,
|
||||
host=self._rpc_host,
|
||||
wallet=self._rpc_wallet_mweb,
|
||||
)
|
||||
|
||||
def getNewMwebAddress(self, use_segwit=False, label='swap_receive') -> str:
|
||||
return self.rpc_wallet_mweb('getnewaddress', [label, 'mweb'])
|
||||
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||
return self.rpc_wallet_mweb("getnewaddress", [label, "mweb"])
|
||||
|
||||
def getNewStealthAddress(self, label=''):
|
||||
def getNewStealthAddress(self, label=""):
|
||||
return self.getNewMwebAddress(False, label)
|
||||
|
||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||
params = [addr_to, value, '', '', subfee, True, self._conf_target]
|
||||
if type_from == 'mweb':
|
||||
return self.rpc_wallet_mweb('sendtoaddress', params)
|
||||
return self.rpc_wallet('sendtoaddress', params)
|
||||
params = [addr_to, value, "", "", subfee, True, self._conf_target]
|
||||
if type_from == "mweb":
|
||||
return self.rpc_wallet_mweb("sendtoaddress", params)
|
||||
return self.rpc_wallet("sendtoaddress", params)
|
||||
|
||||
def createUTXO(self, value_sats: int):
|
||||
# Create a new address and send value_sats to it
|
||||
|
||||
spendable_balance = self.getSpendableBalance()
|
||||
if spendable_balance < value_sats:
|
||||
raise ValueError('Balance too low')
|
||||
raise ValueError("Balance too low")
|
||||
|
||||
address = self.getNewAddress(self._use_segwit, 'create_utxo')
|
||||
return self.withdrawCoin(self.format_amount(value_sats), 'plain', address, False), address
|
||||
address = self.getNewAddress(self._use_segwit, "create_utxo")
|
||||
return (
|
||||
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
|
||||
address,
|
||||
)
|
||||
|
||||
def getWalletInfo(self):
|
||||
rv = super(LTCInterface, self).getWalletInfo()
|
||||
|
||||
mweb_info = self.rpc_wallet_mweb('getwalletinfo')
|
||||
rv['mweb_balance'] = mweb_info['balance']
|
||||
rv['mweb_unconfirmed'] = mweb_info['unconfirmed_balance']
|
||||
rv['mweb_immature'] = mweb_info['immature_balance']
|
||||
mweb_info = self.rpc_wallet_mweb("getwalletinfo")
|
||||
rv["mweb_balance"] = mweb_info["balance"]
|
||||
rv["mweb_unconfirmed"] = mweb_info["unconfirmed_balance"]
|
||||
rv["mweb_immature"] = mweb_info["immature_balance"]
|
||||
return rv
|
||||
|
||||
def getUnspentsByAddr(self):
|
||||
unspent_addr = dict()
|
||||
unspent = self.rpc_wallet('listunspent')
|
||||
unspent = self.rpc_wallet("listunspent")
|
||||
for u in unspent:
|
||||
if u.get('spendable', False) is False:
|
||||
if u.get("spendable", False) is False:
|
||||
continue
|
||||
if u.get('solvable', False) is False: # Filter out mweb outputs
|
||||
if u.get("solvable", False) is False: # Filter out mweb outputs
|
||||
continue
|
||||
if 'address' not in u:
|
||||
if "address" not in u:
|
||||
continue
|
||||
if 'desc' in u:
|
||||
desc = u['desc']
|
||||
if "desc" in u:
|
||||
desc = u["desc"]
|
||||
if self.using_segwit:
|
||||
if self.use_p2shp2wsh():
|
||||
if not desc.startswith('sh(wpkh'):
|
||||
if not desc.startswith("sh(wpkh"):
|
||||
continue
|
||||
else:
|
||||
if not desc.startswith('wpkh'):
|
||||
if not desc.startswith("wpkh"):
|
||||
continue
|
||||
else:
|
||||
if not desc.startswith('pkh'):
|
||||
if not desc.startswith("pkh"):
|
||||
continue
|
||||
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
|
||||
unspent_addr[u["address"]] = unspent_addr.get(
|
||||
u["address"], 0
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
|
||||
@@ -84,8 +94,10 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
|
||||
self._rpc_wallet = 'mweb'
|
||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
|
||||
self._rpc_wallet = "mweb"
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
|
||||
)
|
||||
|
||||
def chainparams(self):
|
||||
return chainparams[Coins.LTC]
|
||||
@@ -95,56 +107,54 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
def coin_name(self) -> str:
|
||||
coin_chainparams = chainparams[Coins.LTC]
|
||||
if coin_chainparams.get('use_ticker_as_name', False):
|
||||
return coin_chainparams['ticker'] + ' MWEB'
|
||||
return coin_chainparams['name'].capitalize() + ' MWEB'
|
||||
return coin_chainparams["name"].capitalize() + " MWEB"
|
||||
|
||||
def ticker(self) -> str:
|
||||
ticker = chainparams[Coins.LTC]['ticker']
|
||||
if self._network == 'testnet':
|
||||
ticker = 't' + ticker
|
||||
elif self._network == 'regtest':
|
||||
ticker = 'rt' + ticker
|
||||
return ticker + '_MWEB'
|
||||
ticker = chainparams[Coins.LTC]["ticker"]
|
||||
if self._network == "testnet":
|
||||
ticker = "t" + ticker
|
||||
elif self._network == "regtest":
|
||||
ticker = "rt" + ticker
|
||||
return ticker + "_MWEB"
|
||||
|
||||
def getNewAddress(self, use_segwit=False, label='swap_receive') -> str:
|
||||
def getNewAddress(self, use_segwit=False, label="swap_receive") -> str:
|
||||
return self.getNewMwebAddress()
|
||||
|
||||
def has_mweb_wallet(self) -> bool:
|
||||
return 'mweb' in self.rpc('listwallets')
|
||||
return "mweb" in self.rpc("listwallets")
|
||||
|
||||
def init_wallet(self, password=None):
|
||||
# If system is encrypted mweb wallet will be created at first unlock
|
||||
|
||||
self._log.info('init_wallet - {}'.format(self.ticker()))
|
||||
self._log.info("init_wallet - {}".format(self.ticker()))
|
||||
|
||||
self._log.info('Creating mweb wallet for {}.'.format(self.coin_name()))
|
||||
self._log.info("Creating mweb wallet for {}.".format(self.coin_name()))
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||
self.rpc('createwallet', ['mweb', False, True, password, False, False, True])
|
||||
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
|
||||
|
||||
if password is not None:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
|
||||
if self.getWalletSeedID() == 'Not found':
|
||||
if self.getWalletSeedID() == "Not found":
|
||||
self._sc.initialiseWallet(self.coin_type())
|
||||
|
||||
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
|
||||
self.rpc('unloadwallet', ['mweb'])
|
||||
self.rpc('loadwallet', ['mweb'])
|
||||
self.rpc("unloadwallet", ["mweb"])
|
||||
self.rpc("loadwallet", ["mweb"])
|
||||
if password is not None:
|
||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||
self.rpc_wallet('keypoolrefill')
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
if password == '':
|
||||
if password == "":
|
||||
return
|
||||
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
if not self.has_mweb_wallet():
|
||||
self.init_wallet(password)
|
||||
else:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet('walletpassphrase', [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -38,7 +39,9 @@ from basicswap.util.address import (
|
||||
encodeAddress,
|
||||
)
|
||||
from basicswap.util import (
|
||||
b2i, i2b, i2h,
|
||||
b2i,
|
||||
i2b,
|
||||
i2h,
|
||||
ensure,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
@@ -49,7 +52,10 @@ from basicswap.interface.contrib.nav_test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_EQUAL,
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG,
|
||||
OP_DUP,
|
||||
OP_HASH160,
|
||||
OP_EQUALVERIFY,
|
||||
OP_CHECKSIG,
|
||||
SIGHASH_ALL,
|
||||
SegwitVersion1SignatureHash,
|
||||
)
|
||||
@@ -71,15 +77,14 @@ class NAVInterface(BTCInterface):
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
|
||||
# No multiwallet support
|
||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
|
||||
def use_p2shp2wsh(self) -> bool:
|
||||
# p2sh-p2wsh
|
||||
return True
|
||||
|
||||
def entropyToMnemonic(self, key: bytes) -> None:
|
||||
return Mnemonic('english').to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
# Load with -importmnemonic= parameter
|
||||
pass
|
||||
@@ -88,31 +93,31 @@ class NAVInterface(BTCInterface):
|
||||
return 1
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc('getwalletinfo')['hdmasterkeyid']
|
||||
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||
|
||||
def withdrawCoin(self, value, addr_to: str, subfee: bool):
|
||||
strdzeel = ''
|
||||
params = [addr_to, value, '', '', strdzeel, subfee]
|
||||
return self.rpc('sendtoaddress', params)
|
||||
strdzeel = ""
|
||||
params = [addr_to, value, "", "", strdzeel, subfee]
|
||||
return self.rpc("sendtoaddress", params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc("getwalletinfo")["balance"])
|
||||
|
||||
def signTxWithWallet(self, tx: bytes) -> bytes:
|
||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||
|
||||
return bytes.fromhex(rv['hex'])
|
||||
return bytes.fromhex(rv["hex"])
|
||||
|
||||
def checkExpectedSeed(self, key_hash: str):
|
||||
try:
|
||||
rv = self.rpc('dumpmnemonic')
|
||||
entropy = Mnemonic('english').to_entropy(rv.split(' '))
|
||||
rv = self.rpc("dumpmnemonic")
|
||||
entropy = Mnemonic("english").to_entropy(rv.split(" "))
|
||||
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
self._have_checked_seed = True
|
||||
return entropy_hash == key_hash
|
||||
except Exception as e:
|
||||
self._log.warning('checkExpectedSeed failed: {}'.format(str(e)))
|
||||
self._log.warning("checkExpectedSeed failed: {}".format(str(e)))
|
||||
return False
|
||||
|
||||
def getScriptForP2PKH(self, pkh: bytes) -> bytearray:
|
||||
@@ -137,13 +142,22 @@ class NAVInterface(BTCInterface):
|
||||
script = CScript([OP_0, pkh])
|
||||
script_hash = hash160(script)
|
||||
assert len(script_hash) == 20
|
||||
return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash)
|
||||
return encodeAddress(
|
||||
bytes((self.chainparams_network()["script_address"],)) + script_hash
|
||||
)
|
||||
|
||||
def encodeSegwitAddressScript(self, script: bytes) -> str:
|
||||
if len(script) == 23 and script[0] == OP_HASH160 and script[1] == 20 and script[22] == OP_EQUAL:
|
||||
if (
|
||||
len(script) == 23
|
||||
and script[0] == OP_HASH160
|
||||
and script[1] == 20
|
||||
and script[22] == OP_EQUAL
|
||||
):
|
||||
script_hash = script[2:22]
|
||||
return encodeAddress(bytes((self.chainparams_network()['script_address'],)) + script_hash)
|
||||
raise ValueError('Unknown Script')
|
||||
return encodeAddress(
|
||||
bytes((self.chainparams_network()["script_address"],)) + script_hash
|
||||
)
|
||||
raise ValueError("Unknown Script")
|
||||
|
||||
def loadTx(self, tx_bytes: bytes) -> CTransaction:
|
||||
# Load tx from bytes to internal representation
|
||||
@@ -151,9 +165,18 @@ class NAVInterface(BTCInterface):
|
||||
tx.deserialize(BytesIO(tx_bytes))
|
||||
return tx
|
||||
|
||||
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script, prevout_value: int):
|
||||
def signTx(
|
||||
self,
|
||||
key_bytes: bytes,
|
||||
tx_bytes: bytes,
|
||||
input_n: int,
|
||||
prevout_script,
|
||||
prevout_value: int,
|
||||
):
|
||||
tx = self.loadTx(tx_bytes)
|
||||
sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
||||
sig_hash = SegwitVersion1SignatureHash(
|
||||
prevout_script, tx, input_n, SIGHASH_ALL, prevout_value
|
||||
)
|
||||
eck = PrivateKey(key_bytes)
|
||||
return eck.sign(sig_hash, hasher=None) + bytes((SIGHASH_ALL,))
|
||||
|
||||
@@ -168,23 +191,25 @@ class NAVInterface(BTCInterface):
|
||||
# TODO: Lock unspent and use same output/s to fund bid
|
||||
|
||||
unspents_by_addr = dict()
|
||||
unspents = self.rpc('listunspent')
|
||||
unspents = self.rpc("listunspent")
|
||||
for u in unspents:
|
||||
if u['spendable'] is not True:
|
||||
if u["spendable"] is not True:
|
||||
continue
|
||||
if u['address'] not in unspents_by_addr:
|
||||
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
|
||||
utxo_amount: int = self.make_int(u['amount'], r=1)
|
||||
unspents_by_addr[u['address']]['total'] += utxo_amount
|
||||
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
|
||||
if u["address"] not in unspents_by_addr:
|
||||
unspents_by_addr[u["address"]] = {"total": 0, "utxos": []}
|
||||
utxo_amount: int = self.make_int(u["amount"], r=1)
|
||||
unspents_by_addr[u["address"]]["total"] += utxo_amount
|
||||
unspents_by_addr[u["address"]]["utxos"].append(
|
||||
(utxo_amount, u["txid"], u["vout"])
|
||||
)
|
||||
|
||||
max_utxos: int = 4
|
||||
|
||||
viable_addrs = []
|
||||
for addr, data in unspents_by_addr.items():
|
||||
if data['total'] >= amount_for:
|
||||
if data["total"] >= amount_for:
|
||||
# Sort from largest to smallest amount
|
||||
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
|
||||
sorted_utxos = sorted(data["utxos"], key=lambda x: x[0])
|
||||
|
||||
# Max outputs required to reach amount_for
|
||||
utxos_req: int = 0
|
||||
@@ -199,13 +224,17 @@ class NAVInterface(BTCInterface):
|
||||
viable_addrs.append(addr)
|
||||
continue
|
||||
|
||||
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
|
||||
ensure(
|
||||
len(viable_addrs) > 0, "Could not find address with enough funds for proof"
|
||||
)
|
||||
|
||||
sign_for_addr: str = random.choice(viable_addrs)
|
||||
self._log.debug('sign_for_addr %s', sign_for_addr)
|
||||
self._log.debug("sign_for_addr %s", sign_for_addr)
|
||||
|
||||
prove_utxos = []
|
||||
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
|
||||
sorted_utxos = sorted(
|
||||
unspents_by_addr[sign_for_addr]["utxos"], key=lambda x: x[0]
|
||||
)
|
||||
|
||||
hasher = hashlib.sha256()
|
||||
|
||||
@@ -215,20 +244,36 @@ class NAVInterface(BTCInterface):
|
||||
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
|
||||
prove_utxos.append(outpoint)
|
||||
hasher.update(outpoint[0])
|
||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||
if sum_value >= amount_for:
|
||||
break
|
||||
utxos_hash = hasher.digest()
|
||||
|
||||
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
if (
|
||||
self.using_segwit()
|
||||
): # TODO: Use isSegwitAddress when scantxoutset can use combo
|
||||
# 'Address does not refer to key' for non p2pkh
|
||||
addr_info = self.rpc('validateaddress', [addr, ])
|
||||
if 'isscript' in addr_info and addr_info['isscript'] and 'hex' in addr_info:
|
||||
pkh = bytes.fromhex(addr_info['hex'])[2:]
|
||||
addr_info = self.rpc(
|
||||
"validateaddress",
|
||||
[
|
||||
addr,
|
||||
],
|
||||
)
|
||||
if "isscript" in addr_info and addr_info["isscript"] and "hex" in addr_info:
|
||||
pkh = bytes.fromhex(addr_info["hex"])[2:]
|
||||
sign_for_addr = self.pkh_to_address(pkh)
|
||||
self._log.debug('sign_for_addr converted %s', sign_for_addr)
|
||||
self._log.debug("sign_for_addr converted %s", sign_for_addr)
|
||||
|
||||
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
|
||||
signature = self.rpc(
|
||||
"signmessage",
|
||||
[
|
||||
sign_for_addr,
|
||||
sign_for_addr
|
||||
+ "_swap_proof_"
|
||||
+ utxos_hash.hex()
|
||||
+ extra_commit_bytes.hex(),
|
||||
],
|
||||
)
|
||||
|
||||
return (sign_for_addr, signature, prove_utxos)
|
||||
|
||||
@@ -237,48 +282,64 @@ class NAVInterface(BTCInterface):
|
||||
sum_value: int = 0
|
||||
for outpoint in utxos:
|
||||
hasher.update(outpoint[0])
|
||||
hasher.update(outpoint[1].to_bytes(2, 'big'))
|
||||
hasher.update(outpoint[1].to_bytes(2, "big"))
|
||||
utxos_hash = hasher.digest()
|
||||
|
||||
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
|
||||
ensure(passed is True, 'Proof of funds signature invalid')
|
||||
passed = self.verifyMessage(
|
||||
address,
|
||||
address + "_swap_proof_" + utxos_hash.hex() + extra_commit_bytes.hex(),
|
||||
signature,
|
||||
)
|
||||
ensure(passed is True, "Proof of funds signature invalid")
|
||||
|
||||
if self.using_segwit():
|
||||
address = self.encodeSegwitAddress(self.decodeAddress(address)[1:])
|
||||
|
||||
sum_value: int = 0
|
||||
for outpoint in utxos:
|
||||
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
|
||||
sum_value += self.make_int(txout['value'])
|
||||
txout = self.rpc("gettxout", [outpoint[0].hex(), outpoint[1]])
|
||||
sum_value += self.make_int(txout["value"])
|
||||
|
||||
return sum_value
|
||||
|
||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
||||
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
def createRawFundedTransaction(
|
||||
self,
|
||||
addr_to: str,
|
||||
amount: int,
|
||||
sub_fee: bool = False,
|
||||
lock_unspents: bool = True,
|
||||
) -> str:
|
||||
txn = self.rpc(
|
||||
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
|
||||
)
|
||||
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
||||
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||
self._log.debug(
|
||||
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
|
||||
)
|
||||
if sub_fee:
|
||||
raise ValueError('Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter')
|
||||
raise ValueError(
|
||||
"Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter"
|
||||
)
|
||||
# options['subtractFeeFromOutputs'] = [0,]
|
||||
|
||||
fee_rate = self.make_int(fee_rate, r=1)
|
||||
return self.fundTx(txn, fee_rate, lock_unspents).hex()
|
||||
|
||||
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
|
||||
addr_info = self.rpc('validateaddress', [address])
|
||||
addr_info = self.rpc("validateaddress", [address])
|
||||
if not or_watch_only:
|
||||
return addr_info['ismine']
|
||||
return addr_info['ismine'] or addr_info['iswatchonly']
|
||||
return addr_info["ismine"]
|
||||
return addr_info["ismine"] or addr_info["iswatchonly"]
|
||||
|
||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||
return self.rpc("signrawtransaction", [txn_funded])["hex"]
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
rv = self.rpc('getblockchaininfo')
|
||||
synced = round(rv['verificationprogress'], 3)
|
||||
rv = self.rpc("getblockchaininfo")
|
||||
synced = round(rv["verificationprogress"], 3)
|
||||
if synced >= 0.997:
|
||||
rv['verificationprogress'] = 1.0
|
||||
rv["verificationprogress"] = 1.0
|
||||
return rv
|
||||
|
||||
def encodeScriptDest(self, script_dest: bytes) -> str:
|
||||
@@ -286,47 +347,75 @@ class NAVInterface(BTCInterface):
|
||||
return self.sh_to_address(script_hash)
|
||||
|
||||
def encode_p2wsh(self, script: bytes) -> str:
|
||||
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
|
||||
return pubkeyToAddress(self.chainparams_network()["script_address"], script)
|
||||
|
||||
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
|
||||
txjs = self.rpc('decoderawtransaction', [txn_hex])
|
||||
txjs = self.rpc("decoderawtransaction", [txn_hex])
|
||||
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
|
||||
|
||||
return {
|
||||
'txid': txjs['txid'],
|
||||
'vout': n,
|
||||
'scriptPubKey': txjs['vout'][n]['scriptPubKey']['hex'],
|
||||
'redeemScript': txn_script.hex(),
|
||||
'amount': txjs['vout'][n]['value']
|
||||
"txid": txjs["txid"],
|
||||
"vout": n,
|
||||
"scriptPubKey": txjs["vout"][n]["scriptPubKey"]["hex"],
|
||||
"redeemScript": txn_script.hex(),
|
||||
"amount": txjs["vout"][n]["value"],
|
||||
}
|
||||
|
||||
def getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str:
|
||||
address: str = self.rpc('getnewaddress', [label,])
|
||||
def getNewAddress(self, use_segwit: bool, label: str = "swap_receive") -> str:
|
||||
address: str = self.rpc(
|
||||
"getnewaddress",
|
||||
[
|
||||
label,
|
||||
],
|
||||
)
|
||||
if use_segwit:
|
||||
return self.rpc('addwitnessaddress', [address,])
|
||||
return self.rpc(
|
||||
"addwitnessaddress",
|
||||
[
|
||||
address,
|
||||
],
|
||||
)
|
||||
return address
|
||||
|
||||
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str:
|
||||
def createRedeemTxn(
|
||||
self, prevout, output_addr: str, output_value: int, txn_script: bytes
|
||||
) -> str:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
prev_txid = b2i(bytes.fromhex(prevout['txid']))
|
||||
prev_txid = b2i(bytes.fromhex(prevout["txid"]))
|
||||
|
||||
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(prev_txid, prevout["vout"]),
|
||||
scriptSig=self.getScriptScriptSig(txn_script),
|
||||
)
|
||||
)
|
||||
pkh = self.decodeAddress(output_addr)
|
||||
script = self.getScriptForPubkeyHash(pkh)
|
||||
tx.vout.append(self.txoType()(output_value, script))
|
||||
tx.rehash()
|
||||
return tx.serialize().hex()
|
||||
|
||||
def createRefundTxn(self, prevout, output_addr: str, output_value: int, locktime: int, sequence: int, txn_script: bytes) -> str:
|
||||
def createRefundTxn(
|
||||
self,
|
||||
prevout,
|
||||
output_addr: str,
|
||||
output_value: int,
|
||||
locktime: int,
|
||||
sequence: int,
|
||||
txn_script: bytes,
|
||||
) -> str:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.nLockTime = locktime
|
||||
prev_txid = b2i(bytes.fromhex(prevout['txid']))
|
||||
tx.vin.append(CTxIn(COutPoint(prev_txid, prevout['vout']),
|
||||
nSequence=sequence,
|
||||
scriptSig=self.getScriptScriptSig(txn_script)))
|
||||
prev_txid = b2i(bytes.fromhex(prevout["txid"]))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(prev_txid, prevout["vout"]),
|
||||
nSequence=sequence,
|
||||
scriptSig=self.getScriptScriptSig(txn_script),
|
||||
)
|
||||
)
|
||||
pkh = self.decodeAddress(output_addr)
|
||||
script = self.getScriptForPubkeyHash(pkh)
|
||||
tx.vout.append(self.txoType()(output_value, script))
|
||||
@@ -335,22 +424,38 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
def getTxSignature(self, tx_hex: str, prevout_data, key_wif: str) -> str:
|
||||
key = decodeWif(key_wif)
|
||||
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
|
||||
sig = self.signTx(key, bytes.fromhex(tx_hex), 0, redeem_script, self.make_int(prevout_data['amount']))
|
||||
redeem_script = bytes.fromhex(prevout_data["redeemScript"])
|
||||
sig = self.signTx(
|
||||
key,
|
||||
bytes.fromhex(tx_hex),
|
||||
0,
|
||||
redeem_script,
|
||||
self.make_int(prevout_data["amount"]),
|
||||
)
|
||||
|
||||
return sig.hex()
|
||||
|
||||
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
|
||||
def verifyTxSig(
|
||||
self,
|
||||
tx_bytes: bytes,
|
||||
sig: bytes,
|
||||
K: bytes,
|
||||
input_n: int,
|
||||
prevout_script: bytes,
|
||||
prevout_value: int,
|
||||
) -> bool:
|
||||
tx = self.loadTx(tx_bytes)
|
||||
sig_hash = SegwitVersion1SignatureHash(prevout_script, tx, input_n, SIGHASH_ALL, prevout_value)
|
||||
sig_hash = SegwitVersion1SignatureHash(
|
||||
prevout_script, tx, input_n, SIGHASH_ALL, prevout_value
|
||||
)
|
||||
|
||||
pubkey = PublicKey(K)
|
||||
return pubkey.verify(sig[: -1], sig_hash, hasher=None) # Pop the hashtype byte
|
||||
return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte
|
||||
|
||||
def verifyRawTransaction(self, tx_hex: str, prevouts):
|
||||
# Only checks signature
|
||||
# verifyrawtransaction
|
||||
self._log.warning('NAV verifyRawTransaction only checks signature')
|
||||
self._log.warning("NAV verifyRawTransaction only checks signature")
|
||||
inputs_valid: bool = False
|
||||
validscripts: int = 0
|
||||
|
||||
@@ -362,22 +467,26 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
input_n: int = 0
|
||||
prevout_data = prevouts[input_n]
|
||||
redeem_script = bytes.fromhex(prevout_data['redeemScript'])
|
||||
prevout_value = self.make_int(prevout_data['amount'])
|
||||
redeem_script = bytes.fromhex(prevout_data["redeemScript"])
|
||||
prevout_value = self.make_int(prevout_data["amount"])
|
||||
|
||||
if self.verifyTxSig(tx_bytes, signature, pubkey, input_n, redeem_script, prevout_value):
|
||||
if self.verifyTxSig(
|
||||
tx_bytes, signature, pubkey, input_n, redeem_script, prevout_value
|
||||
):
|
||||
validscripts += 1
|
||||
|
||||
# TODO: validate inputs
|
||||
inputs_valid = True
|
||||
|
||||
return {
|
||||
'inputs_valid': inputs_valid,
|
||||
'validscripts': validscripts,
|
||||
"inputs_valid": inputs_valid,
|
||||
"validscripts": validscripts,
|
||||
}
|
||||
|
||||
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
|
||||
tx_vsize = 5 # Add a few bytes, sequence in script takes variable amount of bytes
|
||||
tx_vsize = (
|
||||
5 # Add a few bytes, sequence in script takes variable amount of bytes
|
||||
)
|
||||
|
||||
tx_vsize += 184 if redeem else 187
|
||||
return tx_vsize
|
||||
@@ -396,15 +505,15 @@ class NAVInterface(BTCInterface):
|
||||
chain_blocks: int = self.getChainHeight()
|
||||
|
||||
current_height: int = chain_blocks
|
||||
block_hash = self.rpc('getblockhash', [current_height])
|
||||
block_hash = self.rpc("getblockhash", [current_height])
|
||||
|
||||
script_hash: bytes = self.decodeAddress(addr_find)
|
||||
find_scriptPubKey = self.getDestForScriptHash(script_hash)
|
||||
|
||||
while current_height > height_start:
|
||||
block_hash = self.rpc('getblockhash', [current_height])
|
||||
block_hash = self.rpc("getblockhash", [current_height])
|
||||
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
block = self.rpc("getblock", [block_hash, False])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
for tx in decoded_block.vtx:
|
||||
@@ -412,84 +521,116 @@ class NAVInterface(BTCInterface):
|
||||
if txo.scriptPubKey == find_scriptPubKey:
|
||||
tx.rehash()
|
||||
txid = i2b(tx.sha256)
|
||||
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
|
||||
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
|
||||
self.rpc('invalidateblock', [block_hash])
|
||||
self.rpc('reconsiderblock', [block_hash])
|
||||
self._log.info(
|
||||
"Found output to addr: {} in tx {} in block {}".format(
|
||||
addr_find, txid.hex(), block_hash
|
||||
)
|
||||
)
|
||||
self._log.info(
|
||||
"rescanblockchain hack invalidateblock {}".format(
|
||||
block_hash
|
||||
)
|
||||
)
|
||||
self.rpc("invalidateblock", [block_hash])
|
||||
self.rpc("reconsiderblock", [block_hash])
|
||||
return
|
||||
current_height -= 1
|
||||
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
||||
def getLockTxHeight(
|
||||
self,
|
||||
txid,
|
||||
dest_address,
|
||||
bid_amount,
|
||||
rescan_from,
|
||||
find_index: bool = False,
|
||||
vout: int = -1,
|
||||
):
|
||||
# Add watchonly address and rescan if required
|
||||
|
||||
if not self.isAddressMine(dest_address, or_watch_only=True):
|
||||
self.importWatchOnlyAddress(dest_address, 'bid')
|
||||
self._log.info('Imported watch-only addr: {}'.format(dest_address))
|
||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
|
||||
self.importWatchOnlyAddress(dest_address, "bid")
|
||||
self._log.info("Imported watch-only addr: {}".format(dest_address))
|
||||
self._log.info(
|
||||
"Rescanning {} chain from height: {}".format(
|
||||
self.coin_name(), rescan_from
|
||||
)
|
||||
)
|
||||
self.rescanBlockchainForAddress(rescan_from, dest_address)
|
||||
|
||||
return_txid = True if txid is None else False
|
||||
if txid is None:
|
||||
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
|
||||
txns = self.rpc(
|
||||
"listunspent",
|
||||
[
|
||||
0,
|
||||
9999999,
|
||||
[
|
||||
dest_address,
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
for tx in txns:
|
||||
if self.make_int(tx['amount']) == bid_amount:
|
||||
txid = bytes.fromhex(tx['txid'])
|
||||
if self.make_int(tx["amount"]) == bid_amount:
|
||||
txid = bytes.fromhex(tx["txid"])
|
||||
break
|
||||
|
||||
if txid is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
tx = self.rpc('gettransaction', [txid.hex()])
|
||||
tx = self.rpc("gettransaction", [txid.hex()])
|
||||
|
||||
block_height = 0
|
||||
if 'blockhash' in tx:
|
||||
block_header = self.rpc('getblockheader', [tx['blockhash']])
|
||||
block_height = block_header['height']
|
||||
if "blockhash" in tx:
|
||||
block_header = self.rpc("getblockheader", [tx["blockhash"]])
|
||||
block_height = block_header["height"]
|
||||
|
||||
rv = {
|
||||
'depth': 0 if 'confirmations' not in tx else tx['confirmations'],
|
||||
'height': block_height}
|
||||
"depth": 0 if "confirmations" not in tx else tx["confirmations"],
|
||||
"height": block_height,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e))
|
||||
self._log.debug(
|
||||
"getLockTxHeight gettransaction failed: %s, %s", txid.hex(), str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
if find_index:
|
||||
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
|
||||
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
|
||||
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
|
||||
|
||||
if return_txid:
|
||||
rv['txid'] = txid.hex()
|
||||
rv["txid"] = txid.hex()
|
||||
|
||||
return rv
|
||||
|
||||
def getBlockWithTxns(self, block_hash):
|
||||
# TODO: Bypass decoderawtransaction and getblockheader
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
block_header = self.rpc('getblockheader', [block_hash])
|
||||
block = self.rpc("getblock", [block_hash, False])
|
||||
block_header = self.rpc("getblockheader", [block_hash])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
|
||||
tx_rv = []
|
||||
for tx in decoded_block.vtx:
|
||||
tx_hex = tx.serialize_with_witness().hex()
|
||||
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
|
||||
if 'hex' not in tx_dec:
|
||||
tx_dec['hex'] = tx_hex
|
||||
tx_dec = self.rpc("decoderawtransaction", [tx_hex])
|
||||
if "hex" not in tx_dec:
|
||||
tx_dec["hex"] = tx_hex
|
||||
|
||||
tx_rv.append(tx_dec)
|
||||
|
||||
block_rv = {
|
||||
'hash': block_hash,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
"hash": block_hash,
|
||||
"previousblockhash": block_header["previousblockhash"],
|
||||
"tx": tx_rv,
|
||||
"confirmations": block_header["confirmations"],
|
||||
"height": block_header["height"],
|
||||
"time": block_header["time"],
|
||||
"version": block_header["version"],
|
||||
"merkleroot": block_header["merkleroot"],
|
||||
}
|
||||
|
||||
return block_rv
|
||||
@@ -516,15 +657,30 @@ class NAVInterface(BTCInterface):
|
||||
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||
return tx.serialize()
|
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, lock_tx_vout=None) -> bytes:
|
||||
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
|
||||
wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ])
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
|
||||
def spendBLockTx(
|
||||
self,
|
||||
chain_b_lock_txid: bytes,
|
||||
address_to: str,
|
||||
kbv: bytes,
|
||||
kbs: bytes,
|
||||
cb_swap_value: int,
|
||||
b_fee: int,
|
||||
restore_height: int,
|
||||
lock_tx_vout=None,
|
||||
) -> bytes:
|
||||
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
||||
wtx = self.rpc(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
|
||||
Kbs = self.getPubkey(kbs)
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
pkh_to = self.decodeAddress(address_to)
|
||||
|
||||
tx = CTransaction()
|
||||
@@ -534,10 +690,16 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
script_sig = self.getInputScriptForPubkeyHash(self.getPubkeyHash(Kbs))
|
||||
|
||||
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n),
|
||||
nSequence=0,
|
||||
scriptSig=script_sig))
|
||||
tx.vout.append(self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(chain_b_lock_txid_int, locked_n),
|
||||
nSequence=0,
|
||||
scriptSig=script_sig,
|
||||
)
|
||||
)
|
||||
tx.vout.append(
|
||||
self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to))
|
||||
)
|
||||
|
||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||
tx.vout[0].nValue = cb_swap_value - pay_fee
|
||||
@@ -563,16 +725,20 @@ class NAVInterface(BTCInterface):
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# Only works for wallet txns
|
||||
try:
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
rv = self.rpc("gettransaction", [txid_hex])
|
||||
except Exception as e: # noqa: F841
|
||||
self._log.debug(
|
||||
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||
)
|
||||
return None
|
||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||
return None
|
||||
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||
def createSCLockTx(
|
||||
self, value: int, script: bytearray, vkbv: bytes = None
|
||||
) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
@@ -583,20 +749,20 @@ class NAVInterface(BTCInterface):
|
||||
feerate_str = self.format_amount(feerate)
|
||||
# TODO: unlock unspents if bid cancelled
|
||||
options = {
|
||||
'lockUnspents': lock_unspents,
|
||||
'feeRate': feerate_str,
|
||||
"lockUnspents": lock_unspents,
|
||||
"feeRate": feerate_str,
|
||||
}
|
||||
rv = self.rpc('fundrawtransaction', [tx_hex, options])
|
||||
rv = self.rpc("fundrawtransaction", [tx_hex, options])
|
||||
|
||||
# Sign transaction then strip witness data to fill scriptsig
|
||||
rv = self.rpc('signrawtransaction', [rv['hex']])
|
||||
rv = self.rpc("signrawtransaction", [rv["hex"]])
|
||||
|
||||
tx_signed = self.loadTx(bytes.fromhex(rv['hex']))
|
||||
tx_signed = self.loadTx(bytes.fromhex(rv["hex"]))
|
||||
if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit):
|
||||
raise ValueError('txn has non segwit input')
|
||||
raise ValueError("txn has non segwit input")
|
||||
for witness_data in tx_signed.wit.vtxinwit:
|
||||
if len(witness_data.scriptWitness.stack) < 2:
|
||||
raise ValueError('txn has non segwit input')
|
||||
raise ValueError("txn has non segwit input")
|
||||
|
||||
return tx_signed.serialize_without_witness()
|
||||
|
||||
@@ -604,13 +770,23 @@ class NAVInterface(BTCInterface):
|
||||
tx_funded = self.fundTx(tx_bytes.hex(), feerate)
|
||||
return tx_funded
|
||||
|
||||
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None):
|
||||
def createSCLockRefundTx(
|
||||
self,
|
||||
tx_lock_bytes,
|
||||
script_lock,
|
||||
Kal,
|
||||
Kaf,
|
||||
lock1_value,
|
||||
csv_val,
|
||||
tx_fee_rate,
|
||||
vkbv=None,
|
||||
):
|
||||
tx_lock = CTransaction()
|
||||
tx_lock = self.loadTx(tx_lock_bytes)
|
||||
|
||||
output_script = self.getScriptDest(script_lock)
|
||||
locked_n = findOutput(tx_lock, output_script)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
locked_coin = tx_lock.vout[locked_n].nValue
|
||||
|
||||
tx_lock.rehash()
|
||||
@@ -619,9 +795,13 @@ class NAVInterface(BTCInterface):
|
||||
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
|
||||
nSequence=lock1_value,
|
||||
scriptSig=self.getScriptScriptSig(script_lock)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(tx_lock_id_int, locked_n),
|
||||
nSequence=lock1_value,
|
||||
scriptSig=self.getScriptScriptSig(script_lock),
|
||||
)
|
||||
)
|
||||
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
|
||||
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||
@@ -631,12 +811,24 @@ class NAVInterface(BTCInterface):
|
||||
tx.vout[0].nValue = locked_coin - pay_fee
|
||||
|
||||
tx.rehash()
|
||||
self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||
self._log.info(
|
||||
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
)
|
||||
|
||||
return tx.serialize(), refund_script, tx.vout[0].nValue
|
||||
|
||||
def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None):
|
||||
def createSCLockRefundSpendTx(
|
||||
self,
|
||||
tx_lock_refund_bytes,
|
||||
script_lock_refund,
|
||||
pkh_refund_to,
|
||||
tx_fee_rate,
|
||||
vkbv=None,
|
||||
):
|
||||
# Returns the coinA locked coin to the leader
|
||||
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
||||
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
||||
@@ -645,7 +837,7 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
output_script = self.getScriptDest(script_lock_refund)
|
||||
locked_n = findOutput(tx_lock_refund, output_script)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
||||
|
||||
tx_lock_refund.rehash()
|
||||
@@ -653,25 +845,46 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||
nSequence=0,
|
||||
scriptSig=self.getScriptScriptSig(script_lock_refund)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||
nSequence=0,
|
||||
scriptSig=self.getScriptScriptSig(script_lock_refund),
|
||||
)
|
||||
)
|
||||
|
||||
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to)))
|
||||
tx.vout.append(
|
||||
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_refund_to))
|
||||
)
|
||||
|
||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(
|
||||
script_lock_refund
|
||||
)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||
tx.vout[0].nValue = locked_coin - pay_fee
|
||||
|
||||
tx.rehash()
|
||||
self._log.info('createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
|
||||
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
|
||||
def createSCLockRefundSpendToFTx(
|
||||
self,
|
||||
tx_lock_refund_bytes,
|
||||
script_lock_refund,
|
||||
pkh_dest,
|
||||
tx_fee_rate,
|
||||
vkbv=None,
|
||||
kbsf=None,
|
||||
):
|
||||
# lock refund swipe tx
|
||||
# Sends the coinA locked coin to the follower
|
||||
|
||||
@@ -679,7 +892,7 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
output_script = self.getScriptDest(script_lock_refund)
|
||||
locked_n = findOutput(tx_lock_refund, output_script)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
locked_coin = tx_lock_refund.vout[locked_n].nValue
|
||||
|
||||
A, B, lock2_value, C = extractScriptLockRefundScriptValues(script_lock_refund)
|
||||
@@ -689,29 +902,44 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||
nSequence=lock2_value,
|
||||
scriptSig=self.getScriptScriptSig(script_lock_refund)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(tx_lock_refund_hash_int, locked_n),
|
||||
nSequence=lock2_value,
|
||||
scriptSig=self.getScriptScriptSig(script_lock_refund),
|
||||
)
|
||||
)
|
||||
|
||||
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
||||
tx.vout.append(
|
||||
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))
|
||||
)
|
||||
|
||||
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
||||
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(
|
||||
script_lock_refund
|
||||
)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
|
||||
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||
tx.vout[0].nValue = locked_coin - pay_fee
|
||||
|
||||
tx.rehash()
|
||||
self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
|
||||
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}):
|
||||
def createSCLockSpendTx(
|
||||
self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}
|
||||
):
|
||||
tx_lock = self.loadTx(tx_lock_bytes)
|
||||
output_script = self.getScriptDest(script_lock)
|
||||
locked_n = findOutput(tx_lock, output_script)
|
||||
ensure(locked_n is not None, 'Output not found in tx')
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
locked_coin = tx_lock.vout[locked_n].nValue
|
||||
|
||||
tx_lock.rehash()
|
||||
@@ -719,10 +947,16 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
|
||||
scriptSig=self.getScriptScriptSig(script_lock)))
|
||||
tx.vin.append(
|
||||
CTxIn(
|
||||
COutPoint(tx_lock_id_int, locked_n),
|
||||
scriptSig=self.getScriptScriptSig(script_lock),
|
||||
)
|
||||
)
|
||||
|
||||
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
|
||||
tx.vout.append(
|
||||
self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest))
|
||||
)
|
||||
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
@@ -730,13 +964,18 @@ class NAVInterface(BTCInterface):
|
||||
pay_fee = round(tx_fee_rate * vsize / 1000)
|
||||
tx.vout[0].nValue = locked_coin - pay_fee
|
||||
|
||||
fee_info['fee_paid'] = pay_fee
|
||||
fee_info['rate_used'] = tx_fee_rate
|
||||
fee_info['witness_bytes'] = witness_bytes
|
||||
fee_info['vsize'] = vsize
|
||||
fee_info["fee_paid"] = pay_fee
|
||||
fee_info["rate_used"] = tx_fee_rate
|
||||
fee_info["witness_bytes"] = witness_bytes
|
||||
fee_info["vsize"] = vsize
|
||||
|
||||
tx.rehash()
|
||||
self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
|
||||
self._log.info(
|
||||
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
|
||||
@@ -14,26 +14,38 @@ class NMCInterface(BTCInterface):
|
||||
def coin_type():
|
||||
return Coins.NMC
|
||||
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1):
|
||||
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
|
||||
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
|
||||
self._log.debug('[rm] scantxoutset end')
|
||||
def getLockTxHeight(
|
||||
self,
|
||||
txid,
|
||||
dest_address,
|
||||
bid_amount,
|
||||
rescan_from,
|
||||
find_index: bool = False,
|
||||
vout: int = -1,
|
||||
):
|
||||
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
|
||||
ro = self.rpc(
|
||||
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
|
||||
) # TODO: Use combo(address) where possible
|
||||
self._log.debug("[rm] scantxoutset end")
|
||||
return_txid = True if txid is None else False
|
||||
for o in ro['unspents']:
|
||||
if txid and o['txid'] != txid.hex():
|
||||
for o in ro["unspents"]:
|
||||
if txid and o["txid"] != txid.hex():
|
||||
continue
|
||||
# Verify amount
|
||||
if self.make_int(o['amount']) != int(bid_amount):
|
||||
self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
|
||||
if self.make_int(o["amount"]) != int(bid_amount):
|
||||
self._log.warning(
|
||||
"Found output to lock tx address of incorrect value: %s, %s",
|
||||
str(o["amount"]),
|
||||
o["txid"],
|
||||
)
|
||||
continue
|
||||
|
||||
rv = {
|
||||
'depth': 0,
|
||||
'height': o['height']}
|
||||
if o['height'] > 0:
|
||||
rv['depth'] = ro['height'] - o['height']
|
||||
rv = {"depth": 0, "height": o["height"]}
|
||||
if o["height"] > 0:
|
||||
rv["depth"] = ro["height"] - o["height"]
|
||||
if find_index:
|
||||
rv['index'] = o['vout']
|
||||
rv["index"] = o["vout"]
|
||||
if return_txid:
|
||||
rv['txid'] = o['txid']
|
||||
rv["txid"] = o["txid"]
|
||||
return rv
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,7 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from .btc import BTCInterface
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOut)
|
||||
from basicswap.contrib.test_framework.messages import CTxOut
|
||||
|
||||
|
||||
class PassthroughBTCInterface(BTCInterface):
|
||||
@@ -15,5 +14,5 @@ class PassthroughBTCInterface(BTCInterface):
|
||||
super().__init__(coin_settings, network)
|
||||
self.txoType = CTxOut
|
||||
self._network = network
|
||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
||||
self.setConfTarget(coin_settings['conf_target'])
|
||||
self.blocks_confirmed = coin_settings["blocks_confirmed"]
|
||||
self.setConfTarget(coin_settings["conf_target"])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -11,11 +12,7 @@ from .btc import BTCInterface
|
||||
from basicswap.rpc import make_rpc_func
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.util.address import decodeAddress
|
||||
from .contrib.pivx_test_framework.messages import (
|
||||
CBlock,
|
||||
ToHex,
|
||||
FromHex,
|
||||
CTransaction)
|
||||
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_DUP,
|
||||
@@ -33,65 +30,79 @@ class PIVXInterface(BTCInterface):
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
|
||||
# No multiwallet support
|
||||
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def signTxWithWallet(self, tx):
|
||||
rv = self.rpc('signrawtransaction', [tx.hex()])
|
||||
return bytes.fromhex(rv['hex'])
|
||||
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||
return bytes.fromhex(rv["hex"])
|
||||
|
||||
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
|
||||
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
|
||||
def createRawFundedTransaction(
|
||||
self,
|
||||
addr_to: str,
|
||||
amount: int,
|
||||
sub_fee: bool = False,
|
||||
lock_unspents: bool = True,
|
||||
) -> str:
|
||||
txn = self.rpc(
|
||||
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
|
||||
)
|
||||
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
|
||||
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
|
||||
self._log.debug(
|
||||
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
|
||||
)
|
||||
options = {
|
||||
'lockUnspents': lock_unspents,
|
||||
'feeRate': fee_rate,
|
||||
"lockUnspents": lock_unspents,
|
||||
"feeRate": fee_rate,
|
||||
}
|
||||
if sub_fee:
|
||||
options['subtractFeeFromOutputs'] = [0,]
|
||||
return self.rpc('fundrawtransaction', [txn, options])['hex']
|
||||
options["subtractFeeFromOutputs"] = [
|
||||
0,
|
||||
]
|
||||
return self.rpc("fundrawtransaction", [txn, options])["hex"]
|
||||
|
||||
def createRawSignedTransaction(self, addr_to, amount) -> str:
|
||||
txn_funded = self.createRawFundedTransaction(addr_to, amount)
|
||||
return self.rpc('signrawtransaction', [txn_funded])['hex']
|
||||
return self.rpc("signrawtransaction", [txn_funded])["hex"]
|
||||
|
||||
def decodeAddress(self, address):
|
||||
return decodeAddress(address)[1:]
|
||||
|
||||
def getBlockWithTxns(self, block_hash):
|
||||
# TODO: Bypass decoderawtransaction and getblockheader
|
||||
block = self.rpc('getblock', [block_hash, False])
|
||||
block_header = self.rpc('getblockheader', [block_hash])
|
||||
block = self.rpc("getblock", [block_hash, False])
|
||||
block_header = self.rpc("getblockheader", [block_hash])
|
||||
decoded_block = CBlock()
|
||||
decoded_block = FromHex(decoded_block, block)
|
||||
|
||||
tx_rv = []
|
||||
for tx in decoded_block.vtx:
|
||||
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
|
||||
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
|
||||
tx_rv.append(tx_dec)
|
||||
|
||||
block_rv = {
|
||||
'hash': block_hash,
|
||||
'previousblockhash': block_header['previousblockhash'],
|
||||
'tx': tx_rv,
|
||||
'confirmations': block_header['confirmations'],
|
||||
'height': block_header['height'],
|
||||
'time': block_header['time'],
|
||||
'version': block_header['version'],
|
||||
'merkleroot': block_header['merkleroot'],
|
||||
"hash": block_hash,
|
||||
"previousblockhash": block_header["previousblockhash"],
|
||||
"tx": tx_rv,
|
||||
"confirmations": block_header["confirmations"],
|
||||
"height": block_header["height"],
|
||||
"time": block_header["time"],
|
||||
"version": block_header["version"],
|
||||
"merkleroot": block_header["merkleroot"],
|
||||
}
|
||||
|
||||
return block_rv
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee]
|
||||
return self.rpc('sendtoaddress', params)
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc("sendtoaddress", params)
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc('getwalletinfo')['balance'])
|
||||
return self.make_int(self.rpc("getwalletinfo")["balance"])
|
||||
|
||||
def loadTx(self, tx_bytes):
|
||||
# Load tx from bytes to internal representation
|
||||
@@ -107,22 +118,35 @@ class PIVXInterface(BTCInterface):
|
||||
add_bytes = 107
|
||||
size = len(tx.serialize_with_witness()) + add_bytes
|
||||
pay_fee = round(fee_rate * size / 1000)
|
||||
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
|
||||
self._log.info(
|
||||
f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}."
|
||||
)
|
||||
return pay_fee
|
||||
|
||||
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
|
||||
key_wif = self.encodeKey(key)
|
||||
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
|
||||
return bytes.fromhex(rv['hex'])
|
||||
rv = self.rpc(
|
||||
"signrawtransaction",
|
||||
[
|
||||
tx.hex(),
|
||||
[],
|
||||
[
|
||||
key_wif,
|
||||
],
|
||||
],
|
||||
)
|
||||
return bytes.fromhex(rv["hex"])
|
||||
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# Only works for wallet txns
|
||||
try:
|
||||
rv = self.rpc('gettransaction', [txid_hex])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
rv = self.rpc("gettransaction", [txid_hex])
|
||||
except Exception as e: # noqa: F841
|
||||
self._log.debug(
|
||||
"findTxnByHash getrawtransaction failed: {}".format(txid_hex)
|
||||
)
|
||||
return None
|
||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv['blockhash'])['height']
|
||||
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
|
||||
if "confirmations" in rv and rv["confirmations"] >= self.blocks_confirmed:
|
||||
block_height = self.getBlockHeader(rv["blockhash"])["height"]
|
||||
return {"txid": txid_hex, "amount": 0, "height": block_height}
|
||||
return None
|
||||
|
||||
@@ -25,3 +25,7 @@ class WOWInterface(XMRInterface):
|
||||
@staticmethod
|
||||
def exp() -> int:
|
||||
return 11
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
@@ -27,16 +27,9 @@ from coincurve.dleag import (
|
||||
from basicswap.interface.base import (
|
||||
Curves,
|
||||
)
|
||||
from basicswap.util import (
|
||||
i2b, b2i, b2h,
|
||||
dumpj,
|
||||
ensure,
|
||||
TemporaryError)
|
||||
from basicswap.util.network import (
|
||||
is_private_ip_address)
|
||||
from basicswap.rpc_xmr import (
|
||||
make_xmr_rpc_func,
|
||||
make_xmr_rpc2_func)
|
||||
from basicswap.util import i2b, b2i, b2h, dumpj, ensure, TemporaryError
|
||||
from basicswap.util.network import is_private_ip_address
|
||||
from basicswap.rpc_xmr import make_xmr_rpc_func, make_xmr_rpc2_func
|
||||
from basicswap.chainparams import XMR_COIN, Coins
|
||||
from basicswap.interface.base import CoinInterface
|
||||
|
||||
@@ -76,65 +69,105 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
raise ValueError('Not possible')
|
||||
raise ValueError("Not possible")
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 1604
|
||||
|
||||
def is_transient_error(self, ex) -> bool:
|
||||
str_error: str = str(ex).lower()
|
||||
if "failed to get output distribution" in str_error:
|
||||
return True
|
||||
return super().is_transient_error(ex)
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(network)
|
||||
|
||||
self._addr_prefix = self.chainparams_network()['address_prefix']
|
||||
self._addr_prefix = self.chainparams_network()["address_prefix"]
|
||||
|
||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
||||
self._restore_height = coin_settings.get('restore_height', 0)
|
||||
self.setFeePriority(coin_settings.get('fee_priority', 0))
|
||||
self.blocks_confirmed = coin_settings["blocks_confirmed"]
|
||||
self._restore_height = coin_settings.get("restore_height", 0)
|
||||
self.setFeePriority(coin_settings.get("fee_priority", 0))
|
||||
self._sc = swap_client
|
||||
self._log = self._sc.log if self._sc and self._sc.log else logging
|
||||
self._wallet_password = None
|
||||
self._have_checked_seed = False
|
||||
|
||||
daemon_login = None
|
||||
if coin_settings.get('rpcuser', '') != '':
|
||||
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
|
||||
if coin_settings.get("rpcuser", "") != "":
|
||||
daemon_login = (
|
||||
coin_settings.get("rpcuser", ""),
|
||||
coin_settings.get("rpcpassword", ""),
|
||||
)
|
||||
|
||||
rpchost = coin_settings.get('rpchost', '127.0.0.1')
|
||||
rpchost = coin_settings.get("rpchost", "127.0.0.1")
|
||||
proxy_host = None
|
||||
proxy_port = None
|
||||
# Connect to the daemon over a proxy if not running locally
|
||||
if swap_client:
|
||||
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
|
||||
manage_daemon: bool = chain_client_settings['manage_daemon']
|
||||
manage_daemon: bool = chain_client_settings["manage_daemon"]
|
||||
if swap_client.use_tor_proxy:
|
||||
if manage_daemon is False:
|
||||
log_str: str = ''
|
||||
have_cc_tor_opt = 'use_tor' in chain_client_settings
|
||||
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
|
||||
log_str = ' bypassing proxy (use_tor false for XMR)'
|
||||
log_str: str = ""
|
||||
have_cc_tor_opt = "use_tor" in chain_client_settings
|
||||
if have_cc_tor_opt and chain_client_settings["use_tor"] is False:
|
||||
log_str = (
|
||||
f" bypassing proxy (use_tor false for {self.coin_name()})"
|
||||
)
|
||||
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
|
||||
log_str = ' bypassing proxy (private ip address)'
|
||||
log_str = " bypassing proxy (private ip address)"
|
||||
else:
|
||||
proxy_host = swap_client.tor_proxy_host
|
||||
proxy_port = swap_client.tor_proxy_port
|
||||
log_str = f' through proxy at {proxy_host}'
|
||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
|
||||
log_str = f" through proxy at {proxy_host}"
|
||||
self._log.info(
|
||||
f"Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}."
|
||||
)
|
||||
else:
|
||||
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
|
||||
self._log.info(
|
||||
f"Not connecting to local {self.coin_name()} daemon through proxy."
|
||||
)
|
||||
elif manage_daemon is False:
|
||||
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
|
||||
self._log.info(
|
||||
f"Connecting to remote {self.coin_name()} daemon at {rpchost}."
|
||||
)
|
||||
|
||||
self._rpctimeout = coin_settings.get('rpctimeout', 60)
|
||||
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
|
||||
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600)
|
||||
self._rpctimeout = coin_settings.get("rpctimeout", 60)
|
||||
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
|
||||
# walletrpctimeoutlong likely unneeded
|
||||
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
|
||||
|
||||
self.rpc = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ')
|
||||
self.rpc2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint
|
||||
self.rpc_wallet = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ')
|
||||
self.rpc = make_xmr_rpc_func(
|
||||
coin_settings["rpcport"],
|
||||
daemon_login,
|
||||
host=rpchost,
|
||||
proxy_host=proxy_host,
|
||||
proxy_port=proxy_port,
|
||||
default_timeout=self._rpctimeout,
|
||||
tag="Node(j) ",
|
||||
)
|
||||
self.rpc2 = make_xmr_rpc2_func(
|
||||
coin_settings["rpcport"],
|
||||
daemon_login,
|
||||
host=rpchost,
|
||||
proxy_host=proxy_host,
|
||||
proxy_port=proxy_port,
|
||||
default_timeout=self._rpctimeout,
|
||||
tag="Node ",
|
||||
) # non-json endpoint
|
||||
self.rpc_wallet = make_xmr_rpc_func(
|
||||
coin_settings["walletrpcport"],
|
||||
coin_settings["walletrpcauth"],
|
||||
host=coin_settings.get("walletrpchost", "127.0.0.1"),
|
||||
default_timeout=self._walletrpctimeout,
|
||||
tag="Wallet ",
|
||||
)
|
||||
|
||||
def setFeePriority(self, new_priority):
|
||||
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
|
||||
ensure(new_priority >= 0 and new_priority < 4, "Invalid fee_priority value")
|
||||
self._fee_priority = new_priority
|
||||
|
||||
def setWalletFilename(self, wallet_filename):
|
||||
@@ -142,29 +175,46 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def createWallet(self, params):
|
||||
if self._wallet_password is not None:
|
||||
params['password'] = self._wallet_password
|
||||
rv = self.rpc_wallet('generate_from_keys', params)
|
||||
self._log.info('generate_from_keys %s', dumpj(rv))
|
||||
params["password"] = self._wallet_password
|
||||
rv = self.rpc_wallet("generate_from_keys", params)
|
||||
self._log.info("generate_from_keys %s", dumpj(rv))
|
||||
|
||||
def openWallet(self, filename):
|
||||
params = {'filename': filename}
|
||||
params = {"filename": filename}
|
||||
if self._wallet_password is not None:
|
||||
params['password'] = self._wallet_password
|
||||
params["password"] = self._wallet_password
|
||||
|
||||
try:
|
||||
# Can't reopen the same wallet in windows, !is_keys_file_locked()
|
||||
self.rpc_wallet('close_wallet')
|
||||
except Exception:
|
||||
pass
|
||||
self.rpc_wallet('open_wallet', params)
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||
self.rpc_wallet("refresh")
|
||||
except Exception as e:
|
||||
if "no connection to daemon" in str(e):
|
||||
self._log.debug(f"{self.coin_name()} {e}")
|
||||
return # bypass refresh error to allow startup with a busy daemon
|
||||
|
||||
def initialiseWallet(self, key_view: bytes, key_spend: bytes, restore_height=None) -> None:
|
||||
try:
|
||||
# TODO Remove `store` after upstream fix to autosave on close_wallet
|
||||
self.rpc_wallet("store")
|
||||
self.rpc_wallet("close_wallet")
|
||||
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
|
||||
|
||||
def initialiseWallet(
|
||||
self, key_view: bytes, key_spend: bytes, restore_height=None
|
||||
) -> None:
|
||||
with self._mx_wallet:
|
||||
try:
|
||||
self.openWallet(self._wallet_filename)
|
||||
# TODO: Check address
|
||||
return # Wallet exists
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
Kbv = self.getPubkey(key_view)
|
||||
@@ -172,11 +222,11 @@ class XMRInterface(CoinInterface):
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
params = {
|
||||
'filename': self._wallet_filename,
|
||||
'address': address_b58,
|
||||
'viewkey': b2h(key_view[::-1]),
|
||||
'spendkey': b2h(key_spend[::-1]),
|
||||
'restore_height': self._restore_height,
|
||||
"filename": self._wallet_filename,
|
||||
"address": address_b58,
|
||||
"viewkey": b2h(key_view[::-1]),
|
||||
"spendkey": b2h(key_spend[::-1]),
|
||||
"restore_height": self._restore_height,
|
||||
}
|
||||
self.createWallet(params)
|
||||
self.openWallet(self._wallet_filename)
|
||||
@@ -186,84 +236,95 @@ class XMRInterface(CoinInterface):
|
||||
self.openWallet(self._wallet_filename)
|
||||
|
||||
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||
self.rpc_wallet('get_languages')
|
||||
self.rpc_wallet("get_languages")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet('get_version')['version']
|
||||
return self.rpc_wallet("get_version")["version"]
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
rv = {
|
||||
'blocks': get_height['height'],
|
||||
'verificationprogress': 0.0,
|
||||
"blocks": get_height["height"],
|
||||
"verificationprogress": 0.0,
|
||||
}
|
||||
|
||||
try:
|
||||
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
|
||||
# get_block_count returns "Internal error" if bootstrap-daemon is active
|
||||
if get_height['untrusted'] is True:
|
||||
rv['bootstrapping'] = True
|
||||
get_info = self.rpc2('get_info', timeout=self._rpctimeout)
|
||||
if 'height_without_bootstrap' in get_info:
|
||||
rv['blocks'] = get_info['height_without_bootstrap']
|
||||
if get_height["untrusted"] is True:
|
||||
rv["bootstrapping"] = True
|
||||
get_info = self.rpc2("get_info", timeout=self._rpctimeout)
|
||||
if "height_without_bootstrap" in get_info:
|
||||
rv["blocks"] = get_info["height_without_bootstrap"]
|
||||
|
||||
rv['known_block_count'] = get_info['height']
|
||||
if rv['known_block_count'] > rv['blocks']:
|
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||
rv["known_block_count"] = get_info["height"]
|
||||
if rv["known_block_count"] > rv["blocks"]:
|
||||
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
|
||||
else:
|
||||
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
|
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||
rv["known_block_count"] = self.rpc(
|
||||
"get_block_count", timeout=self._rpctimeout
|
||||
)["count"]
|
||||
rv["verificationprogress"] = rv["blocks"] / rv["known_block_count"]
|
||||
except Exception as e:
|
||||
self._log.warning(f'{self.ticker_str()} get_block_count failed with: {e}')
|
||||
rv['verificationprogress'] = 0.0
|
||||
self._log.warning(f"{self.ticker_str()} get_block_count failed with: {e}")
|
||||
rv["verificationprogress"] = 0.0
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
try:
|
||||
self.openWallet(self._wallet_filename)
|
||||
except Exception as e:
|
||||
if 'Failed to open wallet' in str(e):
|
||||
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0}
|
||||
if "Failed to open wallet" in str(e):
|
||||
rv = {
|
||||
"encrypted": True,
|
||||
"locked": True,
|
||||
"balance": 0,
|
||||
"unconfirmed_balance": 0,
|
||||
}
|
||||
return rv
|
||||
raise e
|
||||
|
||||
rv = {}
|
||||
self.rpc_wallet('refresh')
|
||||
balance_info = self.rpc_wallet('get_balance')
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
|
||||
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
|
||||
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
|
||||
rv['encrypted'] = False if self._wallet_password is None else True
|
||||
rv['locked'] = False
|
||||
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
|
||||
rv["balance"] = self.format_amount(balance_info["unlocked_balance"])
|
||||
rv["unconfirmed_balance"] = self.format_amount(
|
||||
balance_info["balance"] - balance_info["unlocked_balance"]
|
||||
)
|
||||
rv["encrypted"] = False if self._wallet_password is None else True
|
||||
rv["locked"] = False
|
||||
return rv
|
||||
|
||||
def getMainWalletAddress(self) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
return self.rpc_wallet('get_address')['address']
|
||||
return self.rpc_wallet("get_address")["address"]
|
||||
|
||||
def getNewAddress(self, placeholder) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
|
||||
self.rpc_wallet('store')
|
||||
new_address = self.rpc_wallet("create_address", {"account_index": 0})[
|
||||
"address"
|
||||
]
|
||||
self.rpc_wallet("store")
|
||||
return new_address
|
||||
|
||||
def get_fee_rate(self, conf_target: int = 2):
|
||||
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
|
||||
fee_est = self.rpc('get_fee_estimate')
|
||||
fee_est = self.rpc("get_fee_estimate")
|
||||
if conf_target <= 1:
|
||||
conf_target = 1 # normal
|
||||
else:
|
||||
conf_target = 0 # slow
|
||||
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000
|
||||
fee_per_k_bytes = fee_est["fees"][conf_target] * 1000
|
||||
|
||||
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate'
|
||||
return float(self.format_amount(fee_per_k_bytes)), "get_fee_estimate"
|
||||
|
||||
def getNewSecretKey(self) -> bytes:
|
||||
# Note: Returned bytes are in big endian order
|
||||
@@ -294,7 +355,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def verifyKey(self, k: int) -> bool:
|
||||
i = b2i(k)
|
||||
return (i < edf.l and i > 8)
|
||||
return i < edf.l and i > 8
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes):
|
||||
# Calls ed25519_decode_check_point() in secp256k1
|
||||
@@ -320,45 +381,56 @@ class XMRInterface(CoinInterface):
|
||||
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
||||
return xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(
|
||||
self,
|
||||
kbv: bytes,
|
||||
Kbs: bytes,
|
||||
output_amount: int,
|
||||
feerate: int,
|
||||
unlock_time: int = 0,
|
||||
) -> bytes:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet('refresh')
|
||||
|
||||
Kbv = self.getPubkey(kbv)
|
||||
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
|
||||
params = {
|
||||
"destinations": [{"amount": output_amount, "address": shared_addr}],
|
||||
"unlock_time": unlock_time,
|
||||
}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet('transfer', params)
|
||||
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
|
||||
tx_hash = bytes.fromhex(rv['tx_hash'])
|
||||
params["priority"] = self._fee_priority
|
||||
rv = self.rpc_wallet("transfer", params)
|
||||
self._log.info(
|
||||
"publishBLockTx %s to address_b58 %s", rv["tx_hash"], shared_addr
|
||||
)
|
||||
tx_hash = bytes.fromhex(rv["tx_hash"])
|
||||
|
||||
return tx_hash
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
):
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
kbv_le = kbv[::-1]
|
||||
params = {
|
||||
'restore_height': restore_height,
|
||||
'filename': address_b58,
|
||||
'address': address_b58,
|
||||
'viewkey': b2h(kbv_le),
|
||||
"restore_height": restore_height,
|
||||
"filename": address_b58,
|
||||
"address": address_b58,
|
||||
"viewkey": b2h(kbv_le),
|
||||
}
|
||||
|
||||
try:
|
||||
self.openWallet(address_b58)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: F841
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||
|
||||
'''
|
||||
"""
|
||||
# Debug
|
||||
try:
|
||||
current_height = self.rpc_wallet('get_height')['height']
|
||||
@@ -367,136 +439,219 @@ class XMRInterface(CoinInterface):
|
||||
self._log.info('rpc failed %s', str(e))
|
||||
current_height = None # If the transfer is available it will be deep enough
|
||||
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
|
||||
'''
|
||||
params = {'transfer_type': 'available'}
|
||||
transfers = self.rpc_wallet('incoming_transfers', params)
|
||||
"""
|
||||
params = {"transfer_type": "available"}
|
||||
transfers = self.rpc_wallet("incoming_transfers", params)
|
||||
rv = None
|
||||
if 'transfers' in transfers:
|
||||
for transfer in transfers['transfers']:
|
||||
if "transfers" in transfers:
|
||||
for transfer in transfers["transfers"]:
|
||||
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
|
||||
if not transfer['unlocked']:
|
||||
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
|
||||
unlock_time = full_tx['transfer']['unlock_time']
|
||||
if not transfer["unlocked"]:
|
||||
full_tx = self.rpc_wallet(
|
||||
"get_transfer_by_txid", {"txid": transfer["tx_hash"]}
|
||||
)
|
||||
unlock_time = full_tx["transfer"]["unlock_time"]
|
||||
if unlock_time != 0:
|
||||
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
|
||||
self._log.warning(
|
||||
"Coin b lock txn is locked: {}, unlock_time {}".format(
|
||||
transfer["tx_hash"], unlock_time
|
||||
)
|
||||
)
|
||||
rv = -1
|
||||
continue
|
||||
if transfer['amount'] == cb_swap_value:
|
||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
|
||||
if transfer["amount"] == cb_swap_value:
|
||||
return {
|
||||
"txid": transfer["tx_hash"],
|
||||
"amount": transfer["amount"],
|
||||
"height": (
|
||||
0
|
||||
if "block_height" not in transfer
|
||||
else transfer["block_height"]
|
||||
),
|
||||
}
|
||||
else:
|
||||
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
|
||||
self._log.warning(
|
||||
"Incorrect amount detected for coin b lock txn: {}".format(
|
||||
transfer["tx_hash"]
|
||||
)
|
||||
)
|
||||
rv = -1
|
||||
return rv
|
||||
|
||||
def findTxnByHash(self, txid):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
|
||||
|
||||
try:
|
||||
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
|
||||
self._log.info(f'findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}')
|
||||
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
|
||||
"height"
|
||||
]
|
||||
self._log.info(
|
||||
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
|
||||
)
|
||||
except Exception as e:
|
||||
self._log.info('rpc failed %s', str(e))
|
||||
current_height = None # If the transfer is available it will be deep enough
|
||||
self._log.info("rpc failed %s", str(e))
|
||||
current_height = (
|
||||
None # If the transfer is available it will be deep enough
|
||||
)
|
||||
|
||||
params = {'transfer_type': 'available'}
|
||||
rv = self.rpc_wallet('incoming_transfers', params)
|
||||
if 'transfers' in rv:
|
||||
for transfer in rv['transfers']:
|
||||
if transfer['tx_hash'] == txid \
|
||||
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed):
|
||||
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']}
|
||||
params = {"transfer_type": "available"}
|
||||
rv = self.rpc_wallet("incoming_transfers", params)
|
||||
if "transfers" in rv:
|
||||
for transfer in rv["transfers"]:
|
||||
if transfer["tx_hash"] == txid and (
|
||||
current_height is None
|
||||
or current_height - transfer["block_height"]
|
||||
> self.blocks_confirmed
|
||||
):
|
||||
return {
|
||||
"txid": transfer["tx_hash"],
|
||||
"amount": transfer["amount"],
|
||||
"height": transfer["block_height"],
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False, lock_tx_vout=None) -> bytes:
|
||||
'''
|
||||
def spendBLockTx(
|
||||
self,
|
||||
chain_b_lock_txid: bytes,
|
||||
address_to: str,
|
||||
kbv: bytes,
|
||||
kbs: bytes,
|
||||
cb_swap_value: int,
|
||||
b_fee_rate: int,
|
||||
restore_height: int,
|
||||
spend_actual_balance: bool = False,
|
||||
lock_tx_vout=None,
|
||||
) -> bytes:
|
||||
"""
|
||||
Notes:
|
||||
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
|
||||
'''
|
||||
"""
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
Kbs = self.getPubkey(kbs)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
|
||||
wallet_filename = address_b58 + '_spend'
|
||||
wallet_filename = address_b58 + "_spend"
|
||||
|
||||
params = {
|
||||
'filename': wallet_filename,
|
||||
'address': address_b58,
|
||||
'viewkey': b2h(kbv[::-1]),
|
||||
'spendkey': b2h(kbs[::-1]),
|
||||
'restore_height': restore_height,
|
||||
"filename": wallet_filename,
|
||||
"address": address_b58,
|
||||
"viewkey": b2h(kbv[::-1]),
|
||||
"spendkey": b2h(kbs[::-1]),
|
||||
"restore_height": restore_height,
|
||||
}
|
||||
|
||||
try:
|
||||
self.openWallet(wallet_filename)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: F841
|
||||
self.createWallet(params)
|
||||
self.openWallet(wallet_filename)
|
||||
|
||||
self.rpc_wallet('refresh')
|
||||
rv = self.rpc_wallet('get_balance')
|
||||
if rv['balance'] < cb_swap_value:
|
||||
self._log.warning('Balance is too low, checking for existing spend.')
|
||||
txns = self.rpc_wallet('get_transfers', {'out': True})
|
||||
if 'out' in txns:
|
||||
txns = txns['out']
|
||||
rv = self.rpc_wallet("get_balance")
|
||||
if rv["balance"] < cb_swap_value:
|
||||
self._log.warning("Balance is too low, checking for existing spend.")
|
||||
txns = self.rpc_wallet("get_transfers", {"out": True})
|
||||
if "out" in txns:
|
||||
txns = txns["out"]
|
||||
if len(txns) > 0:
|
||||
txid = txns[0]['txid']
|
||||
self._log.warning(f'spendBLockTx detected spending tx: {txid}.')
|
||||
if txns[0]['address'] == address_b58:
|
||||
txid = txns[0]["txid"]
|
||||
self._log.warning(f"spendBLockTx detected spending tx: {txid}.")
|
||||
|
||||
# Should check for address_to, but only the from address is found in the output
|
||||
if txns[0]["address"] == address_b58:
|
||||
return bytes.fromhex(txid)
|
||||
|
||||
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
|
||||
self._log.error(
|
||||
"wallet {} balance {}, expected {}".format(
|
||||
wallet_filename, rv["balance"], cb_swap_value
|
||||
)
|
||||
)
|
||||
|
||||
if not spend_actual_balance:
|
||||
raise TemporaryError('Invalid balance')
|
||||
raise TemporaryError("Invalid balance")
|
||||
|
||||
if spend_actual_balance and rv['balance'] != cb_swap_value:
|
||||
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value))
|
||||
cb_swap_value = rv['balance']
|
||||
if rv['unlocked_balance'] < cb_swap_value:
|
||||
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock']))
|
||||
raise TemporaryError('Invalid unlocked_balance')
|
||||
if spend_actual_balance and rv["balance"] != cb_swap_value:
|
||||
self._log.warning(
|
||||
"Spending actual balance {}, not swap value {}.".format(
|
||||
rv["balance"], cb_swap_value
|
||||
)
|
||||
)
|
||||
cb_swap_value = rv["balance"]
|
||||
if rv["unlocked_balance"] < cb_swap_value:
|
||||
self._log.error(
|
||||
"wallet {} balance {}, expected {}, blocks_to_unlock {}".format(
|
||||
wallet_filename,
|
||||
rv["unlocked_balance"],
|
||||
cb_swap_value,
|
||||
rv["blocks_to_unlock"],
|
||||
)
|
||||
)
|
||||
raise TemporaryError("Invalid unlocked_balance")
|
||||
|
||||
params = {'address': address_to}
|
||||
params = {"address": address_to}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
params["priority"] = self._fee_priority
|
||||
|
||||
rv = self.rpc_wallet('sweep_all', params)
|
||||
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
|
||||
rv = self.rpc_wallet("sweep_all", params)
|
||||
|
||||
return bytes.fromhex(rv['tx_hash_list'][0])
|
||||
return bytes.fromhex(rv["tx_hash_list"][0])
|
||||
|
||||
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str:
|
||||
def withdrawCoin(
|
||||
self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False
|
||||
) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet('refresh')
|
||||
|
||||
if sweepall:
|
||||
balance = self.rpc_wallet('get_balance')
|
||||
if balance['balance'] != balance['unlocked_balance']:
|
||||
raise ValueError('Balance must be fully confirmed to use sweep all.')
|
||||
self._log.info('{} {} sweep_all.'.format(self.ticker_str(), 'estimate fee' if estimate_fee else 'withdraw'))
|
||||
self._log.debug('{} balance: {}'.format(self.ticker_str(), balance['balance']))
|
||||
params = {'address': addr_to, 'do_not_relay': estimate_fee}
|
||||
balance = self.rpc_wallet("get_balance")
|
||||
if balance["balance"] != balance["unlocked_balance"]:
|
||||
raise ValueError(
|
||||
"Balance must be fully confirmed to use sweep all."
|
||||
)
|
||||
self._log.info(
|
||||
"{} {} sweep_all.".format(
|
||||
self.ticker_str(),
|
||||
"estimate fee" if estimate_fee else "withdraw",
|
||||
)
|
||||
)
|
||||
self._log.debug(
|
||||
"{} balance: {}".format(self.ticker_str(), balance["balance"])
|
||||
)
|
||||
params = {
|
||||
"address": addr_to,
|
||||
"do_not_relay": estimate_fee,
|
||||
"subaddr_indices_all": True,
|
||||
}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet('sweep_all', params)
|
||||
params["priority"] = self._fee_priority
|
||||
rv = self.rpc_wallet("sweep_all", params)
|
||||
if estimate_fee:
|
||||
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])}
|
||||
return rv['tx_hash_list'][0]
|
||||
return {
|
||||
"num_txns": len(rv["fee_list"]),
|
||||
"sum_amount": sum(rv["amount_list"]),
|
||||
"sum_fee": sum(rv["fee_list"]),
|
||||
"sum_weight": sum(rv["weight_list"]),
|
||||
}
|
||||
return rv["tx_hash_list"][0]
|
||||
|
||||
value_sats: int = self.make_int(value)
|
||||
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee}
|
||||
params = {
|
||||
"destinations": [{"amount": value_sats, "address": addr_to}],
|
||||
"do_not_relay": estimate_fee,
|
||||
}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet('transfer', params)
|
||||
params["priority"] = self._fee_priority
|
||||
rv = self.rpc_wallet("transfer", params)
|
||||
if estimate_fee:
|
||||
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']}
|
||||
return rv['tx_hash']
|
||||
return {
|
||||
"num_txns": 1,
|
||||
"sum_amount": rv["amount"],
|
||||
"sum_fee": rv["fee"],
|
||||
"sum_weight": rv["weight"],
|
||||
}
|
||||
return rv["tx_hash"]
|
||||
|
||||
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
|
||||
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
|
||||
@@ -506,7 +661,7 @@ class XMRInterface(CoinInterface):
|
||||
try:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
wallet_file = address_b58 + '_spend'
|
||||
wallet_file = address_b58 + "_spend"
|
||||
try:
|
||||
self.openWallet(wallet_file)
|
||||
except Exception:
|
||||
@@ -514,54 +669,59 @@ class XMRInterface(CoinInterface):
|
||||
try:
|
||||
self.openWallet(wallet_file)
|
||||
except Exception:
|
||||
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.')
|
||||
self._log.info(
|
||||
f"showLockTransfers trying to create wallet for address {address_b58}."
|
||||
)
|
||||
kbv_le = kbv[::-1]
|
||||
params = {
|
||||
'restore_height': restore_height,
|
||||
'filename': address_b58,
|
||||
'address': address_b58,
|
||||
'viewkey': b2h(kbv_le),
|
||||
"restore_height": restore_height,
|
||||
"filename": address_b58,
|
||||
"address": address_b58,
|
||||
"viewkey": b2h(kbv_le),
|
||||
}
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet('refresh')
|
||||
|
||||
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
|
||||
rv['filename'] = wallet_file
|
||||
rv = self.rpc_wallet(
|
||||
"get_transfers",
|
||||
{"in": True, "out": True, "pending": True, "failed": True},
|
||||
)
|
||||
rv["filename"] = wallet_file
|
||||
return rv
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
return {"error": str(e)}
|
||||
|
||||
def getSpendableBalance(self) -> int:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
|
||||
self.rpc_wallet('refresh')
|
||||
balance_info = self.rpc_wallet('get_balance')
|
||||
return balance_info['unlocked_balance']
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
return balance_info["unlocked_balance"]
|
||||
|
||||
def changeWalletPassword(self, old_password, new_password):
|
||||
self._log.info('changeWalletPassword - {}'.format(self.ticker()))
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
orig_password = self._wallet_password
|
||||
if old_password != '':
|
||||
if old_password != "":
|
||||
self._wallet_password = old_password
|
||||
try:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
|
||||
self.rpc_wallet(
|
||||
"change_wallet_password",
|
||||
{"old_password": old_password, "new_password": new_password},
|
||||
)
|
||||
except Exception as e:
|
||||
self._wallet_password = orig_password
|
||||
raise e
|
||||
|
||||
def unlockWallet(self, password: str) -> None:
|
||||
self._log.info('unlockWallet - {}'.format(self.ticker()))
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
self._wallet_password = password
|
||||
|
||||
if not self._have_checked_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self) -> None:
|
||||
self._log.info('lockWallet - {}'.format(self.ticker()))
|
||||
self._log.info("lockWallet - {}".format(self.ticker()))
|
||||
self._wallet_password = None
|
||||
|
||||
def isAddressMine(self, address):
|
||||
@@ -570,7 +730,14 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def ensureFunds(self, amount: int) -> None:
|
||||
if self.getSpendableBalance() < amount:
|
||||
raise ValueError('Balance too low')
|
||||
raise ValueError("Balance too low")
|
||||
|
||||
def getTransaction(self, txid: bytes):
|
||||
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
||||
return self.rpc2(
|
||||
"get_transactions",
|
||||
{
|
||||
"txs_hashes": [
|
||||
txid.hex(),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
syntax = "proto3";
|
||||
|
||||
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||
@@ -18,12 +18,12 @@ Don't encode fields of default values.
|
||||
When decoding initialise all fields not set from data.
|
||||
|
||||
protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||
'''
|
||||
"""
|
||||
|
||||
from basicswap.util.integer import encode_varint, decode_varint
|
||||
|
||||
|
||||
class NonProtobufClass():
|
||||
class NonProtobufClass:
|
||||
def __init__(self, init_all: bool = True, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
found_field: bool = False
|
||||
@@ -34,7 +34,7 @@ class NonProtobufClass():
|
||||
found_field = True
|
||||
break
|
||||
if found_field is False:
|
||||
raise ValueError(f'got an unexpected keyword argument \'{key}\'')
|
||||
raise ValueError(f"got an unexpected keyword argument '{key}'")
|
||||
|
||||
if init_all:
|
||||
self.init_fields()
|
||||
@@ -53,7 +53,7 @@ class NonProtobufClass():
|
||||
else:
|
||||
setattr(self, field_name, bytes())
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
rv = bytes()
|
||||
@@ -74,11 +74,11 @@ class NonProtobufClass():
|
||||
continue
|
||||
rv += encode_varint(tag)
|
||||
if isinstance(field_value, str):
|
||||
field_value = field_value.encode('utf-8')
|
||||
field_value = field_value.encode("utf-8")
|
||||
rv += encode_varint(len(field_value))
|
||||
rv += field_value
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||
return rv
|
||||
|
||||
def from_bytes(self, b: bytes, init_all: bool = True) -> None:
|
||||
@@ -92,7 +92,9 @@ class NonProtobufClass():
|
||||
|
||||
field_name, wire_type_expect, field_type = self._map[field_num]
|
||||
if wire_type != wire_type_expect:
|
||||
raise ValueError(f'Unexpected wire_type {wire_type} for field {field_num}')
|
||||
raise ValueError(
|
||||
f"Unexpected wire_type {wire_type} for field {field_num}"
|
||||
)
|
||||
|
||||
if wire_type == 0:
|
||||
field_value, lv = decode_varint(b, o)
|
||||
@@ -100,12 +102,12 @@ class NonProtobufClass():
|
||||
elif wire_type == 2:
|
||||
field_len, lv = decode_varint(b, o)
|
||||
o += lv
|
||||
field_value = b[o: o + field_len]
|
||||
field_value = b[o : o + field_len]
|
||||
o += field_len
|
||||
if field_type == 1:
|
||||
field_value = field_value.decode('utf-8')
|
||||
field_value = field_value.decode("utf-8")
|
||||
else:
|
||||
raise ValueError(f'Unknown wire_type {wire_type}')
|
||||
raise ValueError(f"Unknown wire_type {wire_type}")
|
||||
|
||||
setattr(self, field_name, field_value)
|
||||
|
||||
@@ -115,151 +117,150 @@ class NonProtobufClass():
|
||||
|
||||
class OfferMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('coin_from', 0, 0),
|
||||
3: ('coin_to', 0, 0),
|
||||
4: ('amount_from', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('min_bid_amount', 0, 0),
|
||||
7: ('time_valid', 0, 0),
|
||||
8: ('lock_type', 0, 0),
|
||||
9: ('lock_value', 0, 0),
|
||||
10: ('swap_type', 0, 0),
|
||||
11: ('proof_address', 2, 1),
|
||||
12: ('proof_signature', 2, 1),
|
||||
13: ('pkhash_seller', 2, 0),
|
||||
14: ('secret_hash', 2, 0),
|
||||
15: ('fee_rate_from', 0, 0),
|
||||
16: ('fee_rate_to', 0, 0),
|
||||
17: ('amount_negotiable', 0, 2),
|
||||
18: ('rate_negotiable', 0, 2),
|
||||
19: ('proof_utxos', 2, 0),
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("coin_from", 0, 0),
|
||||
3: ("coin_to", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("min_bid_amount", 0, 0),
|
||||
7: ("time_valid", 0, 0),
|
||||
8: ("lock_type", 0, 0),
|
||||
9: ("lock_value", 0, 0),
|
||||
10: ("swap_type", 0, 0),
|
||||
11: ("proof_address", 2, 1),
|
||||
12: ("proof_signature", 2, 1),
|
||||
13: ("pkhash_seller", 2, 0),
|
||||
14: ("secret_hash", 2, 0),
|
||||
15: ("fee_rate_from", 0, 0),
|
||||
16: ("fee_rate_to", 0, 0),
|
||||
17: ("amount_negotiable", 0, 2),
|
||||
18: ("rate_negotiable", 0, 2),
|
||||
19: ("proof_utxos", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('pkhash_buyer', 2, 0),
|
||||
7: ('proof_address', 2, 1),
|
||||
8: ('proof_signature', 2, 1),
|
||||
9: ('proof_utxos', 2, 0),
|
||||
10: ('pkhash_buyer_to', 2, 0),
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkhash_buyer", 2, 0),
|
||||
7: ("proof_address", 2, 1),
|
||||
8: ("proof_signature", 2, 1),
|
||||
9: ("proof_utxos", 2, 0),
|
||||
10: ("pkhash_buyer_to", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidAcceptMessage(NonProtobufClass):
|
||||
# Step 3, seller -> buyer
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('initiate_txid', 2, 0),
|
||||
3: ('contract_script', 2, 0),
|
||||
4: ('pkhash_seller', 2, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("initiate_txid", 2, 0),
|
||||
3: ("contract_script", 2, 0),
|
||||
4: ("pkhash_seller", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class OfferRevokeMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('offer_msg_id', 2, 0),
|
||||
2: ('signature', 2, 0),
|
||||
1: ("offer_msg_id", 2, 0),
|
||||
2: ("signature", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidRejectMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('reject_code', 0, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("reject_code", 0, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidMessage(NonProtobufClass):
|
||||
# MSG1L, F -> L
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
6: ('pkaf', 2, 0),
|
||||
7: ('kbvf', 2, 0),
|
||||
8: ('kbsf_dleag', 2, 0),
|
||||
9: ('dest_af', 2, 0),
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkaf", 2, 0),
|
||||
7: ("kbvf", 2, 0),
|
||||
8: ("kbsf_dleag", 2, 0),
|
||||
9: ("dest_af", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrSplitMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('msg_id', 2, 0),
|
||||
2: ('msg_type', 0, 0),
|
||||
3: ('sequence', 0, 0),
|
||||
4: ('dleag', 2, 0),
|
||||
1: ("msg_id", 2, 0),
|
||||
2: ("msg_type", 0, 0),
|
||||
3: ("sequence", 0, 0),
|
||||
4: ("dleag", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidAcceptMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('pkal', 2, 0),
|
||||
3: ('kbvl', 2, 0),
|
||||
4: ('kbsl_dleag', 2, 0),
|
||||
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkal", 2, 0),
|
||||
3: ("kbvl", 2, 0),
|
||||
4: ("kbsl_dleag", 2, 0),
|
||||
# MSG2F
|
||||
5: ('a_lock_tx', 2, 0),
|
||||
6: ('a_lock_tx_script', 2, 0),
|
||||
7: ('a_lock_refund_tx', 2, 0),
|
||||
8: ('a_lock_refund_tx_script', 2, 0),
|
||||
9: ('a_lock_refund_spend_tx', 2, 0),
|
||||
10: ('al_lock_refund_tx_sig', 2, 0),
|
||||
5: ("a_lock_tx", 2, 0),
|
||||
6: ("a_lock_tx_script", 2, 0),
|
||||
7: ("a_lock_refund_tx", 2, 0),
|
||||
8: ("a_lock_refund_tx_script", 2, 0),
|
||||
9: ("a_lock_refund_spend_tx", 2, 0),
|
||||
10: ("al_lock_refund_tx_sig", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
||||
# MSG3L
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('af_lock_refund_spend_tx_esig', 2, 0),
|
||||
3: ('af_lock_refund_tx_sig', 2, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", 2, 0),
|
||||
3: ("af_lock_refund_tx_sig", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
||||
# MSG4F
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('a_lock_spend_tx', 2, 0),
|
||||
3: ('kal_sig', 2, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("a_lock_spend_tx", 2, 0),
|
||||
3: ("kal_sig", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||
# MSG5F
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('al_lock_spend_tx_esig', 2, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("al_lock_spend_tx_esig", 2, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentMessage(NonProtobufClass):
|
||||
# L -> F Sent from bidder, construct a reverse bid
|
||||
_map = {
|
||||
1: ('protocol_version', 0, 0),
|
||||
2: ('offer_msg_id', 2, 0),
|
||||
3: ('time_valid', 0, 0),
|
||||
4: ('amount_from', 0, 0),
|
||||
5: ('amount_to', 0, 0),
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
||||
# F -> L Sent from offerer, construct a reverse bid
|
||||
_map = {
|
||||
1: ('bid_msg_id', 2, 0),
|
||||
2: ('pkaf', 2, 0),
|
||||
3: ('kbvf', 2, 0),
|
||||
4: ('kbsf_dleag', 2, 0),
|
||||
5: ('dest_af', 2, 0),
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkaf", 2, 0),
|
||||
3: ("kbvf", 2, 0),
|
||||
4: ("kbsf_dleag", 2, 0),
|
||||
5: ("dest_af", 2, 0),
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
'''
|
||||
"""
|
||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||
|
||||
Handshake procedure:
|
||||
@@ -17,7 +17,7 @@
|
||||
Both nodes are initialised
|
||||
|
||||
XChaCha20_Poly1305 mac is 16bytes
|
||||
'''
|
||||
"""
|
||||
|
||||
import time
|
||||
import queue
|
||||
@@ -36,11 +36,12 @@ from Crypto.Cipher import ChaCha20_Poly1305 # TODO: Add to libsecp256k1/coincur
|
||||
from coincurve.keys import PrivateKey, PublicKey
|
||||
from basicswap.contrib.rfc6979 import (
|
||||
rfc6979_hmac_sha256_initialize,
|
||||
rfc6979_hmac_sha256_generate)
|
||||
rfc6979_hmac_sha256_generate,
|
||||
)
|
||||
|
||||
|
||||
START_TOKEN = 0xabcd
|
||||
MSG_START_TOKEN = START_TOKEN.to_bytes(2, 'big')
|
||||
START_TOKEN = 0xABCD
|
||||
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
|
||||
|
||||
MSG_MAX_SIZE = 0x200000 # 2MB
|
||||
|
||||
@@ -63,49 +64,71 @@ class NetMessageTypes(IntEnum):
|
||||
return value in cls._value2member_map_
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
class NetMessage:
|
||||
def __init__(self):
|
||||
self._msg_class = None # 2 bytes
|
||||
self._len = None # 4 bytes
|
||||
self._msg_type = None # 2 bytes
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
# Ensure handshake keys are not reused by including the time in the msg, mac and key hash
|
||||
# Verify timestamp is not too old
|
||||
# Add keys to db to catch concurrent attempts, records can be cleared periodically, the timestamp should catch older replay attempts
|
||||
class MsgHandshake:
|
||||
__slots__ = ('_timestamp', '_ephem_pk', '_ct', '_mac')
|
||||
__slots__ = ("_timestamp", "_ephem_pk", "_ct", "_mac")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def encode_aad(self): # Additional Authenticated Data
|
||||
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
|
||||
self._timestamp.to_bytes(8, 'big') + \
|
||||
self._ephem_pk
|
||||
return (
|
||||
int(NetMessageTypes.HANDSHAKE).to_bytes(2, "big")
|
||||
+ self._timestamp.to_bytes(8, "big")
|
||||
+ self._ephem_pk
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
return self.encode_aad() + self._ct + self._mac
|
||||
|
||||
def decode(self, msg_mv):
|
||||
o = 2
|
||||
self._timestamp = int.from_bytes(msg_mv[o: o + 8], 'big')
|
||||
self._timestamp = int.from_bytes(msg_mv[o : o + 8], "big")
|
||||
o += 8
|
||||
self._ephem_pk = bytes(msg_mv[o: o + 33])
|
||||
self._ephem_pk = bytes(msg_mv[o : o + 33])
|
||||
o += 33
|
||||
self._ct = bytes(msg_mv[o: -16])
|
||||
self._ct = bytes(msg_mv[o:-16])
|
||||
self._mac = bytes(msg_mv[-16:])
|
||||
|
||||
|
||||
class Peer:
|
||||
__slots__ = (
|
||||
'_mx', '_pubkey', '_address', '_socket', '_version', '_ready', '_incoming',
|
||||
'_connected_at', '_last_received_at', '_bytes_sent', '_bytes_received',
|
||||
'_receiving_length', '_receiving_buffer', '_recv_messages', '_misbehaving_score',
|
||||
'_ke', '_km', '_dir', '_sent_nonce', '_recv_nonce', '_last_handshake_at',
|
||||
'_ping_nonce', '_last_ping_at', '_last_ping_rtt')
|
||||
"_mx",
|
||||
"_pubkey",
|
||||
"_address",
|
||||
"_socket",
|
||||
"_version",
|
||||
"_ready",
|
||||
"_incoming",
|
||||
"_connected_at",
|
||||
"_last_received_at",
|
||||
"_bytes_sent",
|
||||
"_bytes_received",
|
||||
"_receiving_length",
|
||||
"_receiving_buffer",
|
||||
"_recv_messages",
|
||||
"_misbehaving_score",
|
||||
"_ke",
|
||||
"_km",
|
||||
"_dir",
|
||||
"_sent_nonce",
|
||||
"_recv_nonce",
|
||||
"_last_handshake_at",
|
||||
"_ping_nonce",
|
||||
"_last_ping_at",
|
||||
"_last_ping_rtt",
|
||||
)
|
||||
|
||||
def __init__(self, address, socket, pubkey):
|
||||
self._mx = threading.Lock()
|
||||
@@ -141,14 +164,16 @@ def listen_thread(cls):
|
||||
max_bytes = 0x10000
|
||||
while cls._running:
|
||||
# logging.info('[rm] network loop %d', cls._running)
|
||||
readable, writable, errored = select.select(cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout)
|
||||
readable, writable, errored = select.select(
|
||||
cls._read_sockets, cls._write_sockets, cls._error_sockets, timeout
|
||||
)
|
||||
cls._mx.acquire()
|
||||
try:
|
||||
disconnected_peers = []
|
||||
for s in readable:
|
||||
if s == cls._socket:
|
||||
peer_socket, address = cls._socket.accept()
|
||||
logging.info('Connection from %s', address)
|
||||
logging.info("Connection from %s", address)
|
||||
new_peer = Peer(address, peer_socket, None)
|
||||
new_peer._incoming = True
|
||||
cls._peers.append(new_peer)
|
||||
@@ -160,12 +185,12 @@ def listen_thread(cls):
|
||||
try:
|
||||
bytes_recv = s.recv(max_bytes, socket.MSG_DONTWAIT)
|
||||
except socket.error as se:
|
||||
if se.args[0] not in (socket.EWOULDBLOCK, ):
|
||||
logging.error('Receive error %s', str(se))
|
||||
if se.args[0] not in (socket.EWOULDBLOCK,):
|
||||
logging.error("Receive error %s", str(se))
|
||||
disconnected_peers.append(peer)
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error('Receive error %s', str(e))
|
||||
logging.error("Receive error %s", str(e))
|
||||
disconnected_peers.append(peer)
|
||||
continue
|
||||
|
||||
@@ -175,7 +200,7 @@ def listen_thread(cls):
|
||||
cls.receive_bytes(peer, bytes_recv)
|
||||
|
||||
for s in errored:
|
||||
logging.warning('Socket error')
|
||||
logging.warning("Socket error")
|
||||
|
||||
for peer in disconnected_peers:
|
||||
cls.disconnect(peer)
|
||||
@@ -193,7 +218,9 @@ def msg_thread(cls):
|
||||
try:
|
||||
now_us = time.time_ns() // 1000
|
||||
if peer._ready is True:
|
||||
if now_us - peer._last_ping_at >= 5000000: # 5 seconds TODO: Make variable
|
||||
if (
|
||||
now_us - peer._last_ping_at >= 5000000
|
||||
): # 5 seconds TODO: Make variable
|
||||
cls.send_ping(peer)
|
||||
msg = peer._recv_messages.get(False)
|
||||
cls.process_message(peer, msg)
|
||||
@@ -201,7 +228,7 @@ def msg_thread(cls):
|
||||
except queue.Empty:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.warning('process message error %s', str(e))
|
||||
logging.warning("process message error %s", str(e))
|
||||
if cls._sc.debug:
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
@@ -211,9 +238,24 @@ def msg_thread(cls):
|
||||
|
||||
class Network:
|
||||
__slots__ = (
|
||||
'_p2p_host', '_p2p_port', '_network_key', '_network_pubkey',
|
||||
'_sc', '_peers', '_max_connections', '_running', '_network_thread', '_msg_thread',
|
||||
'_mx', '_socket', '_read_sockets', '_write_sockets', '_error_sockets', '_csprng', '_seen_ephem_keys')
|
||||
"_p2p_host",
|
||||
"_p2p_port",
|
||||
"_network_key",
|
||||
"_network_pubkey",
|
||||
"_sc",
|
||||
"_peers",
|
||||
"_max_connections",
|
||||
"_running",
|
||||
"_network_thread",
|
||||
"_msg_thread",
|
||||
"_mx",
|
||||
"_socket",
|
||||
"_read_sockets",
|
||||
"_write_sockets",
|
||||
"_error_sockets",
|
||||
"_csprng",
|
||||
"_seen_ephem_keys",
|
||||
)
|
||||
|
||||
def __init__(self, p2p_host, p2p_port, network_key, swap_client):
|
||||
self._p2p_host = p2p_host
|
||||
@@ -278,7 +320,13 @@ class Network:
|
||||
self._mx.release()
|
||||
|
||||
def add_connection(self, host, port, peer_pubkey):
|
||||
self._sc.log.info('Connecting from %s to %s at %s %d', self._network_pubkey.hex(), peer_pubkey.hex(), host, port)
|
||||
self._sc.log.info(
|
||||
"Connecting from %s to %s at %s %d",
|
||||
self._network_pubkey.hex(),
|
||||
peer_pubkey.hex(),
|
||||
host,
|
||||
port,
|
||||
)
|
||||
self._mx.acquire()
|
||||
try:
|
||||
address = (host, port)
|
||||
@@ -294,7 +342,7 @@ class Network:
|
||||
self.send_handshake(peer)
|
||||
|
||||
def disconnect(self, peer):
|
||||
self._sc.log.info('Closing peer socket %s', peer._address)
|
||||
self._sc.log.info("Closing peer socket %s", peer._address)
|
||||
self._read_sockets.pop(self._read_sockets.index(peer._socket))
|
||||
self._error_sockets.pop(self._error_sockets.index(peer._socket))
|
||||
peer.close()
|
||||
@@ -305,7 +353,11 @@ class Network:
|
||||
|
||||
used = self._seen_ephem_keys.get(ephem_pk)
|
||||
if used:
|
||||
raise ValueError('Handshake ephem_pk reused %s peer %s', 'for' if direction == 1 else 'by', used[0])
|
||||
raise ValueError(
|
||||
"Handshake ephem_pk reused %s peer %s",
|
||||
"for" if direction == 1 else "by",
|
||||
used[0],
|
||||
)
|
||||
|
||||
self._seen_ephem_keys[ephem_pk] = (peer._address, timestamp)
|
||||
|
||||
@@ -313,12 +365,14 @@ class Network:
|
||||
self._seen_ephem_keys.popitem(last=False)
|
||||
|
||||
def send_handshake(self, peer):
|
||||
self._sc.log.debug('send_handshake %s', peer._address)
|
||||
self._sc.log.debug("send_handshake %s", peer._address)
|
||||
peer._mx.acquire()
|
||||
try:
|
||||
# TODO: Drain peer._recv_messages
|
||||
if not peer._recv_messages.empty():
|
||||
self._sc.log.warning('send_handshake %s - Receive queue dumped.', peer._address)
|
||||
self._sc.log.warning(
|
||||
"send_handshake %s - Receive queue dumped.", peer._address
|
||||
)
|
||||
while not peer._recv_messages.empty():
|
||||
peer._recv_messages.get(False)
|
||||
|
||||
@@ -332,7 +386,7 @@ class Network:
|
||||
|
||||
ss = k.ecdh(peer._pubkey)
|
||||
|
||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
|
||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
|
||||
peer._ke = hashed[:32]
|
||||
peer._km = hashed[32:]
|
||||
|
||||
@@ -361,11 +415,13 @@ class Network:
|
||||
peer._mx.release()
|
||||
|
||||
def process_handshake(self, peer, msg_mv):
|
||||
self._sc.log.debug('process_handshake %s', peer._address)
|
||||
self._sc.log.debug("process_handshake %s", peer._address)
|
||||
|
||||
# TODO: Drain peer._recv_messages
|
||||
if not peer._recv_messages.empty():
|
||||
self._sc.log.warning('process_handshake %s - Receive queue dumped.', peer._address)
|
||||
self._sc.log.warning(
|
||||
"process_handshake %s - Receive queue dumped.", peer._address
|
||||
)
|
||||
while not peer._recv_messages.empty():
|
||||
peer._recv_messages.get(False)
|
||||
|
||||
@@ -375,17 +431,19 @@ class Network:
|
||||
try:
|
||||
now = int(time.time())
|
||||
if now - peer._last_handshake_at < 30:
|
||||
raise ValueError('Too many handshakes from peer %s', peer._address)
|
||||
raise ValueError("Too many handshakes from peer %s", peer._address)
|
||||
|
||||
if abs(msg._timestamp - now) > TIMESTAMP_LEEWAY:
|
||||
raise ValueError('Bad handshake timestamp from peer %s', peer._address)
|
||||
raise ValueError("Bad handshake timestamp from peer %s", peer._address)
|
||||
|
||||
self.check_handshake_ephem_key(peer, msg._timestamp, msg._ephem_pk, direction=2)
|
||||
self.check_handshake_ephem_key(
|
||||
peer, msg._timestamp, msg._ephem_pk, direction=2
|
||||
)
|
||||
|
||||
nk = PrivateKey(self._network_key)
|
||||
ss = nk.ecdh(msg._ephem_pk)
|
||||
|
||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
|
||||
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, "big")).digest()
|
||||
peer._ke = hashed[:32]
|
||||
peer._km = hashed[32:]
|
||||
|
||||
@@ -395,7 +453,9 @@ class Network:
|
||||
aad += nonce
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(aad)
|
||||
plaintext = cipher.decrypt_and_verify(msg._ct, msg._mac) # Will raise error if mac doesn't match
|
||||
plaintext = cipher.decrypt_and_verify(
|
||||
msg._ct, msg._mac
|
||||
) # Will raise error if mac doesn't match
|
||||
|
||||
peer._version = plaintext[:6]
|
||||
sig = plaintext[6:]
|
||||
@@ -414,26 +474,30 @@ class Network:
|
||||
|
||||
except Exception as e:
|
||||
# TODO: misbehaving
|
||||
self._sc.log.debug('[rm] process_handshake %s', str(e))
|
||||
self._sc.log.debug("[rm] process_handshake %s", str(e))
|
||||
|
||||
def process_ping(self, peer, msg_mv):
|
||||
nonce = peer._recv_nonce[:24]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_mv[0: 2])
|
||||
cipher.update(msg_mv[0:2])
|
||||
cipher.update(nonce)
|
||||
|
||||
mac = msg_mv[-16:]
|
||||
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
|
||||
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
||||
|
||||
ping_nonce = int.from_bytes(plaintext[:4], 'big')
|
||||
ping_nonce = int.from_bytes(plaintext[:4], "big")
|
||||
# Version is added to a ping following a handshake message
|
||||
if len(plaintext) >= 10:
|
||||
peer._ready = True
|
||||
version = plaintext[4: 10]
|
||||
version = plaintext[4:10]
|
||||
if peer._version is None:
|
||||
peer._version = version
|
||||
self._sc.log.debug('Set version from ping %s, %s', peer._pubkey.hex(), peer._version.hex())
|
||||
self._sc.log.debug(
|
||||
"Set version from ping %s, %s",
|
||||
peer._pubkey.hex(),
|
||||
peer._version.hex(),
|
||||
)
|
||||
|
||||
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
||||
|
||||
@@ -443,32 +507,32 @@ class Network:
|
||||
nonce = peer._recv_nonce[:24]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_mv[0: 2])
|
||||
cipher.update(msg_mv[0:2])
|
||||
cipher.update(nonce)
|
||||
|
||||
mac = msg_mv[-16:]
|
||||
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
|
||||
plaintext = cipher.decrypt_and_verify(msg_mv[2:-16], mac)
|
||||
|
||||
pong_nonce = int.from_bytes(plaintext[:4], 'big')
|
||||
pong_nonce = int.from_bytes(plaintext[:4], "big")
|
||||
|
||||
if pong_nonce == peer._ping_nonce:
|
||||
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
|
||||
else:
|
||||
self._sc.log.debug('Pong received out of order %s', peer._address)
|
||||
self._sc.log.debug("Pong received out of order %s", peer._address)
|
||||
|
||||
peer._recv_nonce = hashlib.sha256(nonce + mac).digest()
|
||||
|
||||
def send_ping(self, peer):
|
||||
ping_nonce = random.getrandbits(32)
|
||||
|
||||
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, 'big')
|
||||
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, "big")
|
||||
nonce = peer._sent_nonce[:24]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_bytes)
|
||||
cipher.update(nonce)
|
||||
|
||||
payload = ping_nonce.to_bytes(4, 'big')
|
||||
payload = ping_nonce.to_bytes(4, "big")
|
||||
if peer._last_ping_at == 0:
|
||||
payload += self._sc._version
|
||||
ct, mac = cipher.encrypt_and_digest(payload)
|
||||
@@ -483,14 +547,14 @@ class Network:
|
||||
self.send_msg(peer, msg_bytes)
|
||||
|
||||
def send_pong(self, peer, ping_nonce):
|
||||
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, 'big')
|
||||
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, "big")
|
||||
nonce = peer._sent_nonce[:24]
|
||||
|
||||
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
|
||||
cipher.update(msg_bytes)
|
||||
cipher.update(nonce)
|
||||
|
||||
payload = ping_nonce.to_bytes(4, 'big')
|
||||
payload = ping_nonce.to_bytes(4, "big")
|
||||
ct, mac = cipher.encrypt_and_digest(payload)
|
||||
msg_bytes += ct + mac
|
||||
|
||||
@@ -502,19 +566,21 @@ class Network:
|
||||
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
|
||||
len_encoded = len(msg_encoded)
|
||||
|
||||
msg_packed = bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, 'big') + msg_encoded
|
||||
msg_packed = (
|
||||
bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, "big") + msg_encoded
|
||||
)
|
||||
peer._socket.sendall(msg_packed)
|
||||
|
||||
peer._bytes_sent += len_encoded
|
||||
|
||||
def process_message(self, peer, msg_bytes):
|
||||
logging.info('[rm] process_message %s len %d', peer._address, len(msg_bytes))
|
||||
logging.info("[rm] process_message %s len %d", peer._address, len(msg_bytes))
|
||||
|
||||
peer._mx.acquire()
|
||||
try:
|
||||
mv = memoryview(msg_bytes)
|
||||
o = 0
|
||||
msg_type = int.from_bytes(mv[o: o + 2], 'big')
|
||||
msg_type = int.from_bytes(mv[o : o + 2], "big")
|
||||
if msg_type == NetMessageTypes.HANDSHAKE:
|
||||
self.process_handshake(peer, mv)
|
||||
elif msg_type == NetMessageTypes.PING:
|
||||
@@ -522,7 +588,7 @@ class Network:
|
||||
elif msg_type == NetMessageTypes.PONG:
|
||||
self.process_pong(peer, mv)
|
||||
else:
|
||||
self._sc.log.debug('Unknown message type %d', msg_type)
|
||||
self._sc.log.debug("Unknown message type %d", msg_type)
|
||||
finally:
|
||||
peer._mx.release()
|
||||
|
||||
@@ -533,7 +599,6 @@ class Network:
|
||||
peer._last_received_at = time.time()
|
||||
peer._bytes_received += len_received
|
||||
|
||||
invalid_msg = False
|
||||
mv = memoryview(bytes_recv)
|
||||
|
||||
o = 0
|
||||
@@ -541,34 +606,34 @@ class Network:
|
||||
while o < len_received:
|
||||
if peer._receiving_length == 0:
|
||||
if len(bytes_recv) < MSG_HEADER_LEN:
|
||||
raise ValueError('Msg too short')
|
||||
raise ValueError("Msg too short")
|
||||
|
||||
if mv[o: o + 2] != MSG_START_TOKEN:
|
||||
raise ValueError('Invalid start token')
|
||||
if mv[o : o + 2] != MSG_START_TOKEN:
|
||||
raise ValueError("Invalid start token")
|
||||
o += 2
|
||||
|
||||
msg_len = int.from_bytes(mv[o: o + 4], 'big')
|
||||
msg_len = int.from_bytes(mv[o : o + 4], "big")
|
||||
o += 4
|
||||
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
|
||||
raise ValueError('Invalid data length')
|
||||
raise ValueError("Invalid data length")
|
||||
|
||||
# Precheck msg_type
|
||||
msg_type = int.from_bytes(mv[o: o + 2], 'big')
|
||||
msg_type = int.from_bytes(mv[o : o + 2], "big")
|
||||
# o += 2 # Don't inc offset, msg includes type
|
||||
if not NetMessageTypes.has_value(msg_type):
|
||||
raise ValueError('Invalid msg type')
|
||||
raise ValueError("Invalid msg type")
|
||||
|
||||
peer._receiving_length = msg_len
|
||||
len_pkt = (len_received - o)
|
||||
len_pkt = len_received - o
|
||||
nc = msg_len if len_pkt > msg_len else len_pkt
|
||||
|
||||
peer._receiving_buffer = mv[o: o + nc]
|
||||
peer._receiving_buffer = mv[o : o + nc]
|
||||
o += nc
|
||||
else:
|
||||
len_to_go = peer._receiving_length - len(peer._receiving_buffer)
|
||||
len_pkt = (len_received - o)
|
||||
len_pkt = len_received - o
|
||||
nc = len_to_go if len_pkt > len_to_go else len_pkt
|
||||
peer._receiving_buffer = mv[o: o + nc]
|
||||
peer._receiving_buffer = mv[o : o + nc]
|
||||
o += nc
|
||||
if len(peer._receiving_buffer) == peer._receiving_length:
|
||||
peer._recv_messages.put(peer._receiving_buffer)
|
||||
@@ -576,11 +641,13 @@ class Network:
|
||||
|
||||
except Exception as e:
|
||||
if self._sc.debug:
|
||||
self._sc.log.error('Invalid message received from %s %s', peer._address, str(e))
|
||||
self._sc.log.error(
|
||||
"Invalid message received from %s %s", peer._address, str(e)
|
||||
)
|
||||
# TODO: misbehaving
|
||||
|
||||
def test_onion(self, path):
|
||||
self._sc.log.debug('test_onion packet')
|
||||
self._sc.log.debug("test_onion packet")
|
||||
|
||||
def get_info(self):
|
||||
rv = {}
|
||||
@@ -589,14 +656,14 @@ class Network:
|
||||
with self._mx:
|
||||
for peer in self._peers:
|
||||
peer_info = {
|
||||
'pubkey': 'Unknown' if not peer._pubkey else peer._pubkey.hex(),
|
||||
'address': '{}:{}'.format(peer._address[0], peer._address[1]),
|
||||
'bytessent': peer._bytes_sent,
|
||||
'bytesrecv': peer._bytes_received,
|
||||
'ready': peer._ready,
|
||||
'incoming': peer._incoming,
|
||||
"pubkey": "Unknown" if not peer._pubkey else peer._pubkey.hex(),
|
||||
"address": "{}:{}".format(peer._address[0], peer._address[1]),
|
||||
"bytessent": peer._bytes_sent,
|
||||
"bytesrecv": peer._bytes_received,
|
||||
"ready": peer._ready,
|
||||
"incoming": peer._incoming,
|
||||
}
|
||||
peers.append(peer_info)
|
||||
|
||||
rv['peers'] = peers
|
||||
rv["peers"] = peers
|
||||
return rv
|
||||
|
||||
@@ -16,19 +16,26 @@ class ProtocolInterface:
|
||||
swap_type = None
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
raise ValueError('base class')
|
||||
raise ValueError("base class")
|
||||
|
||||
def getMockScript(self) -> bytearray:
|
||||
return bytearray([
|
||||
OpCodes.OP_RETURN, OpCodes.OP_1])
|
||||
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
|
||||
|
||||
def getMockScriptScriptPubkey(self, ci) -> bytearray:
|
||||
script = self.getMockScript()
|
||||
return ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
||||
return (
|
||||
ci.getScriptDest(script)
|
||||
if ci._use_segwit
|
||||
else ci.get_p2sh_script_pubkey(script)
|
||||
)
|
||||
|
||||
def getMockAddrTo(self, ci):
|
||||
script = self.getMockScript()
|
||||
return ci.encodeScriptDest(ci.getScriptDest(script)) if ci._use_segwit else ci.encode_p2sh(script)
|
||||
return (
|
||||
ci.encodeScriptDest(ci.getScriptDest(script))
|
||||
if ci._use_segwit
|
||||
else ci.encode_p2sh(script)
|
||||
)
|
||||
|
||||
def findMockVout(self, ci, itx_decoded):
|
||||
mock_addr = self.getMockAddrTo(ci)
|
||||
|
||||
@@ -26,73 +26,91 @@ INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
|
||||
ABS_LOCK_TIME_LEEWAY = 10 * 60
|
||||
|
||||
|
||||
def buildContractScript(lock_val: int, secret_hash: bytes, pkh_redeem: bytes, pkh_refund: bytes, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256) -> bytearray:
|
||||
script = bytearray([
|
||||
OpCodes.OP_IF,
|
||||
OpCodes.OP_SIZE,
|
||||
0x01, 0x20, # 32
|
||||
OpCodes.OP_EQUALVERIFY,
|
||||
op_hash,
|
||||
0x20]) \
|
||||
+ secret_hash \
|
||||
+ bytearray([
|
||||
OpCodes.OP_EQUALVERIFY,
|
||||
OpCodes.OP_DUP,
|
||||
OpCodes.OP_HASH160,
|
||||
0x14]) \
|
||||
+ pkh_redeem \
|
||||
+ bytearray([OpCodes.OP_ELSE, ]) \
|
||||
+ SerialiseNum(lock_val) \
|
||||
+ bytearray([
|
||||
op_lock,
|
||||
OpCodes.OP_DROP,
|
||||
OpCodes.OP_DUP,
|
||||
OpCodes.OP_HASH160,
|
||||
0x14]) \
|
||||
+ pkh_refund \
|
||||
+ bytearray([
|
||||
OpCodes.OP_ENDIF,
|
||||
OpCodes.OP_EQUALVERIFY,
|
||||
OpCodes.OP_CHECKSIG])
|
||||
def buildContractScript(
|
||||
lock_val: int,
|
||||
secret_hash: bytes,
|
||||
pkh_redeem: bytes,
|
||||
pkh_refund: bytes,
|
||||
op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY,
|
||||
op_hash=OpCodes.OP_SHA256,
|
||||
) -> bytearray:
|
||||
script = (
|
||||
bytearray(
|
||||
[
|
||||
OpCodes.OP_IF,
|
||||
OpCodes.OP_SIZE,
|
||||
0x01,
|
||||
0x20, # 32
|
||||
OpCodes.OP_EQUALVERIFY,
|
||||
op_hash,
|
||||
0x20,
|
||||
]
|
||||
)
|
||||
+ secret_hash
|
||||
+ bytearray([OpCodes.OP_EQUALVERIFY, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14])
|
||||
+ pkh_redeem
|
||||
+ bytearray(
|
||||
[
|
||||
OpCodes.OP_ELSE,
|
||||
]
|
||||
)
|
||||
+ SerialiseNum(lock_val)
|
||||
+ bytearray(
|
||||
[op_lock, OpCodes.OP_DROP, OpCodes.OP_DUP, OpCodes.OP_HASH160, 0x14]
|
||||
)
|
||||
+ pkh_refund
|
||||
+ bytearray([OpCodes.OP_ENDIF, OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG])
|
||||
)
|
||||
return script
|
||||
|
||||
|
||||
def verifyContractScript(script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256):
|
||||
if script[0] != OpCodes.OP_IF or \
|
||||
script[1] != OpCodes.OP_SIZE or \
|
||||
script[2] != 0x01 or script[3] != 0x20 or \
|
||||
script[4] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[5] != op_hash or \
|
||||
script[6] != 0x20:
|
||||
def verifyContractScript(
|
||||
script, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY, op_hash=OpCodes.OP_SHA256
|
||||
):
|
||||
if (
|
||||
script[0] != OpCodes.OP_IF
|
||||
or script[1] != OpCodes.OP_SIZE
|
||||
or script[2] != 0x01
|
||||
or script[3] != 0x20
|
||||
or script[4] != OpCodes.OP_EQUALVERIFY
|
||||
or script[5] != op_hash
|
||||
or script[6] != 0x20
|
||||
):
|
||||
return False, None, None, None, None
|
||||
o = 7
|
||||
script_hash = script[o: o + 32]
|
||||
script_hash = script[o : o + 32]
|
||||
o += 32
|
||||
if script[o] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[o + 1] != OpCodes.OP_DUP or \
|
||||
script[o + 2] != OpCodes.OP_HASH160 or \
|
||||
script[o + 3] != 0x14:
|
||||
if (
|
||||
script[o] != OpCodes.OP_EQUALVERIFY
|
||||
or script[o + 1] != OpCodes.OP_DUP
|
||||
or script[o + 2] != OpCodes.OP_HASH160
|
||||
or script[o + 3] != 0x14
|
||||
):
|
||||
return False, script_hash, None, None, None
|
||||
o += 4
|
||||
pkh_redeem = script[o: o + 20]
|
||||
pkh_redeem = script[o : o + 20]
|
||||
o += 20
|
||||
if script[o] != OpCodes.OP_ELSE:
|
||||
return False, script_hash, pkh_redeem, None, None
|
||||
o += 1
|
||||
lock_val, nb = decodeScriptNum(script, o)
|
||||
o += nb
|
||||
if script[o] != op_lock or \
|
||||
script[o + 1] != OpCodes.OP_DROP or \
|
||||
script[o + 2] != OpCodes.OP_DUP or \
|
||||
script[o + 3] != OpCodes.OP_HASH160 or \
|
||||
script[o + 4] != 0x14:
|
||||
if (
|
||||
script[o] != op_lock
|
||||
or script[o + 1] != OpCodes.OP_DROP
|
||||
or script[o + 2] != OpCodes.OP_DUP
|
||||
or script[o + 3] != OpCodes.OP_HASH160
|
||||
or script[o + 4] != 0x14
|
||||
):
|
||||
return False, script_hash, pkh_redeem, lock_val, None
|
||||
o += 5
|
||||
pkh_refund = script[o: o + 20]
|
||||
pkh_refund = script[o : o + 20]
|
||||
o += 20
|
||||
if script[o] != OpCodes.OP_ENDIF or \
|
||||
script[o + 1] != OpCodes.OP_EQUALVERIFY or \
|
||||
script[o + 2] != OpCodes.OP_CHECKSIG:
|
||||
if (
|
||||
script[o] != OpCodes.OP_ENDIF
|
||||
or script[o + 1] != OpCodes.OP_EQUALVERIFY
|
||||
or script[o + 2] != OpCodes.OP_CHECKSIG
|
||||
):
|
||||
return False, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||
return True, script_hash, pkh_redeem, lock_val, pkh_refund
|
||||
|
||||
@@ -101,16 +119,23 @@ def extractScriptSecretHash(script):
|
||||
return script[7:39]
|
||||
|
||||
|
||||
def redeemITx(self, bid_id: bytes, session):
|
||||
bid, offer = self.getBidAndOffer(bid_id, session)
|
||||
def redeemITx(self, bid_id: bytes, cursor):
|
||||
bid, offer = self.getBidAndOffer(bid_id, cursor)
|
||||
ci_from = self.ci(offer.coin_from)
|
||||
|
||||
txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate', session=session)
|
||||
txn = self.createRedeemTxn(
|
||||
ci_from.coin_type(), bid, for_txn_type="initiate", cursor=cursor
|
||||
)
|
||||
txid = ci_from.publishTx(bytes.fromhex(txn))
|
||||
|
||||
bid.initiate_tx.spend_txid = bytes.fromhex(txid)
|
||||
self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex())
|
||||
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, '', session)
|
||||
self.log.debug(
|
||||
"Submitted initiate redeem txn %s to %s chain for bid %s",
|
||||
txid,
|
||||
ci_from.coin_name(),
|
||||
bid_id.hex(),
|
||||
)
|
||||
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, "", cursor)
|
||||
|
||||
|
||||
class AtomicSwapInterface(ProtocolInterface):
|
||||
@@ -118,13 +143,19 @@ class AtomicSwapInterface(ProtocolInterface):
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
addr_to = self.getMockAddrTo(ci)
|
||||
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
||||
funded_tx = ci.createRawFundedTransaction(
|
||||
addr_to, amount, sub_fee, lock_unspents=False
|
||||
)
|
||||
|
||||
return bytes.fromhex(funded_tx)
|
||||
|
||||
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
|
||||
mock_txo_script = self.getMockScriptScriptPubkey(ci)
|
||||
real_txo_script = ci.getScriptDest(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
|
||||
real_txo_script = (
|
||||
ci.getScriptDest(script)
|
||||
if ci._use_segwit
|
||||
else ci.get_p2sh_script_pubkey(script)
|
||||
)
|
||||
|
||||
found: int = 0
|
||||
ctx = ci.loadTx(mock_tx)
|
||||
@@ -134,9 +165,9 @@ class AtomicSwapInterface(ProtocolInterface):
|
||||
found += 1
|
||||
|
||||
if found < 1:
|
||||
raise ValueError('Mocked output not found')
|
||||
raise ValueError("Mocked output not found")
|
||||
if found > 1:
|
||||
raise ValueError('Too many mocked outputs found')
|
||||
raise ValueError("Too many mocked outputs found")
|
||||
ctx.nLockTime = 0
|
||||
|
||||
funded_tx = ctx.serialize()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import traceback
|
||||
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
)
|
||||
@@ -17,18 +20,17 @@ from basicswap.basicswap_util import (
|
||||
EventLogTypes,
|
||||
)
|
||||
from . import ProtocolInterface
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript, CScriptOp,
|
||||
OP_CHECKMULTISIG
|
||||
)
|
||||
from basicswap.contrib.test_framework.script import CScript, CScriptOp, OP_CHECKMULTISIG
|
||||
|
||||
|
||||
def addLockRefundSigs(self, xmr_swap, ci):
|
||||
self.log.debug('Setting lock refund tx sigs')
|
||||
self.log.debug("Setting lock refund tx sigs")
|
||||
|
||||
witness_stack = []
|
||||
if ci.coin_type() not in (Coins.DCR, ):
|
||||
witness_stack += [b'', ]
|
||||
if ci.coin_type() not in (Coins.DCR,):
|
||||
witness_stack += [
|
||||
b"",
|
||||
]
|
||||
witness_stack += [
|
||||
xmr_swap.al_lock_refund_tx_sig,
|
||||
xmr_swap.af_lock_refund_tx_sig,
|
||||
@@ -36,63 +38,123 @@ def addLockRefundSigs(self, xmr_swap, ci):
|
||||
]
|
||||
|
||||
signed_tx = ci.setTxSignature(xmr_swap.a_lock_refund_tx, witness_stack)
|
||||
ensure(signed_tx, 'setTxSignature failed')
|
||||
ensure(signed_tx, "setTxSignature failed")
|
||||
xmr_swap.a_lock_refund_tx = signed_tx
|
||||
|
||||
|
||||
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
|
||||
self.log.info('Manually recovering %s', bid_id.hex())
|
||||
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
|
||||
self.log.info(f"Manually recovering {bid_id.hex()}")
|
||||
# Manually recover txn if other key is known
|
||||
session = self.openSession()
|
||||
try:
|
||||
bid, xmr_swap = self.getXmrBidFromSession(session, bid_id)
|
||||
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
|
||||
ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
|
||||
offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
|
||||
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
|
||||
ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
|
||||
ci_to = self.ci(offer.coin_to)
|
||||
use_cursor = self.openDB(cursor)
|
||||
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
|
||||
ensure(bid, "Bid not found: {}.".format(bid_id.hex()))
|
||||
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex()))
|
||||
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
|
||||
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex()))
|
||||
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex()))
|
||||
|
||||
for_ed25519 = True if Coins(offer.coin_to) == Coins.XMR else False
|
||||
# The no-script coin is always the follower
|
||||
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from)
|
||||
ci_from = self.ci(Coins(offer.coin_from))
|
||||
ci_to = self.ci(Coins(offer.coin_to))
|
||||
ci_follower = ci_from if reverse_bid else ci_to
|
||||
|
||||
try:
|
||||
decoded_key_half = ci_to.decodeKey(encoded_key)
|
||||
decoded_key_half = ci_follower.decodeKey(encoded_key)
|
||||
except Exception as e:
|
||||
raise ValueError('Failed to decode provided key-half: ', str(e))
|
||||
raise ValueError("Failed to decode provided key-half: ", str(e))
|
||||
|
||||
if bid.was_sent:
|
||||
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
|
||||
|
||||
localkeyhalf = ci_follower.decodeKey(
|
||||
getChainBSplitKey(self, bid, xmr_swap, offer)
|
||||
)
|
||||
if was_sent:
|
||||
kbsl = decoded_key_half
|
||||
kbsf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSF, for_ed25519)
|
||||
kbsf = localkeyhalf
|
||||
else:
|
||||
kbsl = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519)
|
||||
kbsl = localkeyhalf
|
||||
kbsf = decoded_key_half
|
||||
ensure(ci_to.verifyKey(kbsl), 'Invalid kbsl')
|
||||
ensure(ci_to.verifyKey(kbsf), 'Invalid kbsf')
|
||||
vkbs = ci_to.sumKeys(kbsl, kbsf)
|
||||
|
||||
if offer.coin_to == Coins.XMR:
|
||||
address_to = self.getCachedMainWalletAddress(ci_to)
|
||||
ensure(ci_follower.verifyKey(kbsl), "Invalid kbsl")
|
||||
ensure(ci_follower.verifyKey(kbsf), "Invalid kbsf")
|
||||
if kbsl == kbsf:
|
||||
raise ValueError("Provided key matches local key")
|
||||
vkbs = ci_follower.sumKeys(kbsl, kbsf)
|
||||
|
||||
ensure(ci_follower.verifyPubkey(xmr_swap.pkbs), "Invalid pkbs") # Sanity check
|
||||
|
||||
# Ensure summed key matches the expected pubkey
|
||||
summed_pkbs = ci_follower.getPubkey(vkbs)
|
||||
if summed_pkbs != xmr_swap.pkbs:
|
||||
err_msg: str = "Summed key does not match expected wallet spend pubkey"
|
||||
have_pk = summed_pkbs.hex()
|
||||
expect_pk = xmr_swap.pkbs.hex()
|
||||
self.log.error(f"{err_msg}. Got: {have_pk}, Expect: {expect_pk}")
|
||||
raise ValueError(err_msg)
|
||||
|
||||
if ci_follower.coin_type() in (Coins.XMR, Coins.WOW):
|
||||
address_to = self.getCachedMainWalletAddress(ci_follower, use_cursor)
|
||||
else:
|
||||
address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
|
||||
|
||||
address_to = self.getCachedStealthAddressForCoin(
|
||||
ci_follower.coin_type(), use_cursor
|
||||
)
|
||||
amount = bid.amount_to
|
||||
lock_tx_vout = bid.getLockTXBVout()
|
||||
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, amount, xmr_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout)
|
||||
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
|
||||
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, txid.hex(), session)
|
||||
session.commit()
|
||||
txid = ci_follower.spendBLockTx(
|
||||
xmr_swap.b_lock_tx_id,
|
||||
address_to,
|
||||
xmr_swap.vkbv,
|
||||
vkbs,
|
||||
amount,
|
||||
xmr_offer.b_fee_rate,
|
||||
bid.chain_b_height_start,
|
||||
spend_actual_balance=True,
|
||||
lock_tx_vout=lock_tx_vout,
|
||||
)
|
||||
self.log.debug(
|
||||
"Submitted lock B spend txn %s to %s chain for bid %s",
|
||||
txid.hex(),
|
||||
ci_follower.coin_name(),
|
||||
bid_id.hex(),
|
||||
)
|
||||
self.logBidEvent(
|
||||
bid.bid_id,
|
||||
EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED,
|
||||
txid.hex(),
|
||||
use_cursor,
|
||||
)
|
||||
self.commitDB()
|
||||
|
||||
return txid
|
||||
except Exception as e:
|
||||
self.log.error(traceback.format_exc())
|
||||
raise (e)
|
||||
finally:
|
||||
self.closeSession(session, commit=False)
|
||||
if cursor is None:
|
||||
self.closeDB(use_cursor, commit=False)
|
||||
|
||||
|
||||
def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
|
||||
reverse_bid: bool = offer.bid_reversed
|
||||
ci_leader = swap_client.ci(offer.coin_to if reverse_bid else offer.coin_from)
|
||||
ci_follower = swap_client.ci(offer.coin_from if reverse_bid else offer.coin_to)
|
||||
|
||||
key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL
|
||||
return ci_follower.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, key_type, True if ci_follower.coin_type() == Coins.XMR else False))
|
||||
for_ed25519: bool = True if ci_follower.curve_type() == Curves.ed25519 else False
|
||||
was_sent: bool = bid.was_received if reverse_bid else bid.was_sent
|
||||
|
||||
key_type = KeyTypes.KBSF if was_sent else KeyTypes.KBSL
|
||||
return ci_follower.encodeKey(
|
||||
swap_client.getPathKey(
|
||||
ci_leader.coin_type(),
|
||||
ci_follower.coin_type(),
|
||||
bid.created_at,
|
||||
xmr_swap.contract_count,
|
||||
key_type,
|
||||
for_ed25519,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
||||
@@ -102,13 +164,21 @@ def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
||||
|
||||
if bid.was_sent:
|
||||
if xmr_swap.a_lock_refund_spend_tx:
|
||||
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(xmr_swap.a_lock_refund_spend_tx)
|
||||
kbsl = ci_leader.recoverEncKey(xmr_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xmr_swap.pkasl)
|
||||
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(
|
||||
xmr_swap.a_lock_refund_spend_tx
|
||||
)
|
||||
kbsl = ci_leader.recoverEncKey(
|
||||
xmr_swap.af_lock_refund_spend_tx_esig,
|
||||
af_lock_refund_spend_tx_sig,
|
||||
xmr_swap.pkasl,
|
||||
)
|
||||
return ci_follower.encodeKey(kbsl)
|
||||
else:
|
||||
if xmr_swap.a_lock_spend_tx:
|
||||
al_lock_spend_tx_sig = ci_leader.extractLeaderSig(xmr_swap.a_lock_spend_tx)
|
||||
kbsf = ci_leader.recoverEncKey(xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf)
|
||||
kbsf = ci_leader.recoverEncKey(
|
||||
xmr_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xmr_swap.pkasf
|
||||
)
|
||||
return ci_follower.encodeKey(kbsf)
|
||||
return None
|
||||
|
||||
@@ -116,24 +186,32 @@ def getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer):
|
||||
def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
|
||||
if ci_to.curve_type() == Curves.ed25519:
|
||||
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
|
||||
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0: 33]
|
||||
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
|
||||
elif ci_to.curve_type() == Curves.secp256k1:
|
||||
for i in range(10):
|
||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, 'proof kbsf owned for swap')
|
||||
pk_recovered: bytes = ci_to.verifySigAndRecover(xmr_swap.kbsf_dleag, 'proof kbsf owned for swap')
|
||||
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
|
||||
kbsf, "proof kbsf owned for swap"
|
||||
)
|
||||
pk_recovered: bytes = ci_to.verifySigAndRecover(
|
||||
xmr_swap.kbsf_dleag, "proof kbsf owned for swap"
|
||||
)
|
||||
if pk_recovered == xmr_swap.pkbsf:
|
||||
break
|
||||
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
|
||||
assert (pk_recovered == xmr_swap.pkbsf)
|
||||
assert pk_recovered == xmr_swap.pkbsf
|
||||
xmr_swap.pkasf = xmr_swap.pkbsf
|
||||
else:
|
||||
raise ValueError('Unknown curve')
|
||||
raise ValueError("Unknown curve")
|
||||
|
||||
|
||||
class XmrSwapInterface(ProtocolInterface):
|
||||
swap_type = SwapTypes.XMR_SWAP
|
||||
|
||||
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript:
|
||||
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
|
||||
# fallthrough to ci if genScriptLockTxScript is implemented there
|
||||
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
|
||||
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
|
||||
|
||||
@@ -141,7 +219,9 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
addr_to = self.getMockAddrTo(ci)
|
||||
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
|
||||
funded_tx = ci.createRawFundedTransaction(
|
||||
addr_to, amount, sub_fee, lock_unspents=False
|
||||
)
|
||||
|
||||
return bytes.fromhex(funded_tx)
|
||||
|
||||
@@ -157,9 +237,9 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
found += 1
|
||||
|
||||
if found < 1:
|
||||
raise ValueError('Mocked output not found')
|
||||
raise ValueError("Mocked output not found")
|
||||
if found > 1:
|
||||
raise ValueError('Too many mocked outputs found')
|
||||
raise ValueError("Too many mocked outputs found")
|
||||
ctx.nLockTime = 0
|
||||
|
||||
return ctx.serialize()
|
||||
|
||||
116
basicswap/rpc.py
116
basicswap/rpc.py
@@ -18,31 +18,42 @@ from xmlrpc.client import (
|
||||
from .util import jsonDecimal
|
||||
|
||||
|
||||
class Jsonrpc():
|
||||
class Jsonrpc:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||
allow_none=False, use_datetime=False, use_builtin_types=False,
|
||||
*, context=None):
|
||||
def __init__(
|
||||
self,
|
||||
uri,
|
||||
transport=None,
|
||||
encoding=None,
|
||||
verbose=False,
|
||||
allow_none=False,
|
||||
use_datetime=False,
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
):
|
||||
# establish a "logical" server connection
|
||||
|
||||
# get the url
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
raise OSError('unsupported XML-RPC protocol')
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
self.__host = parsed.netloc
|
||||
self.__handler = parsed.path
|
||||
if not self.__handler:
|
||||
self.__handler = '/RPC2'
|
||||
self.__handler = "/RPC2"
|
||||
|
||||
if transport is None:
|
||||
handler = SafeTransport if parsed.scheme == 'https' else Transport
|
||||
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||
extra_kwargs = {}
|
||||
transport = handler(use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs)
|
||||
transport = handler(
|
||||
use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs,
|
||||
)
|
||||
self.__transport = transport
|
||||
|
||||
self.__encoding = encoding or 'utf-8'
|
||||
self.__encoding = encoding or "utf-8"
|
||||
self.__verbose = verbose
|
||||
self.__allow_none = allow_none
|
||||
|
||||
@@ -57,17 +68,16 @@ class Jsonrpc():
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
headers = self.__transport._extra_headers[:]
|
||||
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'id': self.__request_id
|
||||
}
|
||||
request_body = {"method": method, "params": params, "id": self.__request_id}
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('User-Agent', 'jsonrpc'))
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", "jsonrpc"))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
json.dumps(request_body, default=jsonDecimal).encode("utf-8"),
|
||||
)
|
||||
self.__request_id += 1
|
||||
|
||||
resp = connection.getresponse()
|
||||
@@ -82,55 +92,59 @@ class Jsonrpc():
|
||||
raise
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += 'wallet/' + urllib.parse.quote(wallet)
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
x = Jsonrpc(url)
|
||||
|
||||
v = x.json_request(method, params)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
|
||||
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||
|
||||
if 'error' in r and r['error'] is not None:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
|
||||
return r['result']
|
||||
return r["result"]
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'):
|
||||
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
try:
|
||||
url = 'http://{}@{}:{}/'.format(auth, host, rpc_port)
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += 'wallet/' + urllib.parse.quote(wallet)
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
return Jsonrpc(url)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC error ' + str(ex))
|
||||
raise ValueError("RPC error " + str(ex))
|
||||
|
||||
|
||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None):
|
||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None):
|
||||
cli_bin = os.path.join(bindir, cli_bin)
|
||||
|
||||
args = [cli_bin, ]
|
||||
if chain != 'mainnet':
|
||||
args.append('-' + chain)
|
||||
args.append('-datadir=' + datadir)
|
||||
args = [
|
||||
cli_bin,
|
||||
]
|
||||
if chain != "mainnet":
|
||||
args.append("-" + chain)
|
||||
args.append("-datadir=" + datadir)
|
||||
if wallet is not None:
|
||||
args.append('-rpcwallet=' + wallet)
|
||||
args.append("-rpcwallet=" + wallet)
|
||||
args += shlex.split(cmd)
|
||||
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
out = p.communicate()
|
||||
|
||||
if len(out[1]) > 0:
|
||||
raise ValueError('RPC error ' + str(out[1]))
|
||||
raise ValueError("RPC error " + str(out[1]))
|
||||
|
||||
r = out[0].decode('utf-8').strip()
|
||||
r = out[0].decode("utf-8").strip()
|
||||
try:
|
||||
r = json.loads(r)
|
||||
except Exception:
|
||||
@@ -138,7 +152,7 @@ def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None)
|
||||
return r
|
||||
|
||||
|
||||
def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
|
||||
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
port = port
|
||||
auth = auth
|
||||
wallet = wallet
|
||||
@@ -146,11 +160,19 @@ def make_rpc_func(port, auth, wallet=None, host='127.0.0.1'):
|
||||
|
||||
def rpc_func(method, params=None, wallet_override=None):
|
||||
nonlocal port, auth, wallet, host
|
||||
return callrpc(port, auth, method, params, wallet if wallet_override is None else wallet_override, host)
|
||||
return callrpc(
|
||||
port,
|
||||
auth,
|
||||
method,
|
||||
params,
|
||||
wallet if wallet_override is None else wallet_override,
|
||||
host,
|
||||
)
|
||||
|
||||
return rpc_func
|
||||
|
||||
|
||||
def escape_rpcauth(auth_str: str) -> str:
|
||||
username, password = auth_str.split(':', 1)
|
||||
password = urllib.parse.quote(password, safe='')
|
||||
return f'{username}:{password}'
|
||||
username, password = auth_str.split(":", 1)
|
||||
password = urllib.parse.quote(password, safe="")
|
||||
return f"{username}:{password}"
|
||||
|
||||
@@ -33,31 +33,50 @@ class SocksTransport(Transport):
|
||||
return self._connection[1]
|
||||
# create a HTTP connection object from a host descriptor
|
||||
chost, self._extra_headers, x509 = self.get_host_info(host)
|
||||
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost)
|
||||
self._connection = host, SocksiPyConnection(
|
||||
self.proxy_type,
|
||||
self.proxy_host,
|
||||
self.proxy_port,
|
||||
self.proxy_rdns,
|
||||
self.proxy_username,
|
||||
self.proxy_password,
|
||||
chost,
|
||||
)
|
||||
return self._connection[1]
|
||||
|
||||
|
||||
class JsonrpcDigest():
|
||||
class JsonrpcDigest:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||
allow_none=False, use_datetime=False, use_builtin_types=False,
|
||||
*, context=None):
|
||||
def __init__(
|
||||
self,
|
||||
uri,
|
||||
transport=None,
|
||||
encoding=None,
|
||||
verbose=False,
|
||||
allow_none=False,
|
||||
use_datetime=False,
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
):
|
||||
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
raise OSError('unsupported XML-RPC protocol')
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
self.__host = parsed.netloc
|
||||
self.__handler = parsed.path
|
||||
|
||||
if transport is None:
|
||||
handler = SafeTransport if parsed.scheme == 'https' else Transport
|
||||
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||
extra_kwargs = {}
|
||||
transport = handler(use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs)
|
||||
transport = handler(
|
||||
use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs,
|
||||
)
|
||||
self.__transport = transport
|
||||
|
||||
self.__encoding = encoding or 'utf-8'
|
||||
self.__encoding = encoding or "utf-8"
|
||||
self.__verbose = verbose
|
||||
self.__allow_none = allow_none
|
||||
|
||||
@@ -77,11 +96,18 @@ class JsonrpcDigest():
|
||||
connection.timeout = timeout
|
||||
headers = self.__transport._extra_headers[:]
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('User-Agent', 'jsonrpc'))
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", "jsonrpc"))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8'))
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
(
|
||||
""
|
||||
if params is None
|
||||
else json.dumps(params, default=jsonDecimal).encode("utf-8")
|
||||
),
|
||||
)
|
||||
self.__request_id += 1
|
||||
|
||||
resp = connection.getresponse()
|
||||
@@ -93,7 +119,7 @@ class JsonrpcDigest():
|
||||
self.__transport.close()
|
||||
raise
|
||||
|
||||
def json_request(self, request_body, username='', password='', timeout=None):
|
||||
def json_request(self, request_body, username="", password="", timeout=None):
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
if timeout:
|
||||
@@ -101,65 +127,82 @@ class JsonrpcDigest():
|
||||
|
||||
headers = self.__transport._extra_headers[:]
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('Connection', 'keep-alive'))
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("Connection", "keep-alive"))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
(
|
||||
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
|
||||
if request_body
|
||||
else ""
|
||||
),
|
||||
)
|
||||
resp = connection.getresponse()
|
||||
|
||||
if resp.status == 401:
|
||||
resp_headers = resp.getheaders()
|
||||
v = resp.read()
|
||||
_ = resp.read()
|
||||
|
||||
algorithm = ''
|
||||
realm = ''
|
||||
nonce = ''
|
||||
realm = ""
|
||||
nonce = ""
|
||||
for h in resp_headers:
|
||||
if h[0] != 'WWW-authenticate':
|
||||
if h[0] != "WWW-authenticate":
|
||||
continue
|
||||
fields = h[1].split(',')
|
||||
fields = h[1].split(",")
|
||||
for f in fields:
|
||||
key, value = f.split('=', 1)
|
||||
if key == 'algorithm' and value != 'MD5':
|
||||
key, value = f.split("=", 1)
|
||||
if key == "algorithm" and value != "MD5":
|
||||
break
|
||||
if key == 'realm':
|
||||
if key == "realm":
|
||||
realm = value.strip('"')
|
||||
if key == 'nonce':
|
||||
if key == "nonce":
|
||||
nonce = value.strip('"')
|
||||
if realm != '' and nonce != '':
|
||||
if realm != "" and nonce != "":
|
||||
break
|
||||
|
||||
if realm == '' or nonce == '':
|
||||
raise ValueError('Authenticate header not found.')
|
||||
if realm == "" or nonce == "":
|
||||
raise ValueError("Authenticate header not found.")
|
||||
|
||||
path = self.__handler
|
||||
HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest()
|
||||
HA1 = hashlib.md5(
|
||||
f"{username}:{realm}:{password}".encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
http_method = 'POST'
|
||||
HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest()
|
||||
http_method = "POST"
|
||||
HA2 = hashlib.md5(f"{http_method}:{path}".encode("utf-8")).hexdigest()
|
||||
|
||||
ncvalue = '{:08x}'.format(1)
|
||||
s = ncvalue.encode('utf-8')
|
||||
s += nonce.encode('utf-8')
|
||||
s += time.ctime().encode('utf-8')
|
||||
ncvalue = "{:08x}".format(1)
|
||||
s = ncvalue.encode("utf-8")
|
||||
s += nonce.encode("utf-8")
|
||||
s += time.ctime().encode("utf-8")
|
||||
s += os.urandom(8)
|
||||
cnonce = (hashlib.sha1(s).hexdigest()[:16])
|
||||
cnonce = hashlib.sha1(s).hexdigest()[:16]
|
||||
|
||||
# MD5-SESS
|
||||
HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest()
|
||||
HA1 = hashlib.md5(f"{HA1}:{nonce}:{cnonce}".encode("utf-8")).hexdigest()
|
||||
|
||||
respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest()
|
||||
respdig = hashlib.md5(
|
||||
f"{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}".encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
|
||||
headers = self.__transport._extra_headers[:]
|
||||
headers.append(('Authorization', header_value))
|
||||
headers.append(("Authorization", header_value))
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('Connection', 'keep-alive'))
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("Connection", "keep-alive"))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
(
|
||||
json.dumps(request_body, default=jsonDecimal).encode("utf-8")
|
||||
if request_body
|
||||
else ""
|
||||
),
|
||||
)
|
||||
resp = connection.getresponse()
|
||||
|
||||
self.__request_id += 1
|
||||
@@ -172,57 +215,88 @@ class JsonrpcDigest():
|
||||
raise
|
||||
|
||||
|
||||
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''):
|
||||
def callrpc_xmr(
|
||||
rpc_port,
|
||||
method,
|
||||
params=[],
|
||||
rpc_host="127.0.0.1",
|
||||
path="json_rpc",
|
||||
auth=None,
|
||||
timeout=120,
|
||||
transport=None,
|
||||
tag="",
|
||||
):
|
||||
# auth is a tuple: (username, password)
|
||||
try:
|
||||
if rpc_host.count('://') > 0:
|
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, path)
|
||||
if rpc_host.count("://") > 0:
|
||||
url = "{}:{}/{}".format(rpc_host, rpc_port, path)
|
||||
else:
|
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
||||
url = "http://{}:{}/{}".format(rpc_host, rpc_port, path)
|
||||
|
||||
x = JsonrpcDigest(url, transport=transport)
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'jsonrpc': '2.0',
|
||||
'id': x.request_id()
|
||||
"method": method,
|
||||
"params": params,
|
||||
"jsonrpc": "2.0",
|
||||
"id": x.request_id(),
|
||||
}
|
||||
if auth:
|
||||
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
|
||||
v = x.json_request(
|
||||
request_body, username=auth[0], password=auth[1], timeout=timeout
|
||||
)
|
||||
else:
|
||||
v = x.json_request(request_body, timeout=timeout)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
||||
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
|
||||
|
||||
if 'error' in r and r['error'] is not None:
|
||||
raise ValueError(tag + 'RPC error ' + str(r['error']))
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError(tag + "RPC error " + str(r["error"]))
|
||||
|
||||
return r['result']
|
||||
return r["result"]
|
||||
|
||||
|
||||
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''):
|
||||
def callrpc_xmr2(
|
||||
rpc_port: int,
|
||||
method: str,
|
||||
params=None,
|
||||
auth=None,
|
||||
rpc_host="127.0.0.1",
|
||||
timeout=120,
|
||||
transport=None,
|
||||
tag="",
|
||||
):
|
||||
try:
|
||||
if rpc_host.count('://') > 0:
|
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
|
||||
if rpc_host.count("://") > 0:
|
||||
url = "{}:{}/{}".format(rpc_host, rpc_port, method)
|
||||
else:
|
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
|
||||
url = "http://{}:{}/{}".format(rpc_host, rpc_port, method)
|
||||
|
||||
x = JsonrpcDigest(url, transport=transport)
|
||||
if auth:
|
||||
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
|
||||
v = x.json_request(
|
||||
params, username=auth[0], password=auth[1], timeout=timeout
|
||||
)
|
||||
else:
|
||||
v = x.json_request(params, timeout=timeout)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
|
||||
raise ValueError("{}RPC Server Error: {}".format(tag, str(ex)))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
|
||||
def make_xmr_rpc2_func(
|
||||
port,
|
||||
auth,
|
||||
host="127.0.0.1",
|
||||
proxy_host=None,
|
||||
proxy_port=None,
|
||||
default_timeout=120,
|
||||
tag="",
|
||||
):
|
||||
port = port
|
||||
auth = auth
|
||||
host = host
|
||||
@@ -236,11 +310,29 @@ def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
|
||||
return callrpc_xmr2(
|
||||
port,
|
||||
method,
|
||||
params,
|
||||
auth=auth,
|
||||
rpc_host=host,
|
||||
timeout=timeout,
|
||||
transport=transport,
|
||||
tag=tag,
|
||||
)
|
||||
|
||||
return rpc_func
|
||||
|
||||
|
||||
def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
|
||||
def make_xmr_rpc_func(
|
||||
port,
|
||||
auth,
|
||||
host="127.0.0.1",
|
||||
proxy_host=None,
|
||||
proxy_port=None,
|
||||
default_timeout=120,
|
||||
tag="",
|
||||
):
|
||||
port = port
|
||||
auth = auth
|
||||
host = host
|
||||
@@ -254,5 +346,15 @@ def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
|
||||
return callrpc_xmr(
|
||||
port,
|
||||
method,
|
||||
params,
|
||||
rpc_host=host,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
transport=transport,
|
||||
tag=tag,
|
||||
)
|
||||
|
||||
return rpc_func
|
||||
|
||||
@@ -8,23 +8,23 @@ from enum import IntEnum
|
||||
|
||||
|
||||
class OpCodes(IntEnum):
|
||||
OP_0 = 0x00,
|
||||
OP_PUSHDATA1 = 0x4c,
|
||||
OP_1 = 0x51,
|
||||
OP_16 = 0x60,
|
||||
OP_IF = 0x63,
|
||||
OP_ELSE = 0x67,
|
||||
OP_ENDIF = 0x68,
|
||||
OP_RETURN = 0x6a,
|
||||
OP_DROP = 0x75,
|
||||
OP_DUP = 0x76,
|
||||
OP_SIZE = 0x82,
|
||||
OP_EQUAL = 0x87,
|
||||
OP_EQUALVERIFY = 0x88,
|
||||
OP_SHA256 = 0xa8,
|
||||
OP_HASH160 = 0xa9,
|
||||
OP_CHECKSIG = 0xac,
|
||||
OP_CHECKLOCKTIMEVERIFY = 0xb1,
|
||||
OP_CHECKSEQUENCEVERIFY = 0xb2,
|
||||
OP_0 = (0x00,)
|
||||
OP_PUSHDATA1 = (0x4C,)
|
||||
OP_1 = (0x51,)
|
||||
OP_16 = (0x60,)
|
||||
OP_IF = (0x63,)
|
||||
OP_ELSE = (0x67,)
|
||||
OP_ENDIF = (0x68,)
|
||||
OP_RETURN = (0x6A,)
|
||||
OP_DROP = (0x75,)
|
||||
OP_DUP = (0x76,)
|
||||
OP_SIZE = (0x82,)
|
||||
OP_EQUAL = (0x87,)
|
||||
OP_EQUALVERIFY = (0x88,)
|
||||
OP_SHA256 = (0xA8,)
|
||||
OP_HASH160 = (0xA9,)
|
||||
OP_CHECKSIG = (0xAC,)
|
||||
OP_CHECKLOCKTIMEVERIFY = (0xB1,)
|
||||
OP_CHECKSEQUENCEVERIFY = (0xB2,)
|
||||
|
||||
OP_SHA256_DECRED = 0xc0,
|
||||
OP_SHA256_DECRED = (0xC0,)
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
|
||||
.padded_row td
|
||||
{
|
||||
padding-top:1.5em;
|
||||
/* General Styles */
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bold
|
||||
{
|
||||
font-weight:bold;
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.monospace
|
||||
{
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
.floatright
|
||||
{
|
||||
.floatright {
|
||||
position: fixed;
|
||||
top:1.25rem;
|
||||
right:1.25rem;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.error_msg
|
||||
{
|
||||
/* Table Styles */
|
||||
.padded_row td {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-highest {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
#hide {
|
||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
/* Firefox */
|
||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
/* Safari and Chrome */
|
||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
/* Opera */
|
||||
animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes cssAnimation {
|
||||
to {
|
||||
width:0;
|
||||
height:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes cssAnimation {
|
||||
to {
|
||||
width:0;
|
||||
height:0;
|
||||
visibility:hidden;
|
||||
}
|
||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.custom-select .select {
|
||||
appearance: none;
|
||||
background-image: url('/static/images/other/coin.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
@keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Select Styles */
|
||||
.custom-select .select {
|
||||
appearance: none;
|
||||
background-image: url('/static/images/other/coin.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-select select::-webkit-scrollbar {
|
||||
width: 0;
|
||||
@@ -95,19 +94,20 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Blur and Overlay Styles */
|
||||
.blurred {
|
||||
filter: blur(4px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
filter: blur(3px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error-overlay.non-blurred {
|
||||
filter: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
filter: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* Disable opacity on disabled form elements in Chrome */
|
||||
/* Form Element Styles */
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
select:disabled,
|
||||
input:disabled,
|
||||
@@ -120,6 +120,7 @@
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
|
||||
/* Active Container Styles */
|
||||
.active-container {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
@@ -137,6 +138,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Center Spin Animation */
|
||||
.center-spin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -148,10 +150,11 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.hover-container:hover #coin_to_button, .hover-container:hover #coin_to {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.hover-container:hover #coin_from_button, .hover-container:hover #coin_from {
|
||||
/* Hover Container Styles */
|
||||
.hover-container:hover #coin_to_button,
|
||||
.hover-container:hover #coin_to,
|
||||
.hover-container:hover #coin_from_button,
|
||||
.hover-container:hover #coin_from {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
@@ -161,6 +164,7 @@
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Input-like Container Styles */
|
||||
.input-like-container {
|
||||
max-width: 100%;
|
||||
background-color: #ffffff;
|
||||
@@ -172,41 +176,45 @@
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
word-wrap: break-word;
|
||||
height: 90px;
|
||||
color: #374151;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
height: auto;
|
||||
min-height: 90px;
|
||||
max-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-like-container.dark {
|
||||
background-color: #374151;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.input-like-container.copying {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
/* QR Code Styles */
|
||||
.qrcode {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.qrcode-border {
|
||||
border: 2px solid;
|
||||
border-color: rgba(59, 130, 246, var(--tw-border-opacity));
|
||||
border-radius: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.qrcode img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 15px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#showQR {
|
||||
@@ -214,62 +222,138 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height:25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
||||
select.select-disabled {
|
||||
opacity: 0.40 !important;
|
||||
.qrcode-container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
|
||||
.disabled-input-enabled {
|
||||
opacity: 0.40 !important;
|
||||
}
|
||||
/* Disabled Element Styles */
|
||||
select.select-disabled,
|
||||
.disabled-input-enabled,
|
||||
select.disabled-select-enabled {
|
||||
opacity: 0.40 !important;
|
||||
opacity: 0.40 !important;
|
||||
}
|
||||
|
||||
/* Shutdown Modal Styles */
|
||||
#shutdownModal {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div:first-child {
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
#shutdownModal > div:last-child {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
#shutdownModal.hidden > div {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#shutdownModal:not(.hidden) > div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shutdown-button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled:hover {
|
||||
background-color: #4a5568;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
.custom-select .select {
|
||||
appearance: none;
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
/* Loading line animation */
|
||||
.loading-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.loading-line::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent, #007bff, transparent);
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
/* Hide the loading line once data is loaded */
|
||||
.usd-value:not(.loading) .loading-line,
|
||||
.profit-loss:not(.loading) .loading-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resolution-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4B5563; /* gray-600 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.custom-select select::-webkit-scrollbar {
|
||||
width: 0;
|
||||
.resolution-button:hover {
|
||||
color: #1F2937; /* gray-800 */
|
||||
}
|
||||
|
||||
.custom-select .select option {
|
||||
padding-left: 0;
|
||||
text-indent: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
.resolution-button:focus {
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
|
||||
.custom-select .select option.no-space {
|
||||
padding-left: 0;
|
||||
.resolution-button.active {
|
||||
color: #3B82F6; /* blue-500 */
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
|
||||
.custom-select .select option[data-image] {
|
||||
background-image: url('');
|
||||
.dark .resolution-button {
|
||||
color: #9CA3AF; /* gray-400 */
|
||||
}
|
||||
|
||||
.custom-select .select-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
.dark .resolution-button:hover {
|
||||
color: #F3F4F6; /* gray-100 */
|
||||
}
|
||||
|
||||
.custom-select .select-image {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.custom-select .select:focus+.select-dropdown .select-image {
|
||||
display: block;
|
||||
.dark .resolution-button.active {
|
||||
color: #60A5FA; /* blue-400 */
|
||||
outline-color: #60A5FA; /* blue-400 */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#toggle-volume.active {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
#toggle-auto-refresh[data-enabled="true"] {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
|
||||
BIN
basicswap/static/images/coins/Bitcoin%20Cash.png
Normal file
BIN
basicswap/static/images/coins/Bitcoin%20Cash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
basicswap/static/images/coins/Bitcoin-Cash-20.png
Normal file
BIN
basicswap/static/images/coins/Bitcoin-Cash-20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
basicswap/static/images/coins/Bitcoin-Cash.png
Normal file
BIN
basicswap/static/images/coins/Bitcoin-Cash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
7
basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
7
basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2045
basicswap/static/js/offerstable.js
Normal file
2045
basicswap/static/js/offerstable.js
Normal file
File diff suppressed because it is too large
Load Diff
1483
basicswap/static/js/pricechart.js
Normal file
1483
basicswap/static/js/pricechart.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@
|
||||
<p class="font-normal text-coolGray-200 dark:text-white"><span class="bold">BID ID:</span> {{ bid_id }}</p>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
|
||||
{% if refresh %}
|
||||
{% if refresh %}
|
||||
<a id="refresh" href="/bid/{{ bid_id }}" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
{{ circular_arrows_svg | safe }}
|
||||
<span>Refresh {{ refresh }} seconds</span>
|
||||
@@ -378,13 +378,13 @@
|
||||
{% if data.xmr_b_half_privatekey %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Key Half (WARNING key data!):</td>
|
||||
<td class="py-3 px-6 monospace">{{ data.xmr_b_half_privatekey }}</td>
|
||||
<td class="py-3 px-6 monospace" id="localkeyhalf">{{ data.xmr_b_half_privatekey }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if data.xmr_b_half_privatekey_remote %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Remote Key Half:</td>
|
||||
<td class="py-3 px-6 monospace">{{ data.xmr_b_half_privatekey_remote }}</td>
|
||||
<td class="py-3 px-6 monospace" id="remotekeyhalf">{{ data.xmr_b_half_privatekey_remote }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
@@ -845,4 +845,4 @@
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -5,14 +5,9 @@
|
||||
<div class="flex flex-wrap items-center -m-2">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li>
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">
|
||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
@@ -37,8 +32,8 @@
|
||||
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-3">
|
||||
<div class="w-full md:w-1/2 p-3">
|
||||
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change Password</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Update your BasicSwap / Wallets password.</p>
|
||||
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Change/Set your Password</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">Change or Set your BasicSwap / Wallets password.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,4 +182,4 @@ toggles.forEach(function (toggle_id, index) {
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>{{ title_str }}</h2>
|
||||
<p>Info: {{ message_str }}</p>
|
||||
<p><a href="/">home</a></p>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
</script>
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - Info</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="dark:bg-gray-700">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="text-center">
|
||||
<a class="inline-block mb-6" href="#">
|
||||
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
|
||||
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
|
||||
</a>
|
||||
|
||||
<div class="p-6 bg-coolGray-100 dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h2 class="text-xl font-bold mb-4 text-coolGray-900 dark:text-white">ERROR:</h2>
|
||||
<p class="text-lg text-coolGray-500 dark:text-white mb-6">{{ message_str }}</p>
|
||||
<a href="/" class="inline-block py-2 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition duration-300">Home</a>
|
||||
</div>
|
||||
<p class="mt-10">
|
||||
<span class="text-xs font-medium dark:text-white">Need help?</span>
|
||||
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Image toggling function
|
||||
function toggleImages() {
|
||||
const html = document.querySelector('html');
|
||||
const darkImages = document.querySelectorAll('.dark-image');
|
||||
const lightImages = document.querySelectorAll('.light-image');
|
||||
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImageDisplay(images, display) {
|
||||
images.forEach(img => {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle functionality
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
// Call toggleImages on load
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-dex/basicswap_explained.html" target="_blank">BasicSwap Explained</a></div>
|
||||
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_installation.html" target="_blank">Tutorials and Guides</a></div>
|
||||
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Get Support</a></div>
|
||||
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://basicswapdex.com/terms" target="_blank">Terms and Conditions</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/4 px-4"> </div>
|
||||
@@ -24,16 +25,16 @@
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2024~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.0.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.1.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
|
||||
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
|
||||
{{ love_svg | safe }}
|
||||
<span class="ml-2 text-sm font-bold dark:text-white text-gray-90 ">by TV and CRZ</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2">
|
||||
<div class="flex flex-wrap md:justify-end -mx-5">
|
||||
<div class="px-5">
|
||||
<a class="inline-block text-coolGray-300 hover:text-coolGray-400" href="https://github.com/tecnovert/basicswap" target="_blank">
|
||||
<a class="inline-block text-coolGray-300 hover:text-coolGray-400" href="https://github.com/basicswap/basicswap" target="_blank">
|
||||
{{ github_svg | safe }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -43,69 +44,67 @@
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function() {
|
||||
var toggleImages = function() {
|
||||
var html = document.querySelector('html');
|
||||
var darkImages = document.querySelectorAll('.dark-image');
|
||||
var lightImages = document.querySelectorAll('.light-image');
|
||||
var toggleImages = function() {
|
||||
var html = document.querySelector('html');
|
||||
var darkImages = document.querySelectorAll('.dark-image');
|
||||
var lightImages = document.querySelectorAll('.light-image');
|
||||
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
};
|
||||
|
||||
var toggleImageDisplay = function(images, display) {
|
||||
images.forEach(function(img) {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var themeToggle = document.getElementById('theme-toggle');
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
toggleImages();
|
||||
var toggleImageDisplay = function(images, display) {
|
||||
images.forEach(function(img) {
|
||||
img.style.display = display;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
};
|
||||
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var themeToggle = document.getElementById('theme-toggle');
|
||||
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
{% from 'style.html' import notifications_network_offer_svg, notifications_bid_accepted_svg, notifications_unknow_event_svg, notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, bids_sent_svg, header_arrow_down_svg, love_svg %}
|
||||
{% from 'style.html' import change_password_svg, notifications_network_offer_svg, notifications_bid_accepted_svg, notifications_unknow_event_svg, notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg, wallet_svg, page_back_svg, order_book_svg, new_offer_svg, settings_svg, asettings_svg, cog_svg, rpc_svg, debug_svg, explorer_svg, tor_svg, smsg_svg, outputs_svg, automation_svg, shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg, wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg, swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, bids_sent_svg, header_arrow_down_svg, love_svg %}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -7,11 +7,13 @@
|
||||
<meta http-equiv="refresh" content="{{ refresh }}">
|
||||
{% endif %}
|
||||
<script src="/static/js/libs/chart.js"></script>
|
||||
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
@@ -19,7 +21,7 @@
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
@@ -84,8 +86,73 @@
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
||||
const shutdownModal = document.getElementById('shutdownModal');
|
||||
const closeModalButton = document.getElementById('closeShutdownModal');
|
||||
const confirmShutdownButton = document.getElementById('confirmShutdown');
|
||||
const shutdownWarning = document.getElementById('shutdownWarning');
|
||||
|
||||
function updateShutdownButtons() {
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
|
||||
shutdownButtons.forEach(button => {
|
||||
if (activeSwaps > 0) {
|
||||
button.classList.add('shutdown-disabled');
|
||||
button.setAttribute('data-disabled', 'true');
|
||||
button.setAttribute('title', 'Caution: Swaps in progress');
|
||||
} else {
|
||||
button.classList.remove('shutdown-disabled');
|
||||
button.removeAttribute('data-disabled');
|
||||
button.removeAttribute('title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showShutdownModal() {
|
||||
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
|
||||
if (activeSwaps > 0) {
|
||||
shutdownWarning.classList.remove('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
|
||||
} else {
|
||||
shutdownWarning.classList.add('hidden');
|
||||
confirmShutdownButton.textContent = 'Yes, Shut Down';
|
||||
}
|
||||
shutdownModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideShutdownModal() {
|
||||
shutdownModal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
shutdownButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
showShutdownModal();
|
||||
});
|
||||
});
|
||||
|
||||
closeModalButton.addEventListener('click', hideShutdownModal);
|
||||
|
||||
confirmShutdownButton.addEventListener('click', function() {
|
||||
const shutdownToken = document.querySelector('.shutdown-button').getAttribute('href').split('/').pop();
|
||||
window.location.href = '/shutdown/' + shutdownToken;
|
||||
});
|
||||
|
||||
shutdownModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideShutdownModal();
|
||||
}
|
||||
});
|
||||
|
||||
updateShutdownButtons();
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
</head>
|
||||
@@ -128,6 +195,10 @@
|
||||
<li>
|
||||
<a href="/settings" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Settings</span>
|
||||
{{ cog_svg | safe }} Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/changepassword" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Change/Set Password</span>
|
||||
{{ change_password_svg | safe }} Change/Set Password</a>
|
||||
</li>
|
||||
{% if debug_mode == true %}
|
||||
<li>
|
||||
@@ -165,17 +236,42 @@
|
||||
</li>
|
||||
{% if debug_mode == true %}
|
||||
<li>
|
||||
<a href="/automation" class="flex items-center block py-4 px-4 hover:bg-gray-10 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Automation Strategies</span>
|
||||
<a href="/automation" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Automation Strategies</span>
|
||||
{{ automation_svg | safe }} Automation Strategies</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="text-sm text-gray-700">
|
||||
<a href="/shutdown/{{ shutdown_token }}" class="flex items-center block py-4 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"> <span class="sr-only">Shutdown</span>
|
||||
{{ shutdown_svg | safe }} Shutdown </a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700">
|
||||
<a href="/shutdown/{{ shutdown_token }}"
|
||||
class="shutdown-button flex items-center block py-4 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
data-active-swaps="{{ summary.num_swapping }}">
|
||||
{{ shutdown_svg | safe }}
|
||||
<span class="ml-2">Shutdown</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"></div>
|
||||
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">
|
||||
<div class="bg-white dark:bg-gray-500 rounded-lg shadow-xl max-w-md w-full">
|
||||
<div class="p-6 text-center">
|
||||
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||
</svg>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-700 dark:text-gray-300">Are you sure you want to shut down?</h3>
|
||||
<p id="shutdownWarning" class="mb-5 text-sm text-red-500 font-bold hidden">Warning: Swaps are in progress. Please wait for swaps to complete before shutting down.</p>
|
||||
<p class="mb-5 text-sm text-gray-500 dark:text-gray-300">This action will shut down the application. Are you sure you want to proceed?</p>
|
||||
<button id="confirmShutdown" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-0 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||
Yes, Shut Down
|
||||
</button>
|
||||
<button id="closeShutdownModal" type="button" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-0 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- dropdown settings & tools -->
|
||||
|
||||
<!-- notifications -->
|
||||
@@ -260,10 +356,10 @@
|
||||
<!-- dev mode icons on/off -->
|
||||
<ul class="xl:flex">
|
||||
<li>
|
||||
<div data-tooltip-target="tooltip-debug" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
|
||||
<div data-tooltip-target="tooltip-DEV" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
|
||||
{{ debug_nerd_svg | safe }}
|
||||
<span data-tooltip-target="tooltip-DEV" ></span> </div>
|
||||
<div id="tooltip-debug" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
</div>
|
||||
<div id="tooltip-DEV" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Debug mode:</b> Active</p>
|
||||
{% if debug_ui_mode == true %}
|
||||
<p><b>Debug UI mode:</b> Active</p>
|
||||
@@ -278,8 +374,7 @@
|
||||
<ul class="xl:flex"><li>
|
||||
{% if locked == true %}
|
||||
<div data-tooltip-target="tooltip-locked-wallets" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
|
||||
{{ wallet_locked_svg | safe }}
|
||||
<span data-tooltip-target="tooltip-locked-wallets" ></span> </div>
|
||||
{{ wallet_locked_svg | safe }}</div>
|
||||
<div id="tooltip-locked-wallets" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Wallets:</b> Locked </p>
|
||||
</div>
|
||||
@@ -287,8 +382,7 @@
|
||||
<a href='/lock'>
|
||||
<div data-tooltip-target="tooltip-unlocked-wallets" class="ml-5 flex items-center text-gray-50 hover:text-gray-100 text-sm">
|
||||
{{ wallet_unlocked_svg | safe }}
|
||||
<span data-tooltip-target="tooltip-unlocked-wallets" ></span> </div>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
<div id="tooltip-unlocked-wallets" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Wallets:</b> Unlocked </p>
|
||||
</div>
|
||||
@@ -303,7 +397,7 @@
|
||||
<a href="/tor">
|
||||
<div data-tooltip-target="tooltip-tor" class="flex items-center text-gray-50 hover:text-gray-100 text-sm">
|
||||
{{ tor_purple_svg | safe }}
|
||||
</a> <span data-tooltip-target="tooltip-tor"></span></div>
|
||||
</a></div>
|
||||
<div id="tooltip-tor" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"><b>Tor mode:</b> Active
|
||||
{% if tor_established == true %}
|
||||
<br><b>Tor:</b> Connected
|
||||
@@ -316,7 +410,6 @@
|
||||
<button data-tooltip-target="tooltip-darkmode" id="theme-toggle" type="button" class="text-gray-500 dark:text-gray-400 focus:outline-none rounded-lg text-sm ml-5">
|
||||
{{ sun_svg | safe }}
|
||||
{{ moon_svg | safe }}
|
||||
<span data-tooltip-target="tooltip-darkmode"></span>
|
||||
<div id="tooltip-darkmode" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">Dark mode</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -453,6 +546,11 @@
|
||||
{{ settings_svg | safe }}
|
||||
</span><span>Settings</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="flex items-center pl-3 py-3 pr-2 text-gray-50 hover:bg-gray-900 rounded" href="/changepassword"> <span class="inline-block mr-3">
|
||||
{{ change_password_svg | safe }}
|
||||
</span><span>Change/Set Password</span></a>
|
||||
</li>
|
||||
{% if debug_mode == true %}
|
||||
<li>
|
||||
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/rpc"> <span class="inline-block mr-3">
|
||||
@@ -500,9 +598,12 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="pt-8 text-sm font-medium">
|
||||
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/shutdown/{{ shutdown_token }}"> <span class="inline-block mr-3">
|
||||
{{ shutdown_svg | safe }}
|
||||
</span><span>Shutdown</span></a>
|
||||
<a href="/shutdown/{{ shutdown_token }}"
|
||||
class="shutdown-button flex items-center block py-4 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
data-active-swaps="{{ summary.num_swapping }}">
|
||||
{{ shutdown_svg | safe }}
|
||||
<span class="ml-2">Shutdown</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -510,40 +611,57 @@
|
||||
<!-- mobile sidebar -->
|
||||
|
||||
</section>
|
||||
{% if ws_url %}
|
||||
{% if ws_port %}
|
||||
<script>
|
||||
var ws = new WebSocket("{{ ws_url }}"),
|
||||
// Configuration object
|
||||
const notificationConfig = {
|
||||
showNewOffers: false,
|
||||
showNewBids: true,
|
||||
showBidAccepted: true
|
||||
};
|
||||
|
||||
var ws = new WebSocket("ws://" + window.location.hostname + ":{{ ws_port }}"),
|
||||
floating_div = document.createElement('div');
|
||||
floating_div.classList.add('floatright');
|
||||
messages = document.createElement('ul');
|
||||
messages.setAttribute('id', 'ul_updates');
|
||||
ws.onmessage = function(event) {
|
||||
let json = JSON.parse(event.data);
|
||||
let event_message = 'Unknown event';
|
||||
if (json['event'] == 'new_offer') {
|
||||
floating_div.classList.add('floatright');
|
||||
messages = document.createElement('ul');
|
||||
messages.setAttribute('id', 'ul_updates');
|
||||
|
||||
floating_div.appendChild(messages);
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
let json = JSON.parse(event.data);
|
||||
let event_message = 'Unknown event';
|
||||
let should_display = false;
|
||||
|
||||
if (json['event'] == 'new_offer' && notificationConfig.showNewOffers) {
|
||||
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-blue-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round"><circle cx="5" cy="5" r="4"></circle> <circle cx="19" cy="19" r="4"></circle> <polyline data-cap="butt" points="13,5 21,5 21,11 " stroke="#ffffff"></polyline> <polyline data-cap="butt" points="11,19 3,19 3,13 " stroke="#ffffff"></polyline> <polyline points=" 16,2 13,5 16,8 " stroke="#ffffff"></polyline> <polyline points=" 8,16 11,19 8,22 " stroke="#ffffff"></polyline></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">New network <a class="underline" href=/offer/' + json['offer_id'] + '>offer</a></div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><span class="sr-only">Close</span><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
|
||||
}
|
||||
else
|
||||
if (json['event'] == 'new_bid') {
|
||||
should_display = true;
|
||||
}
|
||||
else if (json['event'] == 'new_bid' && notificationConfig.showNewBids) {
|
||||
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-violet-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round"><rect x="9.843" y="5.379" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -0.7635 13.1569)" width="11.314" height="4.243"></rect> <polyline points="3,23 3,19 15,19 15,23 "></polyline> <line x1="4" y1="15" x2="1" y2="15" stroke="#ffffff"></line> <line x1="5.757" y1="10.757" x2="3.636" y2="8.636" stroke="#ffffff"></line> <line x1="1" y1="23" x2="17" y2="23"></line> <line x1="17" y1="9" x2="23" y2="15"></line></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-normal"><span class="mb-1 text-sm font-semibold text-gray-900"><a class="underline" href=/bid/' + json['bid_id'] + '>New bid</a> on <a class="underline" href=/offer/' + json['offer_id'] + '>offer</a></span></div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-nonefocus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
|
||||
}
|
||||
else
|
||||
if (json['event'] == 'bid_accepted') {
|
||||
should_display = true;
|
||||
}
|
||||
else if (json['event'] == 'bid_accepted' && notificationConfig.showBidAccepted) {
|
||||
event_message = '<div id="hide"><div id="toast-success" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow" role="alert"><div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10 bg-violet-500 rounded-lg"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24"><g fill="#ffffff"><path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z" fill="#ffffff"></path></g></svg></div><div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900"><a class="underline" href=/bid/' + json['bid_id'] + '>Bid</a> accepted</div><button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8" data-dismiss="#toast-success" aria-label="Close"><span class="sr-only">Close</span><svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg></button></div></div>';
|
||||
}
|
||||
let messages = document.getElementById('ul_updates'),
|
||||
message = document.createElement('li');
|
||||
message.innerHTML = event_message;
|
||||
messages.appendChild(message);
|
||||
};
|
||||
floating_div.appendChild(messages);
|
||||
document.body.appendChild(floating_div);
|
||||
function closeAlert(event){
|
||||
should_display = true;
|
||||
}
|
||||
|
||||
if (should_display) {
|
||||
let messages = document.getElementById('ul_updates'),
|
||||
message = document.createElement('li');
|
||||
message.innerHTML = event_message;
|
||||
messages.appendChild(message);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(floating_div);
|
||||
|
||||
function closeAlert(event){
|
||||
let element = event.target;
|
||||
while(element.nodeName !== "BUTTON"){
|
||||
element = element.parentNode;
|
||||
element = element.parentNode;
|
||||
}
|
||||
element.parentNode.parentNode.removeChild(element.parentNode);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
@@ -10,14 +10,11 @@
|
||||
<div class="w-auto p-1">
|
||||
{{ circular_info_messages_svg | safe }}
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="ml-4 mt-1">
|
||||
<li class="font-semibold text-sm text-green-500 error_msg"><span class="bold">INFO:</span></li>
|
||||
<li class="font-medium text-sm text-green-500 infomsg">{{ m[1] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="w-auto p-2">
|
||||
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_{{ m[0] }}" aria-label="Close"><span class="sr-only">Close</span>
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
<section class="relative py-24 overflow-hidden">
|
||||
<div class="container px-4 mx-auto mb-16 md:mb-0">
|
||||
<div class="md:w-1/2 pl-4">
|
||||
<span class="inline-block py-1 px-3 mb-4 text-xs leading-5 bg-blue-500 text-white font-medium rounded-full shadow-sm">(BSX) BasicSwap v{{ version }} - (GUI) v.3.0.0</span>
|
||||
<span class="inline-block py-1 px-3 mb-4 text-xs leading-5 bg-blue-500 text-white font-medium rounded-full shadow-sm">(BSX) BasicSwap v{{ version }} - (GUI) v.3.1.1</span>
|
||||
<h3 class="mb-6 text-4xl md:text-5xl leading-tight text-coolGray-900 font-bold tracking-tighter dark:text-white">Welcome to BasicSwap DEX</h3>
|
||||
<p class="mb-12 text-lg md:text-xl text-coolGray-500 dark:text-gray-300 font-medium">Swap cryptocurrencies in <span class="underline">total privacy</span> with no middlemen, fees, <br> or restrictions. </p>
|
||||
<p class="mb-12 text-lg md:text-xl text-coolGray-500 dark:text-gray-300 font-medium">The World's Most Secure and Decentralized DEX, Safely swap cryptocurrencies without central points of failure.
|
||||
It’s free, completely trustless, and highly secure.</p>
|
||||
<div class="flex flex-wrap mb-10 text-center md:text-left">
|
||||
<div class="w-full md:w-auto mb-6 md:mb-0 md:pr-6">
|
||||
<a href="/wallets">
|
||||
@@ -69,4 +70,4 @@
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>{{ title_str }}</h2>
|
||||
<p>Info: {{ message_str }}</p>
|
||||
<p><a href="/">home</a></p>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
</script>
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - Info</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="dark:bg-gray-700">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="text-center">
|
||||
<a class="inline-block mb-6" href="#">
|
||||
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
|
||||
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
|
||||
</a>
|
||||
|
||||
<div class="p-6 bg-coolGray-100 dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h2 class="text-xl font-bold mb-4 text-coolGray-900 dark:text-white">INFO:</h2>
|
||||
<p class="text-lg text-coolGray-500 dark:text-white mb-6">{{ message_str }}</p>
|
||||
<a href="/" class="inline-block py-2 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition duration-300">Home</a>
|
||||
</div>
|
||||
<p class="mt-10">
|
||||
<span class="text-xs font-medium dark:text-white">Need help?</span>
|
||||
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Image toggling function
|
||||
function toggleImages() {
|
||||
const html = document.querySelector('html');
|
||||
const darkImages = document.querySelectorAll('.dark-image');
|
||||
const lightImages = document.querySelectorAll('.light-image');
|
||||
|
||||
if (html && html.classList.contains('dark')) {
|
||||
toggleImageDisplay(darkImages, 'block');
|
||||
toggleImageDisplay(lightImages, 'none');
|
||||
} else {
|
||||
toggleImageDisplay(darkImages, 'none');
|
||||
toggleImageDisplay(lightImages, 'block');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImageDisplay(images, display) {
|
||||
images.forEach(img => {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
|
||||
// Theme toggle functionality
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
// Call toggleImages on load
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% include 'header.html' %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg %}
|
||||
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg, circular_info_messages_svg %}
|
||||
|
||||
<div class="container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
@@ -57,19 +57,21 @@
|
||||
{% if sent_bid_id %}
|
||||
<section class="py-4" id="messages_send_bid_id" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-6 bg-green-100 border border-green-200 rounded-md">
|
||||
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
|
||||
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||
<div class="flex-1 p-2">
|
||||
<div class="flex flex-wrap -m-1">
|
||||
<div class="w-auto p-1">
|
||||
{{ confirm_green_svg | safe }}
|
||||
{{ circular_info_messages_svg | safe }}
|
||||
</div>
|
||||
<div class="flex-1 p-1">
|
||||
<h3 class="infomsg font-medium text-sm text-green-900">Sent Bid {{ sent_bid_id }}</h3></div>
|
||||
</div>
|
||||
<ul class="ml-4 mt-1">
|
||||
<li class="font-semibold text-sm text-green-500 error_msg"><span class="bold">INFO:</span></li>
|
||||
<li class="font-medium text-sm text-green-500 infomsg">Sent Bid {{ sent_bid_id }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-auto p-2">
|
||||
<button type="button" class="ml-auto bg-green-100 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex h-8 w-8 focus:outline-none" data-dismiss-target="#messages_send_bid_id" aria-label="Close"><span class="sr-only">Close</span>
|
||||
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_send_bid_id" aria-label="Close"><span class="sr-only">Close</span>
|
||||
{{ green_cross_close_svg | safe }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -304,7 +306,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post">
|
||||
<form id="bidForm" action="/offer/{{ offer_id }}" method="post">
|
||||
</section>
|
||||
{% if data.show_edit_form %}
|
||||
<section class="p-6 bg-body dark:bg-gray-700">
|
||||
@@ -405,22 +407,74 @@
|
||||
<td class="py-3 px-6">Amount you will send <span class="bold" id="bid_amt_to">{{ data.amt_to }}</span> {{ data.tla_to }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Send From Address</td>
|
||||
<td class="py-3 px-6">
|
||||
<div class="w-full md:flex-1">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
|
||||
{% for a in addrs %}
|
||||
<option value="{{ a[0] }}" {% if data.nb_addr_from==a[0] %} selected{% endif %}>{{ a[0] }} {{ a[1] }}</option>
|
||||
{% endfor %}
|
||||
<option value="-1" {% if data.nb_addr_from=="-1" %} selected{% endif %}>New Address</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Send From Address</td>
|
||||
<td class="py-3 px-6">
|
||||
<div class="w-full md:flex-1">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
|
||||
<option value="-1" {% if data.nb_addr_from=="-1" %} selected{% endif %}>New Address</option>
|
||||
{% for a in addrs %}
|
||||
<option value="{{ a[0] }}" {% if data.nb_addr_from==a[0] %} selected{% endif %}>{{ a[0] }} {{ a[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<script>
|
||||
function handleBidsPageAddress() {
|
||||
const selectElement = document.querySelector('select[name="addr_from"]');
|
||||
const STORAGE_KEY = 'lastUsedAddressBids';
|
||||
|
||||
if (!selectElement) return;
|
||||
|
||||
function loadInitialAddress() {
|
||||
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedAddressJSON) {
|
||||
try {
|
||||
const savedAddress = JSON.parse(savedAddressJSON);
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstAddress() {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAddress(value, text) {
|
||||
const addressData = {
|
||||
value: value,
|
||||
text: text
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
|
||||
}
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', handleBidsPageAddress);
|
||||
} else {
|
||||
handleBidsPageAddress();
|
||||
}
|
||||
</script>
|
||||
{% if data.amount_negotiable == true %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Amount</td>
|
||||
@@ -467,6 +521,7 @@
|
||||
<div class="w-full md:w-0/12">
|
||||
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
|
||||
<div class="w-full md:w-auto p-1.5 ml-2">
|
||||
<input type="hidden" name="confirm" value="true">
|
||||
<button name="sendbid" value="Send Bid" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Send Bid</button>
|
||||
</div>
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
@@ -483,6 +538,172 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
|
||||
|
||||
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
|
||||
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Confirm Bid</h2>
|
||||
|
||||
<div class="space-y-4 text-left mb-8">
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Amount you will get:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg">
|
||||
<span id="modal-amt-receive"></span>
|
||||
<span id="modal-receive-currency"></span>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1" id="modal-fee-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Amount you will send:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg">
|
||||
<span id="modal-amt-send"></span>
|
||||
<span id="modal-send-currency"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Send From Address:</div>
|
||||
<div class="font-mono text-sm p-2 bg-white dark:bg-gray-500 rounded border border-gray-300 dark:border-gray-400 overflow-x-auto text-gray-900 dark:text-white">
|
||||
<span id="modal-addr-from"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Minutes valid:</div>
|
||||
<div class="font-medium text-gray-900 dark:text-white text-lg" id="modal-valid-mins"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<button type="submit" name="sendbid" value="confirm"
|
||||
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
Confirm
|
||||
</button>
|
||||
<button type="button" onclick="hideConfirmModal()"
|
||||
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateBidParams(value_changed) {
|
||||
const coin_from = document.getElementById('coin_from').value;
|
||||
const coin_to = document.getElementById('coin_to').value;
|
||||
const amt_var = document.getElementById('amt_var').value;
|
||||
const rate_var = document.getElementById('rate_var').value;
|
||||
|
||||
let amt_from = '';
|
||||
let rate = '';
|
||||
|
||||
if (amt_var == 'True') {
|
||||
amt_from = document.getElementById('bid_amount').value;
|
||||
} else {
|
||||
amt_from = document.getElementById('amount_from').value;
|
||||
}
|
||||
|
||||
if (rate_var == 'True') {
|
||||
rate = document.getElementById('bid_rate').value;
|
||||
} else {
|
||||
rate = document.getElementById('offer_rate').value;
|
||||
}
|
||||
|
||||
if (value_changed == 'amount') {
|
||||
document.getElementById('bid_amt_from').innerHTML = amt_from;
|
||||
}
|
||||
|
||||
xhr_bid_params.open('POST', '/json/rate');
|
||||
xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr_bid_params.send('coin_from=' + coin_from + '&coin_to=' + coin_to + '&rate=' + rate + '&amt_from=' + amt_from);
|
||||
}
|
||||
|
||||
function updateModalValues() {
|
||||
document.getElementById('modal-amt-receive').textContent = document.getElementById('bid_amt_from').textContent;
|
||||
document.getElementById('modal-amt-send').textContent = document.getElementById('bid_amt_to').textContent;
|
||||
document.getElementById('modal-receive-currency').textContent = '{{ data.tla_from }}';
|
||||
document.getElementById('modal-send-currency').textContent = '{{ data.tla_to }}';
|
||||
|
||||
{% if data.xmr_type == true %}
|
||||
document.getElementById('modal-fee-info').textContent = `(excluding estimated {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees)`;
|
||||
{% else %}
|
||||
document.getElementById('modal-fee-info').textContent = '(excluding a tx fee)';
|
||||
{% endif %}
|
||||
|
||||
const addrSelect = document.querySelector('select[name="addr_from"]');
|
||||
const selectedText = addrSelect.options[addrSelect.selectedIndex].text;
|
||||
const addrText = selectedText === 'New Address' ? selectedText : selectedText.split(' ')[0];
|
||||
document.getElementById('modal-addr-from').textContent = addrText;
|
||||
|
||||
document.getElementById('modal-valid-mins').textContent = document.querySelector('input[name="validmins"]').value;
|
||||
}
|
||||
function showConfirmModal() {
|
||||
updateModalValues();
|
||||
document.getElementById('confirmModal').classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
function hideConfirmModal() {
|
||||
document.getElementById('confirmModal').classList.add('hidden');
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleCancelClick(event) {
|
||||
event.preventDefault();
|
||||
window.location.href = `/offer/${window.location.pathname.split('/')[2]}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
|
||||
if (sendBidBtn) {
|
||||
sendBidBtn.onclick = showConfirmModal;
|
||||
}
|
||||
|
||||
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.onclick = hideConfirmModal;
|
||||
}
|
||||
|
||||
const mainCancelBtn = document.querySelector('button[name="cancel"]');
|
||||
if (mainCancelBtn) {
|
||||
mainCancelBtn.onclick = handleCancelClick;
|
||||
}
|
||||
|
||||
// Input change listeners
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
if (validMinsInput) {
|
||||
validMinsInput.addEventListener('input', updateModalValues);
|
||||
}
|
||||
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
if (addrFromSelect) {
|
||||
addrFromSelect.addEventListener('change', updateModalValues);
|
||||
}
|
||||
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
if (bidAmountInput) {
|
||||
bidAmountInput.addEventListener('change', () => {
|
||||
updateBidParams('amount');
|
||||
updateModalValues();
|
||||
});
|
||||
}
|
||||
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
if (bidRateInput) {
|
||||
bidRateInput.addEventListener('change', () => {
|
||||
updateBidParams('rate');
|
||||
updateModalValues();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% else %}
|
||||
<section>
|
||||
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
|
||||
@@ -528,6 +749,9 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="sendbid" value="true">
|
||||
<input type="hidden" name="confirm" value="true">
|
||||
<input type="hidden" id="coin_from" value="{{ data.coin_from_ind }}">
|
||||
<input type="hidden" id="coin_to" value="{{ data.coin_to_ind }}">
|
||||
<input type="hidden" id="amt_var" value="{{ data.amount_negotiable }}">
|
||||
|
||||
@@ -94,29 +94,87 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Select Address</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="relative">
|
||||
{{ input_down_arrow_offer_svg | safe }}
|
||||
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
{{ select_address_svg | safe }}
|
||||
</div>
|
||||
<select class="hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
|
||||
{% for a in addrs %}
|
||||
<option{% if data.addr_from==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option>
|
||||
{% endfor %}
|
||||
<option{% if data.addr_from=="-1" %} selected{% endif %} value="-1">New Address</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Select Address</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="relative">
|
||||
{{ input_down_arrow_offer_svg | safe }}
|
||||
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
{{ select_address_svg | safe }}
|
||||
</div>
|
||||
<select class="hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_from">
|
||||
<option{% if data.addr_from=="-1" %} selected{% endif %} value="-1">New Address</option>
|
||||
{% for a in addrs %}
|
||||
<option{% if data.addr_from==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function handleNewOfferAddress() {
|
||||
const selectElement = document.querySelector('select[name="addr_from"]');
|
||||
const STORAGE_KEY = 'lastUsedAddressNewOffer';
|
||||
const form = selectElement?.closest('form');
|
||||
|
||||
if (!selectElement || !form) return;
|
||||
|
||||
function loadInitialAddress() {
|
||||
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedAddressJSON) {
|
||||
try {
|
||||
const savedAddress = JSON.parse(savedAddressJSON);
|
||||
selectElement.value = savedAddress.value;
|
||||
} catch (e) {
|
||||
selectFirstAddress();
|
||||
}
|
||||
} else {
|
||||
selectFirstAddress();
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstAddress() {
|
||||
if (selectElement.options.length > 1) {
|
||||
const firstOption = selectElement.options[1];
|
||||
if (firstOption) {
|
||||
selectElement.value = firstOption.value;
|
||||
saveAddress(firstOption.value, firstOption.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveAddress(value, text) {
|
||||
const addressData = {
|
||||
value: value,
|
||||
text: text
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
saveAddress(event.target.value, event.target.selectedOptions[0].text);
|
||||
});
|
||||
|
||||
loadInitialAddress();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', handleNewOfferAddress);
|
||||
} else {
|
||||
handleNewOfferAddress();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
@@ -248,31 +306,68 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="flex form-check form-check-inline">
|
||||
<div class="flex items-center h-5"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" {% if data.amt_var==true %} checked="checked" {% endif %}> </div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different amount to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 form-check form-check-inline">
|
||||
<div class="flex items-center h-5"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" {% if data.rate_var==true %} checked="checked" {% endif %}> </div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different rate to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if debug_mode == true %}
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="flex form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av">
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different amount to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv">
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Allow bids with a different rate to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
<div class="w-full md:w-1/3 p-6">
|
||||
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
|
||||
</div>
|
||||
<div class="w-full md:flex-1 p-3">
|
||||
<div class="flex form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2" style="opacity: 0.40;">Amount Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different amount to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 form-check form-check-inline">
|
||||
<div class="flex items-center h-5">
|
||||
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
|
||||
</div>
|
||||
<div class="ml-2 text-sm">
|
||||
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3" style="opacity: 0.40;">Rate Variable</label>
|
||||
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different rate to the offer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="pricejsonhidden hidden py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
|
||||
<div class="w-full md:w-10/12">
|
||||
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
|
||||
@@ -409,7 +504,7 @@
|
||||
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
|
||||
|
||||
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
|
||||
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */];
|
||||
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */, '17' /* BCH */];
|
||||
const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */];
|
||||
let make_hidden = false;
|
||||
if (adaptor_sig_only_coins.includes(coin_from) || adaptor_sig_only_coins.includes(coin_to)) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -694,4 +694,12 @@
|
||||
</g>
|
||||
</svg>
|
||||
' %}
|
||||
{% set change_password_svg = '
|
||||
<svg class="text-gray-500 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24">
|
||||
<g fill="#6b7280">
|
||||
<path d="M19,10H5a3,3,0,0,0-3,3v8a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V13A3,3,0,0,0,19,10Zm-7,9a2,2,0,1,1,2-2A2,2,0,0,1,12,19Z" fill="#6b7280"></path>
|
||||
<path data-color="color-2" d="M18,8H16V6a3.958,3.958,0,0,0-3.911-4h-.042A3.978,3.978,0,0,0,8,5.911V8H6V5.9A5.961,5.961,0,0,1,11.949,0h.061A5.979,5.979,0,0,1,18,6.01Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
' %}
|
||||
{% set select_box_class = 'hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0' %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -5,25 +6,25 @@
|
||||
{% if refresh %}
|
||||
<meta http-equiv="refresh" content="{{ refresh }}">
|
||||
{% endif %}
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
if (!localStorage.getItem('color-theme')) {
|
||||
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
</script>
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
</script>
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
<title>(BSX) BasicSwap - v{{ version }}</title>
|
||||
</head>
|
||||
<body class="dark:bg-gray-700">
|
||||
<section class="py-24 md:py-32">
|
||||
@@ -31,25 +32,22 @@
|
||||
<div class="max-w-sm mx-auto">
|
||||
<div class="mb-3 text-center">
|
||||
<a class="inline-block mb-6" href="#">
|
||||
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
|
||||
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
|
||||
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
|
||||
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
|
||||
</a>
|
||||
<p class="text-lg text-coolGray-500 font-medium mb-6 dark:text-white" contenteditable="false">Unlock your wallets</p>
|
||||
<p class="text-lg text-coolGray-500 font-medium mb-6 dark:text-white">Unlock your wallets</p>
|
||||
{% for m in messages %}
|
||||
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-6 bg-green-100 border border-green-200 rounded-md">
|
||||
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
|
||||
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||
<div class="flex-1 p-2">
|
||||
<div class="flex flex-wrap -m-1">
|
||||
<div class="w-auto p-1">
|
||||
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4732 4.80667C12.4112 4.74418 12.3375 4.69458 12.2563 4.66074C12.175 4.62689 12.0879 4.60947 11.9999 4.60947C11.9119 4.60947 11.8247 4.62689 11.7435 4.66074C11.6623 4.69458 11.5885 4.74418 11.5266 4.80667L6.55989 9.78L4.47322 7.68667C4.40887 7.62451 4.33291 7.57563 4.24967 7.54283C4.16644 7.51003 4.07755 7.49394 3.9881 7.49549C3.89865 7.49703 3.81037 7.51619 3.72832 7.55185C3.64627 7.58751 3.57204 7.63898 3.50989 7.70333C3.44773 7.76768 3.39885 7.84364 3.36605 7.92688C3.33324 8.01011 3.31716 8.099 3.31871 8.18845C3.32025 8.2779 3.3394 8.36618 3.37507 8.44823C3.41073 8.53028 3.4622 8.60451 3.52655 8.66667L6.08655 11.2267C6.14853 11.2892 6.22226 11.3387 6.3035 11.3726C6.38474 11.4064 6.47188 11.4239 6.55989 11.4239C6.64789 11.4239 6.73503 11.4064 6.81627 11.3726C6.89751 11.3387 6.97124 11.2892 7.03322 11.2267L12.4732 5.78667C12.5409 5.72424 12.5949 5.64847 12.6318 5.56414C12.6688 5.4798 12.6878 5.38873 12.6878 5.29667C12.6878 5.2046 12.6688 5.11353 12.6318 5.02919C12.5949 4.94486 12.5409 4.86909 12.4732 4.80667Z" fill="#2AD168"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 p-1">
|
||||
<h3 class="font-medium text-sm text-green-900 text-left">{{ m[1] }}</h3>
|
||||
</div>
|
||||
<div class="w-auto p-1"> {{ circular_info_messages_svg | safe }} </div>
|
||||
<ul class="ml-4 mt-1">
|
||||
<li class="font-semibold text-sm text-green-500 error_msg text-left"><span class="bold">ALERT:</span></li>
|
||||
<li class="font-medium text-sm text-green-500 infomsg">This will unlock the system for all users!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,18 +58,15 @@
|
||||
{% for m in err_messages %}
|
||||
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
|
||||
<div class="container px-4 mx-auto">
|
||||
<div class="p-6 bg-red-100 border border-red-200 rounded-md">
|
||||
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
|
||||
<div class="flex flex-wrap justify-between items-center -m-2">
|
||||
<div class="flex-1 p-2">
|
||||
<div class="flex flex-wrap -m-1">
|
||||
<div class="w-auto p-1">
|
||||
<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4733 5.52667C10.4114 5.46419 10.3376 5.41459 10.2564 5.38075C10.1751 5.3469 10.088 5.32947 9.99999 5.32947C9.91198 5.32947 9.82485 5.3469 9.74361 5.38075C9.66237 5.41459 9.58863 5.46419 9.52666 5.52667L7.99999 7.06001L6.47333 5.52667C6.34779 5.40114 6.17753 5.33061 5.99999 5.33061C5.82246 5.33061 5.65219 5.40114 5.52666 5.52667C5.40112 5.65221 5.3306 5.82247 5.3306 6.00001C5.3306 6.17754 5.40112 6.3478 5.52666 6.47334L7.05999 8.00001L5.52666 9.52667C5.46417 9.58865 5.41458 9.66238 5.38073 9.74362C5.34689 9.82486 5.32946 9.912 5.32946 10C5.32946 10.088 5.34689 10.1752 5.38073 10.2564C5.41458 10.3376 5.46417 10.4114 5.52666 10.4733C5.58863 10.5358 5.66237 10.5854 5.74361 10.6193C5.82485 10.6531 5.91198 10.6705 5.99999 10.6705C6.088 10.6705 6.17514 10.6531 6.25638 10.6193C6.33762 10.5854 6.41135 10.5358 6.47333 10.4733L7.99999 8.94001L9.52666 10.4733C9.58863 10.5358 9.66237 10.5854 9.74361 10.6193C9.82485 10.6531 9.91198 10.6705 9.99999 10.6705C10.088 10.6705 10.1751 10.6531 10.2564 10.6193C10.3376 10.5854 10.4114 10.5358 10.4733 10.4733C10.5358 10.4114 10.5854 10.3376 10.6193 10.2564C10.6531 10.1752 10.6705 10.088 10.6705 10C10.6705 9.912 10.6531 9.82486 10.6193 9.74362C10.5854 9.66238 10.5358 9.58865 10.4733 9.52667L8.93999 8.00001L10.4733 6.47334C10.5358 6.41137 10.5854 6.33763 10.6193 6.25639C10.6531 6.17515 10.6705 6.08802 10.6705 6.00001C10.6705 5.912 10.6531 5.82486 10.6193 5.74362C10.5854 5.66238 10.5358 5.58865 10.4733 5.52667ZM12.7133 3.28667C12.0983 2.64994 11.3627 2.14206 10.5494 1.79266C9.736 1.44327 8.8612 1.25936 7.976 1.25167C7.0908 1.24398 6.21294 1.41266 5.39363 1.74786C4.57432 2.08307 3.82998 2.57809 3.20403 3.20404C2.57807 3.82999 2.08305 4.57434 1.74785 5.39365C1.41264 6.21296 1.24396 7.09082 1.25166 7.97602C1.25935 8.86121 1.44326 9.73601 1.79265 10.5494C2.14204 11.3627 2.64992 12.0984 3.28666 12.7133C3.90164 13.3501 4.63727 13.858 5.45063 14.2074C6.26399 14.5567 7.13879 14.7407 8.02398 14.7483C8.90918 14.756 9.78704 14.5874 10.6064 14.2522C11.4257 13.9169 12.17 13.4219 12.796 12.796C13.4219 12.17 13.9169 11.4257 14.2521 10.6064C14.5873 9.78706 14.756 8.90919 14.7483 8.024C14.7406 7.1388 14.5567 6.264 14.2073 5.45064C13.8579 4.63728 13.3501 3.90165 12.7133 3.28667ZM11.7733 11.7733C10.9014 12.6463 9.75368 13.1899 8.52585 13.3115C7.29802 13.4332 6.066 13.1254 5.03967 12.4405C4.01335 11.7557 3.25623 10.7361 2.89731 9.55566C2.53838 8.37518 2.59986 7.10677 3.07127 5.96653C3.54267 4.82629 4.39484 3.88477 5.48259 3.30238C6.57033 2.71999 7.82635 2.53276 9.03666 2.77259C10.247 3.01242 11.3367 3.66447 12.1202 4.61765C12.9036 5.57083 13.3324 6.76617 13.3333 8.00001C13.3357 8.70087 13.1991 9.39524 12.9313 10.0429C12.6635 10.6906 12.2699 11.2788 11.7733 11.7733Z" fill="#EF5944"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 p-1">
|
||||
<h3 class="text-left font-medium text-sm text-red-900 error_msg">Error: {{ m[1] }}</h3>
|
||||
</div>
|
||||
<div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
|
||||
<ul class="ml-4 mt-1">
|
||||
<li class="font-semibold text-sm text-red-500 error_msg text-left"><span class="bold">ERROR:</span></li>
|
||||
<li class="font-medium text-sm text-red-500 error_msg">{{ m[1] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +77,7 @@
|
||||
</div>
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="" contenteditable="false">Your Password</label>
|
||||
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="">Your Password</label>
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2">
|
||||
<input class="hidden js-password-toggle" id="toggle" type="checkbox" />
|
||||
@@ -99,41 +94,40 @@
|
||||
</div>
|
||||
<button type="submit" name="unlock" value="Unlock" class="appearance-none focus:outline-none inline-block py-3 px-7 mb-6 w-full text-base text-blue-50 font-medium text-center leading-6 bg-blue-500 hover:bg-blue-600 focus:ring-0 rounded-md shadow-sm">Unlock</button>
|
||||
<p class="text-center">
|
||||
<span class="text-xs font-medium dark:text-white" contenteditable="false">Need help?</span>
|
||||
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank" contenteditable="false">Help / Tutorials</a>
|
||||
<span class="text-xs font-medium dark:text-white">Need help?</span>
|
||||
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500" contenteditable="false">{{ title }} - GUI 3.0.0</span>
|
||||
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
|
||||
</p>
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
const passwordToggle = document.querySelector('.js-password-toggle')
|
||||
passwordToggle.addEventListener('change', function() {
|
||||
const password = document.querySelector('.js-password'),
|
||||
passwordLabel = document.querySelector('.js-password-label')
|
||||
if (password.type === 'password') {
|
||||
password.type = 'text'
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>'
|
||||
} else {
|
||||
password.type = 'password'
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>'
|
||||
}
|
||||
password.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
window.onload = () => {
|
||||
toggleImages();
|
||||
};
|
||||
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
toggleImages();
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Password toggle functionality
|
||||
const passwordToggle = document.querySelector('.js-password-toggle');
|
||||
if (passwordToggle) {
|
||||
passwordToggle.addEventListener('change', function() {
|
||||
const password = document.querySelector('.js-password');
|
||||
const passwordLabel = document.querySelector('.js-password-label');
|
||||
if (password && passwordLabel) {
|
||||
if (password.type === 'password') {
|
||||
password.type = 'text';
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>';
|
||||
} else {
|
||||
password.type = 'password';
|
||||
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>';
|
||||
}
|
||||
password.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Image toggling function
|
||||
function toggleImages() {
|
||||
const html = document.querySelector('html');
|
||||
const darkImages = document.querySelectorAll('.dark-image');
|
||||
@@ -153,17 +147,8 @@ passwordToggle.addEventListener('change', function() {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Theme toggle functionality
|
||||
function setTheme(theme) {
|
||||
if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
@@ -174,17 +159,33 @@ passwordToggle.addEventListener('change', function() {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
// Initialize theme
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
|
||||
// Call toggleImages on load
|
||||
toggleImages();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -163,6 +163,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if w.wallet_blocks %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Wallet Blocks:</td>
|
||||
<td class="py-3 px-6">{{ w.wallet_blocks }}{% if w.known_block_count %} / {{ w.known_block_count }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Synced:</td>
|
||||
<td class="py-3 px-6">{{ w.synced }}</td>
|
||||
@@ -388,8 +394,8 @@
|
||||
|
||||
var qrCodeStealth = new QRCode(document.getElementById("qrcode-stealth"), {
|
||||
text: stealthAddress,
|
||||
width: 170,
|
||||
height: 170,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
@@ -404,8 +410,8 @@
|
||||
|
||||
var qrCodeMWEB = new QRCode(document.getElementById("qrcode-mweb"), {
|
||||
text: mwebAddress,
|
||||
width: 170,
|
||||
height: 170,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
@@ -420,8 +426,8 @@
|
||||
|
||||
var qrCodeMoneroSub = new QRCode(document.getElementById("qrcode-monero-sub"), {
|
||||
text: moneroSubAddress,
|
||||
width: 170,
|
||||
height: 170,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
@@ -433,8 +439,8 @@
|
||||
|
||||
var qrCodeMoneroMain = new QRCode(document.getElementById("qrcode-monero-main"), {
|
||||
text: moneroMainAddress,
|
||||
width: 170,
|
||||
height: 170,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
@@ -447,8 +453,8 @@
|
||||
|
||||
var qrCodeDepost = new QRCode(document.getElementById("qrcode-deposit"), {
|
||||
text: defaultAddress,
|
||||
width: 170,
|
||||
height: 170,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.L
|
||||
@@ -456,63 +462,63 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
let clickTimeout = null;
|
||||
|
||||
function copyAndShowMessage(elementId) {
|
||||
const addressElement = document.getElementById(elementId);
|
||||
if (!addressElement) return;
|
||||
const addressText = addressElement.innerText.trim();
|
||||
copyToClipboard(addressText);
|
||||
addressElement.innerText = 'Copied to clipboard';
|
||||
function copyToClipboard(text) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
const originalWidth = addressElement.offsetWidth;
|
||||
function copyAndShowMessage(elementId) {
|
||||
const addressElement = document.getElementById(elementId);
|
||||
if (!addressElement) return;
|
||||
const addressText = addressElement.innerText.trim();
|
||||
|
||||
addressElement.classList.add('copying');
|
||||
if (addressText === 'Copied to clipboard') return;
|
||||
|
||||
addressElement.parentElement.style.width = `${originalWidth}px`;
|
||||
copyToClipboard(addressText);
|
||||
addressElement.innerText = 'Copied to clipboard';
|
||||
const originalWidth = addressElement.offsetWidth;
|
||||
addressElement.classList.add('copying');
|
||||
addressElement.parentElement.style.width = `${originalWidth}px`;
|
||||
setTimeout(function () {
|
||||
addressElement.innerText = addressText;
|
||||
addressElement.classList.remove('copying');
|
||||
addressElement.parentElement.style.width = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
addressElement.innerText = addressText;
|
||||
addressElement.classList.remove('copying');
|
||||
addressElement.parentElement.style.width = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const stealthAddressElement = document.getElementById('stealth_address');
|
||||
if (stealthAddressElement) {
|
||||
stealthAddressElement.addEventListener('click', function () {
|
||||
copyAndShowMessage('stealth_address');
|
||||
});
|
||||
}
|
||||
|
||||
const mainDepositAddressElement = document.getElementById('main_deposit_address');
|
||||
if (mainDepositAddressElement) {
|
||||
mainDepositAddressElement.addEventListener('click', function () {
|
||||
copyAndShowMessage('main_deposit_address');
|
||||
});
|
||||
}
|
||||
|
||||
const moneroMainAddressElement = document.getElementById('monero_main_address');
|
||||
if (moneroMainAddressElement) {
|
||||
moneroMainAddressElement.addEventListener('click', function () {
|
||||
copyAndShowMessage('monero_main_address');
|
||||
});
|
||||
}
|
||||
|
||||
const moneroSubAddressElement = document.getElementById('monero_sub_address');
|
||||
if (moneroSubAddressElement) {
|
||||
moneroSubAddressElement.addEventListener('click', function () {
|
||||
copyAndShowMessage('monero_sub_address');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const stealthAddressElement = document.getElementById('stealth_address');
|
||||
if (stealthAddressElement) {
|
||||
stealthAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('stealth_address');
|
||||
});
|
||||
}
|
||||
const mainDepositAddressElement = document.getElementById('main_deposit_address');
|
||||
if (mainDepositAddressElement) {
|
||||
mainDepositAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('main_deposit_address');
|
||||
});
|
||||
}
|
||||
const moneroMainAddressElement = document.getElementById('monero_main_address');
|
||||
if (moneroMainAddressElement) {
|
||||
moneroMainAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('monero_main_address');
|
||||
});
|
||||
}
|
||||
const moneroSubAddressElement = document.getElementById('monero_sub_address');
|
||||
if (moneroSubAddressElement) {
|
||||
moneroSubAddressElement.addEventListener('click', function() {
|
||||
copyAndShowMessage('monero_sub_address');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<section class="p-6">
|
||||
<div class="flex items-center">
|
||||
<h4 class="font-semibold text-2xl text-black dark:text-white">Withdraw</h4>
|
||||
@@ -564,122 +570,145 @@
|
||||
<td class="py-3 px-6">
|
||||
<div class="flex"> <input placeholder="{{ w.ticker }} Amount" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" id="amount" name="amt_{{ w.cid }}" value="{{ w.wd_value }}">
|
||||
<div class="ml-2 flex">
|
||||
{% if w.cid == '1' %}
|
||||
{# PART #}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, blindBalance, anonBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
{% if w.cid == '1' %}
|
||||
{# PART #}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
case 'blind':
|
||||
floatBalance = parseFloat(blindBalance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
case 'anon':
|
||||
floatBalance = parseFloat(anonBalance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
default:
|
||||
floatBalance = parseFloat(balance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
}
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
case 'blind':
|
||||
floatBalance = parseFloat(blindBalance);
|
||||
break;
|
||||
case 'anon':
|
||||
floatBalance = parseFloat(anonBalance);
|
||||
break;
|
||||
default:
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
}
|
||||
calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
}
|
||||
</script>
|
||||
{# / PART #}
|
||||
{% elif w.cid == '3' %}
|
||||
{# LTC #}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.mweb_balance }}', 'mweb')">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, mwebBalance, selectedType) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{# / PART #}
|
||||
{% elif w.cid == '3' %}
|
||||
{# LTC #}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">25%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, cid, mwebBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
case 'mweb':
|
||||
floatBalance = parseFloat(mwebBalance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
default:
|
||||
floatBalance = parseFloat(balance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
break;
|
||||
}
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
case 'mweb':
|
||||
floatBalance = parseFloat(mwebBalance);
|
||||
break;
|
||||
default:
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
}
|
||||
calculatedAmount = floatBalance * percent;
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
}
|
||||
</script>
|
||||
{# / LTC #}
|
||||
{% else %}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}')">25%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}')">50%</button> <button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', '{{ w.cid }}')">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, cid) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{# / LTC #}
|
||||
{% else %}
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }})">100%</button>
|
||||
<script>
|
||||
function setAmount(percent, balance, cid) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var floatBalance = parseFloat(balance);
|
||||
var calculatedAmount = floatBalance * percent;
|
||||
|
||||
floatBalance = parseFloat(balance);
|
||||
calculatedAmount = floatBalance * percent;
|
||||
if (cid in '6, 9' && percent === 1) {
|
||||
amountInput.setAttribute('data-hidden', 'true');
|
||||
amountInput.placeholder = 'Sweep All';
|
||||
amountInput.value = '';
|
||||
amountInput.disabled = true;
|
||||
} else if (amountInput.getAttribute('data-hidden') === 'true' && percent !== 1) {
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.setAttribute('data-hidden', 'false');
|
||||
amountInput.placeholder = '';
|
||||
amountInput.disabled = false;
|
||||
} else {
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.placeholder = '';
|
||||
amountInput.disabled = false;
|
||||
}
|
||||
if (cid in '6, 9' && percent === 1) {
|
||||
var sweepAllCheckbox = document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = true;
|
||||
}
|
||||
} else {
|
||||
var sweepAllCheckbox = document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
sweepAllCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const specialCids = [6, 9];
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
{% if w.cid in '6, 9' %} {# XMR | WOW #}
|
||||
<td class="py-3 px-6 bold">Sweep All:</td>
|
||||
<td class="py-3 px-6"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="sweepall" name="sweepall_{{ w.cid }}" {% if w.wd_sweepall==true %} checked=checked{% endif %}> </td> {% else %} <td class="py-3 px-6 bold">Subtract Fee:</td>
|
||||
<td class="py-3 px-6"> <input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked=checked{% endif %}> </td>
|
||||
{% endif %}
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
console.log("CID:", cid);
|
||||
console.log("Percent:", percent);
|
||||
console.log("Balance:", balance);
|
||||
console.log("Calculated Amount:", calculatedAmount);
|
||||
|
||||
if (specialCids.includes(parseInt(cid)) && percent === 1) {
|
||||
amountInput.setAttribute('data-hidden', 'true');
|
||||
amountInput.placeholder = 'Sweep All';
|
||||
amountInput.value = '';
|
||||
amountInput.disabled = true;
|
||||
console.log("Sweep All activated for special CID:", cid);
|
||||
} else {
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.setAttribute('data-hidden', 'false');
|
||||
amountInput.placeholder = '';
|
||||
amountInput.disabled = false;
|
||||
}
|
||||
|
||||
let sweepAllCheckbox = document.getElementById('sweepall');
|
||||
if (sweepAllCheckbox) {
|
||||
if (specialCids.includes(parseInt(cid)) && percent === 1) {
|
||||
sweepAllCheckbox.checked = true;
|
||||
console.log("Sweep All checkbox checked");
|
||||
} else {
|
||||
sweepAllCheckbox.checked = false;
|
||||
console.log("Sweep All checkbox unchecked");
|
||||
}
|
||||
}
|
||||
|
||||
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
{% if w.cid in '6, 9' %} {# XMR | WOW #}
|
||||
<td class="py-3 px-6 bold">Sweep All:</td>
|
||||
<td class="py-3 px-6">
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="sweepall" name="sweepall_{{ w.cid }}" {% if w.wd_sweepall==true %} checked=checked{% endif %}>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="py-3 px-6 bold">Subtract Fee:</td>
|
||||
<td class="py-3 px-6">
|
||||
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked=checked{% endif %}>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% if w.cid == '1' %}
|
||||
{# PART #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
@@ -842,30 +871,47 @@
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
<script>
|
||||
const coinNameToSymbol = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Litecoin': 'LTC',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'DECRED': 'DCR',
|
||||
'WOWNERO': 'WOW'
|
||||
};
|
||||
const coinNameToSymbol = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Zano': 'ZANO',
|
||||
'Bitcoin Cash': 'BCH',
|
||||
};
|
||||
|
||||
const getUsdValue = (cryptoValue, coinSymbol) => fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.USD;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return cryptoValue * exchangeRate;
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
});
|
||||
const getUsdValue = (cryptoValue, coinSymbol) => {
|
||||
if (coinSymbol === 'WOW') {
|
||||
return fetch(`https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.wownero.usd;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return cryptoValue * exchangeRate;
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.USD;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return cryptoValue * exchangeRate;
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateUsdValue = async (cryptoCell, coinFullName, usdValueSpan) => {
|
||||
const coinSymbol = coinNameToSymbol[coinFullName] || '';
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
|
||||
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change Password</span></a>
|
||||
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
|
||||
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">{{ circular_arrows_svg | safe }}<span>Refresh</span></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@
|
||||
<h4 class="text-xl font-bold dark:text-white">{{ w.name }}
|
||||
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="hidden inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
|
||||
@@ -207,245 +207,442 @@
|
||||
{% include 'footer.html' %}
|
||||
|
||||
<script>
|
||||
const coinNameToSymbol = {
|
||||
const CONFIG = {
|
||||
MAX_RETRIES: 3,
|
||||
BASE_DELAY: 1000,
|
||||
CACHE_EXPIRATION: 5 * 60 * 1000,
|
||||
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
|
||||
API_TIMEOUT: 30000,
|
||||
DEBOUNCE_DELAY: 500,
|
||||
CACHE_MIN_INTERVAL: 60 * 1000
|
||||
};
|
||||
|
||||
const STATE_KEYS = {
|
||||
LAST_UPDATE: 'last-update-time',
|
||||
PREVIOUS_TOTAL: 'previous-total-usd',
|
||||
CURRENT_TOTAL: 'current-total-usd',
|
||||
BALANCES_VISIBLE: 'balancesVisible'
|
||||
};
|
||||
|
||||
const COIN_SYMBOLS = {
|
||||
'Bitcoin': 'bitcoin',
|
||||
'Particl': 'particl',
|
||||
'Monero': 'monero',
|
||||
'Wownero': 'wownero',
|
||||
'Litecoin': 'litecoin',
|
||||
'Firo': 'zcoin',
|
||||
'Dash': 'dash',
|
||||
'PIVX': 'pivx',
|
||||
'Decred': 'decred',
|
||||
'Zano': 'zano',
|
||||
'Bitcoin Cash': 'bitcoin-cash'
|
||||
};
|
||||
|
||||
const SHORT_NAMES = {
|
||||
'Bitcoin': 'BTC',
|
||||
'Particl': 'PART',
|
||||
'Particl Blind': 'PART',
|
||||
'Particl Anon': 'PART',
|
||||
'Monero': 'XMR',
|
||||
'Wownero': 'WOW',
|
||||
'Litecoin': 'LTC',
|
||||
'Litecoin MWEB': 'LTC MWEB',
|
||||
'Firo': 'FIRO',
|
||||
'Dash': 'DASH',
|
||||
'PIVX': 'PIVX',
|
||||
'Decred': 'DCR',
|
||||
'Zano': 'ZANO',
|
||||
'Bitcoin Cash': 'BCH'
|
||||
};
|
||||
|
||||
const getUsdValue = (cryptoValue, coinSymbol) => {
|
||||
return fetch(`https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const exchangeRate = data.USD;
|
||||
if (!isNaN(exchangeRate)) {
|
||||
return {
|
||||
usdValue: cryptoValue * exchangeRate,
|
||||
btcValue: cryptoValue / exchangeRate
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||
class Cache {
|
||||
constructor(expirationTime) {
|
||||
this.data = null;
|
||||
this.timestamp = null;
|
||||
this.expirationTime = expirationTime;
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return Boolean(
|
||||
this.data &&
|
||||
this.timestamp &&
|
||||
(Date.now() - this.timestamp < this.expirationTime)
|
||||
);
|
||||
}
|
||||
|
||||
set(data) {
|
||||
this.data = data;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
|
||||
get() {
|
||||
if (this.isValid()) {
|
||||
return this.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.data = null;
|
||||
this.timestamp = null;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
|
||||
this.lastFetchTime = 0;
|
||||
}
|
||||
|
||||
makeRequest(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/json/readurl');
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.timeout = CONFIG.API_TIMEOUT;
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
reject(new Error('Request timed out'));
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.Error) {
|
||||
reject(new Error(response.Error));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid JSON response: ${error.message}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
reject(new Error('Network error occurred'));
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({ url, headers }));
|
||||
});
|
||||
}
|
||||
|
||||
async fetchPrices(forceUpdate = false) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastFetch = now - this.lastFetchTime;
|
||||
|
||||
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
|
||||
const cachedData = this.cache.get();
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const prices = await this.makeRequest(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'
|
||||
);
|
||||
this.cache.set(prices);
|
||||
this.lastFetchTime = now;
|
||||
return prices;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt < CONFIG.MAX_RETRIES - 1) {
|
||||
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cachedData = this.cache.get();
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
throw lastError || new Error('Failed to fetch prices');
|
||||
}
|
||||
}
|
||||
|
||||
class UiManager {
|
||||
constructor() {
|
||||
this.api = new ApiClient();
|
||||
this.toggleInProgress = false;
|
||||
this.toggleDebounceTimer = null;
|
||||
this.priceUpdateInterval = null;
|
||||
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
|
||||
}
|
||||
|
||||
getShortName(fullName) {
|
||||
return SHORT_NAMES[fullName] || fullName;
|
||||
}
|
||||
|
||||
storeOriginalValues() {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const value = el.textContent?.trim() || '';
|
||||
|
||||
if (coinName) {
|
||||
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinId = COIN_SYMBOLS[coinName];
|
||||
const shortName = this.getShortName(coinName);
|
||||
|
||||
if (coinId) {
|
||||
if (coinId === 'particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinId === 'litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
|
||||
el.setAttribute('data-original-value', `${amount} ${shortName}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateValues = async () => {
|
||||
const coinNameValues = document.querySelectorAll('.coinname-value');
|
||||
|
||||
for (const coinNameValue of coinNameValues) {
|
||||
const coinFullName = coinNameValue.getAttribute('data-coinname');
|
||||
const cryptoValue = parseFloat(coinNameValue.textContent);
|
||||
const coinSymbol = coinNameToSymbol[coinFullName];
|
||||
|
||||
if (coinSymbol) {
|
||||
try {
|
||||
const { usdValue, btcValue } = await getUsdValue(cryptoValue, coinSymbol);
|
||||
|
||||
const usdValueElement = coinNameValue.nextElementSibling.querySelector('.usd-value');
|
||||
if (usdValueElement) {
|
||||
usdValueElement.textContent = `$${usdValue.toFixed(2)}`;
|
||||
}
|
||||
|
||||
const btcValueElement = coinNameValue.nextElementSibling.querySelector('.btc-value');
|
||||
if (btcValueElement) {
|
||||
btcValueElement.textContent = `${btcValue.toFixed(8)} BTC`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
||||
}
|
||||
}
|
||||
|
||||
calculateTotalUsdValue();
|
||||
calculateTotalBtcValue();
|
||||
};
|
||||
async updatePrices(forceUpdate = false) {
|
||||
try {
|
||||
const prices = await this.api.fetchPrices(forceUpdate);
|
||||
let newTotal = 0;
|
||||
|
||||
const toggleUsdAmount = async (usdCell, isVisible) => {
|
||||
const currentTime = Date.now();
|
||||
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
|
||||
this.lastUpdateTime = currentTime;
|
||||
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (prices) {
|
||||
Object.entries(COIN_SYMBOLS).forEach(([coinName, coinId]) => {
|
||||
if (prices[coinId]?.usd) {
|
||||
localStorage.setItem(`${coinId}-price`, prices[coinId].usd.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const coinName = el.getAttribute('data-coinname');
|
||||
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
|
||||
|
||||
if (usdText) {
|
||||
usdText.style.display = isVisible ? 'inline' : 'none';
|
||||
}
|
||||
if (!coinName) return;
|
||||
|
||||
if (isVisible) {
|
||||
const coinNameValueElement = usdCell.parentElement.querySelector('.coinname-value');
|
||||
const amount = amountStr ? parseFloat(amountStr.replace(/[^0-9.-]+/g, '')) : 0;
|
||||
const coinId = COIN_SYMBOLS[coinName];
|
||||
if (!coinId) return;
|
||||
|
||||
const price = prices?.[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
|
||||
const usdValue = (amount * price).toFixed(2);
|
||||
|
||||
if (coinNameValueElement) {
|
||||
const coinFullName = coinNameValueElement.getAttribute('data-coinname');
|
||||
console.log("coinFullName:", coinFullName);
|
||||
|
||||
if (coinFullName) {
|
||||
const cryptoValue = parseFloat(coinNameValueElement.textContent);
|
||||
console.log("cryptoValue:", cryptoValue);
|
||||
|
||||
const coinSymbol = coinNameToSymbol[coinFullName.trim()];
|
||||
console.log("coinSymbol:", coinSymbol);
|
||||
|
||||
if (coinSymbol) {
|
||||
await updateValues();
|
||||
} else {
|
||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Data attribute 'data-coinname' not found.`);
|
||||
}
|
||||
if (coinId === 'particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinId === 'litecoin') {
|
||||
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
|
||||
const balanceType = isMWEB ? 'mweb' : 'public';
|
||||
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
|
||||
} else {
|
||||
localStorage.setItem(`${coinId}-last-value`, usdValue);
|
||||
localStorage.setItem(`${coinId}-amount`, amount.toString());
|
||||
}
|
||||
} else {
|
||||
usdCell.textContent = "******";
|
||||
const btcValueElement = usdCell.nextElementSibling;
|
||||
if (btcValueElement) {
|
||||
btcValueElement.textContent = "****";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUsdIcon = (isVisible) => {
|
||||
const usdIcon = document.querySelector("#hide-usd-amount-toggle svg");
|
||||
if (usdIcon) {
|
||||
newTotal += parseFloat(usdValue);
|
||||
|
||||
const usdEl = el.closest('.flex')?.nextElementSibling?.querySelector('.usd-value');
|
||||
if (usdEl) {
|
||||
usdEl.textContent = `$${usdValue} USD`;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
|
||||
|
||||
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
|
||||
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Price update failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
updateTotalValues(totalUsd, btcPrice) {
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
|
||||
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
|
||||
localStorage.setItem('total-usd', totalUsd.toString());
|
||||
}
|
||||
|
||||
if (btcPrice) {
|
||||
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
|
||||
const totalBtcEl = document.getElementById('total-btc-value');
|
||||
if (totalBtcEl) {
|
||||
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
|
||||
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleBalances() {
|
||||
if (this.toggleInProgress) return;
|
||||
|
||||
try {
|
||||
this.toggleInProgress = true;
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
const newVisibility = !balancesVisible;
|
||||
|
||||
localStorage.setItem('balancesVisible', newVisibility.toString());
|
||||
this.updateVisibility(newVisibility);
|
||||
|
||||
if (this.toggleDebounceTimer) {
|
||||
clearTimeout(this.toggleDebounceTimer);
|
||||
}
|
||||
|
||||
this.toggleDebounceTimer = window.setTimeout(async () => {
|
||||
this.toggleInProgress = false;
|
||||
if (newVisibility) {
|
||||
await this.updatePrices(true);
|
||||
}
|
||||
}, CONFIG.DEBOUNCE_DELAY);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle balances:', error);
|
||||
this.toggleInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibility(isVisible) {
|
||||
if (isVisible) {
|
||||
usdIcon.innerHTML = `
|
||||
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>
|
||||
`;
|
||||
this.showBalances();
|
||||
} else {
|
||||
usdIcon.innerHTML = `
|
||||
<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path>
|
||||
<path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path>
|
||||
<path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>
|
||||
`;
|
||||
this.hideBalances();
|
||||
}
|
||||
|
||||
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
|
||||
if (eyeIcon) {
|
||||
eyeIcon.innerHTML = isVisible ?
|
||||
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
|
||||
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTotalUsdValue = async () => {
|
||||
const coinNameValues = document.querySelectorAll('.coinname-value');
|
||||
let totalUsdValue = 0;
|
||||
showBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'inline';
|
||||
}
|
||||
|
||||
for (const coinNameValue of coinNameValues) {
|
||||
const coinFullName = coinNameValue.getAttribute('data-coinname');
|
||||
const cryptoValue = parseFloat(coinNameValue.textContent);
|
||||
const coinSymbol = coinNameToSymbol[coinFullName];
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
const originalValue = el.getAttribute('data-original-value');
|
||||
if (originalValue) {
|
||||
el.textContent = originalValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (coinSymbol) {
|
||||
try {
|
||||
const { usdValue } = await getUsdValue(cryptoValue, coinSymbol);
|
||||
totalUsdValue += usdValue;
|
||||
document.querySelectorAll('.usd-value').forEach(el => {
|
||||
const storedValue = el.getAttribute('data-original-value');
|
||||
if (storedValue) {
|
||||
el.textContent = `$${parseFloat(storedValue).toFixed(2)} USD`;
|
||||
el.style.color = 'white';
|
||||
}
|
||||
});
|
||||
|
||||
const usdValueElement = coinNameValue.parentElement.nextElementSibling.querySelector('.usd-value');
|
||||
if (usdValueElement) {
|
||||
usdValueElement.textContent = `$${usdValue.toFixed(2)}`;
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
const originalValue = el?.getAttribute('data-original-value');
|
||||
if (el && originalValue) {
|
||||
if (id === 'total-usd-value') {
|
||||
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
|
||||
el.classList.add('font-extrabold');
|
||||
} else {
|
||||
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving exchange rate for ${coinSymbol}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
||||
});
|
||||
}
|
||||
|
||||
hideBalances() {
|
||||
const usdText = document.getElementById('usd-text');
|
||||
if (usdText) {
|
||||
usdText.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const totalUsdValueElement = document.getElementById('total-usd-value');
|
||||
if (totalUsdValueElement) {
|
||||
totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`;
|
||||
}
|
||||
};
|
||||
document.querySelectorAll('.coinname-value, .usd-value').forEach(el => {
|
||||
el.textContent = '****';
|
||||
});
|
||||
|
||||
const calculateTotalBtcValue = async () => {
|
||||
const usdValueElements = document.getElementsByClassName('usd-value');
|
||||
let totalBtcValue = 0;
|
||||
|
||||
try {
|
||||
const btcToUsdRate = await fetch('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD')
|
||||
.then(response => response.json())
|
||||
.then(data => data.USD);
|
||||
|
||||
for (const usdValueElement of usdValueElements) {
|
||||
const usdValue = parseFloat(usdValueElement.textContent.replace('$', ''));
|
||||
const btcValue = usdValue / btcToUsdRate;
|
||||
|
||||
if (!isNaN(btcValue)) {
|
||||
totalBtcValue += btcValue;
|
||||
} else {
|
||||
console.error(`Invalid USD value: ${usdValue}`);
|
||||
['total-usd-value', 'total-btc-value'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = '****';
|
||||
}
|
||||
});
|
||||
|
||||
const totalUsdEl = document.getElementById('total-usd-value');
|
||||
if (totalUsdEl) {
|
||||
totalUsdEl.classList.remove('font-extrabold');
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.storeOriginalValues();
|
||||
|
||||
if (localStorage.getItem('balancesVisible') === null) {
|
||||
localStorage.setItem('balancesVisible', 'true');
|
||||
}
|
||||
|
||||
const totalBtcValueElement = document.getElementById('total-btc-value');
|
||||
if (totalBtcValueElement) {
|
||||
if (localStorage.getItem('usdAmountVisible') === 'false') {
|
||||
totalBtcValueElement.textContent = "****";
|
||||
} else {
|
||||
totalBtcValueElement.textContent = `~ ${totalBtcValue.toFixed(8)} BTC`;
|
||||
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
|
||||
if (hideBalancesToggle) {
|
||||
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
|
||||
}
|
||||
|
||||
await this.loadBalanceVisibility();
|
||||
|
||||
if (this.priceUpdateInterval) {
|
||||
clearInterval(this.priceUpdateInterval);
|
||||
}
|
||||
|
||||
this.priceUpdateInterval = setInterval(() => {
|
||||
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
|
||||
this.updatePrices(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving BTC to USD exchange rate: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsdAmountVisibility = async () => {
|
||||
let usdAmountVisible = localStorage.getItem('usdAmountVisible') === 'true';
|
||||
toggleUsdIcon(usdAmountVisible);
|
||||
|
||||
const usdValueElements = document.getElementsByClassName('usd-value');
|
||||
for (const usdValueElement of usdValueElements) {
|
||||
toggleUsdAmount(usdValueElement, usdAmountVisible, !usdAmountVisible);
|
||||
}, CONFIG.PRICE_UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
const totalUsdValueElement = document.getElementById('total-usd-value');
|
||||
if (!usdAmountVisible && totalUsdValueElement) {
|
||||
totalUsdValueElement.textContent = "*****";
|
||||
} else if (usdAmountVisible && totalUsdValueElement) {
|
||||
await calculateTotalUsdValue();
|
||||
calculateTotalBtcValue();
|
||||
async loadBalanceVisibility() {
|
||||
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
|
||||
this.updateVisibility(balancesVisible);
|
||||
|
||||
if (balancesVisible) {
|
||||
await this.updatePrices(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
const hideUsdAmountToggle = document.getElementById('hide-usd-amount-toggle');
|
||||
let usdAmountVisible = localStorage.getItem('usdAmountVisible') === 'true';
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const uiManager = window.uiManager;
|
||||
if (uiManager?.priceUpdateInterval) {
|
||||
clearInterval(uiManager.priceUpdateInterval);
|
||||
}
|
||||
});
|
||||
|
||||
hideUsdAmountToggle.addEventListener('click', async () => {
|
||||
usdAmountVisible = !usdAmountVisible;
|
||||
localStorage.setItem('usdAmountVisible', usdAmountVisible);
|
||||
|
||||
toggleUsdIcon(usdAmountVisible);
|
||||
|
||||
const usdValueElements = document.getElementsByClassName('usd-value');
|
||||
for (const usdValueElement of usdValueElements) {
|
||||
toggleUsdAmount(usdValueElement, usdAmountVisible);
|
||||
}
|
||||
|
||||
const totalUsdValueElement = document.getElementById('total-usd-value');
|
||||
if (!usdAmountVisible && totalUsdValueElement) {
|
||||
totalUsdValueElement.textContent = "*****";
|
||||
} else if (usdAmountVisible && totalUsdValueElement) {
|
||||
await calculateTotalUsdValue();
|
||||
}
|
||||
|
||||
const totalBtcValueElement = document.getElementById('total-btc-value');
|
||||
if (!usdAmountVisible && totalBtcValueElement) {
|
||||
totalBtcValueElement.textContent = "";
|
||||
} else if (usdAmountVisible && totalBtcValueElement) {
|
||||
await calculateTotalBtcValue();
|
||||
}
|
||||
window.addEventListener('load', () => {
|
||||
const uiManager = new UiManager();
|
||||
window.uiManager = uiManager;
|
||||
uiManager.initialize().catch(error => {
|
||||
console.error('Failed to initialize application:', error);
|
||||
});
|
||||
|
||||
await loadUsdAmountVisibility();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -28,33 +29,33 @@ def page_automation_strategies(self, url_split, post_string):
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
filters = {
|
||||
'page_no': 1,
|
||||
'limit': PAGE_LIMIT,
|
||||
'sort_by': 'created_at',
|
||||
'sort_dir': 'desc',
|
||||
"page_no": 1,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
}
|
||||
|
||||
messages = []
|
||||
form_data = self.checkForm(post_string, 'automationstrategies', messages)
|
||||
form_data = self.checkForm(post_string, "automationstrategies", messages)
|
||||
|
||||
if form_data:
|
||||
if have_data_entry(form_data, 'clearfilters'):
|
||||
swap_client.clearFilters('page_automation_strategies')
|
||||
if have_data_entry(form_data, "clearfilters"):
|
||||
swap_client.clearFilters("page_automation_strategies")
|
||||
else:
|
||||
if have_data_entry(form_data, 'sort_by'):
|
||||
sort_by = get_data_entry(form_data, 'sort_by')
|
||||
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
|
||||
filters['sort_by'] = sort_by
|
||||
if have_data_entry(form_data, 'sort_dir'):
|
||||
sort_dir = get_data_entry(form_data, 'sort_dir')
|
||||
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
|
||||
filters['sort_dir'] = sort_dir
|
||||
if have_data_entry(form_data, "sort_by"):
|
||||
sort_by = get_data_entry(form_data, "sort_by")
|
||||
ensure(sort_by in ["created_at", "rate"], "Invalid sort by")
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(form_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(form_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
|
||||
set_pagination_filters(form_data, filters)
|
||||
if have_data_entry(form_data, 'applyfilters'):
|
||||
swap_client.setFilters('page_automation_strategies', filters)
|
||||
if have_data_entry(form_data, "applyfilters"):
|
||||
swap_client.setFilters("page_automation_strategies", filters)
|
||||
else:
|
||||
saved_filters = swap_client.getFilters('page_automation_strategies')
|
||||
saved_filters = swap_client.getFilters("page_automation_strategies")
|
||||
if saved_filters:
|
||||
filters.update(saved_filters)
|
||||
|
||||
@@ -62,13 +63,16 @@ def page_automation_strategies(self, url_split, post_string):
|
||||
for s in swap_client.listAutomationStrategies(filters):
|
||||
formatted_strategies.append((s[0], s[1], strConcepts(s[2])))
|
||||
|
||||
template = server.env.get_template('automation_strategies.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'filters': filters,
|
||||
'strategies': formatted_strategies,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("automation_strategies.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"filters": filters,
|
||||
"strategies": formatted_strategies,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_automation_strategy_new(self, url_split, post_string):
|
||||
@@ -78,21 +82,24 @@ def page_automation_strategy_new(self, url_split, post_string):
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
messages = []
|
||||
form_data = self.checkForm(post_string, 'automationstrategynew', messages)
|
||||
_ = self.checkForm(post_string, "automationstrategynew", messages)
|
||||
|
||||
template = server.env.get_template('automation_strategy_new.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("automation_strategy_new.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_automation_strategy(self, url_split, post_string):
|
||||
ensure(len(url_split) > 2, 'Strategy ID not specified')
|
||||
ensure(len(url_split) > 2, "Strategy ID not specified")
|
||||
try:
|
||||
strategy_id = int(url_split[2])
|
||||
except Exception:
|
||||
raise ValueError('Bad strategy ID')
|
||||
raise ValueError("Bad strategy ID")
|
||||
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
@@ -101,17 +108,19 @@ def page_automation_strategy(self, url_split, post_string):
|
||||
|
||||
messages = []
|
||||
err_messages = []
|
||||
form_data = self.checkForm(post_string, 'automation_strategy', err_messages)
|
||||
form_data = self.checkForm(post_string, "automation_strategy", err_messages)
|
||||
show_edit_form = False
|
||||
if form_data:
|
||||
if have_data_entry(form_data, 'edit'):
|
||||
if have_data_entry(form_data, "edit"):
|
||||
show_edit_form = True
|
||||
if have_data_entry(form_data, 'apply'):
|
||||
if have_data_entry(form_data, "apply"):
|
||||
try:
|
||||
data = json.loads(get_data_entry_or(form_data, 'data', ''))
|
||||
note = get_data_entry_or(form_data, 'note', '')
|
||||
swap_client.updateAutomationStrategy(strategy_id, data, note)
|
||||
messages.append('Updated')
|
||||
data = {
|
||||
"data": json.loads(get_data_entry_or(form_data, "data", "")),
|
||||
"note": get_data_entry_or(form_data, "note", ""),
|
||||
}
|
||||
swap_client.updateAutomationStrategy(strategy_id, data)
|
||||
messages.append("Updated")
|
||||
except Exception as e:
|
||||
err_messages.append(str(e))
|
||||
show_edit_form = True
|
||||
@@ -119,19 +128,24 @@ def page_automation_strategy(self, url_split, post_string):
|
||||
strategy = swap_client.getAutomationStrategy(strategy_id)
|
||||
|
||||
formatted_strategy = {
|
||||
'label': strategy.label,
|
||||
'type': strConcepts(strategy.type_ind),
|
||||
'only_known_identities': 'True' if strategy.only_known_identities is True else 'False',
|
||||
'data': strategy.data.decode('utf-8'),
|
||||
'note': '' if not strategy.note else strategy.note,
|
||||
'created_at': strategy.created_at,
|
||||
"label": strategy.label,
|
||||
"type": strConcepts(strategy.type_ind),
|
||||
"only_known_identities": (
|
||||
"True" if strategy.only_known_identities is True else "False"
|
||||
),
|
||||
"data": strategy.data.decode("utf-8"),
|
||||
"note": "" if not strategy.note else strategy.note,
|
||||
"created_at": strategy.created_at,
|
||||
}
|
||||
|
||||
template = server.env.get_template('automation_strategy.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'strategy': formatted_strategy,
|
||||
'show_edit_form': show_edit_form,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("automation_strategy.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"strategy": formatted_strategy,
|
||||
"show_edit_form": show_edit_form,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -24,18 +25,19 @@ from basicswap.basicswap_util import (
|
||||
BidStates,
|
||||
SwapTypes,
|
||||
DebugTypes,
|
||||
canAcceptBidState,
|
||||
strTxState,
|
||||
strBidState,
|
||||
)
|
||||
|
||||
|
||||
def page_bid(self, url_split, post_string):
|
||||
ensure(len(url_split) > 2, 'Bid ID not specified')
|
||||
ensure(len(url_split) > 2, "Bid ID not specified")
|
||||
try:
|
||||
bid_id = bytes.fromhex(url_split[2])
|
||||
assert len(bid_id) == 28
|
||||
except Exception:
|
||||
raise ValueError('Bad bid ID')
|
||||
raise ValueError("Bad bid ID")
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -49,129 +51,163 @@ def page_bid(self, url_split, post_string):
|
||||
show_lock_transfers = False
|
||||
edit_bid = False
|
||||
view_tx_ind = None
|
||||
form_data = self.checkForm(post_string, 'bid', err_messages)
|
||||
form_data = self.checkForm(post_string, "bid", err_messages)
|
||||
if form_data:
|
||||
if b'abandon_bid' in form_data:
|
||||
if b"abandon_bid" in form_data:
|
||||
try:
|
||||
swap_client.abandonBid(bid_id)
|
||||
messages.append('Bid abandoned')
|
||||
messages.append("Bid abandoned")
|
||||
except Exception as ex:
|
||||
err_messages.append('Abandon failed ' + str(ex))
|
||||
elif b'accept_bid' in form_data:
|
||||
err_messages.append("Abandon failed " + str(ex))
|
||||
elif b"accept_bid" in form_data:
|
||||
try:
|
||||
swap_client.acceptBid(bid_id)
|
||||
messages.append('Bid accepted')
|
||||
messages.append("Bid accepted")
|
||||
except Exception as ex:
|
||||
err_messages.append('Accept failed ' + str(ex))
|
||||
elif b'show_txns' in form_data:
|
||||
err_messages.append("Accept failed " + str(ex))
|
||||
elif b"show_txns" in form_data:
|
||||
show_txns = True
|
||||
elif b'show_offerer_seq_diagram' in form_data:
|
||||
elif b"show_offerer_seq_diagram" in form_data:
|
||||
show_offerer_seq_diagram = True
|
||||
elif b'show_bidder_seq_diagram' in form_data:
|
||||
elif b"show_bidder_seq_diagram" in form_data:
|
||||
show_bidder_seq_diagram = True
|
||||
elif b'edit_bid' in form_data:
|
||||
elif b"edit_bid" in form_data:
|
||||
edit_bid = True
|
||||
elif b'edit_bid_submit' in form_data:
|
||||
elif b"edit_bid_submit" in form_data:
|
||||
data = {
|
||||
'bid_state': int(form_data[b'new_state'][0]),
|
||||
'bid_action': int(get_data_entry_or(form_data, 'new_action', -1)),
|
||||
'debug_ind': int(get_data_entry_or(form_data, 'debugind', -1)),
|
||||
'kbs_other': get_data_entry_or(form_data, 'kbs_other', None),
|
||||
"bid_state": int(form_data[b"new_state"][0]),
|
||||
"bid_action": int(get_data_entry_or(form_data, "new_action", -1)),
|
||||
"debug_ind": int(get_data_entry_or(form_data, "debugind", -1)),
|
||||
"kbs_other": get_data_entry_or(form_data, "kbs_other", None),
|
||||
}
|
||||
try:
|
||||
swap_client.manualBidUpdate(bid_id, data)
|
||||
messages.append('Bid edited')
|
||||
messages.append("Bid edited")
|
||||
except Exception as ex:
|
||||
err_messages.append('Edit failed ' + str(ex))
|
||||
elif b'view_tx_submit' in form_data:
|
||||
err_messages.append("Edit failed " + str(ex))
|
||||
elif b"view_tx_submit" in form_data:
|
||||
show_txns = True
|
||||
view_tx_ind = form_data[b'view_tx'][0].decode('utf-8')
|
||||
view_tx_ind = form_data[b"view_tx"][0].decode("utf-8")
|
||||
if len(view_tx_ind) != 64:
|
||||
err_messages.append('Invalid transaction selected.')
|
||||
err_messages.append("Invalid transaction selected.")
|
||||
view_tx_ind = None
|
||||
elif b'view_lock_transfers' in form_data:
|
||||
elif b"view_lock_transfers" in form_data:
|
||||
show_txns = True
|
||||
show_lock_transfers = True
|
||||
|
||||
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
|
||||
ensure(bid, 'Unknown bid ID')
|
||||
ensure(bid, "Unknown bid ID")
|
||||
|
||||
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, view_tx_ind, show_lock_transfers=show_lock_transfers)
|
||||
data = describeBid(
|
||||
swap_client,
|
||||
bid,
|
||||
xmr_swap,
|
||||
offer,
|
||||
xmr_offer,
|
||||
events,
|
||||
edit_bid,
|
||||
show_txns,
|
||||
view_tx_ind,
|
||||
show_lock_transfers=show_lock_transfers,
|
||||
)
|
||||
|
||||
if bid.debug_ind is not None and bid.debug_ind > 0:
|
||||
messages.append('Debug flag set: {}, {}'.format(bid.debug_ind, DebugTypes(bid.debug_ind).name))
|
||||
messages.append(
|
||||
"Debug flag set: {}, {}".format(
|
||||
bid.debug_ind, DebugTypes(bid.debug_ind).name
|
||||
)
|
||||
)
|
||||
|
||||
data['show_bidder_seq_diagram'] = show_bidder_seq_diagram
|
||||
data['show_offerer_seq_diagram'] = show_offerer_seq_diagram
|
||||
data["show_bidder_seq_diagram"] = show_bidder_seq_diagram
|
||||
data["show_offerer_seq_diagram"] = show_offerer_seq_diagram
|
||||
|
||||
old_states = listOldBidStates(bid)
|
||||
|
||||
if len(data['addr_from_label']) > 0:
|
||||
data['addr_from_label'] = '(' + data['addr_from_label'] + ')'
|
||||
data['can_accept_bid'] = True if bid.state == BidStates.BID_RECEIVED else False
|
||||
if len(data["addr_from_label"]) > 0:
|
||||
data["addr_from_label"] = "(" + data["addr_from_label"] + ")"
|
||||
data["can_accept_bid"] = True if canAcceptBidState(bid.state) else False
|
||||
|
||||
if swap_client.debug_ui:
|
||||
data['bid_actions'] = [(-1, 'None'), ] + listBidActions()
|
||||
data["bid_actions"] = [
|
||||
(-1, "None"),
|
||||
] + listBidActions()
|
||||
|
||||
template = server.env.get_template('bid_xmr.html' if offer.swap_type == SwapTypes.XMR_SWAP else 'bid.html')
|
||||
return self.render_template(template, {
|
||||
'bid_id': bid_id.hex(),
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'data': data,
|
||||
'edit_bid': edit_bid,
|
||||
'old_states': old_states,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template(
|
||||
"bid_xmr.html" if offer.swap_type == SwapTypes.XMR_SWAP else "bid.html"
|
||||
)
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"bid_id": bid_id.hex(),
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"data": data,
|
||||
"edit_bid": edit_bid,
|
||||
"old_states": old_states,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
|
||||
def page_bids(
|
||||
self, url_split, post_string, sent=False, available=False, received=False
|
||||
):
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
filters = {
|
||||
'page_no': 1,
|
||||
'bid_state_ind': -1,
|
||||
'with_expired': True,
|
||||
'limit': PAGE_LIMIT,
|
||||
'sort_by': 'created_at',
|
||||
'sort_dir': 'desc',
|
||||
"page_no": 1,
|
||||
"bid_state_ind": -1,
|
||||
"with_expired": True,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
}
|
||||
if available:
|
||||
filters['bid_state_ind'] = BidStates.BID_RECEIVED
|
||||
filters['with_expired'] = False
|
||||
filters["bid_state_ind"] = BidStates.BID_RECEIVED
|
||||
filters["with_expired"] = False
|
||||
|
||||
filter_prefix = 'page_bids_sent' if sent else 'page_bids_available' if available else 'page_bids_received'
|
||||
filter_prefix = (
|
||||
"page_bids_sent"
|
||||
if sent
|
||||
else "page_bids_available" if available else "page_bids_received"
|
||||
)
|
||||
messages = []
|
||||
form_data = self.checkForm(post_string, 'bids', messages)
|
||||
form_data = self.checkForm(post_string, "bids", messages)
|
||||
if form_data:
|
||||
if have_data_entry(form_data, 'clearfilters'):
|
||||
if have_data_entry(form_data, "clearfilters"):
|
||||
swap_client.clearFilters(filter_prefix)
|
||||
else:
|
||||
if have_data_entry(form_data, 'sort_by'):
|
||||
sort_by = get_data_entry(form_data, 'sort_by')
|
||||
ensure(sort_by in ['created_at', ], 'Invalid sort by')
|
||||
filters['sort_by'] = sort_by
|
||||
if have_data_entry(form_data, 'sort_dir'):
|
||||
sort_dir = get_data_entry(form_data, 'sort_dir')
|
||||
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
|
||||
filters['sort_dir'] = sort_dir
|
||||
if have_data_entry(form_data, 'state'):
|
||||
state_ind = int(get_data_entry(form_data, 'state'))
|
||||
if have_data_entry(form_data, "sort_by"):
|
||||
sort_by = get_data_entry(form_data, "sort_by")
|
||||
ensure(
|
||||
sort_by
|
||||
in [
|
||||
"created_at",
|
||||
],
|
||||
"Invalid sort by",
|
||||
)
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(form_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(form_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
if have_data_entry(form_data, "state"):
|
||||
state_ind = int(get_data_entry(form_data, "state"))
|
||||
if state_ind != -1:
|
||||
try:
|
||||
state = BidStates(state_ind)
|
||||
except Exception:
|
||||
raise ValueError('Invalid state')
|
||||
filters['bid_state_ind'] = state_ind
|
||||
if have_data_entry(form_data, 'with_expired'):
|
||||
with_expired = toBool(get_data_entry(form_data, 'with_expired'))
|
||||
filters['with_expired'] = with_expired
|
||||
_ = BidStates(state_ind)
|
||||
except Exception as e: # noqa: F841
|
||||
raise ValueError("Invalid state")
|
||||
filters["bid_state_ind"] = state_ind
|
||||
if have_data_entry(form_data, "with_expired"):
|
||||
with_expired = toBool(get_data_entry(form_data, "with_expired"))
|
||||
filters["with_expired"] = with_expired
|
||||
|
||||
set_pagination_filters(form_data, filters)
|
||||
if have_data_entry(form_data, 'applyfilters'):
|
||||
if have_data_entry(form_data, "applyfilters"):
|
||||
swap_client.setFilters(filter_prefix, filters)
|
||||
else:
|
||||
saved_filters = swap_client.getFilters(filter_prefix)
|
||||
@@ -181,21 +217,40 @@ def page_bids(self, url_split, post_string, sent=False, available=False, receive
|
||||
bids = swap_client.listBids(sent=sent, filters=filters)
|
||||
|
||||
page_data = {
|
||||
'bid_states': listBidStates(),
|
||||
"bid_states": listBidStates(),
|
||||
}
|
||||
template = server.env.get_template('bids.html')
|
||||
return self.render_template(template, {
|
||||
'page_type_sent': 'Bids Sent' if sent else '',
|
||||
'page_type_available': 'Bids Available' if available else '',
|
||||
'page_type_received': 'Received Bids' if received else '',
|
||||
'page_type_sent_description': 'All the bids you have placed on offers.' if sent else '',
|
||||
'page_type_available_description': 'Bids available for you to accept.' if available else '',
|
||||
'page_type_received_description': 'All the bids placed on your offers.' if received else '',
|
||||
'messages': messages,
|
||||
'filters': filters,
|
||||
'data': page_data,
|
||||
'summary': summary,
|
||||
'bids': [(format_timestamp(b[0]),
|
||||
b[2].hex(), b[3].hex(), strBidState(b[5]), strTxState(b[7]), strTxState(b[8]), b[11]) for b in bids],
|
||||
'bids_count': len(bids),
|
||||
})
|
||||
template = server.env.get_template("bids.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"page_type_sent": "Bids Sent" if sent else "",
|
||||
"page_type_available": "Bids Available" if available else "",
|
||||
"page_type_received": "Received Bids" if received else "",
|
||||
"page_type_sent_description": (
|
||||
"All the bids you have placed on offers." if sent else ""
|
||||
),
|
||||
"page_type_available_description": (
|
||||
"Bids available for you to accept." if available else ""
|
||||
),
|
||||
"page_type_received_description": (
|
||||
"All the bids placed on your offers." if received else ""
|
||||
),
|
||||
"messages": messages,
|
||||
"filters": filters,
|
||||
"data": page_data,
|
||||
"summary": summary,
|
||||
"bids": [
|
||||
(
|
||||
format_timestamp(b[0]),
|
||||
b[2].hex(),
|
||||
b[3].hex(),
|
||||
strBidState(b[5]),
|
||||
strTxState(b[7]),
|
||||
strTxState(b[8]),
|
||||
b[11],
|
||||
)
|
||||
for b in bids
|
||||
],
|
||||
"bids_count": len(bids),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023 The BSX Developers
|
||||
# Copyright (c) 2023-2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -25,31 +25,34 @@ def page_debug(self, url_split, post_string):
|
||||
result = None
|
||||
messages = []
|
||||
err_messages = []
|
||||
form_data = self.checkForm(post_string, 'wallets', err_messages)
|
||||
form_data = self.checkForm(post_string, "wallets", err_messages)
|
||||
if form_data:
|
||||
if have_data_entry(form_data, 'reinit_xmr'):
|
||||
if have_data_entry(form_data, "reinit_xmr"):
|
||||
try:
|
||||
swap_client.initialiseWallet(Coins.XMR)
|
||||
messages.append('Done.')
|
||||
messages.append("Done.")
|
||||
except Exception as e:
|
||||
err_messages.append('Failed.')
|
||||
err_messages.append(f"Failed: {e}.")
|
||||
|
||||
if have_data_entry(form_data, 'remove_expired'):
|
||||
if have_data_entry(form_data, "remove_expired"):
|
||||
try:
|
||||
swap_client.log.warning('Removing expired data.')
|
||||
swap_client.log.warning("Removing expired data.")
|
||||
remove_expired_data(swap_client)
|
||||
messages.append('Done.')
|
||||
messages.append("Done.")
|
||||
except Exception as e:
|
||||
if swap_client.debug is True:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
else:
|
||||
swap_client.log.error(f'remove_expired_data: {e}')
|
||||
err_messages.append('Failed.')
|
||||
swap_client.log.error(f"remove_expired_data: {e}")
|
||||
err_messages.append("Failed.")
|
||||
|
||||
template = server.env.get_template('debug.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'result': result,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("debug.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"result": result,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -17,47 +17,52 @@ def page_changepassword(self, url_split, post_string):
|
||||
messages = []
|
||||
err_messages = []
|
||||
|
||||
form_data = self.checkForm(post_string, 'changepassword', err_messages)
|
||||
form_data = self.checkForm(post_string, "changepassword", err_messages)
|
||||
if form_data:
|
||||
old_password = get_data_entry_or(form_data, 'oldpassword', '')
|
||||
new_password = get_data_entry_or(form_data, 'newpassword', '')
|
||||
confirm_password = get_data_entry_or(form_data, 'confirmpassword', '')
|
||||
old_password = get_data_entry_or(form_data, "oldpassword", "")
|
||||
new_password = get_data_entry_or(form_data, "newpassword", "")
|
||||
confirm_password = get_data_entry_or(form_data, "confirmpassword", "")
|
||||
|
||||
try:
|
||||
if new_password == '':
|
||||
raise ValueError('New password must be entered.')
|
||||
if new_password == "":
|
||||
raise ValueError("New password must be entered.")
|
||||
if new_password != confirm_password:
|
||||
raise ValueError('New password and confirm password must match.')
|
||||
raise ValueError("New password and confirm password must match.")
|
||||
swap_client.changeWalletPasswords(old_password, new_password)
|
||||
messages.append('Password changed')
|
||||
messages.append("Password changed")
|
||||
except Exception as e:
|
||||
err_messages.append(str(e))
|
||||
|
||||
template = server.env.get_template('changepassword.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'summary': swap_client.getSummary(),
|
||||
})
|
||||
template = server.env.get_template("changepassword.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"summary": swap_client.getSummary(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_unlock(self, url_split, post_string):
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
|
||||
messages = ['Warning: This will unlock the system for all users!', ]
|
||||
messages = [
|
||||
"Warning: This will unlock the system for all users!",
|
||||
]
|
||||
err_messages = []
|
||||
|
||||
form_data = self.checkForm(post_string, 'unlock', err_messages)
|
||||
form_data = self.checkForm(post_string, "unlock", err_messages)
|
||||
if form_data:
|
||||
password = get_data_entry_or(form_data, 'password', '')
|
||||
password = get_data_entry_or(form_data, "password", "")
|
||||
|
||||
try:
|
||||
if password == '':
|
||||
raise ValueError('Password must be entered.')
|
||||
if password == "":
|
||||
raise ValueError("Password must be entered.")
|
||||
swap_client.unlockWallets(password)
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/')
|
||||
self.send_header("Location", "/")
|
||||
self.end_headers()
|
||||
return bytes()
|
||||
except Exception as e:
|
||||
@@ -65,11 +70,14 @@ def page_unlock(self, url_split, post_string):
|
||||
swap_client.log.error(str(e))
|
||||
err_messages.append(str(e))
|
||||
|
||||
template = server.env.get_template('unlock.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
})
|
||||
template = server.env.get_template("unlock.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_lock(self, url_split, post_string):
|
||||
@@ -79,6 +87,6 @@ def page_lock(self, url_split, post_string):
|
||||
|
||||
swap_client.lockWallets()
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/')
|
||||
self.send_header("Location", "/")
|
||||
self.end_headers()
|
||||
return bytes()
|
||||
|
||||
@@ -25,54 +25,71 @@ def page_identity(self, url_split, post_string):
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
ensure(len(url_split) > 2, 'Address not specified')
|
||||
ensure(len(url_split) > 2, "Address not specified")
|
||||
identity_address = url_split[2]
|
||||
|
||||
page_data = {'identity_address': identity_address}
|
||||
page_data = {"identity_address": identity_address}
|
||||
messages = []
|
||||
err_messages = []
|
||||
form_data = self.checkForm(post_string, 'identity', err_messages)
|
||||
form_data = self.checkForm(post_string, "identity", err_messages)
|
||||
if form_data:
|
||||
if have_data_entry(form_data, 'edit'):
|
||||
page_data['show_edit_form'] = True
|
||||
if have_data_entry(form_data, 'apply'):
|
||||
if have_data_entry(form_data, "edit"):
|
||||
page_data["show_edit_form"] = True
|
||||
if have_data_entry(form_data, "apply"):
|
||||
try:
|
||||
data = {
|
||||
'label': get_data_entry_or(form_data, 'label', ''),
|
||||
'note': get_data_entry_or(form_data, 'note', ''),
|
||||
'automation_override': get_data_entry(form_data, 'automation_override'),
|
||||
"label": get_data_entry_or(form_data, "label", ""),
|
||||
"note": get_data_entry_or(form_data, "note", ""),
|
||||
"automation_override": get_data_entry(
|
||||
form_data, "automation_override"
|
||||
),
|
||||
}
|
||||
swap_client.setIdentityData({'address': identity_address}, data)
|
||||
messages.append('Updated')
|
||||
swap_client.setIdentityData({"address": identity_address}, data)
|
||||
messages.append("Updated")
|
||||
except Exception as e:
|
||||
err_messages.append(str(e))
|
||||
|
||||
try:
|
||||
identity = swap_client.getIdentity(identity_address)
|
||||
if identity is None:
|
||||
raise ValueError('Unknown address')
|
||||
raise ValueError("Unknown address")
|
||||
|
||||
automation_override = zeroIfNone(identity.automation_override)
|
||||
page_data.update({
|
||||
'label': '' if identity.label is None else identity.label,
|
||||
'num_sent_bids_successful': zeroIfNone(identity.num_sent_bids_successful),
|
||||
'num_recv_bids_successful': zeroIfNone(identity.num_recv_bids_successful),
|
||||
'num_sent_bids_rejected': zeroIfNone(identity.num_sent_bids_rejected),
|
||||
'num_recv_bids_rejected': zeroIfNone(identity.num_recv_bids_rejected),
|
||||
'num_sent_bids_failed': zeroIfNone(identity.num_sent_bids_failed),
|
||||
'num_recv_bids_failed': zeroIfNone(identity.num_recv_bids_failed),
|
||||
'automation_override': automation_override,
|
||||
'str_automation_override': strAutomationOverrideOption(automation_override),
|
||||
'note': '' if identity.note is None else identity.note,
|
||||
})
|
||||
page_data.update(
|
||||
{
|
||||
"label": "" if identity.label is None else identity.label,
|
||||
"num_sent_bids_successful": zeroIfNone(
|
||||
identity.num_sent_bids_successful
|
||||
),
|
||||
"num_recv_bids_successful": zeroIfNone(
|
||||
identity.num_recv_bids_successful
|
||||
),
|
||||
"num_sent_bids_rejected": zeroIfNone(identity.num_sent_bids_rejected),
|
||||
"num_recv_bids_rejected": zeroIfNone(identity.num_recv_bids_rejected),
|
||||
"num_sent_bids_failed": zeroIfNone(identity.num_sent_bids_failed),
|
||||
"num_recv_bids_failed": zeroIfNone(identity.num_recv_bids_failed),
|
||||
"automation_override": automation_override,
|
||||
"str_automation_override": strAutomationOverrideOption(
|
||||
automation_override
|
||||
),
|
||||
"note": "" if identity.note is None else identity.note,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
err_messages.append(e)
|
||||
|
||||
template = server.env.get_template('identity.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'data': page_data,
|
||||
'automation_override_options': [(int(opt), strAutomationOverrideOption(opt)) for opt in AutomationOverrideOptions if opt > 0],
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("identity.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"data": page_data,
|
||||
"automation_override_options": [
|
||||
(int(opt), strAutomationOverrideOption(opt))
|
||||
for opt in AutomationOverrideOptions
|
||||
if opt > 0
|
||||
],
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,140 +28,195 @@ def page_settings(self, url_split, post_string):
|
||||
|
||||
messages = []
|
||||
err_messages = []
|
||||
active_tab = 'default'
|
||||
form_data = self.checkForm(post_string, 'settings', err_messages)
|
||||
active_tab = "default"
|
||||
form_data = self.checkForm(post_string, "settings", err_messages)
|
||||
if form_data:
|
||||
try:
|
||||
if have_data_entry(form_data, 'apply_general'):
|
||||
active_tab = 'general'
|
||||
if have_data_entry(form_data, "apply_general"):
|
||||
active_tab = "general"
|
||||
data = {
|
||||
'debug': toBool(get_data_entry(form_data, 'debugmode')),
|
||||
'debug_ui': toBool(get_data_entry(form_data, 'debugui')),
|
||||
'expire_db_records': toBool(get_data_entry(form_data, 'expire_db_records')),
|
||||
"debug": toBool(get_data_entry(form_data, "debugmode")),
|
||||
"debug_ui": toBool(get_data_entry(form_data, "debugui")),
|
||||
"expire_db_records": toBool(
|
||||
get_data_entry(form_data, "expire_db_records")
|
||||
),
|
||||
}
|
||||
swap_client.editGeneralSettings(data)
|
||||
elif have_data_entry(form_data, 'apply_chart'):
|
||||
active_tab = 'general'
|
||||
elif have_data_entry(form_data, "apply_chart"):
|
||||
active_tab = "general"
|
||||
data = {
|
||||
'show_chart': toBool(get_data_entry(form_data, 'showchart')),
|
||||
'chart_api_key': html.unescape(get_data_entry_or(form_data, 'chartapikey', '')),
|
||||
'coingecko_api_key': html.unescape(get_data_entry_or(form_data, 'coingeckoapikey', '')),
|
||||
'enabled_chart_coins': get_data_entry_or(form_data, 'enabledchartcoins', ''),
|
||||
"show_chart": toBool(get_data_entry(form_data, "showchart")),
|
||||
"chart_api_key": html.unescape(
|
||||
get_data_entry_or(form_data, "chartapikey", "")
|
||||
),
|
||||
"coingecko_api_key": html.unescape(
|
||||
get_data_entry_or(form_data, "coingeckoapikey", "")
|
||||
),
|
||||
"enabled_chart_coins": get_data_entry_or(
|
||||
form_data, "enabledchartcoins", ""
|
||||
),
|
||||
}
|
||||
swap_client.editGeneralSettings(data)
|
||||
elif have_data_entry(form_data, 'apply_tor'):
|
||||
active_tab = 'tor'
|
||||
elif have_data_entry(form_data, "apply_tor"):
|
||||
active_tab = "tor"
|
||||
# TODO: Detect if running in docker
|
||||
raise ValueError('TODO: If running in docker see doc/tor.md to enable/disable tor.')
|
||||
raise ValueError(
|
||||
"TODO: If running in docker see doc/tor.md to enable/disable tor."
|
||||
)
|
||||
|
||||
for name, c in swap_client.settings['chainclients'].items():
|
||||
if have_data_entry(form_data, 'apply_' + name):
|
||||
data = {'lookups': get_data_entry(form_data, 'lookups_' + name)}
|
||||
if name in ('monero', 'wownero'):
|
||||
data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name))
|
||||
data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
|
||||
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
|
||||
data['rpcport'] = int(get_data_entry(form_data, 'rpcport_' + name))
|
||||
data['remotedaemonurls'] = get_data_entry_or(form_data, 'remotedaemonurls_' + name, '')
|
||||
data['automatically_select_daemon'] = True if get_data_entry(form_data, 'autosetdaemon_' + name) == 'true' else False
|
||||
for name, c in swap_client.settings["chainclients"].items():
|
||||
if have_data_entry(form_data, "apply_" + name):
|
||||
data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
|
||||
if name in ("monero", "wownero"):
|
||||
data["fee_priority"] = int(
|
||||
get_data_entry(form_data, "fee_priority_" + name)
|
||||
)
|
||||
data["manage_daemon"] = (
|
||||
True
|
||||
if get_data_entry(form_data, "managedaemon_" + name)
|
||||
== "true"
|
||||
else False
|
||||
)
|
||||
data["rpchost"] = get_data_entry(form_data, "rpchost_" + name)
|
||||
data["rpcport"] = int(
|
||||
get_data_entry(form_data, "rpcport_" + name)
|
||||
)
|
||||
data["remotedaemonurls"] = get_data_entry_or(
|
||||
form_data, "remotedaemonurls_" + name, ""
|
||||
)
|
||||
data["automatically_select_daemon"] = (
|
||||
True
|
||||
if get_data_entry(form_data, "autosetdaemon_" + name)
|
||||
== "true"
|
||||
else False
|
||||
)
|
||||
else:
|
||||
data['conf_target'] = int(get_data_entry(form_data, 'conf_target_' + name))
|
||||
if name == 'particl':
|
||||
data['anon_tx_ring_size'] = int(get_data_entry(form_data, 'rct_ring_size_' + name))
|
||||
data["conf_target"] = int(
|
||||
get_data_entry(form_data, "conf_target_" + name)
|
||||
)
|
||||
if name == "particl":
|
||||
data["anon_tx_ring_size"] = int(
|
||||
get_data_entry(form_data, "rct_ring_size_" + name)
|
||||
)
|
||||
|
||||
settings_changed, suggest_reboot = swap_client.editSettings(name, data)
|
||||
settings_changed, suggest_reboot = swap_client.editSettings(
|
||||
name, data
|
||||
)
|
||||
if settings_changed is True:
|
||||
messages.append('Settings applied.')
|
||||
messages.append("Settings applied.")
|
||||
if suggest_reboot is True:
|
||||
messages.append('Please restart BasicSwap.')
|
||||
elif have_data_entry(form_data, 'enable_' + name):
|
||||
messages.append("Please restart BasicSwap.")
|
||||
elif have_data_entry(form_data, "enable_" + name):
|
||||
swap_client.enableCoin(name)
|
||||
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
||||
messages.append(display_name + ' enabled, shutting down.')
|
||||
messages.append(display_name + " enabled, shutting down.")
|
||||
swap_client.stopRunning()
|
||||
elif have_data_entry(form_data, 'disable_' + name):
|
||||
elif have_data_entry(form_data, "disable_" + name):
|
||||
swap_client.disableCoin(name)
|
||||
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
||||
messages.append(display_name + ' disabled, shutting down.')
|
||||
messages.append(display_name + " disabled, shutting down.")
|
||||
swap_client.stopRunning()
|
||||
except InactiveCoin as ex:
|
||||
err_messages.append('InactiveCoin {}'.format(Coins(ex.coinid).name))
|
||||
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
|
||||
except Exception as e:
|
||||
err_messages.append(str(e))
|
||||
chains_formatted = []
|
||||
|
||||
sorted_names = sorted(swap_client.settings['chainclients'].keys())
|
||||
sorted_names = sorted(swap_client.settings["chainclients"].keys())
|
||||
for name in sorted_names:
|
||||
c = swap_client.settings['chainclients'][name]
|
||||
c = swap_client.settings["chainclients"][name]
|
||||
try:
|
||||
display_name = getCoinName(swap_client.getCoinIdFromName(name))
|
||||
except Exception:
|
||||
display_name = name
|
||||
chains_formatted.append({
|
||||
'name': name,
|
||||
'display_name': display_name,
|
||||
'lookups': c.get('chain_lookups', 'local'),
|
||||
'manage_daemon': c.get('manage_daemon', 'Unknown'),
|
||||
'connection_type': c.get('connection_type', 'Unknown'),
|
||||
})
|
||||
if name in ('monero', 'wownero'):
|
||||
chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0)
|
||||
chains_formatted[-1]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown')
|
||||
chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost')
|
||||
chains_formatted[-1]['rpcport'] = int(c.get('rpcport', 18081))
|
||||
chains_formatted[-1]['remotedaemonurls'] = '\n'.join(c.get('remote_daemon_urls', []))
|
||||
chains_formatted[-1]['autosetdaemon'] = c.get('automatically_select_daemon', False)
|
||||
chains_formatted.append(
|
||||
{
|
||||
"name": name,
|
||||
"display_name": display_name,
|
||||
"lookups": c.get("chain_lookups", "local"),
|
||||
"manage_daemon": c.get("manage_daemon", "Unknown"),
|
||||
"connection_type": c.get("connection_type", "Unknown"),
|
||||
}
|
||||
)
|
||||
if name in ("monero", "wownero"):
|
||||
chains_formatted[-1]["fee_priority"] = c.get("fee_priority", 0)
|
||||
chains_formatted[-1]["manage_wallet_daemon"] = c.get(
|
||||
"manage_wallet_daemon", "Unknown"
|
||||
)
|
||||
chains_formatted[-1]["rpchost"] = c.get("rpchost", "localhost")
|
||||
chains_formatted[-1]["rpcport"] = int(c.get("rpcport", 18081))
|
||||
chains_formatted[-1]["remotedaemonurls"] = "\n".join(
|
||||
c.get("remote_daemon_urls", [])
|
||||
)
|
||||
chains_formatted[-1]["autosetdaemon"] = c.get(
|
||||
"automatically_select_daemon", False
|
||||
)
|
||||
else:
|
||||
chains_formatted[-1]['conf_target'] = c.get('conf_target', 2)
|
||||
chains_formatted[-1]["conf_target"] = c.get("conf_target", 2)
|
||||
|
||||
if name == 'particl':
|
||||
chains_formatted[-1]['anon_tx_ring_size'] = c.get('anon_tx_ring_size', 12)
|
||||
if name == "particl":
|
||||
chains_formatted[-1]["anon_tx_ring_size"] = c.get("anon_tx_ring_size", 12)
|
||||
else:
|
||||
if c.get('connection_type', 'Unknown') == 'none':
|
||||
if 'connection_type_prev' in c:
|
||||
chains_formatted[-1]['can_reenable'] = True
|
||||
if c.get("connection_type", "Unknown") == "none":
|
||||
if "connection_type_prev" in c:
|
||||
chains_formatted[-1]["can_reenable"] = True
|
||||
else:
|
||||
chains_formatted[-1]['can_disable'] = True
|
||||
chains_formatted[-1]["can_disable"] = True
|
||||
|
||||
general_settings = {
|
||||
'debug': swap_client.debug,
|
||||
'debug_ui': swap_client.debug_ui,
|
||||
'expire_db_records': swap_client._expire_db_records,
|
||||
"debug": swap_client.debug,
|
||||
"debug_ui": swap_client.debug_ui,
|
||||
"expire_db_records": swap_client._expire_db_records,
|
||||
}
|
||||
if 'chart_api_key_enc' in swap_client.settings:
|
||||
chart_api_key = html.escape(bytes.fromhex(swap_client.settings.get('chart_api_key_enc', '')).decode('utf-8'))
|
||||
if "chart_api_key_enc" in swap_client.settings:
|
||||
chart_api_key = html.escape(
|
||||
bytes.fromhex(swap_client.settings.get("chart_api_key_enc", "")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
else:
|
||||
chart_api_key = swap_client.settings.get('chart_api_key', '')
|
||||
chart_api_key = swap_client.settings.get("chart_api_key", "")
|
||||
|
||||
if 'coingecko_api_key_enc' in swap_client.settings:
|
||||
coingecko_api_key = html.escape(bytes.fromhex(swap_client.settings.get('coingecko_api_key_enc', '')).decode('utf-8'))
|
||||
if "coingecko_api_key_enc" in swap_client.settings:
|
||||
coingecko_api_key = html.escape(
|
||||
bytes.fromhex(swap_client.settings.get("coingecko_api_key_enc", "")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
else:
|
||||
coingecko_api_key = swap_client.settings.get('coingecko_api_key', '')
|
||||
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
|
||||
|
||||
chart_settings = {
|
||||
'show_chart': swap_client.settings.get('show_chart', True),
|
||||
'chart_api_key': chart_api_key,
|
||||
'coingecko_api_key': coingecko_api_key,
|
||||
'enabled_chart_coins': swap_client.settings.get('enabled_chart_coins', ''),
|
||||
"show_chart": swap_client.settings.get("show_chart", True),
|
||||
"chart_api_key": chart_api_key,
|
||||
"coingecko_api_key": coingecko_api_key,
|
||||
"enabled_chart_coins": swap_client.settings.get("enabled_chart_coins", ""),
|
||||
}
|
||||
|
||||
tor_control_password = '' if swap_client.tor_control_password is None else swap_client.tor_control_password
|
||||
tor_control_password = (
|
||||
""
|
||||
if swap_client.tor_control_password is None
|
||||
else swap_client.tor_control_password
|
||||
)
|
||||
tor_settings = {
|
||||
'use_tor': swap_client.use_tor_proxy,
|
||||
'proxy_host': swap_client.tor_proxy_host,
|
||||
'proxy_port': swap_client.tor_proxy_port,
|
||||
'control_password': html.escape(tor_control_password),
|
||||
'control_port': swap_client.tor_control_port,
|
||||
"use_tor": swap_client.use_tor_proxy,
|
||||
"proxy_host": swap_client.tor_proxy_host,
|
||||
"proxy_port": swap_client.tor_proxy_port,
|
||||
"control_password": html.escape(tor_control_password),
|
||||
"control_port": swap_client.tor_control_port,
|
||||
}
|
||||
|
||||
template = server.env.get_template('settings.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'summary': swap_client.getSummary(),
|
||||
'chains': chains_formatted,
|
||||
'general_settings': general_settings,
|
||||
'chart_settings': chart_settings,
|
||||
'tor_settings': tor_settings,
|
||||
'active_tab': active_tab,
|
||||
})
|
||||
template = server.env.get_template("settings.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"summary": swap_client.getSummary(),
|
||||
"chains": chains_formatted,
|
||||
"general_settings": general_settings,
|
||||
"chart_settings": chart_settings,
|
||||
"tor_settings": tor_settings,
|
||||
"active_tab": active_tab,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -28,11 +28,11 @@ def page_smsgaddresses(self, url_split, post_string):
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
filters = {
|
||||
'page_no': 1,
|
||||
'limit': PAGE_LIMIT,
|
||||
'sort_by': 'created_at',
|
||||
'sort_dir': 'desc',
|
||||
'addr_type': -1,
|
||||
"page_no": 1,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
"addr_type": -1,
|
||||
}
|
||||
|
||||
page_data = {}
|
||||
@@ -41,96 +41,114 @@ def page_smsgaddresses(self, url_split, post_string):
|
||||
smsgaddresses = []
|
||||
|
||||
listaddresses = True
|
||||
form_data = self.checkForm(post_string, 'smsgaddresses', err_messages)
|
||||
form_data = self.checkForm(post_string, "smsgaddresses", err_messages)
|
||||
if form_data:
|
||||
edit_address_id = None
|
||||
for key in form_data:
|
||||
if key.startswith(b'editaddr_'):
|
||||
edit_address_id = int(key.split(b'_')[1])
|
||||
if key.startswith(b"editaddr_"):
|
||||
edit_address_id = int(key.split(b"_")[1])
|
||||
break
|
||||
if edit_address_id is not None:
|
||||
listaddresses = False
|
||||
page_data['edit_address'] = edit_address_id
|
||||
page_data['addr_data'] = swap_client.listAllSMSGAddresses({'addr_id': edit_address_id})[0]
|
||||
elif have_data_entry(form_data, 'saveaddr'):
|
||||
edit_address_id = int(get_data_entry(form_data, 'edit_address_id'))
|
||||
edit_addr = get_data_entry(form_data, 'edit_address')
|
||||
active_ind = int(get_data_entry(form_data, 'active_ind'))
|
||||
ensure(active_ind in (0, 1), 'Invalid sort by')
|
||||
addressnote = get_data_entry_or(form_data, 'addressnote', '')
|
||||
if not validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
|
||||
page_data["edit_address"] = edit_address_id
|
||||
page_data["addr_data"] = swap_client.listAllSMSGAddresses(
|
||||
{"addr_id": edit_address_id}
|
||||
)[0]
|
||||
elif have_data_entry(form_data, "saveaddr"):
|
||||
edit_address_id = int(get_data_entry(form_data, "edit_address_id"))
|
||||
edit_addr = get_data_entry(form_data, "edit_address")
|
||||
active_ind = int(get_data_entry(form_data, "active_ind"))
|
||||
ensure(active_ind in (0, 1), "Invalid sort by")
|
||||
addressnote = get_data_entry_or(form_data, "addressnote", "")
|
||||
if not validateTextInput(
|
||||
addressnote, "Address note", err_messages, max_length=30
|
||||
):
|
||||
listaddresses = False
|
||||
page_data['edit_address'] = edit_address_id
|
||||
page_data["edit_address"] = edit_address_id
|
||||
else:
|
||||
swap_client.editSMSGAddress(edit_addr, active_ind=active_ind, addressnote=addressnote)
|
||||
messages.append(f'Edited address {edit_addr}')
|
||||
elif have_data_entry(form_data, 'shownewaddr'):
|
||||
swap_client.editSMSGAddress(
|
||||
edit_addr, active_ind=active_ind, addressnote=addressnote
|
||||
)
|
||||
messages.append(f"Edited address {edit_addr}")
|
||||
elif have_data_entry(form_data, "shownewaddr"):
|
||||
listaddresses = False
|
||||
page_data['new_address'] = True
|
||||
elif have_data_entry(form_data, 'showaddaddr'):
|
||||
page_data["new_address"] = True
|
||||
elif have_data_entry(form_data, "showaddaddr"):
|
||||
listaddresses = False
|
||||
page_data['new_send_address'] = True
|
||||
elif have_data_entry(form_data, 'createnewaddr'):
|
||||
addressnote = get_data_entry_or(form_data, 'addressnote', '')
|
||||
if not validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
|
||||
page_data["new_send_address"] = True
|
||||
elif have_data_entry(form_data, "createnewaddr"):
|
||||
addressnote = get_data_entry_or(form_data, "addressnote", "")
|
||||
if not validateTextInput(
|
||||
addressnote, "Address note", err_messages, max_length=30
|
||||
):
|
||||
listaddresses = False
|
||||
page_data['new_address'] = True
|
||||
page_data["new_address"] = True
|
||||
else:
|
||||
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
||||
messages.append(f'Created address {new_addr}, pubkey {pubkey}')
|
||||
elif have_data_entry(form_data, 'createnewsendaddr'):
|
||||
pubkey_hex = get_data_entry(form_data, 'addresspubkey')
|
||||
addressnote = get_data_entry_or(form_data, 'addressnote', '')
|
||||
if not validateTextInput(addressnote, 'Address note', messages, max_length=30) or \
|
||||
not validateTextInput(pubkey_hex, 'Pubkey', messages, max_length=66):
|
||||
messages.append(f"Created address {new_addr}, pubkey {pubkey}")
|
||||
elif have_data_entry(form_data, "createnewsendaddr"):
|
||||
pubkey_hex = get_data_entry(form_data, "addresspubkey")
|
||||
addressnote = get_data_entry_or(form_data, "addressnote", "")
|
||||
if not validateTextInput(
|
||||
addressnote, "Address note", messages, max_length=30
|
||||
) or not validateTextInput(pubkey_hex, "Pubkey", messages, max_length=66):
|
||||
listaddresses = False
|
||||
page_data['new_send_address'] = True
|
||||
page_data["new_send_address"] = True
|
||||
else:
|
||||
new_addr = swap_client.addSMSGAddress(pubkey_hex, addressnote=addressnote)
|
||||
messages.append(f'Added address {new_addr}')
|
||||
new_addr = swap_client.addSMSGAddress(
|
||||
pubkey_hex, addressnote=addressnote
|
||||
)
|
||||
messages.append(f"Added address {new_addr}")
|
||||
|
||||
if have_data_entry(form_data, 'clearfilters'):
|
||||
swap_client.clearFilters('page_smsgaddresses')
|
||||
if have_data_entry(form_data, "clearfilters"):
|
||||
swap_client.clearFilters("page_smsgaddresses")
|
||||
else:
|
||||
if have_data_entry(form_data, 'sort_by'):
|
||||
sort_by = get_data_entry(form_data, 'sort_by')
|
||||
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
|
||||
filters['sort_by'] = sort_by
|
||||
if have_data_entry(form_data, 'sort_dir'):
|
||||
sort_dir = get_data_entry(form_data, 'sort_dir')
|
||||
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
|
||||
filters['sort_dir'] = sort_dir
|
||||
if have_data_entry(form_data, 'filter_addressnote'):
|
||||
addressnote = get_data_entry(form_data, 'filter_addressnote')
|
||||
if validateTextInput(addressnote, 'Address note', err_messages, max_length=30):
|
||||
filters['addressnote'] = addressnote
|
||||
if have_data_entry(form_data, 'filter_addr_type'):
|
||||
filters['addr_type'] = int(get_data_entry(form_data, 'filter_addr_type'))
|
||||
if have_data_entry(form_data, "sort_by"):
|
||||
sort_by = get_data_entry(form_data, "sort_by")
|
||||
ensure(sort_by in ["created_at", "rate"], "Invalid sort by")
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(form_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(form_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
if have_data_entry(form_data, "filter_addressnote"):
|
||||
addressnote = get_data_entry(form_data, "filter_addressnote")
|
||||
if validateTextInput(
|
||||
addressnote, "Address note", err_messages, max_length=30
|
||||
):
|
||||
filters["addressnote"] = addressnote
|
||||
if have_data_entry(form_data, "filter_addr_type"):
|
||||
filters["addr_type"] = int(
|
||||
get_data_entry(form_data, "filter_addr_type")
|
||||
)
|
||||
|
||||
set_pagination_filters(form_data, filters)
|
||||
if have_data_entry(form_data, 'applyfilters'):
|
||||
swap_client.setFilters('page_smsgaddresses', filters)
|
||||
if have_data_entry(form_data, "applyfilters"):
|
||||
swap_client.setFilters("page_smsgaddresses", filters)
|
||||
else:
|
||||
saved_filters = swap_client.getFilters('page_smsgaddresses')
|
||||
saved_filters = swap_client.getFilters("page_smsgaddresses")
|
||||
if saved_filters:
|
||||
filters.update(saved_filters)
|
||||
|
||||
if listaddresses is True:
|
||||
smsgaddresses = swap_client.listAllSMSGAddresses(filters)
|
||||
|
||||
page_data['addr_types'] = [(int(t), strAddressType(t)) for t in AddressTypes]
|
||||
page_data['network_addr'] = swap_client.network_addr
|
||||
page_data["addr_types"] = [(int(t), strAddressType(t)) for t in AddressTypes]
|
||||
page_data["network_addr"] = swap_client.network_addr
|
||||
|
||||
for addr in smsgaddresses:
|
||||
addr['type'] = strAddressType(addr['type'])
|
||||
addr["type"] = strAddressType(addr["type"])
|
||||
|
||||
template = self.server.env.get_template('smsgaddresses.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'filters': filters,
|
||||
'data': page_data,
|
||||
'smsgaddresses': smsgaddresses,
|
||||
'page_data': page_data,
|
||||
'summary': summary,
|
||||
})
|
||||
template = self.server.env.get_template("smsgaddresses.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"filters": filters,
|
||||
"data": page_data,
|
||||
"smsgaddresses": smsgaddresses,
|
||||
"page_data": page_data,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
|
||||
def extract_data(bytes_in):
|
||||
str_in = bytes_in.decode('utf-8')
|
||||
start = str_in.find('=')
|
||||
if bytes_in is None:
|
||||
return None
|
||||
str_in = bytes_in.decode("utf-8")
|
||||
start = str_in.find("=")
|
||||
if start < 0:
|
||||
return None
|
||||
start += 1
|
||||
end = str_in.find('\r', start)
|
||||
end = str_in.find("\r", start)
|
||||
if end < 0:
|
||||
return None
|
||||
return str_in[start: end]
|
||||
return str_in[start:end]
|
||||
|
||||
|
||||
def get_tor_established_state(swap_client):
|
||||
rv = swap_client.torControl('GETINFO status/circuit-established')
|
||||
rv = swap_client.torControl("GETINFO status/circuit-established")
|
||||
return extract_data(rv)
|
||||
|
||||
|
||||
def page_tor(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
page_data = {}
|
||||
|
||||
try:
|
||||
page_data['circuit_established'] = get_tor_established_state(swap_client)
|
||||
page_data["circuit_established"] = get_tor_established_state(swap_client)
|
||||
except Exception:
|
||||
page_data['circuit_established'] = 'error'
|
||||
|
||||
page_data["circuit_established"] = "error"
|
||||
try:
|
||||
rv = swap_client.torControl('GETINFO traffic/read')
|
||||
page_data['bytes_written'] = extract_data(rv)
|
||||
rv = swap_client.torControl("GETINFO traffic/read")
|
||||
page_data["bytes_written"] = extract_data(rv)
|
||||
except Exception:
|
||||
page_data['bytes_written'] = 'error'
|
||||
|
||||
page_data["bytes_written"] = "error"
|
||||
try:
|
||||
rv = swap_client.torControl('GETINFO traffic/written')
|
||||
page_data['bytes_read'] = extract_data(rv)
|
||||
rv = swap_client.torControl("GETINFO traffic/written")
|
||||
page_data["bytes_read"] = extract_data(rv)
|
||||
except Exception:
|
||||
page_data['bytes_read'] = 'error'
|
||||
|
||||
page_data["bytes_read"] = "error"
|
||||
messages = []
|
||||
|
||||
template = self.server.env.get_template('tor.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'data': page_data,
|
||||
'summary': summary,
|
||||
})
|
||||
template = self.server.env.get_template("tor.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"data": page_data,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -23,52 +24,55 @@ from basicswap.chainparams import (
|
||||
|
||||
def format_wallet_data(swap_client, ci, w):
|
||||
wf = {
|
||||
'name': ci.coin_name(),
|
||||
'version': w.get('version', '?'),
|
||||
'ticker': ci.ticker_mainnet(),
|
||||
'cid': str(int(ci.coin_type())),
|
||||
'balance': w.get('balance', '?'),
|
||||
'blocks': w.get('blocks', '?'),
|
||||
'synced': w.get('synced', '?'),
|
||||
'expected_seed': w.get('expected_seed', '?'),
|
||||
'encrypted': w.get('encrypted', '?'),
|
||||
'locked': w.get('locked', '?'),
|
||||
'updating': w.get('updating', '?'),
|
||||
'havedata': True,
|
||||
"name": ci.coin_name(),
|
||||
"version": w.get("version", "?"),
|
||||
"ticker": ci.ticker_mainnet(),
|
||||
"cid": str(int(ci.coin_type())),
|
||||
"balance": w.get("balance", "?"),
|
||||
"blocks": w.get("blocks", "?"),
|
||||
"synced": w.get("synced", "?"),
|
||||
"expected_seed": w.get("expected_seed", "?"),
|
||||
"encrypted": w.get("encrypted", "?"),
|
||||
"locked": w.get("locked", "?"),
|
||||
"updating": w.get("updating", "?"),
|
||||
"havedata": True,
|
||||
}
|
||||
|
||||
if w.get('bootstrapping', False) is True:
|
||||
wf['bootstrapping'] = True
|
||||
if 'known_block_count' in w:
|
||||
wf['known_block_count'] = w['known_block_count']
|
||||
if 'locked_utxos' in w:
|
||||
wf['locked_utxos'] = w['locked_utxos']
|
||||
if "wallet_blocks" in w:
|
||||
wf["wallet_blocks"] = w["wallet_blocks"]
|
||||
|
||||
if 'balance' in w and 'unconfirmed' in w:
|
||||
wf['balance_all'] = float(w['balance']) + float(w['unconfirmed'])
|
||||
if 'lastupdated' in w:
|
||||
wf['lastupdated'] = format_timestamp(w['lastupdated'])
|
||||
if w.get("bootstrapping", False) is True:
|
||||
wf["bootstrapping"] = True
|
||||
if "known_block_count" in w:
|
||||
wf["known_block_count"] = w["known_block_count"]
|
||||
if "locked_utxos" in w:
|
||||
wf["locked_utxos"] = w["locked_utxos"]
|
||||
|
||||
if "balance" in w and "unconfirmed" in w:
|
||||
wf["balance_all"] = float(w["balance"]) + float(w["unconfirmed"])
|
||||
if "lastupdated" in w:
|
||||
wf["lastupdated"] = format_timestamp(w["lastupdated"])
|
||||
|
||||
pending: int = 0
|
||||
if 'unconfirmed' in w and float(w['unconfirmed']) > 0.0:
|
||||
pending += ci.make_int(w['unconfirmed'])
|
||||
if 'immature' in w and float(w['immature']) > 0.0:
|
||||
pending += ci.make_int(w['immature'])
|
||||
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
|
||||
pending += ci.make_int(w["unconfirmed"])
|
||||
if "immature" in w and float(w["immature"]) > 0.0:
|
||||
pending += ci.make_int(w["immature"])
|
||||
if pending > 0.0:
|
||||
wf['pending'] = ci.format_amount(pending)
|
||||
wf["pending"] = ci.format_amount(pending)
|
||||
|
||||
if ci.coin_type() == Coins.PART:
|
||||
wf['stealth_address'] = w.get('stealth_address', '?')
|
||||
wf['blind_balance'] = w.get('blind_balance', '?')
|
||||
if 'blind_unconfirmed' in w and float(w['blind_unconfirmed']) > 0.0:
|
||||
wf['blind_unconfirmed'] = w['blind_unconfirmed']
|
||||
wf['anon_balance'] = w.get('anon_balance', '?')
|
||||
if 'anon_pending' in w and float(w['anon_pending']) > 0.0:
|
||||
wf['anon_pending'] = w['anon_pending']
|
||||
wf["stealth_address"] = w.get("stealth_address", "?")
|
||||
wf["blind_balance"] = w.get("blind_balance", "?")
|
||||
if "blind_unconfirmed" in w and float(w["blind_unconfirmed"]) > 0.0:
|
||||
wf["blind_unconfirmed"] = w["blind_unconfirmed"]
|
||||
wf["anon_balance"] = w.get("anon_balance", "?")
|
||||
if "anon_pending" in w and float(w["anon_pending"]) > 0.0:
|
||||
wf["anon_pending"] = w["anon_pending"]
|
||||
elif ci.coin_type() == Coins.LTC:
|
||||
wf['mweb_address'] = w.get('mweb_address', '?')
|
||||
wf['mweb_balance'] = w.get('mweb_balance', '?')
|
||||
wf['mweb_pending'] = w.get('mweb_pending', '?')
|
||||
wf["mweb_address"] = w.get("mweb_address", "?")
|
||||
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
||||
wf["mweb_pending"] = w.get("mweb_pending", "?")
|
||||
|
||||
checkAddressesOwned(swap_client, ci, wf)
|
||||
return wf
|
||||
@@ -91,19 +95,18 @@ def page_wallets(self, url_split, post_string):
|
||||
|
||||
for k in sk:
|
||||
w = wallets[k]
|
||||
if 'error' in w:
|
||||
wallets_formatted.append({
|
||||
'cid': str(int(k)),
|
||||
'error': w['error']
|
||||
})
|
||||
if "error" in w:
|
||||
wallets_formatted.append({"cid": str(int(k)), "error": w["error"]})
|
||||
continue
|
||||
|
||||
if 'no_data' in w:
|
||||
wallets_formatted.append({
|
||||
'name': w['name'],
|
||||
'havedata': False,
|
||||
'updating': w['updating'],
|
||||
})
|
||||
if "no_data" in w:
|
||||
wallets_formatted.append(
|
||||
{
|
||||
"name": w["name"],
|
||||
"havedata": False,
|
||||
"updating": w["updating"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
ci = swap_client.ci(k)
|
||||
@@ -111,17 +114,20 @@ def page_wallets(self, url_split, post_string):
|
||||
|
||||
wallets_formatted.append(wf)
|
||||
|
||||
template = server.env.get_template('wallets.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'wallets': wallets_formatted,
|
||||
'summary': summary,
|
||||
})
|
||||
template = server.env.get_template("wallets.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"wallets": wallets_formatted,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def page_wallet(self, url_split, post_string):
|
||||
ensure(len(url_split) > 2, 'Wallet not specified')
|
||||
ensure(len(url_split) > 2, "Wallet not specified")
|
||||
wallet_ticker = url_split[2]
|
||||
server = self.server
|
||||
swap_client = server.swap_client
|
||||
@@ -136,94 +142,130 @@ def page_wallet(self, url_split, post_string):
|
||||
show_utxo_groups: bool = False
|
||||
withdrawal_successful: bool = False
|
||||
force_refresh: bool = False
|
||||
form_data = self.checkForm(post_string, 'wallet', err_messages)
|
||||
form_data = self.checkForm(post_string, "wallet", err_messages)
|
||||
if form_data:
|
||||
cid = str(int(coin_id))
|
||||
|
||||
estimate_fee: bool = have_data_entry(form_data, 'estfee_' + cid)
|
||||
withdraw: bool = have_data_entry(form_data, 'withdraw_' + cid)
|
||||
if have_data_entry(form_data, 'newaddr_' + cid):
|
||||
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
|
||||
withdraw: bool = have_data_entry(form_data, "withdraw_" + cid)
|
||||
if have_data_entry(form_data, "newaddr_" + cid):
|
||||
swap_client.cacheNewAddressForCoin(coin_id)
|
||||
elif have_data_entry(form_data, 'forcerefresh'):
|
||||
elif have_data_entry(form_data, "forcerefresh"):
|
||||
force_refresh = True
|
||||
elif have_data_entry(form_data, 'newmwebaddr_' + cid):
|
||||
elif have_data_entry(form_data, "newmwebaddr_" + cid):
|
||||
swap_client.cacheNewStealthAddressForCoin(coin_id)
|
||||
elif have_data_entry(form_data, 'reseed_' + cid):
|
||||
elif have_data_entry(form_data, "reseed_" + cid):
|
||||
try:
|
||||
swap_client.reseedWallet(coin_id)
|
||||
messages.append('Reseed complete ' + str(coin_id))
|
||||
messages.append("Reseed complete " + str(coin_id))
|
||||
except Exception as ex:
|
||||
err_messages.append('Reseed failed ' + str(ex))
|
||||
err_messages.append("Reseed failed " + str(ex))
|
||||
swap_client.updateWalletsInfo(True, coin_id)
|
||||
elif withdraw or estimate_fee:
|
||||
subfee = True if have_data_entry(form_data, 'subfee_' + cid) else False
|
||||
page_data['wd_subfee_' + cid] = subfee
|
||||
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
|
||||
page_data["wd_subfee_" + cid] = subfee
|
||||
|
||||
sweepall = True if have_data_entry(form_data, 'sweepall_' + cid) else False
|
||||
page_data['wd_sweepall_' + cid] = sweepall
|
||||
sweepall = True if have_data_entry(form_data, "sweepall_" + cid) else False
|
||||
page_data["wd_sweepall_" + cid] = sweepall
|
||||
value = None
|
||||
if not sweepall:
|
||||
try:
|
||||
value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
page_data['wd_value_' + cid] = value
|
||||
except Exception as e:
|
||||
err_messages.append('Missing value')
|
||||
value = form_data[bytes("amt_" + cid, "utf-8")][0].decode("utf-8")
|
||||
page_data["wd_value_" + cid] = value
|
||||
except Exception as e: # noqa: F841
|
||||
err_messages.append("Missing value")
|
||||
try:
|
||||
address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
page_data['wd_address_' + cid] = address
|
||||
except Exception as e:
|
||||
err_messages.append('Missing address')
|
||||
address = form_data[bytes("to_" + cid, "utf-8")][0].decode("utf-8")
|
||||
page_data["wd_address_" + cid] = address
|
||||
except Exception as e: # noqa: F841
|
||||
err_messages.append("Missing address")
|
||||
|
||||
if estimate_fee and withdraw:
|
||||
err_messages.append('Estimate fee and withdraw can\'t be used together.')
|
||||
err_messages.append("Estimate fee and withdraw can't be used together.")
|
||||
if estimate_fee and coin_id not in (Coins.XMR, Coins.WOW):
|
||||
ci = swap_client.ci(coin_id)
|
||||
ticker: str = ci.ticker()
|
||||
err_messages.append(f'Estimate fee unavailable for {ticker}.')
|
||||
err_messages.append(f"Estimate fee unavailable for {ticker}.")
|
||||
|
||||
if coin_id == Coins.PART:
|
||||
try:
|
||||
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
page_data['wd_type_from_' + cid] = type_from
|
||||
page_data['wd_type_to_' + cid] = type_to
|
||||
except Exception as e:
|
||||
err_messages.append('Missing type')
|
||||
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
|
||||
0
|
||||
].decode("utf-8")
|
||||
type_to = form_data[bytes("withdraw_type_to_" + cid, "utf-8")][
|
||||
0
|
||||
].decode("utf-8")
|
||||
page_data["wd_type_from_" + cid] = type_from
|
||||
page_data["wd_type_to_" + cid] = type_to
|
||||
except Exception as e: # noqa: F841
|
||||
err_messages.append("Missing type")
|
||||
elif coin_id == Coins.LTC:
|
||||
try:
|
||||
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
page_data['wd_type_from_' + cid] = type_from
|
||||
except Exception as e:
|
||||
err_messages.append('Missing type')
|
||||
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
|
||||
0
|
||||
].decode("utf-8")
|
||||
page_data["wd_type_from_" + cid] = type_from
|
||||
except Exception as e: # noqa: F841
|
||||
err_messages.append("Missing type")
|
||||
|
||||
if len(err_messages) == 0:
|
||||
ci = swap_client.ci(coin_id)
|
||||
ticker: str = ci.ticker()
|
||||
try:
|
||||
if coin_id == Coins.PART:
|
||||
txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
|
||||
messages.append('Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, type_to, address, txid))
|
||||
txid = swap_client.withdrawParticl(
|
||||
type_from, type_to, value, address, subfee
|
||||
)
|
||||
messages.append(
|
||||
"Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}".format(
|
||||
value, ticker, type_from, type_to, address, txid
|
||||
)
|
||||
)
|
||||
elif coin_id == Coins.LTC:
|
||||
txid = swap_client.withdrawLTC(type_from, value, address, subfee)
|
||||
messages.append('Withdrew {} {} (from {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, address, txid))
|
||||
txid = swap_client.withdrawLTC(
|
||||
type_from, value, address, subfee
|
||||
)
|
||||
messages.append(
|
||||
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
|
||||
value, ticker, type_from, address, txid
|
||||
)
|
||||
)
|
||||
elif coin_id in (Coins.XMR, Coins.WOW):
|
||||
if estimate_fee:
|
||||
fee_estimate = ci.estimateFee(value, address, sweepall)
|
||||
suffix = 's' if fee_estimate['num_txns'] > 1 else ''
|
||||
sum_fees = ci.format_amount(fee_estimate['sum_fee'])
|
||||
value_str = ci.format_amount(fee_estimate['sum_amount'])
|
||||
messages.append(f'Estimated fee for {value_str} {ticker} to address {address}: {sum_fees} in {fee_estimate["num_txns"]} transaction{suffix}.')
|
||||
page_data['fee_estimate'] = fee_estimate
|
||||
suffix = "s" if fee_estimate["num_txns"] > 1 else ""
|
||||
sum_fees = ci.format_amount(fee_estimate["sum_fee"])
|
||||
value_str = ci.format_amount(fee_estimate["sum_amount"])
|
||||
messages.append(
|
||||
f'Estimated fee for {value_str} {ticker} to address {address}: {sum_fees} in {fee_estimate["num_txns"]} transaction{suffix}.'
|
||||
)
|
||||
page_data["fee_estimate"] = fee_estimate
|
||||
else:
|
||||
txid = swap_client.withdrawCoin(coin_id, value, address, sweepall)
|
||||
txid = swap_client.withdrawCoin(
|
||||
coin_id, value, address, sweepall
|
||||
)
|
||||
if sweepall:
|
||||
messages.append('Swept all {} to address {}<br/>In txid: {}'.format(ticker, address, txid))
|
||||
messages.append(
|
||||
"Swept all {} to address {}<br/>In txid: {}".format(
|
||||
ticker, address, txid
|
||||
)
|
||||
)
|
||||
else:
|
||||
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
|
||||
messages.append('Note: The wallet balance can take a while to update.')
|
||||
messages.append(
|
||||
"Withdrew {} {} to address {}<br/>In txid: {}".format(
|
||||
value, ticker, address, txid
|
||||
)
|
||||
)
|
||||
messages.append(
|
||||
"Note: The wallet balance can take a while to update."
|
||||
)
|
||||
else:
|
||||
txid = swap_client.withdrawCoin(coin_id, value, address, subfee)
|
||||
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
|
||||
messages.append(
|
||||
"Withdrew {} {} to address {}<br/>In txid: {}".format(
|
||||
value, ticker, address, txid
|
||||
)
|
||||
)
|
||||
if not estimate_fee:
|
||||
withdrawal_successful = True
|
||||
except Exception as e:
|
||||
@@ -232,40 +274,44 @@ def page_wallet(self, url_split, post_string):
|
||||
err_messages.append(str(e))
|
||||
if not estimate_fee:
|
||||
swap_client.updateWalletsInfo(True, only_coin=coin_id)
|
||||
elif have_data_entry(form_data, 'showutxogroups'):
|
||||
elif have_data_entry(form_data, "showutxogroups"):
|
||||
show_utxo_groups = True
|
||||
elif have_data_entry(form_data, 'create_utxo'):
|
||||
elif have_data_entry(form_data, "create_utxo"):
|
||||
show_utxo_groups = True
|
||||
try:
|
||||
value = get_data_entry(form_data, 'utxo_value')
|
||||
page_data['utxo_value'] = value
|
||||
value = get_data_entry(form_data, "utxo_value")
|
||||
page_data["utxo_value"] = value
|
||||
|
||||
ci = swap_client.ci(coin_id)
|
||||
value_sats = ci.make_int(value)
|
||||
|
||||
txid, address = ci.createUTXO(value_sats)
|
||||
messages.append('Created new utxo of value {} and address {}<br/>In txid: {}'.format(value, address, txid))
|
||||
messages.append(
|
||||
"Created new utxo of value {} and address {}<br/>In txid: {}".format(
|
||||
value, address, txid
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
err_messages.append(str(e))
|
||||
if swap_client.debug is True:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
swap_client.updateWalletsInfo(force_refresh, only_coin=coin_id, wait_for_complete=True)
|
||||
wallets = swap_client.getCachedWalletsInfo({'coin_id': coin_id})
|
||||
swap_client.updateWalletsInfo(
|
||||
force_refresh, only_coin=coin_id, wait_for_complete=True
|
||||
)
|
||||
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
|
||||
wallet_data = {}
|
||||
for k in wallets.keys():
|
||||
w = wallets[k]
|
||||
if 'error' in w:
|
||||
wallet_data = {
|
||||
'cid': str(int(k)),
|
||||
'error': w['error']
|
||||
}
|
||||
if "error" in w:
|
||||
wallet_data = {"cid": str(int(k)), "error": w["error"]}
|
||||
continue
|
||||
|
||||
if 'no_data' in w:
|
||||
if "no_data" in w:
|
||||
wallet_data = {
|
||||
'name': w['name'],
|
||||
'havedata': False,
|
||||
'updating': w['updating'],
|
||||
"name": w["name"],
|
||||
"havedata": False,
|
||||
"updating": w["updating"],
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -276,55 +322,68 @@ def page_wallet(self, url_split, post_string):
|
||||
|
||||
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
|
||||
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
|
||||
wallet_data['fee_rate'] = ci.format_amount(int(fee_rate * ci.COIN()))
|
||||
wallet_data['fee_rate_src'] = fee_src
|
||||
wallet_data['est_fee'] = 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN()))
|
||||
wallet_data['deposit_address'] = w.get('deposit_address', 'Refresh necessary')
|
||||
wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN()))
|
||||
wallet_data["fee_rate_src"] = fee_src
|
||||
wallet_data["est_fee"] = (
|
||||
"Unknown" if est_fee is None else ci.format_amount(int(est_fee * ci.COIN()))
|
||||
)
|
||||
wallet_data["deposit_address"] = w.get("deposit_address", "Refresh necessary")
|
||||
|
||||
if k in (Coins.XMR, Coins.WOW):
|
||||
wallet_data['main_address'] = w.get('main_address', 'Refresh necessary')
|
||||
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
|
||||
elif k == Coins.LTC:
|
||||
wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary')
|
||||
wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary")
|
||||
|
||||
if 'wd_type_from_' + cid in page_data:
|
||||
wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid]
|
||||
if 'wd_type_to_' + cid in page_data:
|
||||
wallet_data['wd_type_to'] = page_data['wd_type_to_' + cid]
|
||||
if "wd_type_from_" + cid in page_data:
|
||||
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
|
||||
if "wd_type_to_" + cid in page_data:
|
||||
wallet_data["wd_type_to"] = page_data["wd_type_to_" + cid]
|
||||
|
||||
if 'utxo_value' in page_data:
|
||||
wallet_data['utxo_value'] = page_data['utxo_value']
|
||||
if "utxo_value" in page_data:
|
||||
wallet_data["utxo_value"] = page_data["utxo_value"]
|
||||
|
||||
if not withdrawal_successful:
|
||||
if 'wd_value_' + cid in page_data:
|
||||
wallet_data['wd_value'] = page_data['wd_value_' + cid]
|
||||
if 'wd_address_' + cid in page_data:
|
||||
wallet_data['wd_address'] = page_data['wd_address_' + cid]
|
||||
if 'wd_subfee_' + cid in page_data:
|
||||
wallet_data['wd_subfee'] = page_data['wd_subfee_' + cid]
|
||||
if 'wd_sweepall_' + cid in page_data:
|
||||
wallet_data['wd_sweepall'] = page_data['wd_sweepall_' + cid]
|
||||
if 'fee_estimate' in page_data:
|
||||
wallet_data['est_fee'] = ci.format_amount(page_data['fee_estimate']['sum_fee'])
|
||||
wallet_data['fee_rate'] = ci.format_amount(page_data['fee_estimate']['sum_fee'] * 1000 // page_data['fee_estimate']['sum_weight'])
|
||||
if "wd_value_" + cid in page_data:
|
||||
wallet_data["wd_value"] = page_data["wd_value_" + cid]
|
||||
if "wd_address_" + cid in page_data:
|
||||
wallet_data["wd_address"] = page_data["wd_address_" + cid]
|
||||
if "wd_subfee_" + cid in page_data:
|
||||
wallet_data["wd_subfee"] = page_data["wd_subfee_" + cid]
|
||||
if "wd_sweepall_" + cid in page_data:
|
||||
wallet_data["wd_sweepall"] = page_data["wd_sweepall_" + cid]
|
||||
if "fee_estimate" in page_data:
|
||||
wallet_data["est_fee"] = ci.format_amount(
|
||||
page_data["fee_estimate"]["sum_fee"]
|
||||
)
|
||||
wallet_data["fee_rate"] = ci.format_amount(
|
||||
page_data["fee_estimate"]["sum_fee"]
|
||||
* 1000
|
||||
// page_data["fee_estimate"]["sum_weight"]
|
||||
)
|
||||
|
||||
if show_utxo_groups:
|
||||
utxo_groups = ''
|
||||
utxo_groups = ""
|
||||
unspent_by_addr = ci.getUnspentsByAddr()
|
||||
|
||||
sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True)
|
||||
sorted_unspent_by_addr = sorted(
|
||||
unspent_by_addr.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
for kv in sorted_unspent_by_addr:
|
||||
utxo_groups += kv[0] + ' ' + ci.format_amount(kv[1]) + '\n'
|
||||
utxo_groups += kv[0] + " " + ci.format_amount(kv[1]) + "\n"
|
||||
|
||||
wallet_data['show_utxo_groups'] = True
|
||||
wallet_data['utxo_groups'] = utxo_groups
|
||||
wallet_data["show_utxo_groups"] = True
|
||||
wallet_data["utxo_groups"] = utxo_groups
|
||||
|
||||
checkAddressesOwned(swap_client, ci, wallet_data)
|
||||
|
||||
template = server.env.get_template('wallet.html')
|
||||
return self.render_template(template, {
|
||||
'messages': messages,
|
||||
'err_messages': err_messages,
|
||||
'w': wallet_data,
|
||||
'summary': summary,
|
||||
'block_unknown_seeds': swap_client._restrict_unknown_seed_wallets,
|
||||
})
|
||||
template = server.env.get_template("wallet.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"messages": messages,
|
||||
"err_messages": err_messages,
|
||||
"w": wallet_data,
|
||||
"summary": summary,
|
||||
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import struct
|
||||
import traceback
|
||||
from basicswap.util import (
|
||||
hex_or_none,
|
||||
make_int,
|
||||
format_timestamp,
|
||||
)
|
||||
@@ -19,6 +20,7 @@ from basicswap.basicswap_util import (
|
||||
ActionTypes,
|
||||
BidStates,
|
||||
DebugTypes,
|
||||
canAcceptBidState,
|
||||
getLastBidState,
|
||||
strBidState,
|
||||
strTxState,
|
||||
@@ -31,9 +33,23 @@ from basicswap.basicswap_util import (
|
||||
|
||||
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
|
||||
|
||||
PAGE_LIMIT = 25
|
||||
PAGE_LIMIT = 1000
|
||||
invalid_coins_from = []
|
||||
known_chart_coins = ['BTC', 'PART', 'XMR', 'LTC', 'FIRO', 'DASH', 'PIVX', 'DOGE', 'ETH', 'DCR', 'ZANO', 'WOW']
|
||||
known_chart_coins = [
|
||||
"BTC",
|
||||
"PART",
|
||||
"XMR",
|
||||
"LTC",
|
||||
"FIRO",
|
||||
"DASH",
|
||||
"PIVX",
|
||||
"DOGE",
|
||||
"ETH",
|
||||
"DCR",
|
||||
"ZANO",
|
||||
"WOW",
|
||||
"BCH",
|
||||
]
|
||||
|
||||
|
||||
def tickerToCoinId(ticker):
|
||||
@@ -41,7 +57,7 @@ def tickerToCoinId(ticker):
|
||||
for c in Coins:
|
||||
if c.name == search_str:
|
||||
return c.value
|
||||
raise ValueError('Unknown coin')
|
||||
raise ValueError("Unknown coin")
|
||||
|
||||
|
||||
def getCoinType(coin_type_ind):
|
||||
@@ -55,9 +71,9 @@ def getCoinType(coin_type_ind):
|
||||
def validateAmountString(amount, ci):
|
||||
if not isinstance(amount, str):
|
||||
return
|
||||
ar = amount.split('.')
|
||||
ar = amount.split(".")
|
||||
if len(ar) > 1 and len(ar[1]) > ci.exp():
|
||||
raise ValueError('Too many decimal places in amount {}'.format(amount))
|
||||
raise ValueError("Too many decimal places in amount {}".format(amount))
|
||||
|
||||
|
||||
def inputAmount(amount_str, ci):
|
||||
@@ -66,24 +82,24 @@ def inputAmount(amount_str, ci):
|
||||
|
||||
|
||||
def get_data_entry_or(post_data, name, default):
|
||||
if 'is_json' in post_data:
|
||||
if "is_json" in post_data:
|
||||
return post_data.get(name, default)
|
||||
key_bytes = name.encode('utf-8')
|
||||
key_bytes = name.encode("utf-8")
|
||||
if key_bytes in post_data:
|
||||
return post_data[key_bytes][0].decode('utf-8')
|
||||
return post_data[key_bytes][0].decode("utf-8")
|
||||
return default
|
||||
|
||||
|
||||
def get_data_entry(post_data, name):
|
||||
if 'is_json' in post_data:
|
||||
if "is_json" in post_data:
|
||||
return post_data[name]
|
||||
return post_data[name.encode('utf-8')][0].decode('utf-8')
|
||||
return post_data[name.encode("utf-8")][0].decode("utf-8")
|
||||
|
||||
|
||||
def have_data_entry(post_data, name):
|
||||
if 'is_json' in post_data:
|
||||
if "is_json" in post_data:
|
||||
return name in post_data
|
||||
return name.encode('utf-8') in post_data
|
||||
return name.encode("utf-8") in post_data
|
||||
|
||||
|
||||
def setCoinFilter(form_data, field_name):
|
||||
@@ -96,18 +112,18 @@ def setCoinFilter(form_data, field_name):
|
||||
try:
|
||||
return Coins(coin_type)
|
||||
except Exception:
|
||||
raise ValueError('Unknown Coin Type {}'.format(str(field_name)))
|
||||
raise ValueError("Unknown Coin Type {}".format(str(field_name)))
|
||||
|
||||
|
||||
def set_pagination_filters(form_data, filters):
|
||||
if form_data and have_data_entry(form_data, 'pageback'):
|
||||
filters['page_no'] = int(form_data[b'pageno'][0]) - 1
|
||||
if filters['page_no'] < 1:
|
||||
filters['page_no'] = 1
|
||||
elif form_data and have_data_entry(form_data, 'pageforwards'):
|
||||
filters['page_no'] = int(form_data[b'pageno'][0]) + 1
|
||||
if filters['page_no'] > 1:
|
||||
filters['offset'] = (filters['page_no'] - 1) * PAGE_LIMIT
|
||||
if form_data and have_data_entry(form_data, "pageback"):
|
||||
filters["page_no"] = int(form_data[b"pageno"][0]) - 1
|
||||
if filters["page_no"] < 1:
|
||||
filters["page_no"] = 1
|
||||
elif form_data and have_data_entry(form_data, "pageforwards"):
|
||||
filters["page_no"] = int(form_data[b"pageno"][0]) + 1
|
||||
if filters["page_no"] > 1:
|
||||
filters["offset"] = (filters["page_no"] - 1) * PAGE_LIMIT
|
||||
|
||||
|
||||
def getTxIdHex(bid, tx_type, suffix):
|
||||
@@ -116,12 +132,12 @@ def getTxIdHex(bid, tx_type, suffix):
|
||||
elif tx_type == TxTypes.PTX:
|
||||
obj = bid.participate_tx
|
||||
else:
|
||||
return 'Unknown Type'
|
||||
return "Unknown Type"
|
||||
|
||||
if not obj:
|
||||
return 'None'
|
||||
return "None"
|
||||
if not obj.txid:
|
||||
return 'None'
|
||||
return "None"
|
||||
return obj.txid.hex() + suffix
|
||||
|
||||
|
||||
@@ -131,13 +147,13 @@ def getTxSpendHex(bid, tx_type):
|
||||
elif tx_type == TxTypes.PTX:
|
||||
obj = bid.participate_tx
|
||||
else:
|
||||
return 'Unknown Type'
|
||||
return "Unknown Type"
|
||||
|
||||
if not obj:
|
||||
return 'None'
|
||||
return "None"
|
||||
if not obj.spend_txid:
|
||||
return 'None'
|
||||
return obj.spend_txid.hex() + ' {}'.format(obj.spend_n)
|
||||
return "None"
|
||||
return obj.spend_txid.hex() + " {}".format(obj.spend_n)
|
||||
|
||||
|
||||
def listBidStates():
|
||||
@@ -154,11 +170,23 @@ def listBidActions():
|
||||
return rv
|
||||
|
||||
|
||||
def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_bid, show_txns, view_tx_ind=None, for_api=False, show_lock_transfers=False):
|
||||
def describeBid(
|
||||
swap_client,
|
||||
bid,
|
||||
xmr_swap,
|
||||
offer,
|
||||
xmr_offer,
|
||||
bid_events,
|
||||
edit_bid,
|
||||
show_txns,
|
||||
view_tx_ind=None,
|
||||
for_api=False,
|
||||
show_lock_transfers=False,
|
||||
):
|
||||
ci_from = swap_client.ci(Coins(offer.coin_from))
|
||||
ci_to = swap_client.ci(Coins(offer.coin_to))
|
||||
|
||||
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from)
|
||||
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
|
||||
ci_leader = ci_to if reverse_bid else ci_from
|
||||
ci_follower = ci_from if reverse_bid else ci_to
|
||||
|
||||
@@ -166,238 +194,404 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
|
||||
bid_amount_to: int = bid.amount_to
|
||||
bid_rate: int = offer.rate if bid.rate is None else bid.rate
|
||||
|
||||
initiator_role: str = 'offerer' # Leader
|
||||
participant_role: str = 'bidder' # Follower
|
||||
initiator_role: str = "offerer" # Leader
|
||||
participant_role: str = "bidder" # Follower
|
||||
if reverse_bid:
|
||||
bid_amount = bid.amount_to
|
||||
bid_amount_to = bid.amount
|
||||
bid_rate = ci_from.make_int(bid.amount / bid.amount_to, r=1)
|
||||
initiator_role = 'bidder'
|
||||
participant_role = 'offerer'
|
||||
initiator_role = "bidder"
|
||||
participant_role = "offerer"
|
||||
|
||||
state_description = ''
|
||||
state_description = ""
|
||||
if offer.swap_type == SwapTypes.SELLER_FIRST:
|
||||
if bid.state == BidStates.BID_SENT:
|
||||
state_description = 'Waiting for seller to accept.'
|
||||
state_description = "Waiting for seller to accept."
|
||||
elif bid.state == BidStates.BID_RECEIVED:
|
||||
state_description = 'Waiting for seller to accept.'
|
||||
state_description = "Waiting for seller to accept."
|
||||
elif bid.state == BidStates.BID_ACCEPTED:
|
||||
if not bid.initiate_tx:
|
||||
state_description = 'Waiting for seller to send initiate tx.'
|
||||
state_description = "Waiting for seller to send initiate tx."
|
||||
else:
|
||||
state_description = 'Waiting for initiate tx to confirm.'
|
||||
state_description = "Waiting for initiate tx to confirm."
|
||||
elif bid.state == BidStates.SWAP_INITIATED:
|
||||
state_description = 'Waiting for participate txn to be confirmed in {} chain'.format(ci_follower.ticker())
|
||||
state_description = (
|
||||
"Waiting for participate txn to be confirmed in {} chain".format(
|
||||
ci_follower.ticker()
|
||||
)
|
||||
)
|
||||
elif bid.state == BidStates.SWAP_PARTICIPATING:
|
||||
if bid.was_sent:
|
||||
state_description = 'Waiting for participate txn to be spent in {} chain'.format(ci_follower.ticker())
|
||||
state_description = (
|
||||
"Waiting for participate txn to be spent in {} chain".format(
|
||||
ci_follower.ticker()
|
||||
)
|
||||
)
|
||||
else:
|
||||
state_description = 'Waiting for initiate txn to be spent in {} chain'.format(ci_leader.ticker())
|
||||
state_description = (
|
||||
"Waiting for initiate txn to be spent in {} chain".format(
|
||||
ci_leader.ticker()
|
||||
)
|
||||
)
|
||||
elif bid.state == BidStates.SWAP_COMPLETED:
|
||||
state_description = 'Swap completed'
|
||||
if bid.getITxState() == TxStates.TX_REDEEMED and bid.getPTxState() == TxStates.TX_REDEEMED:
|
||||
state_description += ' successfully'
|
||||
state_description = "Swap completed"
|
||||
if (
|
||||
bid.getITxState() == TxStates.TX_REDEEMED
|
||||
and bid.getPTxState() == TxStates.TX_REDEEMED
|
||||
):
|
||||
state_description += " successfully"
|
||||
else:
|
||||
state_description += ', ITX ' + strTxState(bid.getITxState()) + ', PTX ' + strTxState(bid.getPTxState())
|
||||
state_description += (
|
||||
", ITX "
|
||||
+ strTxState(bid.getITxState())
|
||||
+ ", PTX "
|
||||
+ strTxState(bid.getPTxState())
|
||||
)
|
||||
elif bid.state == BidStates.SWAP_TIMEDOUT:
|
||||
state_description = 'Timed out waiting for initiate txn'
|
||||
state_description = "Timed out waiting for initiate txn"
|
||||
elif bid.state == BidStates.BID_ABANDONED:
|
||||
state_description = 'Bid abandoned'
|
||||
state_description = "Bid abandoned"
|
||||
elif bid.state == BidStates.BID_ERROR:
|
||||
state_description = bid.state_note
|
||||
elif offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
if bid.state == BidStates.BID_SENT:
|
||||
state_description = 'Waiting for offerer to accept'
|
||||
state_description = "Waiting for offerer to accept"
|
||||
if bid.state == BidStates.BID_RECEIVING:
|
||||
# Offerer receiving bid from bidder
|
||||
state_description = 'Waiting for bid to be fully received'
|
||||
elif bid.state == BidStates.BID_RECEIVED:
|
||||
state_description = "Waiting for bid to be fully received"
|
||||
elif canAcceptBidState(bid.state):
|
||||
# Offerer received bid from bidder
|
||||
# TODO: Manual vs automatic
|
||||
state_description = 'Bid must be accepted'
|
||||
state_description = "Bid must be accepted"
|
||||
elif bid.state == BidStates.BID_RECEIVING_ACC:
|
||||
state_description = 'Receiving accepted bid message'
|
||||
state_description = "Receiving accepted bid message"
|
||||
elif bid.state == BidStates.BID_ACCEPTED:
|
||||
state_description = 'Offerer has accepted bid, waiting for bidder to respond'
|
||||
state_description = (
|
||||
"Offerer has accepted bid, waiting for bidder to respond"
|
||||
)
|
||||
elif bid.state == BidStates.SWAP_DELAYING:
|
||||
last_state = getLastBidState(bid.states)
|
||||
if last_state == BidStates.BID_RECEIVED:
|
||||
state_description = 'Delaying before accepting bid'
|
||||
if canAcceptBidState(last_state):
|
||||
state_description = "Delaying before accepting bid"
|
||||
elif last_state == BidStates.BID_RECEIVING_ACC:
|
||||
state_description = 'Delaying before responding to accepted bid'
|
||||
state_description = "Delaying before responding to accepted bid"
|
||||
elif last_state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
||||
state_description = f'Delaying before spending from {ci_follower.ticker()} lock tx'
|
||||
state_description = (
|
||||
f"Delaying before spending from {ci_follower.ticker()} lock tx"
|
||||
)
|
||||
elif last_state == BidStates.BID_ACCEPTED:
|
||||
state_description = f'Delaying before sending {ci_leader.ticker()} lock tx'
|
||||
state_description = (
|
||||
f"Delaying before sending {ci_leader.ticker()} lock tx"
|
||||
)
|
||||
else:
|
||||
state_description = 'Delaying before automated action'
|
||||
state_description = "Delaying before automated action"
|
||||
elif bid.state == BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX:
|
||||
state_description = f'Waiting for {ci_leader.ticker()} lock tx to confirm in chain ({ci_leader.blocks_confirmed} blocks)'
|
||||
state_description = f"Waiting for {ci_leader.ticker()} lock tx to confirm in chain ({ci_leader.blocks_confirmed} blocks)"
|
||||
elif bid.state == BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED:
|
||||
if xmr_swap.b_lock_tx_id is None:
|
||||
state_description = f'Waiting for {ci_follower.ticker()} lock tx'
|
||||
state_description = f"Waiting for {ci_follower.ticker()} lock tx"
|
||||
else:
|
||||
state_description = f'Waiting for {ci_follower.ticker()} lock tx to confirm in chain ({ci_follower.blocks_confirmed} blocks)'
|
||||
state_description = f"Waiting for {ci_follower.ticker()} lock tx to confirm in chain ({ci_follower.blocks_confirmed} blocks)"
|
||||
elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED:
|
||||
state_description = f'Waiting for {initiator_role} to unlock {ci_leader.ticker()} lock tx'
|
||||
state_description = (
|
||||
f"Waiting for {initiator_role} to unlock {ci_leader.ticker()} lock tx"
|
||||
)
|
||||
elif bid.state == BidStates.XMR_SWAP_LOCK_RELEASED:
|
||||
state_description = f'Waiting for {participant_role} to spend from {ci_leader.ticker()} lock tx'
|
||||
state_description = f"Waiting for {participant_role} to spend from {ci_leader.ticker()} lock tx"
|
||||
elif bid.state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
|
||||
state_description = f'Waiting for {initiator_role} to spend from {ci_follower.ticker()} lock tx'
|
||||
state_description = f"Waiting for {initiator_role} to spend from {ci_follower.ticker()} lock tx"
|
||||
elif bid.state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED:
|
||||
state_description = f'Waiting for {ci_follower.ticker()} lock tx spend tx to confirm in chain'
|
||||
state_description = f"Waiting for {ci_follower.ticker()} lock tx spend tx to confirm in chain"
|
||||
elif bid.state == BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND:
|
||||
if bid.was_sent:
|
||||
state_description = f'Waiting for {initiator_role} to redeem or locktime to expire'
|
||||
state_description = (
|
||||
f"Waiting for {initiator_role} to redeem or locktime to expire"
|
||||
)
|
||||
else:
|
||||
state_description = 'Redeeming output'
|
||||
state_description = "Redeeming output"
|
||||
|
||||
addr_label = swap_client.getAddressLabel([bid.bid_addr, ])[0]
|
||||
addr_label = swap_client.getAddressLabel(
|
||||
[
|
||||
bid.bid_addr,
|
||||
]
|
||||
)[0]
|
||||
|
||||
can_abandon: bool = False
|
||||
if swap_client.debug and bid.state not in (BidStates.BID_ABANDONED, BidStates.SWAP_COMPLETED):
|
||||
if swap_client.debug and bid.state not in (
|
||||
BidStates.BID_ABANDONED,
|
||||
BidStates.SWAP_COMPLETED,
|
||||
):
|
||||
can_abandon = True
|
||||
|
||||
data = {
|
||||
'coin_from': ci_from.coin_name(),
|
||||
'coin_to': ci_to.coin_name(),
|
||||
'amt_from': ci_from.format_amount(bid_amount),
|
||||
'amt_to': ci_to.format_amount(bid_amount_to),
|
||||
'bid_rate': ci_to.format_amount(bid_rate),
|
||||
'ticker_from': ci_from.ticker(),
|
||||
'ticker_to': ci_to.ticker(),
|
||||
'bid_state': strBidState(bid.state),
|
||||
'state_description': state_description,
|
||||
'itx_state': strTxState(bid.getITxState()),
|
||||
'ptx_state': strTxState(bid.getPTxState()),
|
||||
'offer_id': bid.offer_id.hex(),
|
||||
'addr_from': bid.bid_addr,
|
||||
'addr_from_label': addr_label,
|
||||
'addr_fund_proof': bid.proof_address,
|
||||
'created_at': bid.created_at if for_api else format_timestamp(bid.created_at, with_seconds=True),
|
||||
'expired_at': bid.expire_at if for_api else format_timestamp(bid.expire_at, with_seconds=True),
|
||||
'was_sent': 'True' if bid.was_sent else 'False',
|
||||
'was_received': 'True' if bid.was_received else 'False',
|
||||
'initiate_tx': getTxIdHex(bid, TxTypes.ITX, ' ' + ci_leader.ticker()),
|
||||
'initiate_conf': 'None' if (not bid.initiate_tx or not bid.initiate_tx.conf) else bid.initiate_tx.conf,
|
||||
'participate_tx': getTxIdHex(bid, TxTypes.PTX, ' ' + ci_follower.ticker()),
|
||||
'participate_conf': 'None' if (not bid.participate_tx or not bid.participate_tx.conf) else bid.participate_tx.conf,
|
||||
'show_txns': show_txns,
|
||||
'can_abandon': can_abandon,
|
||||
'events': bid_events,
|
||||
'debug_ui': swap_client.debug_ui,
|
||||
'reverse_bid': reverse_bid,
|
||||
"coin_from": ci_from.coin_name(),
|
||||
"coin_to": ci_to.coin_name(),
|
||||
"amt_from": ci_from.format_amount(bid_amount),
|
||||
"amt_to": ci_to.format_amount(bid_amount_to),
|
||||
"bid_rate": ci_to.format_amount(bid_rate),
|
||||
"ticker_from": ci_from.ticker(),
|
||||
"ticker_to": ci_to.ticker(),
|
||||
"bid_state": strBidState(bid.state),
|
||||
"state_description": state_description,
|
||||
"itx_state": strTxState(bid.getITxState()),
|
||||
"ptx_state": strTxState(bid.getPTxState()),
|
||||
"offer_id": bid.offer_id.hex(),
|
||||
"addr_from": bid.bid_addr,
|
||||
"addr_from_label": addr_label,
|
||||
"addr_fund_proof": bid.proof_address,
|
||||
"created_at": (
|
||||
bid.created_at
|
||||
if for_api
|
||||
else format_timestamp(bid.created_at, with_seconds=True)
|
||||
),
|
||||
"expired_at": (
|
||||
bid.expire_at
|
||||
if for_api
|
||||
else format_timestamp(bid.expire_at, with_seconds=True)
|
||||
),
|
||||
"was_sent": "True" if bid.was_sent else "False",
|
||||
"was_received": "True" if bid.was_received else "False",
|
||||
"initiate_tx": getTxIdHex(bid, TxTypes.ITX, " " + ci_leader.ticker()),
|
||||
"initiate_conf": (
|
||||
"None"
|
||||
if (not bid.initiate_tx or not bid.initiate_tx.conf)
|
||||
else bid.initiate_tx.conf
|
||||
),
|
||||
"participate_tx": getTxIdHex(bid, TxTypes.PTX, " " + ci_follower.ticker()),
|
||||
"participate_conf": (
|
||||
"None"
|
||||
if (not bid.participate_tx or not bid.participate_tx.conf)
|
||||
else bid.participate_tx.conf
|
||||
),
|
||||
"show_txns": show_txns,
|
||||
"can_abandon": can_abandon,
|
||||
"events": bid_events,
|
||||
"debug_ui": swap_client.debug_ui,
|
||||
"reverse_bid": reverse_bid,
|
||||
}
|
||||
|
||||
if edit_bid:
|
||||
data['bid_state_ind'] = int(bid.state)
|
||||
data['bid_states'] = listBidStates()
|
||||
data["bid_state_ind"] = int(bid.state)
|
||||
data["bid_states"] = listBidStates()
|
||||
|
||||
if swap_client.debug_ui:
|
||||
data['debug_ind'] = bid.debug_ind
|
||||
data['debug_options'] = [(int(t), t.name) for t in DebugTypes]
|
||||
data["debug_ind"] = bid.debug_ind
|
||||
data["debug_options"] = [(int(t), t.name) for t in DebugTypes]
|
||||
|
||||
if show_txns:
|
||||
if offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
txns = []
|
||||
if bid.xmr_a_lock_tx:
|
||||
confirms = None
|
||||
if swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] and bid.xmr_a_lock_tx.chain_height:
|
||||
confirms = (swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] - bid.xmr_a_lock_tx.chain_height) + 1
|
||||
txns.append({'type': 'Chain A Lock', 'txid': bid.xmr_a_lock_tx.txid.hex(), 'confirms': confirms})
|
||||
if (
|
||||
swap_client.coin_clients[ci_leader.coin_type()]["chain_height"]
|
||||
and bid.xmr_a_lock_tx.chain_height
|
||||
):
|
||||
confirms = (
|
||||
swap_client.coin_clients[ci_leader.coin_type()]["chain_height"]
|
||||
- bid.xmr_a_lock_tx.chain_height
|
||||
) + 1
|
||||
txns.append(
|
||||
{
|
||||
"type": "Chain A Lock",
|
||||
"txid": hex_or_none(bid.xmr_a_lock_tx.txid),
|
||||
"confirms": confirms,
|
||||
}
|
||||
)
|
||||
if bid.xmr_a_lock_spend_tx:
|
||||
txns.append({'type': 'Chain A Lock Spend', 'txid': bid.xmr_a_lock_spend_tx.txid.hex()})
|
||||
txns.append(
|
||||
{
|
||||
"type": "Chain A Lock Spend",
|
||||
"txid": bid.xmr_a_lock_spend_tx.txid.hex(),
|
||||
}
|
||||
)
|
||||
if bid.xmr_b_lock_tx:
|
||||
confirms = None
|
||||
if swap_client.coin_clients[ci_follower.coin_type()]['chain_height'] and bid.xmr_b_lock_tx.chain_height:
|
||||
confirms = (swap_client.coin_clients[ci_follower.coin_type()]['chain_height'] - bid.xmr_b_lock_tx.chain_height) + 1
|
||||
txns.append({'type': 'Chain B Lock', 'txid': bid.xmr_b_lock_tx.txid.hex(), 'confirms': confirms})
|
||||
if (
|
||||
swap_client.coin_clients[ci_follower.coin_type()]["chain_height"]
|
||||
and bid.xmr_b_lock_tx.chain_height
|
||||
):
|
||||
confirms = (
|
||||
swap_client.coin_clients[ci_follower.coin_type()][
|
||||
"chain_height"
|
||||
]
|
||||
- bid.xmr_b_lock_tx.chain_height
|
||||
) + 1
|
||||
txns.append(
|
||||
{
|
||||
"type": "Chain B Lock",
|
||||
"txid": bid.xmr_b_lock_tx.txid.hex(),
|
||||
"confirms": confirms,
|
||||
}
|
||||
)
|
||||
if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.spend_txid:
|
||||
txns.append({'type': 'Chain B Lock Spend', 'txid': bid.xmr_b_lock_tx.spend_txid.hex()})
|
||||
txns.append(
|
||||
{
|
||||
"type": "Chain B Lock Spend",
|
||||
"txid": bid.xmr_b_lock_tx.spend_txid.hex(),
|
||||
}
|
||||
)
|
||||
if xmr_swap.a_lock_refund_tx:
|
||||
txns.append({'type': strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND), 'txid': xmr_swap.a_lock_refund_tx_id.hex()})
|
||||
txns.append(
|
||||
{
|
||||
"type": strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND),
|
||||
"txid": xmr_swap.a_lock_refund_tx_id.hex(),
|
||||
}
|
||||
)
|
||||
if xmr_swap.a_lock_refund_spend_tx:
|
||||
txns.append({'type': strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND), 'txid': xmr_swap.a_lock_refund_spend_tx_id.hex()})
|
||||
txns.append(
|
||||
{
|
||||
"type": strTxType(TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND),
|
||||
"txid": xmr_swap.a_lock_refund_spend_tx_id.hex(),
|
||||
}
|
||||
)
|
||||
for tx_type, tx in bid.txns.items():
|
||||
if tx_type in (TxTypes.XMR_SWAP_A_LOCK_REFUND, TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND):
|
||||
if tx_type in (
|
||||
TxTypes.XMR_SWAP_A_LOCK_REFUND,
|
||||
TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND,
|
||||
):
|
||||
continue
|
||||
txns.append({'type': strTxType(tx_type), 'txid': tx.txid.hex()})
|
||||
data['txns'] = txns
|
||||
txns.append({"type": strTxType(tx_type), "txid": tx.txid.hex()})
|
||||
data["txns"] = txns
|
||||
|
||||
data['xmr_b_shared_address'] = ci_to.encodeSharedAddress(xmr_swap.pkbv, xmr_swap.pkbs) if xmr_swap.pkbs else None
|
||||
data['xmr_b_shared_viewkey'] = ci_to.encodeKey(xmr_swap.vkbv) if xmr_swap.vkbv else None
|
||||
data["xmr_b_shared_address"] = (
|
||||
ci_to.encodeSharedAddress(xmr_swap.pkbv, xmr_swap.pkbs)
|
||||
if xmr_swap.pkbs
|
||||
else None
|
||||
)
|
||||
data["xmr_b_shared_viewkey"] = (
|
||||
ci_to.encodeKey(xmr_swap.vkbv) if xmr_swap.vkbv else None
|
||||
)
|
||||
|
||||
if swap_client.debug_ui:
|
||||
try:
|
||||
data['xmr_b_half_privatekey'] = getChainBSplitKey(swap_client, bid, xmr_swap, offer)
|
||||
|
||||
remote_split_key = getChainBRemoteSplitKey(swap_client, bid, xmr_swap, offer)
|
||||
data["xmr_b_half_privatekey"] = getChainBSplitKey(
|
||||
swap_client, bid, xmr_swap, offer
|
||||
)
|
||||
except Exception as e: # noqa: F841
|
||||
swap_client.log.debug(
|
||||
"Unable to get xmr_b_half_privatekey for bid: {}".format(
|
||||
bid.bid_id.hex()
|
||||
)
|
||||
)
|
||||
try:
|
||||
remote_split_key = getChainBRemoteSplitKey(
|
||||
swap_client, bid, xmr_swap, offer
|
||||
)
|
||||
if remote_split_key:
|
||||
data['xmr_b_half_privatekey_remote'] = remote_split_key
|
||||
except Exception as e:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
data["xmr_b_half_privatekey_remote"] = remote_split_key
|
||||
except Exception as e: # noqa: F841
|
||||
swap_client.log.debug(
|
||||
"Unable to get xmr_b_half_privatekey_remote for bid: {}".format(
|
||||
bid.bid_id.hex()
|
||||
)
|
||||
)
|
||||
|
||||
if show_lock_transfers:
|
||||
if xmr_swap.pkbs:
|
||||
data['lock_transfers'] = json.dumps(ci_to.showLockTransfers(xmr_swap.vkbv, xmr_swap.pkbs, bid.chain_b_height_start), indent=4)
|
||||
data["lock_transfers"] = json.dumps(
|
||||
ci_to.showLockTransfers(
|
||||
xmr_swap.vkbv, xmr_swap.pkbs, bid.chain_b_height_start
|
||||
),
|
||||
indent=4,
|
||||
)
|
||||
else:
|
||||
data['lock_transfers'] = 'Shared address not yet known.'
|
||||
data["lock_transfers"] = "Shared address not yet known."
|
||||
else:
|
||||
data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex()
|
||||
data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex()
|
||||
data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX)
|
||||
data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX)
|
||||
data["initiate_tx_refund"] = (
|
||||
"None" if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex()
|
||||
)
|
||||
data["participate_tx_refund"] = (
|
||||
"None"
|
||||
if not bid.participate_txn_refund
|
||||
else bid.participate_txn_refund.hex()
|
||||
)
|
||||
data["initiate_tx_spend"] = getTxSpendHex(bid, TxTypes.ITX)
|
||||
data["participate_tx_spend"] = getTxSpendHex(bid, TxTypes.PTX)
|
||||
|
||||
if bid.initiate_tx and bid.initiate_tx.tx_data is not None:
|
||||
data['initiate_tx_inputs'] = ci_from.listInputs(bid.initiate_tx.tx_data)
|
||||
data["initiate_tx_inputs"] = ci_from.listInputs(bid.initiate_tx.tx_data)
|
||||
if bid.participate_tx and bid.participate_tx.tx_data is not None:
|
||||
data['initiate_tx_inputs'] = ci_from.listInputs(bid.participate_tx.tx_data)
|
||||
data["initiate_tx_inputs"] = ci_from.listInputs(
|
||||
bid.participate_tx.tx_data
|
||||
)
|
||||
|
||||
if offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
data['coin_a_lock_refund_tx_est_final'] = 'None'
|
||||
data['coin_a_lock_refund_swipe_tx_est_final'] = 'None'
|
||||
data["coin_a_lock_refund_tx_est_final"] = "None"
|
||||
data["coin_a_lock_refund_swipe_tx_est_final"] = "None"
|
||||
|
||||
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
|
||||
if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.block_time:
|
||||
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
|
||||
raw_sequence = ci_leader.getExpectedSequence(
|
||||
offer.lock_type, offer.lock_value
|
||||
)
|
||||
seconds_locked = ci_leader.decodeSequence(raw_sequence)
|
||||
data['coin_a_lock_refund_tx_est_final'] = bid.xmr_a_lock_tx.block_time + seconds_locked
|
||||
data['coin_a_last_median_time'] = swap_client.coin_clients[offer.coin_from]['chain_median_time']
|
||||
data["coin_a_lock_refund_tx_est_final"] = (
|
||||
bid.xmr_a_lock_tx.block_time + seconds_locked
|
||||
)
|
||||
data["coin_a_last_median_time"] = swap_client.coin_clients[
|
||||
offer.coin_from
|
||||
]["chain_median_time"]
|
||||
|
||||
if TxTypes.XMR_SWAP_A_LOCK_REFUND in bid.txns:
|
||||
refund_tx = bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND]
|
||||
if refund_tx.block_time is not None:
|
||||
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
|
||||
raw_sequence = ci_leader.getExpectedSequence(
|
||||
offer.lock_type, offer.lock_value
|
||||
)
|
||||
seconds_locked = ci_leader.decodeSequence(raw_sequence)
|
||||
data['coin_a_lock_refund_swipe_tx_est_final'] = refund_tx.block_time + seconds_locked
|
||||
data["coin_a_lock_refund_swipe_tx_est_final"] = (
|
||||
refund_tx.block_time + seconds_locked
|
||||
)
|
||||
|
||||
if view_tx_ind:
|
||||
data['view_tx_ind'] = view_tx_ind
|
||||
data["view_tx_ind"] = view_tx_ind
|
||||
view_tx_id = bytes.fromhex(view_tx_ind)
|
||||
|
||||
if xmr_swap:
|
||||
if view_tx_id == xmr_swap.a_lock_tx_id and xmr_swap.a_lock_tx:
|
||||
data['view_tx_hex'] = xmr_swap.a_lock_tx.hex()
|
||||
data['chain_a_lock_tx_inputs'] = ci_leader.listInputs(xmr_swap.a_lock_tx)
|
||||
if view_tx_id == xmr_swap.a_lock_refund_tx_id and xmr_swap.a_lock_refund_tx:
|
||||
data['view_tx_hex'] = xmr_swap.a_lock_refund_tx.hex()
|
||||
if view_tx_id == xmr_swap.a_lock_refund_spend_tx_id and xmr_swap.a_lock_refund_spend_tx:
|
||||
data['view_tx_hex'] = xmr_swap.a_lock_refund_spend_tx.hex()
|
||||
if view_tx_id == xmr_swap.a_lock_spend_tx_id and xmr_swap.a_lock_spend_tx:
|
||||
data['view_tx_hex'] = xmr_swap.a_lock_spend_tx.hex()
|
||||
data["view_tx_hex"] = xmr_swap.a_lock_tx.hex()
|
||||
data["chain_a_lock_tx_inputs"] = ci_leader.listInputs(
|
||||
xmr_swap.a_lock_tx
|
||||
)
|
||||
if (
|
||||
view_tx_id == xmr_swap.a_lock_refund_tx_id
|
||||
and xmr_swap.a_lock_refund_tx
|
||||
):
|
||||
data["view_tx_hex"] = xmr_swap.a_lock_refund_tx.hex()
|
||||
if (
|
||||
view_tx_id == xmr_swap.a_lock_refund_spend_tx_id
|
||||
and xmr_swap.a_lock_refund_spend_tx
|
||||
):
|
||||
data["view_tx_hex"] = xmr_swap.a_lock_refund_spend_tx.hex()
|
||||
if (
|
||||
view_tx_id == xmr_swap.a_lock_spend_tx_id
|
||||
and xmr_swap.a_lock_spend_tx
|
||||
):
|
||||
data["view_tx_hex"] = xmr_swap.a_lock_spend_tx.hex()
|
||||
|
||||
if 'view_tx_hex' in data:
|
||||
data['view_tx_desc'] = json.dumps(ci_leader.describeTx(data['view_tx_hex']), indent=4)
|
||||
if "view_tx_hex" in data:
|
||||
data["view_tx_desc"] = json.dumps(
|
||||
ci_leader.describeTx(data["view_tx_hex"]), indent=4
|
||||
)
|
||||
else:
|
||||
if offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
|
||||
if bid.initiate_tx and bid.initiate_tx.block_time is not None:
|
||||
raw_sequence = ci_leader.getExpectedSequence(offer.lock_type, offer.lock_value)
|
||||
raw_sequence = ci_leader.getExpectedSequence(
|
||||
offer.lock_type, offer.lock_value
|
||||
)
|
||||
seconds_locked = ci_leader.decodeSequence(raw_sequence)
|
||||
data['itx_refund_tx_est_final'] = bid.initiate_tx.block_time + seconds_locked
|
||||
data["itx_refund_tx_est_final"] = (
|
||||
bid.initiate_tx.block_time + seconds_locked
|
||||
)
|
||||
if bid.participate_tx and bid.participate_tx.block_time is not None:
|
||||
raw_sequence = ci_follower.getExpectedSequence(offer.lock_type, offer.lock_value // 2)
|
||||
raw_sequence = ci_follower.getExpectedSequence(
|
||||
offer.lock_type, offer.lock_value // 2
|
||||
)
|
||||
seconds_locked = ci_follower.decodeSequence(raw_sequence)
|
||||
data['ptx_refund_tx_est_final'] = bid.participate_tx.block_time + seconds_locked
|
||||
data["ptx_refund_tx_est_final"] = (
|
||||
bid.participate_tx.block_time + seconds_locked
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -406,20 +600,24 @@ def listOldBidStates(bid):
|
||||
old_states = []
|
||||
num_states = len(bid.states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.states[i * 12:(i + 1) * 12])
|
||||
old_states.append((up[1], 'Bid ' + strBidState(up[0])))
|
||||
up = struct.unpack_from("<iq", bid.states[i * 12 : (i + 1) * 12])
|
||||
old_states.append((up[1], "Bid " + strBidState(up[0])))
|
||||
if bid.initiate_tx and bid.initiate_tx.states is not None:
|
||||
num_states = len(bid.initiate_tx.states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.initiate_tx.states[i * 12:(i + 1) * 12])
|
||||
up = struct.unpack_from(
|
||||
"<iq", bid.initiate_tx.states[i * 12 : (i + 1) * 12]
|
||||
)
|
||||
if up[0] != TxStates.TX_NONE:
|
||||
old_states.append((up[1], 'ITX ' + strTxState(up[0])))
|
||||
old_states.append((up[1], "ITX " + strTxState(up[0])))
|
||||
if bid.participate_tx and bid.participate_tx.states is not None:
|
||||
num_states = len(bid.participate_tx.states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.participate_tx.states[i * 12:(i + 1) * 12])
|
||||
up = struct.unpack_from(
|
||||
"<iq", bid.participate_tx.states[i * 12 : (i + 1) * 12]
|
||||
)
|
||||
if up[0] != TxStates.TX_NONE:
|
||||
old_states.append((up[1], 'PTX ' + strTxState(up[0])))
|
||||
old_states.append((up[1], "PTX " + strTxState(up[0])))
|
||||
if len(old_states) > 0:
|
||||
old_states.sort(key=lambda x: x[0])
|
||||
return old_states
|
||||
@@ -427,16 +625,16 @@ def listOldBidStates(bid):
|
||||
|
||||
def getCoinName(c):
|
||||
if c == Coins.PART_ANON:
|
||||
return chainparams[Coins.PART]['name'].capitalize() + ' Anon'
|
||||
return chainparams[Coins.PART]["name"].capitalize() + " Anon"
|
||||
if c == Coins.PART_BLIND:
|
||||
return chainparams[Coins.PART]['name'].capitalize() + ' Blind'
|
||||
return chainparams[Coins.PART]["name"].capitalize() + " Blind"
|
||||
if c == Coins.LTC_MWEB:
|
||||
return chainparams[Coins.LTC]['name'].capitalize() + ' MWEB'
|
||||
return chainparams[Coins.LTC]["name"].capitalize() + " MWEB"
|
||||
|
||||
coin_chainparams = chainparams[c]
|
||||
if coin_chainparams.get('use_ticker_as_name', False):
|
||||
return coin_chainparams['ticker']
|
||||
return coin_chainparams['name'].capitalize()
|
||||
if "display_name" in coin_chainparams:
|
||||
return coin_chainparams["display_name"]
|
||||
return coin_chainparams["name"].capitalize()
|
||||
|
||||
|
||||
def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
@@ -445,7 +643,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v['connection_type'] == 'rpc':
|
||||
if v["connection_type"] == "rpc":
|
||||
coins.append((int(k), getCoinName(k)))
|
||||
if split_from and k not in invalid_coins_from:
|
||||
coins_from.append(coins[-1])
|
||||
@@ -455,7 +653,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
if split_from and v not in invalid_coins_from:
|
||||
coins_from.append(coins[-1])
|
||||
if with_variants and k == Coins.LTC:
|
||||
for v in (Coins.LTC_MWEB, ):
|
||||
for v in (Coins.LTC_MWEB,):
|
||||
pass # Add when swappable
|
||||
if split_from:
|
||||
return coins_from, coins
|
||||
@@ -463,29 +661,37 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
|
||||
|
||||
def checkAddressesOwned(swap_client, ci, wallet_info):
|
||||
if 'stealth_address' in wallet_info:
|
||||
if "stealth_address" in wallet_info:
|
||||
|
||||
if wallet_info['stealth_address'] != '?':
|
||||
if not ci.isAddressMine(wallet_info['stealth_address']):
|
||||
ci._log.error('Unowned stealth address: {}'.format(wallet_info['stealth_address']))
|
||||
wallet_info['stealth_address'] = 'Error: unowned address'
|
||||
elif swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
|
||||
wallet_info['stealth_address'] = 'WARNING: Unknown wallet seed'
|
||||
if wallet_info["stealth_address"] != "?":
|
||||
if not ci.isAddressMine(wallet_info["stealth_address"]):
|
||||
ci._log.error(
|
||||
"Unowned stealth address: {}".format(wallet_info["stealth_address"])
|
||||
)
|
||||
wallet_info["stealth_address"] = "Error: unowned address"
|
||||
elif (
|
||||
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
||||
):
|
||||
wallet_info["stealth_address"] = "WARNING: Unknown wallet seed"
|
||||
|
||||
if 'deposit_address' in wallet_info:
|
||||
if wallet_info['deposit_address'] != 'Refresh necessary':
|
||||
if not ci.isAddressMine(wallet_info['deposit_address']):
|
||||
ci._log.error('Unowned deposit address: {}'.format(wallet_info['deposit_address']))
|
||||
wallet_info['deposit_address'] = 'Error: unowned address'
|
||||
elif swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
|
||||
wallet_info['deposit_address'] = 'WARNING: Unknown wallet seed'
|
||||
if "deposit_address" in wallet_info:
|
||||
if wallet_info["deposit_address"] != "Refresh necessary":
|
||||
if not ci.isAddressMine(wallet_info["deposit_address"]):
|
||||
ci._log.error(
|
||||
"Unowned deposit address: {}".format(wallet_info["deposit_address"])
|
||||
)
|
||||
wallet_info["deposit_address"] = "Error: unowned address"
|
||||
elif (
|
||||
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
|
||||
):
|
||||
wallet_info["deposit_address"] = "WARNING: Unknown wallet seed"
|
||||
|
||||
|
||||
def validateTextInput(text, name, messages, max_length=None):
|
||||
if max_length is not None and len(text) > max_length:
|
||||
messages.append(f'Error: {name} is too long')
|
||||
messages.append(f"Error: {name} is too long")
|
||||
return False
|
||||
if len(text) > 0 and all(c.isalnum() or c.isspace() for c in text) is False:
|
||||
messages.append(f'Error: {name} must consist of only letters and digits')
|
||||
messages.append(f"Error: {name} must consist of only letters and digits")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -25,6 +26,10 @@ class AutomationConstraint(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class AutomationConstraintTemporary(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InactiveCoin(Exception):
|
||||
def __init__(self, coinid):
|
||||
self.coinid = coinid
|
||||
@@ -38,7 +43,7 @@ class LockedCoinError(Exception):
|
||||
self.coinid = coinid
|
||||
|
||||
def __str__(self):
|
||||
return 'Coin must be unlocked: ' + str(self.coinid)
|
||||
return "Coin must be unlocked: " + str(self.coinid)
|
||||
|
||||
|
||||
def ensure(v, err_string):
|
||||
@@ -49,7 +54,7 @@ def ensure(v, err_string):
|
||||
def toBool(s) -> bool:
|
||||
if isinstance(s, bool):
|
||||
return s
|
||||
return s.lower() in ['1', 'true']
|
||||
return s.lower() in ["1", "true"]
|
||||
|
||||
|
||||
def jsonDecimal(obj):
|
||||
@@ -75,8 +80,8 @@ def SerialiseNum(n: int) -> bytes:
|
||||
rv = bytearray()
|
||||
neg = n < 0
|
||||
absvalue = -n if neg else n
|
||||
while (absvalue):
|
||||
rv.append(absvalue & 0xff)
|
||||
while absvalue:
|
||||
rv.append(absvalue & 0xFF)
|
||||
absvalue >>= 8
|
||||
if rv[-1] & 0x80:
|
||||
rv.append(0x80 if neg else 0)
|
||||
@@ -105,34 +110,36 @@ def DeserialiseNum(b: bytes, o: int = 0) -> int:
|
||||
def float_to_str(f: float) -> str:
|
||||
# stackoverflow.com/questions/38847690
|
||||
d1 = decimal_ctx.create_decimal(repr(f))
|
||||
return format(d1, 'f')
|
||||
return format(d1, "f")
|
||||
|
||||
|
||||
def make_int(v, scale: int = 8, r: int = 0) -> int: # r = 0, no rounding (fail), r > 0 round off, r < 0 floor
|
||||
def make_int(
|
||||
v, scale: int = 8, r: int = 0
|
||||
) -> int: # r = 0, no rounding (fail), r > 0 round off, r < 0 floor
|
||||
if isinstance(v, float):
|
||||
v = float_to_str(v)
|
||||
elif isinstance(v, int):
|
||||
return v * 10 ** scale
|
||||
return v * 10**scale
|
||||
|
||||
sign = 1
|
||||
if v[0] == '-':
|
||||
if v[0] == "-":
|
||||
v = v[1:]
|
||||
sign = -1
|
||||
ep = 10 ** scale
|
||||
ep = 10**scale
|
||||
have_dp = False
|
||||
rv = 0
|
||||
for c in v:
|
||||
if c == '.':
|
||||
if c == ".":
|
||||
rv *= ep
|
||||
have_dp = True
|
||||
continue
|
||||
if not c.isdigit():
|
||||
raise ValueError('Invalid char: ' + c)
|
||||
raise ValueError("Invalid char: " + c)
|
||||
if have_dp:
|
||||
ep //= 10
|
||||
if ep <= 0:
|
||||
if r == 0:
|
||||
raise ValueError('Mantissa too long')
|
||||
raise ValueError("Mantissa too long")
|
||||
if r > 0:
|
||||
# Round off
|
||||
if int(c) > 4:
|
||||
@@ -150,51 +157,53 @@ def validate_amount(amount, scale: int = 8) -> bool:
|
||||
str_amount = float_to_str(amount) if isinstance(amount, float) else str(amount)
|
||||
has_decimal = False
|
||||
for c in str_amount:
|
||||
if c == '.' and not has_decimal:
|
||||
if c == "." and not has_decimal:
|
||||
has_decimal = True
|
||||
continue
|
||||
if not c.isdigit():
|
||||
raise ValueError('Invalid amount')
|
||||
raise ValueError("Invalid amount")
|
||||
|
||||
ar = str_amount.split('.')
|
||||
ar = str_amount.split(".")
|
||||
if len(ar) > 1 and len(ar[1]) > scale:
|
||||
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
|
||||
raise ValueError("Too many decimal places in amount {}".format(str_amount))
|
||||
return True
|
||||
|
||||
|
||||
def format_amount(i: int, display_scale: int, scale: int = None) -> str:
|
||||
if not isinstance(i, int):
|
||||
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
|
||||
raise ValueError(
|
||||
"Amount must be an integer."
|
||||
) # Raise error instead of converting as amounts should always be integers
|
||||
if scale is None:
|
||||
scale = display_scale
|
||||
ep = 10 ** scale
|
||||
ep = 10**scale
|
||||
n = abs(i)
|
||||
quotient = n // ep
|
||||
remainder = n % ep
|
||||
if display_scale != scale:
|
||||
remainder %= (10 ** display_scale)
|
||||
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
|
||||
remainder %= 10**display_scale
|
||||
rv = "{}.{:0>{scale}}".format(quotient, remainder, scale=display_scale)
|
||||
if i < 0:
|
||||
rv = '-' + rv
|
||||
rv = "-" + rv
|
||||
return rv
|
||||
|
||||
|
||||
def format_timestamp(value: int, with_seconds: bool = False) -> str:
|
||||
str_format = '%Y-%m-%d %H:%M'
|
||||
str_format = "%Y-%m-%d %H:%M"
|
||||
if with_seconds:
|
||||
str_format += ':%S'
|
||||
str_format += ' %z'
|
||||
str_format += ":%S"
|
||||
str_format += " %z"
|
||||
return time.strftime(str_format, time.localtime(value))
|
||||
|
||||
|
||||
def b2i(b: bytes) -> int:
|
||||
# bytes32ToInt
|
||||
return int.from_bytes(b, byteorder='big')
|
||||
return int.from_bytes(b, byteorder="big")
|
||||
|
||||
|
||||
def i2b(i: int) -> bytes:
|
||||
# intToBytes32
|
||||
return i.to_bytes(32, byteorder='big')
|
||||
return i.to_bytes(32, byteorder="big")
|
||||
|
||||
|
||||
def b2h(b: bytes) -> str:
|
||||
@@ -202,7 +211,7 @@ def b2h(b: bytes) -> str:
|
||||
|
||||
|
||||
def h2b(h: str) -> bytes:
|
||||
if h.startswith('0x'):
|
||||
if h.startswith("0x"):
|
||||
h = h[2:]
|
||||
return bytes.fromhex(h)
|
||||
|
||||
@@ -215,3 +224,9 @@ def zeroIfNone(value) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
return value
|
||||
|
||||
|
||||
def hex_or_none(value: bytes) -> str:
|
||||
if value is None:
|
||||
return "None"
|
||||
return value.hex()
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
|
||||
from basicswap.util.crypto import ripemd160, sha256
|
||||
|
||||
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
__b58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
|
||||
|
||||
def b58decode(v, length=None):
|
||||
long_value = 0
|
||||
for (i, c) in enumerate(v[::-1]):
|
||||
for i, c in enumerate(v[::-1]):
|
||||
ofs = __b58chars.find(c)
|
||||
if ofs < 0:
|
||||
return None
|
||||
@@ -38,10 +38,10 @@ def b58decode(v, length=None):
|
||||
|
||||
def b58encode(v):
|
||||
long_value = 0
|
||||
for (i, c) in enumerate(v[::-1]):
|
||||
for i, c in enumerate(v[::-1]):
|
||||
long_value += (256**i) * c
|
||||
|
||||
result = ''
|
||||
result = ""
|
||||
while long_value >= 58:
|
||||
div, mod = divmod(long_value, 58)
|
||||
result = __b58chars[mod] + result
|
||||
@@ -58,7 +58,9 @@ def b58encode(v):
|
||||
return (__b58chars[0] * nPad) + result
|
||||
|
||||
|
||||
def encodeStealthAddress(prefix_byte: int, scan_pubkey: bytes, spend_pubkey: bytes) -> str:
|
||||
def encodeStealthAddress(
|
||||
prefix_byte: int, scan_pubkey: bytes, spend_pubkey: bytes
|
||||
) -> str:
|
||||
data = bytes((0x00,))
|
||||
data += scan_pubkey
|
||||
data += bytes((0x01,))
|
||||
@@ -114,7 +116,7 @@ def decodeAddress(address: str) -> bytes:
|
||||
prefixed_data = addr_data[:-4]
|
||||
checksum = addr_data[-4:]
|
||||
if sha256(sha256(prefixed_data))[:4] != checksum:
|
||||
raise ValueError('Checksum mismatch')
|
||||
raise ValueError("Checksum mismatch")
|
||||
return prefixed_data
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from basicswap.contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_sym
|
||||
from . import i2b
|
||||
|
||||
|
||||
class ECCParameters():
|
||||
class ECCParameters:
|
||||
def __init__(self, p, a, b, Gx, Gy, o):
|
||||
self.p = p
|
||||
self.a = a
|
||||
@@ -20,12 +20,13 @@ class ECCParameters():
|
||||
|
||||
|
||||
ep = ECCParameters(
|
||||
p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f,
|
||||
p=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
|
||||
a=0x0,
|
||||
b=0x7,
|
||||
Gx=0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,
|
||||
Gy=0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8,
|
||||
o=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141)
|
||||
Gx=0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
|
||||
Gy=0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
|
||||
o=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,
|
||||
)
|
||||
|
||||
|
||||
curve_secp256k1 = CurveFp(ep.p, ep.a, ep.b)
|
||||
@@ -34,7 +35,11 @@ SECP256K1_ORDER_HALF = ep.o // 2
|
||||
|
||||
|
||||
def ToDER(P) -> bytes:
|
||||
return bytes((4, )) + int(P.x()).to_bytes(32, byteorder='big') + int(P.y()).to_bytes(32, byteorder='big')
|
||||
return (
|
||||
bytes((4,))
|
||||
+ int(P.x()).to_bytes(32, byteorder="big")
|
||||
+ int(P.y()).to_bytes(32, byteorder="big")
|
||||
)
|
||||
|
||||
|
||||
def getSecretBytes() -> bytes:
|
||||
@@ -50,7 +55,7 @@ def getInsecureBytes() -> bytes:
|
||||
while True:
|
||||
s = os.urandom(32)
|
||||
|
||||
s_test = int.from_bytes(s, byteorder='big')
|
||||
s_test = int.from_bytes(s, byteorder="big")
|
||||
if s_test > 1 and s_test < ep.o:
|
||||
return s
|
||||
|
||||
@@ -59,7 +64,7 @@ def getInsecureInt() -> int:
|
||||
while True:
|
||||
s = os.urandom(32)
|
||||
|
||||
s_test = int.from_bytes(s, byteorder='big')
|
||||
s_test = int.from_bytes(s, byteorder="big")
|
||||
if s_test > 1 and s_test < ep.o:
|
||||
return s_test
|
||||
|
||||
@@ -77,7 +82,7 @@ def powMod(x, y, z) -> int:
|
||||
|
||||
|
||||
def ExpandPoint(xb, sign):
|
||||
x = int.from_bytes(xb, byteorder='big')
|
||||
x = int.from_bytes(xb, byteorder="big")
|
||||
a = (powMod(x, 3, ep.p) + 7) % ep.p
|
||||
y = powMod(a, (ep.p + 1) // 4, ep.p)
|
||||
|
||||
@@ -89,7 +94,7 @@ def ExpandPoint(xb, sign):
|
||||
def CPKToPoint(cpk):
|
||||
y_parity = cpk[0] - 2
|
||||
|
||||
x = int.from_bytes(cpk[1:], byteorder='big')
|
||||
x = int.from_bytes(cpk[1:], byteorder="big")
|
||||
a = (powMod(x, 3, ep.p) + 7) % ep.p
|
||||
y = powMod(a, (ep.p + 1) // 4, ep.p)
|
||||
|
||||
@@ -102,28 +107,29 @@ def CPKToPoint(cpk):
|
||||
def pointToCPK2(point, ind=0x09):
|
||||
# The function is_square(x), where x is an integer, returns whether or not x is a quadratic residue modulo p. Since p is prime, it is equivalent to the Legendre symbol (x / p) = x(p-1)/2 mod p being equal to 1[8].
|
||||
ind = bytes((ind ^ (1 if jacobi_symbol(point.y(), ep.p) == 1 else 0),))
|
||||
return ind + point.x().to_bytes(32, byteorder='big')
|
||||
return ind + point.x().to_bytes(32, byteorder="big")
|
||||
|
||||
|
||||
def pointToCPK(point):
|
||||
|
||||
y = point.y().to_bytes(32, byteorder='big')
|
||||
y = point.y().to_bytes(32, byteorder="big")
|
||||
ind = bytes((0x03,)) if y[31] % 2 else bytes((0x02,))
|
||||
|
||||
cpk = ind + point.x().to_bytes(32, byteorder='big')
|
||||
cpk = ind + point.x().to_bytes(32, byteorder="big")
|
||||
return cpk
|
||||
|
||||
|
||||
def secretToCPK(secret):
|
||||
secretInt = secret if isinstance(secret, int) \
|
||||
else int.from_bytes(secret, byteorder='big')
|
||||
secretInt = (
|
||||
secret if isinstance(secret, int) else int.from_bytes(secret, byteorder="big")
|
||||
)
|
||||
|
||||
R = G * secretInt
|
||||
|
||||
Y = R.y().to_bytes(32, byteorder='big')
|
||||
Y = R.y().to_bytes(32, byteorder="big")
|
||||
ind = bytes((0x03,)) if Y[31] % 2 else bytes((0x02,))
|
||||
|
||||
pubkey = ind + R.x().to_bytes(32, byteorder='big')
|
||||
pubkey = ind + R.x().to_bytes(32, byteorder="big")
|
||||
|
||||
return pubkey
|
||||
|
||||
@@ -136,7 +142,7 @@ def getKeypair():
|
||||
def hashToCurve(pubkey):
|
||||
|
||||
xBytes = hashlib.sha256(pubkey).digest()
|
||||
x = int.from_bytes(xBytes, byteorder='big')
|
||||
x = int.from_bytes(xBytes, byteorder="big")
|
||||
|
||||
for k in range(0, 100):
|
||||
# get matching y element for point
|
||||
@@ -155,12 +161,14 @@ def hashToCurve(pubkey):
|
||||
x = (x + 1) % ep.p # % P?
|
||||
continue
|
||||
|
||||
if R == INFINITY or R * ep.o != INFINITY: # is R * O != INFINITY check necessary? Validation of Elliptic Curve Public Keys says no if cofactor = 1
|
||||
if (
|
||||
R == INFINITY or R * ep.o != INFINITY
|
||||
): # is R * O != INFINITY check necessary? Validation of Elliptic Curve Public Keys says no if cofactor = 1
|
||||
x = (x + 1) % ep.p # % P?
|
||||
continue
|
||||
return R
|
||||
|
||||
raise ValueError('hashToCurve failed for 100 tries')
|
||||
raise ValueError("hashToCurve failed for 100 tries")
|
||||
|
||||
|
||||
def hash256(inb):
|
||||
@@ -168,23 +176,35 @@ def hash256(inb):
|
||||
|
||||
|
||||
def testEccUtils():
|
||||
print('testEccUtils()')
|
||||
print("testEccUtils()")
|
||||
|
||||
G_enc = ToDER(G)
|
||||
assert (G_enc.hex() == '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')
|
||||
assert (
|
||||
G_enc.hex()
|
||||
== "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
|
||||
)
|
||||
|
||||
G_enc = pointToCPK(G)
|
||||
assert (G_enc.hex() == '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
|
||||
assert (
|
||||
G_enc.hex()
|
||||
== "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
)
|
||||
G_dec = CPKToPoint(G_enc)
|
||||
assert (G_dec == G)
|
||||
assert G_dec == G
|
||||
|
||||
G_enc = pointToCPK2(G)
|
||||
assert (G_enc.hex() == '0879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
|
||||
assert (
|
||||
G_enc.hex()
|
||||
== "0879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
)
|
||||
|
||||
H = hashToCurve(ToDER(G))
|
||||
assert (pointToCPK(H).hex() == '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0')
|
||||
assert (
|
||||
pointToCPK(H).hex()
|
||||
== "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
|
||||
)
|
||||
|
||||
print('Passed.')
|
||||
print("Passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -7,21 +7,30 @@
|
||||
|
||||
from .crypto import blake256, hash160, hmac_sha512, ripemd160
|
||||
|
||||
from coincurve.keys import (
|
||||
PrivateKey,
|
||||
PublicKey)
|
||||
from coincurve.keys import PrivateKey, PublicKey
|
||||
|
||||
|
||||
def BIP32Hash(chaincode: bytes, child_no: int, key_data_type: int, keydata: bytes):
|
||||
return hmac_sha512(chaincode, key_data_type.to_bytes(1, 'big') + keydata + child_no.to_bytes(4, 'big'))
|
||||
return hmac_sha512(
|
||||
chaincode,
|
||||
key_data_type.to_bytes(1, "big") + keydata + child_no.to_bytes(4, "big"),
|
||||
)
|
||||
|
||||
|
||||
def hash160_dcr(data: bytes) -> bytes:
|
||||
return ripemd160(blake256(data))
|
||||
|
||||
|
||||
class ExtKeyPair():
|
||||
__slots__ = ('_depth', '_fingerprint', '_child_no', '_chaincode', '_key', '_pubkey', 'hash_func')
|
||||
class ExtKeyPair:
|
||||
__slots__ = (
|
||||
"_depth",
|
||||
"_fingerprint",
|
||||
"_child_no",
|
||||
"_chaincode",
|
||||
"_key",
|
||||
"_pubkey",
|
||||
"hash_func",
|
||||
)
|
||||
|
||||
def __init__(self, coin_type=1):
|
||||
if coin_type == 4:
|
||||
@@ -30,20 +39,20 @@ class ExtKeyPair():
|
||||
self.hash_func = hash160
|
||||
|
||||
def set_seed(self, seed: bytes) -> None:
|
||||
hashout: bytes = hmac_sha512(b'Bitcoin seed', seed)
|
||||
hashout: bytes = hmac_sha512(b"Bitcoin seed", seed)
|
||||
self._key = hashout[:32]
|
||||
self._pubkey = None
|
||||
self._chaincode = hashout[32:]
|
||||
self._depth = 0
|
||||
self._child_no = 0
|
||||
self._fingerprint = b'\0' * 4
|
||||
self._fingerprint = b"\0" * 4
|
||||
|
||||
def has_key(self) -> bool:
|
||||
return False if self._key is None else True
|
||||
|
||||
def neuter(self) -> None:
|
||||
if self._key is None:
|
||||
raise ValueError('Already neutered')
|
||||
raise ValueError("Already neutered")
|
||||
self._pubkey = PublicKey.from_secret(self._key).format()
|
||||
self._key = None
|
||||
|
||||
@@ -74,7 +83,11 @@ class ExtKeyPair():
|
||||
out._pubkey = K.format()
|
||||
else:
|
||||
k = PrivateKey(self._key)
|
||||
out._fingerprint = self.hash_func(self._pubkey if self._pubkey else PublicKey.from_secret(self._key).format())[:4]
|
||||
out._fingerprint = self.hash_func(
|
||||
self._pubkey
|
||||
if self._pubkey
|
||||
else PublicKey.from_secret(self._key).format()
|
||||
)[:4]
|
||||
new_hash = BIP32Hash(self._chaincode, child_no, 0, self._key)
|
||||
out._chaincode = new_hash[32:]
|
||||
k.add(new_hash[:32], update=True)
|
||||
@@ -85,27 +98,35 @@ class ExtKeyPair():
|
||||
return out
|
||||
|
||||
def encode_v(self) -> bytes:
|
||||
return self._depth.to_bytes(1, 'big') + \
|
||||
self._fingerprint + \
|
||||
self._child_no.to_bytes(4, 'big') + \
|
||||
self._chaincode + \
|
||||
b'\x00' + \
|
||||
self._key
|
||||
return (
|
||||
self._depth.to_bytes(1, "big")
|
||||
+ self._fingerprint
|
||||
+ self._child_no.to_bytes(4, "big")
|
||||
+ self._chaincode
|
||||
+ b"\x00"
|
||||
+ self._key
|
||||
)
|
||||
|
||||
def encode_p(self) -> bytes:
|
||||
pubkey = PublicKey.from_secret(self._key).format() if self._pubkey is None else self._pubkey
|
||||
return self._depth.to_bytes(1, 'big') + \
|
||||
self._fingerprint + \
|
||||
self._child_no.to_bytes(4, 'big') + \
|
||||
self._chaincode + \
|
||||
pubkey
|
||||
pubkey = (
|
||||
PublicKey.from_secret(self._key).format()
|
||||
if self._pubkey is None
|
||||
else self._pubkey
|
||||
)
|
||||
return (
|
||||
self._depth.to_bytes(1, "big")
|
||||
+ self._fingerprint
|
||||
+ self._child_no.to_bytes(4, "big")
|
||||
+ self._chaincode
|
||||
+ pubkey
|
||||
)
|
||||
|
||||
def decode(self, data: bytes) -> None:
|
||||
if len(data) != 74:
|
||||
raise ValueError('Unexpected extkey length')
|
||||
raise ValueError("Unexpected extkey length")
|
||||
self._depth = data[0]
|
||||
self._fingerprint = data[1:5]
|
||||
self._child_no = int.from_bytes(data[5:9], 'big')
|
||||
self._child_no = int.from_bytes(data[5:9], "big")
|
||||
self._chaincode = data[9:41]
|
||||
|
||||
if data[41] == 0:
|
||||
|
||||
@@ -7,25 +7,25 @@
|
||||
|
||||
def decode_compactsize(b: bytes, offset: int = 0) -> (int, int):
|
||||
i = b[offset]
|
||||
if i < 0xfd:
|
||||
if i < 0xFD:
|
||||
return i, 1
|
||||
offset += 1
|
||||
if i == 0xfd:
|
||||
return int.from_bytes(b[offset: offset + 2], 'little'), 3
|
||||
if i == 0xfe:
|
||||
return int.from_bytes(b[offset: offset + 4], 'little'), 5
|
||||
if i == 0xFD:
|
||||
return int.from_bytes(b[offset : offset + 2], "little"), 3
|
||||
if i == 0xFE:
|
||||
return int.from_bytes(b[offset : offset + 4], "little"), 5
|
||||
# 0xff
|
||||
return int.from_bytes(b[offset: offset + 8], 'little'), 9
|
||||
return int.from_bytes(b[offset : offset + 8], "little"), 9
|
||||
|
||||
|
||||
def encode_compactsize(i: int) -> bytes:
|
||||
if i < 0xfd:
|
||||
if i < 0xFD:
|
||||
return bytes((i,))
|
||||
if i <= 0xffff:
|
||||
return bytes((0xfd,)) + i.to_bytes(2, 'little')
|
||||
if i <= 0xffffffff:
|
||||
return bytes((0xfe,)) + i.to_bytes(4, 'little')
|
||||
return bytes((0xff,)) + i.to_bytes(8, 'little')
|
||||
if i <= 0xFFFF:
|
||||
return bytes((0xFD,)) + i.to_bytes(2, "little")
|
||||
if i <= 0xFFFFFFFF:
|
||||
return bytes((0xFE,)) + i.to_bytes(4, "little")
|
||||
return bytes((0xFF,)) + i.to_bytes(8, "little")
|
||||
|
||||
|
||||
def decode_varint(b: bytes, offset: int = 0) -> (int, int):
|
||||
@@ -38,7 +38,7 @@ def decode_varint(b: bytes, offset: int = 0) -> (int, int):
|
||||
if not c & 0x80:
|
||||
break
|
||||
if num_bytes > 8:
|
||||
raise ValueError('Too many bytes')
|
||||
raise ValueError("Too many bytes")
|
||||
return i, num_bytes
|
||||
|
||||
|
||||
@@ -46,6 +46,6 @@ def encode_varint(i: int) -> bytes:
|
||||
b = bytearray()
|
||||
while i > 0x7F:
|
||||
b += bytes(((i & 0x7F) | 0x80,))
|
||||
i = (i >> 7)
|
||||
i = i >> 7
|
||||
b += bytes((i,))
|
||||
return b
|
||||
|
||||
@@ -4,14 +4,140 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import contextlib
|
||||
import ipaddress
|
||||
import os
|
||||
import time
|
||||
|
||||
from urllib.error import ContentTooShortError
|
||||
from urllib.parse import _splittype
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
def is_private_ip_address(addr: str):
|
||||
# Will return false for all URLs
|
||||
if addr == 'localhost':
|
||||
if addr == "localhost":
|
||||
return True
|
||||
if addr.endswith(".local"):
|
||||
return True
|
||||
try:
|
||||
return ipaddress.ip_address(addr).is_private
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def make_reporthook(read_start: int, logger):
|
||||
read = read_start # Number of bytes read so far
|
||||
last_percent_str = ""
|
||||
time_last = time.time()
|
||||
read_last = read_start
|
||||
display_last = time_last
|
||||
abo = 7
|
||||
average_buffer = [-1] * 8
|
||||
|
||||
if read_start > 0:
|
||||
logger.info(f"Attempting to resume from byte {read_start}")
|
||||
|
||||
def reporthook(blocknum, blocksize, totalsize):
|
||||
nonlocal read, last_percent_str, time_last, read_last, display_last, read_start
|
||||
nonlocal average_buffer, abo, logger
|
||||
read += blocksize
|
||||
|
||||
# totalsize excludes read_start
|
||||
use_size = totalsize + read_start
|
||||
dl_complete: bool = totalsize > 0 and read >= use_size
|
||||
time_now = time.time()
|
||||
time_delta = time_now - time_last
|
||||
if time_delta < 4.0 and not dl_complete:
|
||||
return
|
||||
|
||||
# Avoid division by zero by picking a value
|
||||
if time_delta <= 0.0:
|
||||
time_delta = 0.01
|
||||
|
||||
bytes_delta = read - read_last
|
||||
time_last = time_now
|
||||
read_last = read
|
||||
bits_per_second = (bytes_delta * 8) / time_delta
|
||||
|
||||
abo = 0 if abo >= 7 else abo + 1
|
||||
average_buffer[abo] = bits_per_second
|
||||
|
||||
samples = 0
|
||||
average_bits_per_second = 0
|
||||
for sample in average_buffer:
|
||||
if sample < 0:
|
||||
continue
|
||||
average_bits_per_second += sample
|
||||
samples += 1
|
||||
average_bits_per_second /= samples
|
||||
|
||||
speed_str: str
|
||||
if average_bits_per_second > 1000**3:
|
||||
speed_str = "{:.2f} Gbps".format(average_bits_per_second / (1000**3))
|
||||
elif average_bits_per_second > 1000**2:
|
||||
speed_str = "{:.2f} Mbps".format(average_bits_per_second / (1000**2))
|
||||
else:
|
||||
speed_str = "{:.2f} kbps".format(average_bits_per_second / 1000)
|
||||
|
||||
if totalsize > 0:
|
||||
percent_str = "%5.0f%%" % (read * 1e2 / use_size)
|
||||
if percent_str != last_percent_str or time_now - display_last > 10:
|
||||
logger.info(percent_str + " " + speed_str)
|
||||
last_percent_str = percent_str
|
||||
display_last = time_now
|
||||
else:
|
||||
logger.info(f"Read {read}, {speed_str}")
|
||||
|
||||
return reporthook
|
||||
|
||||
|
||||
def urlretrieve(url, filename, reporthook=None, data=None, resume_from=0):
|
||||
"""urlretrieve with resume"""
|
||||
url_type, path = _splittype(url)
|
||||
|
||||
req = Request(url)
|
||||
if resume_from > 0:
|
||||
req.add_header("Range", f"bytes={resume_from}-")
|
||||
with contextlib.closing(urlopen(req)) as fp:
|
||||
headers = fp.info()
|
||||
|
||||
# Just return the local path and the "headers" for file://
|
||||
# URLs. No sense in performing a copy unless requested.
|
||||
if url_type == "file" and not filename:
|
||||
return os.path.normpath(path), headers
|
||||
|
||||
with open(filename, "ab" if resume_from > 0 else "wb") as tfp:
|
||||
result = filename, headers
|
||||
bs = 1024 * 8
|
||||
size = -1
|
||||
read = resume_from
|
||||
blocknum = 0
|
||||
range_from = 0
|
||||
if "content-length" in headers:
|
||||
size = int(headers["Content-Length"])
|
||||
if "Content-Range" in headers:
|
||||
range_str = headers["Content-Range"]
|
||||
offset = range_str.find("-")
|
||||
range_from = int(range_str[6:offset])
|
||||
if resume_from != range_from:
|
||||
raise ValueError("Download is not resuming from the expected byte")
|
||||
|
||||
if reporthook:
|
||||
reporthook(blocknum, bs, size)
|
||||
|
||||
while True:
|
||||
block = fp.read(bs)
|
||||
if not block:
|
||||
break
|
||||
read += len(block)
|
||||
tfp.write(block)
|
||||
blocknum += 1
|
||||
if reporthook:
|
||||
reporthook(blocknum, bs, size)
|
||||
|
||||
if size >= 0 and read < size:
|
||||
raise ContentTooShortError(
|
||||
"retrieval incomplete: got only %i out of %i bytes" % (read, size), result
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -16,7 +16,7 @@ def rfc2440_hash_password(password, salt=None):
|
||||
salt = secrets.token_bytes(8)
|
||||
assert len(salt) == 8
|
||||
|
||||
hashbytes = salt + password.encode('utf-8')
|
||||
hashbytes = salt + password.encode("utf-8")
|
||||
len_hashbytes = len(hashbytes)
|
||||
h = hashlib.sha1()
|
||||
|
||||
@@ -27,5 +27,5 @@ def rfc2440_hash_password(password, salt=None):
|
||||
continue
|
||||
h.update(hashbytes[:count])
|
||||
break
|
||||
rv = '16:' + salt.hex() + '60' + h.hexdigest()
|
||||
rv = "16:" + salt.hex() + "60" + h.hexdigest()
|
||||
return rv.upper()
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
OP_PUSHDATA1,
|
||||
OP_PUSHDATA2,
|
||||
OP_PUSHDATA4,
|
||||
CScriptInvalidError,
|
||||
CScriptTruncatedPushDataError,
|
||||
)
|
||||
from basicswap.script import OpCodes
|
||||
|
||||
|
||||
@@ -16,15 +23,15 @@ def decodeScriptNum(script_bytes, o):
|
||||
return ((num_len - OpCodes.OP_1) + 1, 1)
|
||||
|
||||
if num_len > 4:
|
||||
raise ValueError('Bad scriptnum length') # Max 4 bytes
|
||||
raise ValueError("Bad scriptnum length") # Max 4 bytes
|
||||
if num_len + o >= len(script_bytes):
|
||||
raise ValueError('Bad script length')
|
||||
raise ValueError("Bad script length")
|
||||
o += 1
|
||||
for i in range(num_len):
|
||||
b = script_bytes[o + i]
|
||||
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
|
||||
if i == num_len - 1 and b & 0x80:
|
||||
b &= (~(0x80) & 0xFF)
|
||||
b &= ~(0x80) & 0xFF
|
||||
v += int(b) << 8 * i
|
||||
v *= -1
|
||||
else:
|
||||
@@ -32,10 +39,58 @@ def decodeScriptNum(script_bytes, o):
|
||||
return (v, 1 + num_len)
|
||||
|
||||
|
||||
def decodePushData(script_bytes, o):
|
||||
datasize = None
|
||||
pushdata_type = None
|
||||
i = o
|
||||
opcode = script_bytes[i]
|
||||
i += 1
|
||||
|
||||
if opcode < OP_PUSHDATA1:
|
||||
pushdata_type = "PUSHDATA(%d)" % opcode
|
||||
datasize = opcode
|
||||
|
||||
elif opcode == OP_PUSHDATA1:
|
||||
pushdata_type = "PUSHDATA1"
|
||||
if i >= len(script_bytes):
|
||||
raise CScriptInvalidError("PUSHDATA1: missing data length")
|
||||
datasize = script_bytes[i]
|
||||
i += 1
|
||||
|
||||
elif opcode == OP_PUSHDATA2:
|
||||
pushdata_type = "PUSHDATA2"
|
||||
if i + 1 >= len(script_bytes):
|
||||
raise CScriptInvalidError("PUSHDATA2: missing data length")
|
||||
datasize = script_bytes[i] + (script_bytes[i + 1] << 8)
|
||||
i += 2
|
||||
|
||||
elif opcode == OP_PUSHDATA4:
|
||||
pushdata_type = "PUSHDATA4"
|
||||
if i + 3 >= len(script_bytes):
|
||||
raise CScriptInvalidError("PUSHDATA4: missing data length")
|
||||
datasize = (
|
||||
script_bytes[i]
|
||||
+ (script_bytes[i + 1] << 8)
|
||||
+ (script_bytes[i + 2] << 16)
|
||||
+ (script_bytes[i + 3] << 24)
|
||||
)
|
||||
i += 4
|
||||
|
||||
else:
|
||||
assert False # shouldn't happen
|
||||
|
||||
data = bytes(script_bytes[i : i + datasize])
|
||||
|
||||
# Check for truncation
|
||||
if len(data) < datasize:
|
||||
raise CScriptTruncatedPushDataError("%s: truncated data" % pushdata_type, data)
|
||||
|
||||
# return data and the number of bytes to skip forward
|
||||
return (data, i + datasize - o)
|
||||
|
||||
|
||||
def getP2SHScriptForHash(p2sh):
|
||||
return bytes((OpCodes.OP_HASH160, 0x14)) \
|
||||
+ p2sh \
|
||||
+ bytes((OpCodes.OP_EQUAL,))
|
||||
return bytes((OpCodes.OP_HASH160, 0x14)) + p2sh + bytes((OpCodes.OP_EQUAL,))
|
||||
|
||||
|
||||
def getP2WSH(script):
|
||||
@@ -45,26 +100,26 @@ def getP2WSH(script):
|
||||
def SerialiseNumCompact(v):
|
||||
if v < 253:
|
||||
return bytes((v,))
|
||||
if v <= 0xffff: # USHRT_MAX
|
||||
if v <= 0xFFFF: # USHRT_MAX
|
||||
return struct.pack("<BH", 253, v)
|
||||
if v <= 0xffffffff: # UINT_MAX
|
||||
if v <= 0xFFFFFFFF: # UINT_MAX
|
||||
return struct.pack("<BI", 254, v)
|
||||
if v <= 0xffffffffffffffff: # UINT_MAX
|
||||
if v <= 0xFFFFFFFFFFFFFFFF: # UINT_MAX
|
||||
return struct.pack("<BQ", 255, v)
|
||||
raise ValueError('Value too large')
|
||||
raise ValueError("Value too large")
|
||||
|
||||
|
||||
def getCompactSizeLen(v):
|
||||
# Compact Size
|
||||
if v < 253:
|
||||
return 1
|
||||
if v <= 0xffff: # USHRT_MAX
|
||||
if v <= 0xFFFF: # USHRT_MAX
|
||||
return 3
|
||||
if v <= 0xffffffff: # UINT_MAX
|
||||
if v <= 0xFFFFFFFF: # UINT_MAX
|
||||
return 5
|
||||
if v <= 0xffffffffffffffff: # UINT_MAX
|
||||
if v <= 0xFFFFFFFFFFFFFFFF: # UINT_MAX
|
||||
return 9
|
||||
raise ValueError('Value too large')
|
||||
raise ValueError("Value too large")
|
||||
|
||||
|
||||
def getWitnessElementLen(v):
|
||||
|
||||
@@ -7,13 +7,15 @@ from .contrib.MoneroPy.base58 import encode as xmr_b58encode
|
||||
|
||||
def cn_fast_hash(s):
|
||||
k = Keccak.Keccak()
|
||||
return k.Keccak((len(s) * 8, s.hex()), 1088, 512, 32 * 8, False).lower() # r = bitrate = 1088, c = capacity, n = output length in bits
|
||||
return k.Keccak(
|
||||
(len(s) * 8, s.hex()), 1088, 512, 32 * 8, False
|
||||
).lower() # r = bitrate = 1088, c = capacity, n = output length in bits
|
||||
|
||||
|
||||
def encode_address(view_point: bytes, spend_point: bytes, version=18) -> str:
|
||||
prefix_bytes = version if isinstance(version, bytes) else encode_varint(version)
|
||||
buf = prefix_bytes + spend_point + view_point
|
||||
h = cn_fast_hash(buf)
|
||||
buf = buf + bytes.fromhex(h[0: 8])
|
||||
buf = buf + bytes.fromhex(h[0:8])
|
||||
|
||||
return xmr_b58encode(buf.hex())
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
name = "bin"
|
||||
|
||||
@@ -1 +1 @@
|
||||
basicswap_prepare.py
|
||||
../basicswap/bin/prepare.py
|
||||
@@ -1 +1 @@
|
||||
basicswap_run.py
|
||||
../basicswap/bin/run.py
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,450 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import signal
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.chainparams import chainparams
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.level = logging.DEBUG
|
||||
if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
swap_client = None
|
||||
|
||||
|
||||
class Daemon:
|
||||
__slots__ = ('handle', 'files')
|
||||
|
||||
def __init__(self, handle, files):
|
||||
self.handle = handle
|
||||
self.files = files
|
||||
|
||||
|
||||
def is_known_coin(coin_name: str) -> bool:
|
||||
for k, v in chainparams.items():
|
||||
if coin_name == v['name']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global swap_client
|
||||
logger.info('Signal %d detected, ending program.' % (sig))
|
||||
if swap_client is not None:
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
|
||||
# Rewrite litecoin.conf for 0.21.3
|
||||
ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf')
|
||||
if os.path.exists(ltc_conf_path):
|
||||
config_to_add = ['blockfilterindex=0', 'peerblockfilters=0']
|
||||
with open(ltc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line in config_to_add:
|
||||
config_to_add.remove(line)
|
||||
|
||||
if len(config_to_add) > 0:
|
||||
logging.info('Rewriting litecoin.conf')
|
||||
shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last')
|
||||
with open(ltc_conf_path, 'a') as fp:
|
||||
for line in config_to_add:
|
||||
fp.write(line + '\n')
|
||||
|
||||
args = [daemon_bin, ]
|
||||
add_datadir: bool = extra_config.get('add_datadir', True)
|
||||
if add_datadir:
|
||||
args.append('-datadir=' + datadir_path)
|
||||
args += opts
|
||||
logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else ''))
|
||||
|
||||
opened_files = []
|
||||
if extra_config.get('stdout_to_file', False):
|
||||
stdout_dest = open(os.path.join(datadir_path, extra_config.get('stdout_filename', 'core_stdout.log')), 'w')
|
||||
opened_files.append(stdout_dest)
|
||||
stderr_dest = stdout_dest
|
||||
else:
|
||||
stdout_dest = subprocess.PIPE
|
||||
stderr_dest = subprocess.PIPE
|
||||
|
||||
if extra_config.get('use_shell', False):
|
||||
str_args = ' '.join(args)
|
||||
return Daemon(subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
|
||||
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
|
||||
|
||||
|
||||
def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
config_filename = 'wownerod.conf' if daemon_bin.startswith('wow') else 'monerod.conf'
|
||||
args = [daemon_path, '--non-interactive', '--config-file=' + os.path.join(datadir_path, config_filename)] + opts
|
||||
logging.info('Starting node {} --data-dir={}'.format(daemon_path, node_dir))
|
||||
|
||||
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
file_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w')
|
||||
file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w')
|
||||
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=file_stdout, stderr=file_stderr, cwd=datadir_path), [file_stdout, file_stderr])
|
||||
|
||||
|
||||
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
|
||||
args = [daemon_bin, '--non-interactive']
|
||||
|
||||
needs_rewrite: bool = False
|
||||
config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy=']
|
||||
|
||||
data_dir = os.path.expanduser(node_dir)
|
||||
|
||||
wallet_config_filename = 'wownero-wallet-rpc.conf' if wallet_bin.startswith('wow') else 'monero_wallet.conf'
|
||||
config_path = os.path.join(data_dir, wallet_config_filename)
|
||||
if os.path.exists(config_path):
|
||||
args += ['--config-file=' + config_path]
|
||||
with open(config_path) as fp:
|
||||
for line in fp:
|
||||
if any(line.startswith(config_line) for config_line in config_to_remove):
|
||||
logging.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
|
||||
needs_rewrite = True
|
||||
args += opts
|
||||
|
||||
if needs_rewrite:
|
||||
logging.info('Rewriting wallet config')
|
||||
shutil.copyfile(config_path, config_path + '.last')
|
||||
with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to:
|
||||
for line in fp_from:
|
||||
if not any(line.startswith(config_line) for config_line in config_to_remove):
|
||||
fp_to.write(line)
|
||||
|
||||
logging.info('Starting wallet daemon {} --wallet-dir={}'.format(daemon_bin, node_dir))
|
||||
|
||||
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
|
||||
wallet_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w')
|
||||
wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w')
|
||||
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir), [wallet_stdout, wallet_stderr])
|
||||
|
||||
|
||||
def ws_new_client(client, server):
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_new_client {client["id"]}')
|
||||
|
||||
|
||||
def ws_client_left(client, server):
|
||||
if client is None:
|
||||
return
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_client_left {client["id"]}')
|
||||
|
||||
|
||||
def ws_message_received(client, server, message):
|
||||
if len(message) > 200:
|
||||
message = message[:200] + '..'
|
||||
if swap_client:
|
||||
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
|
||||
|
||||
|
||||
def runClient(fp, data_dir, chain, start_only_coins):
|
||||
global swap_client
|
||||
daemons = []
|
||||
pids = []
|
||||
threads = []
|
||||
settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
||||
pids_path = os.path.join(data_dir, '.pids')
|
||||
|
||||
if os.getenv('WALLET_ENCRYPTION_PWD', '') != '':
|
||||
if 'decred' in start_only_coins:
|
||||
# Workaround for dcrwallet requiring password for initial startup
|
||||
logger.warning('Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred.')
|
||||
else:
|
||||
raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.')
|
||||
|
||||
if not os.path.exists(settings_path):
|
||||
raise ValueError('Settings file not found: ' + str(settings_path))
|
||||
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
for ln in fd:
|
||||
# TODO: try close
|
||||
logger.warning('Found pid for daemon {} '.format(ln.strip()))
|
||||
|
||||
# Ensure daemons are stopped
|
||||
swap_client.stopDaemons()
|
||||
|
||||
# Settings may have been modified
|
||||
settings = swap_client.settings
|
||||
try:
|
||||
# Try start daemons
|
||||
for c, v in settings['chainclients'].items():
|
||||
if len(start_only_coins) > 0 and c not in start_only_coins:
|
||||
continue
|
||||
try:
|
||||
coin_id = swap_client.getCoinIdFromName(c)
|
||||
display_name = getCoinName(coin_id)
|
||||
except Exception as e:
|
||||
logger.warning('Not starting unknown coin: {}'.format(c))
|
||||
continue
|
||||
if c in ('monero', 'wownero'):
|
||||
if v['manage_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} daemon')
|
||||
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
|
||||
if v['manage_wallet_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} wallet daemon')
|
||||
daemon_addr = '{}:{}'.format(v['rpchost'], v['rpcport'])
|
||||
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(coin_id, v['rpchost'])
|
||||
opts = ['--daemon-address', daemon_addr, ]
|
||||
|
||||
proxy_log_str = ''
|
||||
proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost'])
|
||||
if proxy_host:
|
||||
proxy_log_str = ' through proxy'
|
||||
opts += ['--proxy', f'{proxy_host}:{proxy_port}', '--daemon-ssl-allow-any-cert', ]
|
||||
|
||||
swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str))
|
||||
|
||||
daemon_rpcuser = v.get('rpcuser', '')
|
||||
daemon_rpcpass = v.get('rpcpassword', '')
|
||||
if daemon_rpcuser != '':
|
||||
opts.append('--daemon-login')
|
||||
opts.append(daemon_rpcuser + ':' + daemon_rpcpass)
|
||||
|
||||
opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
|
||||
filename = c + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
|
||||
daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
|
||||
continue # /monero
|
||||
|
||||
if c == 'decred':
|
||||
appdata = v['datadir']
|
||||
extra_opts = [f'--appdata="{appdata}"', ]
|
||||
use_shell: bool = True if os.name == 'nt' else False
|
||||
if v['manage_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} daemon')
|
||||
filename = 'dcrd' + ('.exe' if os.name == 'nt' else '')
|
||||
|
||||
extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrd_stdout.log', 'use_shell': use_shell}
|
||||
daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
|
||||
if v['manage_wallet_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} wallet daemon')
|
||||
filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '')
|
||||
|
||||
wallet_pwd = v['wallet_pwd']
|
||||
if wallet_pwd == '':
|
||||
# Only set when in startonlycoin mode
|
||||
wallet_pwd = os.getenv('WALLET_ENCRYPTION_PWD', '')
|
||||
if wallet_pwd != '':
|
||||
extra_opts.append(f'--pass="{wallet_pwd}"')
|
||||
extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrwallet_stdout.log', 'use_shell': use_shell}
|
||||
daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
|
||||
continue # /decred
|
||||
|
||||
if v['manage_daemon'] is True:
|
||||
swap_client.log.info(f'Starting {display_name} daemon')
|
||||
|
||||
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
pids.append((c, pid))
|
||||
swap_client.setDaemonPID(c, pid)
|
||||
swap_client.log.info('Started {} {}'.format(filename, pid))
|
||||
if len(pids) > 0:
|
||||
with open(pids_path, 'w') as fd:
|
||||
for p in pids:
|
||||
fd.write('{}:{}\n'.format(*p))
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
if len(start_only_coins) > 0:
|
||||
logger.info(f'Only running {start_only_coins}. Manually exit with Ctrl + c when ready.')
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
pass
|
||||
else:
|
||||
swap_client.start()
|
||||
if 'htmlhost' in settings:
|
||||
swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
|
||||
allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
|
||||
thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
|
||||
threads.append(thread_http)
|
||||
thread_http.start()
|
||||
|
||||
if 'wshost' in settings:
|
||||
ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport'])
|
||||
swap_client.log.info(f'Starting ws server at {ws_url}.')
|
||||
|
||||
swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport'])
|
||||
swap_client.ws_server.set_fn_new_client(ws_new_client)
|
||||
swap_client.ws_server.set_fn_client_left(ws_client_left)
|
||||
swap_client.ws_server.set_fn_message_received(ws_message_received)
|
||||
swap_client.ws_server.run_forever(threaded=True)
|
||||
|
||||
logger.info('Exit with Ctrl + c.')
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
swap_client.update()
|
||||
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
if swap_client.ws_server:
|
||||
try:
|
||||
swap_client.log.info('Stopping websocket server.')
|
||||
swap_client.ws_server.shutdown_gracefully()
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.finalise()
|
||||
swap_client.log.info('Stopping HTTP threads.')
|
||||
for t in threads:
|
||||
try:
|
||||
t.stop()
|
||||
t.join()
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
|
||||
closed_pids = []
|
||||
for d in daemons:
|
||||
swap_client.log.info('Interrupting {}'.format(d.handle.pid))
|
||||
try:
|
||||
d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT)
|
||||
except Exception as e:
|
||||
swap_client.log.info('Interrupting %d, error %s', d.handle.pid, str(e))
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
|
||||
if fp:
|
||||
fp.close()
|
||||
closed_pids.append(d.handle.pid)
|
||||
except Exception as ex:
|
||||
swap_client.log.error('Error: {}'.format(ex))
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
lines = fd.read().split('\n')
|
||||
still_running = ''
|
||||
for ln in lines:
|
||||
try:
|
||||
if not int(ln.split(':')[1]) in closed_pids:
|
||||
still_running += ln + '\n'
|
||||
except Exception:
|
||||
pass
|
||||
with open(pids_path, 'w') as fd:
|
||||
fd.write(still_running)
|
||||
|
||||
|
||||
def printVersion():
|
||||
logger.info('Basicswap version: %s', __version__)
|
||||
|
||||
|
||||
def printHelp():
|
||||
print('Usage: basicswap-run ')
|
||||
print('\n--help, -h Print help.')
|
||||
print('--version, -v Print version.')
|
||||
print('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR))
|
||||
print('--mainnet Run in mainnet mode.')
|
||||
print('--testnet Run in testnet mode.')
|
||||
print('--regtest Run in regtest mode.')
|
||||
print('--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing.')
|
||||
|
||||
|
||||
def main():
|
||||
data_dir = None
|
||||
chain = 'mainnet'
|
||||
start_only_coins = set()
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != '-':
|
||||
logger.warning('Unknown argument %s', v)
|
||||
continue
|
||||
|
||||
s = v.split('=')
|
||||
name = s[0].strip()
|
||||
|
||||
for i in range(2):
|
||||
if name[0] == '-':
|
||||
name = name[1:]
|
||||
|
||||
if name == 'v' or name == 'version':
|
||||
printVersion()
|
||||
return 0
|
||||
if name == 'h' or name == 'help':
|
||||
printHelp()
|
||||
return 0
|
||||
|
||||
if name in ('mainnet', 'testnet', 'regtest'):
|
||||
chain = name
|
||||
continue
|
||||
|
||||
if len(s) == 2:
|
||||
if name == 'datadir':
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
continue
|
||||
if name == 'startonlycoin':
|
||||
for coin in [s.lower() for s in s[1].split(',')]:
|
||||
if is_known_coin(coin) is False:
|
||||
raise ValueError(f'Unknown coin: {coin}')
|
||||
start_only_coins.add(coin)
|
||||
continue
|
||||
|
||||
logger.warning('Unknown argument %s', v)
|
||||
|
||||
if os.name == 'nt':
|
||||
logger.warning('Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead.')
|
||||
|
||||
if data_dir is None:
|
||||
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
|
||||
logger.info('Using datadir: %s', data_dir)
|
||||
logger.info('Chain: %s', chain)
|
||||
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
|
||||
logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n')
|
||||
runClient(fp, data_dir, chain, start_only_coins)
|
||||
|
||||
logger.info('Done.')
|
||||
return swap_client.fail_code if swap_client is not None else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user