mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
Compare commits
361 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53b06859fc | ||
|
|
7755b4c505 | ||
|
|
95da26211b | ||
|
|
15b2030b92 | ||
|
|
336e92fff6 | ||
|
|
fd4fa37b9d | ||
|
|
005abee85d | ||
|
|
c6d5f47cea | ||
|
|
d16dc9e124 | ||
|
|
a9953c5ffe | ||
|
|
19fd15b9dc | ||
|
|
3794b58021 | ||
|
|
2d1ff4f8bf | ||
|
|
6a8c90a04b | ||
|
|
e9704510f9 | ||
|
|
14a1b0dd7d | ||
|
|
de501f4bb5 | ||
|
|
4c1c5cd1a6 | ||
|
|
1a9c153306 | ||
|
|
0a3afd4a5a | ||
|
|
3dbc5f329c | ||
|
|
eb46a4fcc5 | ||
|
|
73d486d6f0 | ||
|
|
cc6fbb9685 | ||
|
|
4ad8a3f07c | ||
|
|
2f7e425da9 | ||
|
|
a6c2251146 | ||
|
|
071675d359 | ||
|
|
9cc731d313 | ||
|
|
4e152d5a2b | ||
|
|
26392eafb4 | ||
|
|
c27ea87e9f | ||
|
|
b35f74c659 | ||
|
|
93e5ce0ab9 | ||
|
|
292a3713c0 | ||
|
|
add3a1d83e | ||
|
|
a4cc20022e | ||
|
|
390fb71aa7 | ||
|
|
91dbe6bf0e | ||
|
|
fda2d1f578 | ||
|
|
7e53af3616 | ||
|
|
6172785e2e | ||
|
|
ad472cf16f | ||
|
|
9d6e566c3b | ||
|
|
911ca189bc | ||
|
|
f309256a7f | ||
|
|
4ebb6d6441 | ||
|
|
42c40244a1 | ||
|
|
918bf60200 | ||
|
|
19b8e89836 | ||
|
|
d117938bb0 | ||
|
|
ab827833a6 | ||
|
|
a5a727a9ac | ||
|
|
c160ba5114 | ||
|
|
30226c37af | ||
|
|
43f9ae8acf | ||
|
|
4c9aa7b777 | ||
|
|
84b6850a0b | ||
|
|
ba8168938f | ||
|
|
ed69a36d5d | ||
|
|
672747cc7d | ||
|
|
a2239c0a5b | ||
|
|
667851c24a | ||
|
|
bae6aac12a | ||
|
|
6fce77f34a | ||
|
|
e3f51a7ac3 | ||
|
|
7ee1931176 | ||
|
|
a171bbb48a | ||
|
|
72481337e1 | ||
|
|
cd147da7dd | ||
|
|
aa26111665 | ||
|
|
235a8f6830 | ||
|
|
9cc4734bda | ||
|
|
11bbc9b128 | ||
|
|
4fa61e8e49 | ||
|
|
dd2e8d1b59 | ||
|
|
4b010cfee0 | ||
|
|
0174715dd2 | ||
|
|
1ea8b80bdc | ||
|
|
6b218773dc | ||
|
|
fafbd0defe | ||
|
|
e68fc6509b | ||
|
|
55bad836a9 | ||
|
|
4ba2b877dd | ||
|
|
f932a41b1a | ||
|
|
fea7130835 | ||
|
|
6d4200f871 | ||
|
|
53fc673e71 | ||
|
|
6e614ff76d | ||
|
|
355da5ee90 | ||
|
|
d0ebed93d8 | ||
|
|
10d6b13930 | ||
|
|
e73e084a6d | ||
|
|
1e0a7c7395 | ||
|
|
b6e9118797 | ||
|
|
02ceb89d14 | ||
|
|
d92fa0c61d | ||
|
|
dc692209ca | ||
|
|
56ec500797 | ||
|
|
faf76e3269 | ||
|
|
e19a99b113 | ||
|
|
27220d5d36 | ||
|
|
ba1678ad26 | ||
|
|
11f1454627 | ||
|
|
90a162f0ea | ||
|
|
96faa26c5b | ||
|
|
a5cc83157d | ||
|
|
bf5396dd17 | ||
|
|
d6ef4f2edb | ||
|
|
221a06ba44 | ||
|
|
5cecef676d | ||
|
|
d45e0bcd85 | ||
|
|
3e3b8c1cfe | ||
|
|
f2c73f6238 | ||
|
|
94b972502e | ||
|
|
543a820a12 | ||
|
|
266bbd1807 | ||
|
|
8c06508e7c | ||
|
|
6489b80666 | ||
|
|
bc71ec8246 | ||
|
|
2b945f3e3a | ||
|
|
6e5b8fb0ad | ||
|
|
f031d41a38 | ||
|
|
1797ab055b | ||
|
|
bd4ecc5306 | ||
|
|
b3dfae4289 | ||
|
|
7bfd79812f | ||
|
|
94d02ff1cc | ||
|
|
0e19f4139c | ||
|
|
dd53c8e76d | ||
|
|
6ad9cb24fe | ||
|
|
1c11767d1e | ||
|
|
b19edd6771 | ||
|
|
740924632e | ||
|
|
0e6f37a479 | ||
|
|
d1fb11e92a | ||
|
|
ff149e988c | ||
|
|
45b4ac8ca0 | ||
|
|
125fbb43db | ||
|
|
b3c946d056 | ||
|
|
4055b7d6c8 | ||
|
|
aa9b1c0eb9 | ||
|
|
0c40f14855 | ||
|
|
1a42e5e123 | ||
|
|
bc20fecc82 | ||
|
|
7f6077815a | ||
|
|
69acf00e0d | ||
|
|
f918652b6c | ||
|
|
fea19c00f2 | ||
|
|
f269881990 | ||
|
|
c6f8e5e2ba | ||
|
|
4f47267598 | ||
|
|
3faf947588 | ||
|
|
f3adc17bb8 | ||
|
|
b57ff3497a | ||
|
|
df4a6af6a0 | ||
|
|
7ba2daf671 | ||
|
|
d08e09061f | ||
|
|
f7a4798014 | ||
|
|
13847e129b | ||
|
|
f6914d7c30 | ||
|
|
2a8ac051fc | ||
|
|
3ea7a219d1 | ||
|
|
80915d9865 | ||
|
|
38302d2d79 | ||
|
|
e7b47486f5 | ||
|
|
b3c0ad7e9c | ||
|
|
ece9d7fb4b | ||
|
|
868b2475c1 | ||
|
|
27c3b93ff9 | ||
|
|
7df2f1b290 | ||
|
|
d57a148ff4 | ||
|
|
aa898a9601 | ||
|
|
ec5ea4ca3c | ||
|
|
ed18b36da6 | ||
|
|
058270ec7a | ||
|
|
2818afc933 | ||
|
|
48bfdb7462 | ||
|
|
e14b9b7e6e | ||
|
|
a87180f2ef | ||
|
|
66d763e8ea | ||
|
|
061a09f3fb | ||
|
|
e7af4f9005 | ||
|
|
a22274b06d | ||
|
|
3b2b666c75 | ||
|
|
ec092eaa6e | ||
|
|
b605bd4bc3 | ||
|
|
934aab9d8a | ||
|
|
21c0a534f2 | ||
|
|
b293b5daee | ||
|
|
8cfc405bc1 | ||
|
|
3c18a3ed26 | ||
|
|
b826d9658a | ||
|
|
d89a58242f | ||
|
|
5a4b1c737c | ||
|
|
6bc654f57e | ||
|
|
3e98f174cd | ||
|
|
550435e15f | ||
|
|
232e72882b | ||
|
|
9708657411 | ||
|
|
9387c43ff5 | ||
|
|
d0e35d1846 | ||
|
|
2af574c828 | ||
|
|
087dcefb2c | ||
|
|
6777aff0b9 | ||
|
|
db2ba19220 | ||
|
|
fa0760b172 | ||
|
|
748dd388cb | ||
|
|
f15f073b12 | ||
|
|
c9ef7bec44 | ||
|
|
2817d2d8e2 | ||
|
|
c5908d5e0f | ||
|
|
2d88491d48 | ||
|
|
96b44bef27 | ||
|
|
027d5c7adf | ||
|
|
25ad396dcf | ||
|
|
7972a50341 | ||
|
|
5a202e447c | ||
|
|
c28eb9ab9b | ||
|
|
e1a6dbeaed | ||
|
|
31978d9f2a | ||
|
|
c205607bb4 | ||
|
|
aa9babdc69 | ||
|
|
dc44cc5ebe | ||
|
|
99bc8b6bd2 | ||
|
|
6b724ece84 | ||
|
|
e9ed334a54 | ||
|
|
f263bb53c3 | ||
|
|
8967f677c3 | ||
|
|
3ffe55e5a2 | ||
|
|
4f11e830af | ||
|
|
9c252323be | ||
|
|
a0c31fb87d | ||
|
|
447f32d6b2 | ||
|
|
eeade736a4 | ||
|
|
d15466f656 | ||
|
|
d5f48ce6b9 | ||
|
|
65cf6789a7 | ||
|
|
1f6ef7dfc7 | ||
|
|
fbfb4c95ba | ||
|
|
7c17ff2dd2 | ||
|
|
6d68026808 | ||
|
|
54c8e3fb36 | ||
|
|
5e5b404a48 | ||
|
|
cc57d3537d | ||
|
|
3e7b3925f6 | ||
|
|
082a7f3d44 | ||
|
|
ec31f2eb35 | ||
|
|
03a8ddc863 | ||
|
|
5270c7da0b | ||
|
|
826527fea9 | ||
|
|
7d5f7e0936 | ||
|
|
6f14e24485 | ||
|
|
2b93276666 | ||
|
|
2bd82153bd | ||
|
|
0cf77a4854 | ||
|
|
1fc8bcea58 | ||
|
|
5bedc6289f | ||
|
|
3cdab962d3 | ||
|
|
0e9bb47902 | ||
|
|
e54f57f63a | ||
|
|
19968ed496 | ||
|
|
15b2038d65 | ||
|
|
5ce607541e | ||
|
|
7c482bab5c | ||
|
|
07bd7d3bd0 | ||
|
|
30270d87f1 | ||
|
|
3489ebe908 | ||
|
|
a5c3c692a0 | ||
|
|
b2df4ea80d | ||
|
|
18a7105f20 | ||
|
|
fcdb2e7dfe | ||
|
|
3c5e8481cd | ||
|
|
97bb615176 | ||
|
|
f1c2b41714 | ||
|
|
8d317e4b67 | ||
|
|
45ed2cdb87 | ||
|
|
d64e3f4be9 | ||
|
|
57d885bc0c | ||
|
|
205c6e2b58 | ||
|
|
d95f3ccd24 | ||
|
|
d6a9425b22 | ||
|
|
e4cc5da490 | ||
|
|
e177d36bd4 | ||
|
|
05ffa5e3ac | ||
|
|
6165cbc4c3 | ||
|
|
7e6f94319d | ||
|
|
71fd3d10aa | ||
|
|
e4ed9aebdf | ||
|
|
b97a9f4a27 | ||
|
|
510eff6163 | ||
|
|
efb84f58af | ||
|
|
831ef40977 | ||
|
|
a0456cb689 | ||
|
|
c7818f5fac | ||
|
|
713577d868 | ||
|
|
37be3bcab5 | ||
|
|
4ae97790aa | ||
|
|
8928451af0 | ||
|
|
473e4fd400 | ||
|
|
ff2fc35f72 | ||
|
|
edb3b19dcf | ||
|
|
aac2f51b88 | ||
|
|
57b96cd985 | ||
|
|
4d5551cd84 | ||
|
|
7ee4720738 | ||
|
|
c76fe79848 | ||
|
|
f13c481b51 | ||
|
|
6f776971b1 | ||
|
|
c79ed493aa | ||
|
|
b6709d0cdc | ||
|
|
c945e267e7 | ||
|
|
ef65420978 | ||
|
|
6da4bf6aaf | ||
|
|
6e56b7f421 | ||
|
|
f084c6f538 | ||
|
|
443bd6917f | ||
|
|
b55d126a0a | ||
|
|
586ff3288f | ||
|
|
0398fce5a8 | ||
|
|
ef082ff7be | ||
|
|
168284ce25 | ||
|
|
e797e23625 | ||
|
|
d3fcdc8052 | ||
|
|
2c176a8c86 | ||
|
|
e92d5560af | ||
|
|
0171ad6889 | ||
|
|
5d381d4b73 | ||
|
|
9e24d9a12a | ||
|
|
21ef6f3129 | ||
|
|
67d808cbe4 | ||
|
|
5d1bed6423 | ||
|
|
edc11b4c96 | ||
|
|
5daf591985 | ||
|
|
aee66712b8 | ||
|
|
8de365f9d3 | ||
|
|
765ef9571a | ||
|
|
c575625097 | ||
|
|
fe02441619 | ||
|
|
c992ef571a | ||
|
|
5f275132de | ||
|
|
64151f4203 | ||
|
|
734214af53 | ||
|
|
1cb8ffb632 | ||
|
|
40d06df325 | ||
|
|
62031173f5 | ||
|
|
f473d66de5 | ||
|
|
e548cf2b3b | ||
|
|
d1baf4bc10 | ||
|
|
3b8e084b2e | ||
|
|
0a697c61e8 | ||
|
|
5af59dd8da | ||
|
|
a75cd28995 | ||
|
|
f40d98ef23 | ||
|
|
b14fba0e1f | ||
|
|
4d928dc98e | ||
|
|
1845f802a2 | ||
|
|
7ec9dfa35a | ||
|
|
b70e46ffc1 | ||
|
|
07de2d61af | ||
|
|
b87e034719 |
77
.github/workflows/ci.yml
vendored
77
.github/workflows/ci.yml
vendored
@@ -9,6 +9,9 @@ concurrency:
|
||||
env:
|
||||
BIN_DIR: /tmp/cached_bin
|
||||
TEST_RELOAD_PATH: /tmp/test_basicswap
|
||||
BSX_SELENIUM_DRIVER: firefox-ci
|
||||
XMR_RPC_USER: xmr_user
|
||||
XMR_RPC_PWD: xmr_pwd
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -24,19 +27,36 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
if [ $(dpkg-query -W -f='${Status}' firefox 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
|
||||
install -d -m 0755 /etc/apt/keyrings
|
||||
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
|
||||
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | sudo tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
|
||||
echo "Package: *" | sudo tee /etc/apt/preferences.d/mozilla
|
||||
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y firefox
|
||||
fi
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 codespell pytest
|
||||
pip install python-gnupg
|
||||
pip install -e .[dev]
|
||||
pip install -r requirements.txt --require-hashes
|
||||
- name: Install
|
||||
run: |
|
||||
pip install .
|
||||
- name: Running flake8
|
||||
# Print the core versions to a file for caching
|
||||
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
|
||||
cat core_versions.txt
|
||||
- name: Run flake8
|
||||
run: |
|
||||
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
|
||||
- name: Running codespell
|
||||
- name: Run 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
|
||||
- name: Run black
|
||||
run: |
|
||||
black --check --diff --exclude="contrib" .
|
||||
- name: Run test_other
|
||||
run: |
|
||||
pytest tests/basicswap/test_other.py
|
||||
- name: Cache coin cores
|
||||
@@ -44,26 +64,57 @@ jobs:
|
||||
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
|
||||
path: /tmp/cached_bin
|
||||
key: cores-${{ runner.os }}-${{ hashFiles('**/core_versions.txt') }}
|
||||
|
||||
- if: ${{ steps.cache-yarn.outputs.cache-hit != 'true' }}
|
||||
- if: ${{ steps.cache-cores.outputs.cache-hit != 'true' }}
|
||||
name: Running basicswap-prepare
|
||||
run: |
|
||||
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
|
||||
- name: Running test_xmr
|
||||
- name: Run test_prepare
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export PARTICL_BINDIR="$BIN_DIR/particl";
|
||||
export BITCOIN_BINDIR="$BIN_DIR/bitcoin";
|
||||
export XMR_BINDIR="$BIN_DIR/monero";
|
||||
export TEST_BIN_PATH="$BIN_DIR"
|
||||
export TEST_PATH=/tmp/test_prepare
|
||||
pytest tests/basicswap/extended/test_prepare.py
|
||||
- name: Run test_xmr
|
||||
run: |
|
||||
export PYTHONPATH=$(pwd)
|
||||
export PARTICL_BINDIR="$BIN_DIR/particl"
|
||||
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
|
||||
export XMR_BINDIR="$BIN_DIR/monero"
|
||||
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
|
||||
- name: Running test_encrypted_xmr_reload
|
||||
- name: Run 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
|
||||
- name: Run selenium tests
|
||||
run: |
|
||||
export TEST_PATH=/tmp/test_persistent
|
||||
mkdir -p ${TEST_PATH}/bin
|
||||
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
|
||||
export PYTHONPATH=$(pwd)
|
||||
python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & TEST_NETWORK_PID=$!
|
||||
echo "Starting test_xmr_persistent, PID $TEST_NETWORK_PID"
|
||||
i=0
|
||||
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
|
||||
do
|
||||
tail -n 1 /tmp/log.txt
|
||||
sleep 2
|
||||
((++i))
|
||||
if [ $i -ge 60 ]; then
|
||||
echo "Timed out waiting for test_xmr_persistent, PID $TEST_NETWORK_PID"
|
||||
kill $TEST_NETWORK_PID
|
||||
(exit 1) # Fail test
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "Running test_settings.py"
|
||||
python tests/basicswap/selenium/test_settings.py
|
||||
echo "Running test_swap_direction.py"
|
||||
python tests/basicswap/selenium/test_swap_direction.py
|
||||
kill $TEST_NETWORK_PID
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
old/
|
||||
build/
|
||||
venv/
|
||||
*.pyc
|
||||
__pycache__
|
||||
/dist/
|
||||
@@ -8,6 +9,9 @@ __pycache__
|
||||
/*.eggs
|
||||
.tox
|
||||
.eggs
|
||||
.ruff_cache
|
||||
.pytest_cache
|
||||
.vectorcode
|
||||
*~
|
||||
|
||||
# geckodriver.log
|
||||
@@ -15,4 +19,4 @@ __pycache__
|
||||
docker/.env
|
||||
|
||||
# vscode dev container settings
|
||||
compose-dev.yaml
|
||||
compose-dev.yaml
|
||||
|
||||
40
.pre-commit-config.yaml
Normal file
40
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
repos:
|
||||
# Common hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
args: ["--assume-in-merge"]
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
args: ["--markdown-linebreak-ext=md"]
|
||||
|
||||
# Black - Python formatter
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: (basicswap/contrib|basicswap/interface/contrib)/
|
||||
|
||||
# Flake8 - Lint Python
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--ignore=E203,E501,W503", "--exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py"]
|
||||
|
||||
# ESLint - Lint Javascript and fix issues where possible
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v9.30.1
|
||||
hooks:
|
||||
- id: eslint
|
||||
#args: ["--fix"]
|
||||
|
||||
# djLint - Lint HTML
|
||||
#- repo: https://github.com/djlint/djlint
|
||||
# rev: v1.36.4
|
||||
# hooks:
|
||||
# - id: djlint
|
||||
12
README.md
12
README.md
@@ -112,6 +112,18 @@ BasicSwap is compatible with the following digital assets.
|
||||
<td>PART
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dogecoin
|
||||
</td>
|
||||
<td>DOGE
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namecoin
|
||||
</td>
|
||||
<td>NMC
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
If you’d like to add a cryptocurrency to BasicSwap, refer to how other cryptocurrencies have been integrated to the DEX by following [this link](https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_apply.html).
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.14.3"
|
||||
__version__ = "0.15.0"
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import time
|
||||
import shlex
|
||||
import socks
|
||||
import random
|
||||
import socket
|
||||
import urllib
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import socket
|
||||
import socks
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
from sockshandler import SocksiPyHandler
|
||||
|
||||
@@ -28,6 +29,10 @@ from .rpc import (
|
||||
from .util import (
|
||||
TemporaryError,
|
||||
)
|
||||
from .util.logging import (
|
||||
BSXLogger,
|
||||
LogCategories as LC,
|
||||
)
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
chainparams,
|
||||
@@ -39,9 +44,9 @@ def getaddrinfo_tor(*args):
|
||||
|
||||
|
||||
class BaseApp(DBMethods):
|
||||
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
|
||||
def __init__(self, data_dir, settings, chain, log_name="BasicSwap", **kwargs):
|
||||
self.fp = None
|
||||
self.log_name = log_name
|
||||
self.fp = fp
|
||||
self.fail_code = 0
|
||||
self.mock_time_offset = 0
|
||||
|
||||
@@ -57,7 +62,7 @@ class BaseApp(DBMethods):
|
||||
|
||||
self._network = None
|
||||
self.prepareLogging()
|
||||
self.log.info("Network: {}".format(self.chain))
|
||||
self.log.info(f"Network: {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")
|
||||
@@ -67,24 +72,67 @@ class BaseApp(DBMethods):
|
||||
self.default_socket = socket.socket
|
||||
self.default_socket_timeout = socket.getdefaulttimeout()
|
||||
self.default_socket_getaddrinfo = socket.getaddrinfo
|
||||
self._force_db_upgrade = False
|
||||
|
||||
self._enabled_log_categories = set()
|
||||
for category in self.settings.get("enabled_log_categories", []):
|
||||
category = category.lower()
|
||||
if category == "net":
|
||||
self._enabled_log_categories.add(LC.NET)
|
||||
else:
|
||||
self.log.warning(
|
||||
f'Unknown entry "{category}" in "enabled_log_categories"'
|
||||
)
|
||||
|
||||
if len(self._enabled_log_categories) > 0:
|
||||
self.log.info(
|
||||
"Enabled logging categories: {}".format(
|
||||
",".join(sorted([c.name for c in self._enabled_log_categories]))
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
data_dir=data_dir,
|
||||
settings=settings,
|
||||
chain=chain,
|
||||
log_name=log_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def __del__(self):
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
|
||||
def stopRunning(self, with_code=0):
|
||||
self.fail_code = with_code
|
||||
with self.mxDB:
|
||||
|
||||
# Wait for lock to shutdown gracefully.
|
||||
if self.mxDB.acquire(timeout=5):
|
||||
self.chainstate_delay_event.set()
|
||||
self.delay_event.set()
|
||||
self.mxDB.release()
|
||||
else:
|
||||
# Waiting for lock timed out, stop anyway
|
||||
self.chainstate_delay_event.set()
|
||||
self.delay_event.set()
|
||||
|
||||
def openLogFile(self):
|
||||
self.fp = open(os.path.join(self.data_dir, "basicswap.log"), "a")
|
||||
|
||||
def prepareLogging(self):
|
||||
logging.setLoggerClass(BSXLogger)
|
||||
self.log = logging.getLogger(self.log_name)
|
||||
self.log.propagate = False
|
||||
|
||||
self.openLogFile()
|
||||
|
||||
# Remove any existing handlers
|
||||
self.log.handlers = []
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
stream_stdout = logging.StreamHandler()
|
||||
stream_stdout = logging.StreamHandler(sys.stdout)
|
||||
if self.log_name != "BasicSwap":
|
||||
stream_stdout.setFormatter(
|
||||
logging.Formatter(
|
||||
@@ -94,6 +142,7 @@ class BaseApp(DBMethods):
|
||||
)
|
||||
else:
|
||||
stream_stdout.setFormatter(formatter)
|
||||
self.log_formatter = formatter
|
||||
stream_fp = logging.StreamHandler(self.fp)
|
||||
stream_fp.setFormatter(formatter)
|
||||
|
||||
@@ -128,7 +177,7 @@ class BaseApp(DBMethods):
|
||||
for c, params in chainparams.items():
|
||||
if coin_name.lower() == params["name"].lower():
|
||||
return c
|
||||
raise ValueError("Unknown coin: {}".format(coin_name))
|
||||
raise ValueError(f"Unknown coin: {coin_name}")
|
||||
|
||||
def callrpc(self, method, params=[], wallet=None):
|
||||
cc = self.coin_clients[Coins.PART]
|
||||
@@ -213,11 +262,16 @@ class BaseApp(DBMethods):
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
return opener.open(request, timeout=timeout).read()
|
||||
|
||||
def logException(self, message) -> None:
|
||||
def logException(self, message: str) -> None:
|
||||
self.log.error(message)
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
def logD(self, log_category: int, message: str) -> None:
|
||||
if log_category not in self._enabled_log_categories:
|
||||
return
|
||||
self.log.debug("(" + LC(log_category).name + ") " + message)
|
||||
|
||||
def torControl(self, query):
|
||||
try:
|
||||
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
import struct
|
||||
import hashlib
|
||||
from enum import IntEnum, auto
|
||||
from html import escape as html_escape
|
||||
from .util.address import (
|
||||
encodeAddress,
|
||||
decodeAddress,
|
||||
)
|
||||
from .chainparams import (
|
||||
chainparams,
|
||||
Fiat,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,6 +36,16 @@ class KeyTypes(IntEnum):
|
||||
KAF = 6
|
||||
|
||||
|
||||
class MessageNetworks(IntEnum):
|
||||
SMSG = auto()
|
||||
SIMPLEX = auto()
|
||||
|
||||
|
||||
class MessageNetworkLinkTypes(IntEnum):
|
||||
RECEIVED_ON = auto()
|
||||
SENT_ON = auto()
|
||||
|
||||
|
||||
class MessageTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
@@ -51,12 +63,18 @@ class MessageTypes(IntEnum):
|
||||
ADS_BID_LF = auto()
|
||||
ADS_BID_ACCEPT_FL = auto()
|
||||
|
||||
CONNECT_REQ = auto()
|
||||
PORTAL_OFFER = auto()
|
||||
PORTAL_SEND = auto()
|
||||
|
||||
|
||||
class AddressTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
RECV_OFFER = auto()
|
||||
SEND_OFFER = auto()
|
||||
PORTAL_LOCAL = auto()
|
||||
PORTAL = auto()
|
||||
|
||||
|
||||
class SwapTypes(IntEnum):
|
||||
@@ -109,6 +127,7 @@ class BidStates(IntEnum):
|
||||
BID_EXPIRED = 31
|
||||
BID_AACCEPT_DELAY = 32
|
||||
BID_AACCEPT_FAIL = 33
|
||||
CONNECT_REQ_SENT = 34
|
||||
|
||||
|
||||
class TxStates(IntEnum):
|
||||
@@ -191,6 +210,8 @@ class EventLogTypes(IntEnum):
|
||||
LOCK_TX_B_IN_MEMPOOL = auto()
|
||||
BCH_MERCY_TX_PUBLISHED = auto()
|
||||
BCH_MERCY_TX_FOUND = auto()
|
||||
LOCK_TX_A_IN_MEMPOOL = auto()
|
||||
LOCK_TX_A_CONFLICTS = auto()
|
||||
|
||||
|
||||
class XmrSplitMsgTypes(IntEnum):
|
||||
@@ -224,6 +245,12 @@ class NotificationTypes(IntEnum):
|
||||
OFFER_RECEIVED = auto()
|
||||
BID_RECEIVED = auto()
|
||||
BID_ACCEPTED = auto()
|
||||
SWAP_COMPLETED = auto()
|
||||
UPDATE_AVAILABLE = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
BID = 1
|
||||
|
||||
|
||||
class AutomationOverrideOptions(IntEnum):
|
||||
@@ -337,6 +364,8 @@ def strBidState(state):
|
||||
return "Auto accept delay"
|
||||
if state == BidStates.BID_AACCEPT_FAIL:
|
||||
return "Auto accept failed"
|
||||
if state == BidStates.CONNECT_REQ_SENT:
|
||||
return "Connect request sent"
|
||||
return "Unknown" + " " + str(state)
|
||||
|
||||
|
||||
@@ -379,15 +408,14 @@ def strTxType(tx_type):
|
||||
|
||||
|
||||
def strAddressType(addr_type):
|
||||
if addr_type == AddressTypes.OFFER:
|
||||
return "Offer"
|
||||
if addr_type == AddressTypes.BID:
|
||||
return "Bid"
|
||||
if addr_type == AddressTypes.RECV_OFFER:
|
||||
return "Offer recv"
|
||||
if addr_type == AddressTypes.SEND_OFFER:
|
||||
return "Offer send"
|
||||
return "Unknown"
|
||||
return {
|
||||
AddressTypes.OFFER: "Offer",
|
||||
AddressTypes.BID: "Bid",
|
||||
AddressTypes.RECV_OFFER: "Offer recv",
|
||||
AddressTypes.SEND_OFFER: "Offer send",
|
||||
AddressTypes.PORTAL_LOCAL: "Portal (local)",
|
||||
AddressTypes.PORTAL: "Portal",
|
||||
}.get(addr_type, "Unknown")
|
||||
|
||||
|
||||
def getLockName(lock_type):
|
||||
@@ -410,6 +438,10 @@ def describeEventEntry(event_type, event_msg):
|
||||
return "Lock tx B published"
|
||||
if event_type == EventLogTypes.FAILED_TX_B_SPEND:
|
||||
return "Failed to publish lock tx B spend: " + event_msg
|
||||
if event_type == EventLogTypes.LOCK_TX_A_IN_MEMPOOL:
|
||||
return "Lock tx A seen in mempool"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFLICTS:
|
||||
return "Lock tx A conflicting txn/s"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_SEEN:
|
||||
return "Lock tx A seen in chain"
|
||||
if event_type == EventLogTypes.LOCK_TX_A_CONFIRMED:
|
||||
@@ -520,7 +552,7 @@ def getLastBidState(packed_states):
|
||||
return BidStates.BID_STATE_UNKNOWN
|
||||
|
||||
|
||||
def strSwapType(swap_type):
|
||||
def strSwapType(swap_type) -> str:
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return "seller_first"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
@@ -528,7 +560,7 @@ def strSwapType(swap_type):
|
||||
return None
|
||||
|
||||
|
||||
def strSwapDesc(swap_type):
|
||||
def strSwapDesc(swap_type) -> str:
|
||||
if swap_type == SwapTypes.SELLER_FIRST:
|
||||
return "Secret Hash"
|
||||
if swap_type == SwapTypes.XMR_SWAP:
|
||||
@@ -536,6 +568,31 @@ def strSwapDesc(swap_type):
|
||||
return None
|
||||
|
||||
|
||||
def fiatTicker(fiat_ind: int) -> str:
|
||||
try:
|
||||
return Fiat(fiat_ind).name
|
||||
except Exception as e: # noqa: F841
|
||||
raise ValueError(f"Unknown fiat ind {fiat_ind}")
|
||||
|
||||
|
||||
def fiatFromTicker(ticker: str) -> int:
|
||||
ticker_uc = ticker.upper()
|
||||
for entry in Fiat:
|
||||
if entry.name == ticker_uc:
|
||||
return entry
|
||||
raise ValueError(f"Unknown fiat {ticker}")
|
||||
|
||||
|
||||
def get_api_key_setting(
|
||||
settings, setting_name: str, default_value: str = "", escape: bool = False
|
||||
):
|
||||
setting_name_enc: str = setting_name + "_enc"
|
||||
if setting_name_enc in settings:
|
||||
rv = bytes.fromhex(settings[setting_name_enc]).decode("utf-8")
|
||||
return html_escape(rv) if escape else rv
|
||||
return settings.get(setting_name, default_value)
|
||||
|
||||
|
||||
inactive_states = [
|
||||
BidStates.SWAP_COMPLETED,
|
||||
BidStates.BID_ERROR,
|
||||
@@ -554,6 +611,26 @@ def canAcceptBidState(state):
|
||||
)
|
||||
|
||||
|
||||
def canExpireBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_SENT,
|
||||
BidStates.BID_RECEIVING,
|
||||
BidStates.BID_RECEIVED,
|
||||
BidStates.BID_AACCEPT_DELAY,
|
||||
BidStates.BID_AACCEPT_FAIL,
|
||||
BidStates.BID_REQUEST_SENT,
|
||||
)
|
||||
|
||||
|
||||
def canTimeoutBidState(state):
|
||||
return state in (
|
||||
BidStates.BID_ACCEPTED,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS,
|
||||
BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX,
|
||||
BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX,
|
||||
)
|
||||
|
||||
|
||||
def isActiveBidState(state):
|
||||
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
|
||||
return True
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,66 +6,107 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.chainparams import chainparams, Coins
|
||||
from basicswap.http_server import HttpThread
|
||||
from basicswap.contrib.websocket_server import WebsocketServer
|
||||
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
|
||||
from basicswap.network.simplex_chat import startSimplexClient
|
||||
from basicswap.ui.util import getCoinName
|
||||
from basicswap.util.daemon import Daemon
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.level = logging.DEBUG
|
||||
if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
initial_logger = logging.getLogger()
|
||||
initial_logger.level = logging.DEBUG
|
||||
if not len(initial_logger.handlers):
|
||||
initial_logger.addHandler(initial_logger.StreamHandler(sys.stdout))
|
||||
logger = initial_logger
|
||||
|
||||
swap_client = None
|
||||
|
||||
|
||||
class Daemon:
|
||||
__slots__ = ("handle", "files")
|
||||
|
||||
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:
|
||||
os.write(
|
||||
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
||||
)
|
||||
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
logger.info("Signal handler stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
logger.info(f"AMM signal shutdown: {msg}")
|
||||
else:
|
||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping AMM in signal handler: {e}")
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||
try:
|
||||
datadir = part_settings.get("datadir")
|
||||
if not datadir:
|
||||
return
|
||||
|
||||
config_path = os.path.join(datadir, "particl.conf")
|
||||
if not os.path.exists(config_path):
|
||||
return
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
|
||||
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
|
||||
zmq_port = swap_settings.get("zmqport", 14792)
|
||||
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
|
||||
|
||||
if "zmqpubhashwtx=" not in config_content:
|
||||
with open(config_path, "a") as f:
|
||||
f.write(f"{expected_line}\n")
|
||||
elif expected_line not in config_content:
|
||||
lines = config_content.split("\n")
|
||||
updated_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("zmqpubhashwtx="):
|
||||
updated_lines.append(expected_line)
|
||||
else:
|
||||
updated_lines.append(line)
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("\n".join(updated_lines))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking PART ZMQ config: {e}")
|
||||
|
||||
|
||||
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)
|
||||
coin_name = extra_config.get("coin_name", "")
|
||||
|
||||
# Rewrite litecoin.conf
|
||||
# TODO: Remove
|
||||
needs_rewrite: bool = False
|
||||
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
|
||||
if os.path.exists(ltc_conf_path):
|
||||
needs_rewrite: bool = False
|
||||
add_changetype: bool = True
|
||||
with open(ltc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("changetype="):
|
||||
add_changetype = False
|
||||
break
|
||||
if line.endswith("=onion"):
|
||||
needs_rewrite = True
|
||||
break
|
||||
@@ -81,6 +122,29 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
fp_to.write(line.strip()[:-6] + "\n")
|
||||
else:
|
||||
fp_to.write(line)
|
||||
if add_changetype:
|
||||
fp_to.write("changetype=bech32\n")
|
||||
add_changetype = False
|
||||
if add_changetype:
|
||||
logger.info("Adding changetype to litecoin.conf")
|
||||
with open(ltc_conf_path, "a") as fp:
|
||||
fp.write("changetype=bech32\n")
|
||||
|
||||
# Rewrite bitcoin.conf
|
||||
# TODO: Remove
|
||||
btc_conf_path = os.path.join(datadir_path, "bitcoin.conf")
|
||||
if coin_name == "bitcoin" and os.path.exists(btc_conf_path):
|
||||
add_changetype: bool = True
|
||||
with open(btc_conf_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("changetype="):
|
||||
add_changetype = False
|
||||
break
|
||||
if add_changetype:
|
||||
logger.info("Adding changetype to bitcoin.conf")
|
||||
with open(btc_conf_path, "a") as fp:
|
||||
fp.write("changetype=bech32\n")
|
||||
|
||||
args = [
|
||||
daemon_bin,
|
||||
@@ -89,7 +153,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
if add_datadir:
|
||||
args.append("-datadir=" + datadir_path)
|
||||
args += opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.info(f"Starting node {daemon_bin}")
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
opened_files = []
|
||||
@@ -120,6 +184,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
opened_files,
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
@@ -135,7 +200,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
"--non-interactive",
|
||||
"--config-file=" + os.path.join(datadir_path, config_filename),
|
||||
] + opts
|
||||
logger.info("Starting node {}".format(daemon_bin))
|
||||
logger.info(f"Starting node {daemon_bin}")
|
||||
logger.debug("Arguments {}".format(" ".join(args)))
|
||||
|
||||
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -150,6 +215,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
|
||||
cwd=datadir_path,
|
||||
),
|
||||
[file_stdout, file_stderr],
|
||||
os.path.basename(daemon_bin),
|
||||
)
|
||||
|
||||
|
||||
@@ -198,7 +264,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
):
|
||||
fp_to.write(line)
|
||||
|
||||
logger.info("Starting wallet daemon {}".format(wallet_bin))
|
||||
logger.info(f"Starting wallet daemon {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)
|
||||
@@ -213,28 +279,10 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
|
||||
cwd=data_dir,
|
||||
),
|
||||
[wallet_stdout, wallet_stderr],
|
||||
os.path.basename(wallet_bin),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@@ -251,7 +299,7 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
|
||||
extra_args = []
|
||||
if "config_filename" in coin_settings:
|
||||
extra_args.append("--conf=" + coin_settings["config_filename"])
|
||||
if "port" in coin_settings:
|
||||
if "port" in coin_settings and coin_id != Coins.BTC:
|
||||
if prepare is False and use_tor_proxy:
|
||||
if coin_id == Coins.BCH:
|
||||
# Without this BCH (27.1) will bind to the default BTC port, even with proxy set
|
||||
@@ -263,17 +311,46 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
|
||||
# As BCH may use port 8334, disable it here.
|
||||
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
|
||||
if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
|
||||
if (
|
||||
prepare is False
|
||||
and use_tor_proxy is False
|
||||
and coin_id in (Coins.BTC, Coins.NMC)
|
||||
):
|
||||
port: int = coin_settings.get("port", 8333)
|
||||
extra_args.append(f"--bind=0.0.0.0:{port}")
|
||||
return extra_args
|
||||
|
||||
|
||||
def runClient(fp, data_dir, chain, start_only_coins):
|
||||
def mainLoop(daemons, update: bool = True):
|
||||
while not swap_client.delay_event.wait(0.5):
|
||||
if update:
|
||||
swap_client.update()
|
||||
else:
|
||||
pass
|
||||
|
||||
for daemon in daemons:
|
||||
if daemon.running is False:
|
||||
continue
|
||||
poll = daemon.handle.poll()
|
||||
if poll is None:
|
||||
pass # Process is running
|
||||
else:
|
||||
daemon.running = False
|
||||
swap_client.log.error(
|
||||
f"Process {daemon.handle.pid} for {daemon.name} terminated unexpectedly returning {poll}."
|
||||
)
|
||||
|
||||
|
||||
def runClient(
|
||||
data_dir: str,
|
||||
chain: str,
|
||||
start_only_coins: bool,
|
||||
log_prefix: str = "BasicSwap",
|
||||
extra_opts=dict(),
|
||||
) -> int:
|
||||
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")
|
||||
|
||||
@@ -294,30 +371,76 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
||||
swap_client = BasicSwap(
|
||||
data_dir, settings, chain, log_name=log_prefix, extra_opts=extra_opts
|
||||
)
|
||||
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()))
|
||||
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 network in settings.get("networks", []):
|
||||
if network.get("enabled", True) is False:
|
||||
continue
|
||||
network_type: str = network.get("type", "unknown")
|
||||
if network_type == "simplex":
|
||||
simplex_dir = os.path.join(data_dir, "simplex")
|
||||
|
||||
log_level = "debug" if swap_client.debug else "info"
|
||||
|
||||
socks_proxy = None
|
||||
if "socks_proxy_override" in network:
|
||||
socks_proxy = network["socks_proxy_override"]
|
||||
elif swap_client.use_tor_proxy:
|
||||
socks_proxy = (
|
||||
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
|
||||
)
|
||||
|
||||
daemons.append(
|
||||
startSimplexClient(
|
||||
network["client_path"],
|
||||
simplex_dir,
|
||||
network["server_address"],
|
||||
network["ws_port"],
|
||||
logger,
|
||||
swap_client.delay_event,
|
||||
socks_proxy=socks_proxy,
|
||||
log_level=log_level,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info(f"Started Simplex client {pid}")
|
||||
|
||||
for c, v in settings["chainclients"].items():
|
||||
if len(start_only_coins) > 0 and c not in start_only_coins:
|
||||
continue
|
||||
if (
|
||||
len(swap_client.with_coins_override) > 0
|
||||
and c not in swap_client.with_coins_override
|
||||
) or c in swap_client.without_coins_override:
|
||||
if v.get("manage_daemon", False) or v.get(
|
||||
"manage_wallet_daemon", False
|
||||
):
|
||||
logger.warning(
|
||||
f"Not starting coin {c.capitalize()}, disabled by arguments."
|
||||
)
|
||||
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))
|
||||
logger.warning(f"Not starting unknown coin: {c}")
|
||||
continue
|
||||
if c in ("monero", "wownero"):
|
||||
if v["manage_daemon"] is True:
|
||||
@@ -326,7 +449,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
|
||||
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
@@ -374,7 +497,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
continue # /monero
|
||||
|
||||
@@ -393,6 +516,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrd_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -404,7 +528,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
if v["manage_wallet_daemon"] is True:
|
||||
swap_client.log.info(f"Starting {display_name} wallet daemon")
|
||||
@@ -421,6 +545,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
"stdout_to_file": True,
|
||||
"stdout_filename": "dcrwallet_stdout.log",
|
||||
"use_shell": use_shell,
|
||||
"coin_name": "decred",
|
||||
}
|
||||
daemons.append(
|
||||
startDaemon(
|
||||
@@ -432,22 +557,34 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
|
||||
continue # /decred
|
||||
|
||||
if v["manage_daemon"] is True:
|
||||
if c == "particl" and swap_client._zmq_queue_enabled:
|
||||
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
|
||||
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
|
||||
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||
extra_opts = getCoreBinArgs(coin_id, v, use_tor_proxy=swap_client.use_tor_proxy)
|
||||
extra_opts = getCoreBinArgs(
|
||||
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
|
||||
)
|
||||
extra_config = {"coin_name": c}
|
||||
daemons.append(
|
||||
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
|
||||
startDaemon(
|
||||
v["datadir"],
|
||||
v["bindir"],
|
||||
filename,
|
||||
opts=extra_opts,
|
||||
extra_config=extra_config,
|
||||
)
|
||||
)
|
||||
pid = daemons[-1].handle.pid
|
||||
pids.append((c, pid))
|
||||
swap_client.setDaemonPID(c, pid)
|
||||
swap_client.log.info("Started {} {}".format(filename, pid))
|
||||
swap_client.log.info(f"Started {filename} {pid}")
|
||||
if len(pids) > 0:
|
||||
with open(pids_path, "w") as fd:
|
||||
for p in pids:
|
||||
@@ -461,48 +598,12 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
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
|
||||
mainLoop(daemons, update=False)
|
||||
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()
|
||||
mainLoop(daemons)
|
||||
|
||||
except Exception as e: # noqa: F841
|
||||
traceback.print_exc()
|
||||
@@ -515,23 +616,16 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
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))
|
||||
swap_client.log.info(f"Interrupting {d.name} {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))
|
||||
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
@@ -539,8 +633,11 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
if fp:
|
||||
fp.close()
|
||||
closed_pids.append(d.handle.pid)
|
||||
except Exception as ex:
|
||||
swap_client.log.error("Error: {}".format(ex))
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error: {e}")
|
||||
|
||||
fail_code: int = swap_client.fail_code
|
||||
del swap_client
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
@@ -555,9 +652,18 @@ def runClient(fp, data_dir, chain, start_only_coins):
|
||||
with open(pids_path, "w") as fd:
|
||||
fd.write(still_running)
|
||||
|
||||
return fail_code
|
||||
|
||||
|
||||
def printVersion():
|
||||
logger.info("Basicswap version: %s", __version__)
|
||||
logger.info(
|
||||
f"Basicswap version: {__version__}",
|
||||
)
|
||||
|
||||
|
||||
def ensure_coin_valid(coin: str) -> bool:
|
||||
if isKnownCoinName(coin) is False:
|
||||
raise ValueError(f"Unknown coin: {coin}")
|
||||
|
||||
|
||||
def printHelp():
|
||||
@@ -565,26 +671,34 @@ def printHelp():
|
||||
print("\n--help, -h Print help.")
|
||||
print("--version, -v Print version.")
|
||||
print(
|
||||
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
|
||||
cfg.BASICSWAP_DATADIR
|
||||
)
|
||||
f"--datadir=PATH Path to basicswap data directory, default:{cfg.BASICSWAP_DATADIR}."
|
||||
)
|
||||
print("--mainnet Run in mainnet mode.")
|
||||
print("--testnet Run in testnet mode.")
|
||||
print("--regtest Run in regtest mode.")
|
||||
print("--withcoin= Run only with coin/s.")
|
||||
print("--withoutcoin= Run without coin/s.")
|
||||
print(
|
||||
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
|
||||
)
|
||||
print("--logprefix Specify log prefix.")
|
||||
print(
|
||||
"--forcedbupgrade Recheck database against schema regardless of version."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
data_dir = None
|
||||
chain = "mainnet"
|
||||
start_only_coins = set()
|
||||
log_prefix: str = "BasicSwap"
|
||||
options = dict()
|
||||
with_coins = set()
|
||||
without_coins = set()
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != "-":
|
||||
logger.warning("Unknown argument %s", v)
|
||||
logger.warning(f"Unknown argument {v}")
|
||||
continue
|
||||
|
||||
s = v.split("=")
|
||||
@@ -604,19 +718,35 @@ def main():
|
||||
if name in ("mainnet", "testnet", "regtest"):
|
||||
chain = name
|
||||
continue
|
||||
|
||||
if name in ("withcoin", "withcoins"):
|
||||
for coin in [s.strip().lower() for s in s[1].split(",")]:
|
||||
ensure_coin_valid(coin)
|
||||
with_coins.add(coin)
|
||||
continue
|
||||
if name in ("withoutcoin", "withoutcoins"):
|
||||
for coin in [s.strip().lower() for s in s[1].split(",")]:
|
||||
if coin == "particl":
|
||||
raise ValueError("Particl is required.")
|
||||
ensure_coin_valid(coin)
|
||||
without_coins.add(coin)
|
||||
continue
|
||||
if name == "forcedbupgrade":
|
||||
options["force_db_upgrade"] = True
|
||||
continue
|
||||
if len(s) == 2:
|
||||
if name == "datadir":
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
data_dir = os.path.abspath(os.path.expanduser(s[1]))
|
||||
continue
|
||||
if name == "logprefix":
|
||||
log_prefix = 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}")
|
||||
ensure_coin_valid(coin)
|
||||
start_only_coins.add(coin)
|
||||
continue
|
||||
|
||||
logger.warning("Unknown argument %s", v)
|
||||
logger.warning(f"Unknown argument {v}")
|
||||
|
||||
if os.name == "nt":
|
||||
logger.warning(
|
||||
@@ -625,20 +755,23 @@ def main():
|
||||
|
||||
if data_dir is None:
|
||||
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
|
||||
logger.info("Using datadir: %s", data_dir)
|
||||
logger.info("Chain: %s", chain)
|
||||
logger.info(f"Using datadir: {data_dir}")
|
||||
logger.info(f"Chain: {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)
|
||||
if len(with_coins) > 0:
|
||||
with_coins.add("particl")
|
||||
options["with_coins"] = with_coins
|
||||
if len(without_coins) > 0:
|
||||
options["without_coins"] = without_coins
|
||||
|
||||
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
|
||||
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix, options)
|
||||
|
||||
print("Done.")
|
||||
return swap_client.fail_code if swap_client is not None else 0
|
||||
return fail_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -35,6 +35,12 @@ class Coins(IntEnum):
|
||||
DOGE = 18
|
||||
|
||||
|
||||
class Fiat(IntEnum):
|
||||
USD = -1
|
||||
GBP = -2
|
||||
EUR = -3
|
||||
|
||||
|
||||
chainparams = {
|
||||
Coins.PART: {
|
||||
"name": "particl",
|
||||
@@ -52,6 +58,8 @@ chainparams = {
|
||||
"bip44": 44,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x696E82D1,
|
||||
"ext_secret_key_prefix": 0x8F1DAEB8,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 51935,
|
||||
@@ -63,6 +71,8 @@ chainparams = {
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0xE1427800,
|
||||
"ext_secret_key_prefix": 0x04889478,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 51936,
|
||||
@@ -74,6 +84,8 @@ chainparams = {
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0xE1427800,
|
||||
"ext_secret_key_prefix": 0x04889478,
|
||||
},
|
||||
},
|
||||
Coins.BTC: {
|
||||
@@ -91,6 +103,8 @@ chainparams = {
|
||||
"bip44": 0,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x0488B21E,
|
||||
"ext_secret_key_prefix": 0x0488ADE4,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 18332,
|
||||
@@ -102,6 +116,8 @@ chainparams = {
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
@@ -112,6 +128,8 @@ chainparams = {
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
},
|
||||
Coins.LTC: {
|
||||
@@ -199,6 +217,7 @@ chainparams = {
|
||||
"message_magic": "Decred Signed Message:\n",
|
||||
"blocks_target": 60 * 5,
|
||||
"decimal_places": 8,
|
||||
"has_multiwallet": False,
|
||||
"mainnet": {
|
||||
"rpcport": 9109,
|
||||
"pubkey_address": 0x073F,
|
||||
@@ -238,29 +257,38 @@ chainparams = {
|
||||
"rpcport": 8336,
|
||||
"pubkey_address": 52,
|
||||
"script_address": 13,
|
||||
"key_prefix": 180,
|
||||
"hrp": "nc",
|
||||
"bip44": 7,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x0488B21E, # base58Prefixes[EXT_PUBLIC_KEY]
|
||||
"ext_secret_key_prefix": 0x0488ADE4,
|
||||
},
|
||||
"testnet": {
|
||||
"rpcport": 18336,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "tn",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"name": "testnet3",
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
"regtest": {
|
||||
"rpcport": 18443,
|
||||
"pubkey_address": 111,
|
||||
"script_address": 196,
|
||||
"key_prefix": 239,
|
||||
"hrp": "ncrt",
|
||||
"bip44": 1,
|
||||
"min_amount": 100000,
|
||||
"max_amount": 10000000 * COIN,
|
||||
"ext_public_key_prefix": 0x043587CF,
|
||||
"ext_secret_key_prefix": 0x04358394,
|
||||
},
|
||||
},
|
||||
Coins.XMR: {
|
||||
@@ -404,6 +432,7 @@ chainparams = {
|
||||
"has_cltv": False,
|
||||
"has_csv": False,
|
||||
"has_segwit": False,
|
||||
"has_multiwallet": False,
|
||||
"mainnet": {
|
||||
"rpcport": 8888,
|
||||
"pubkey_address": 82,
|
||||
@@ -443,6 +472,7 @@ chainparams = {
|
||||
"decimal_places": 8,
|
||||
"has_csv": True,
|
||||
"has_segwit": True,
|
||||
"has_multiwallet": False,
|
||||
"mainnet": {
|
||||
"rpcport": 44444,
|
||||
"pubkey_address": 53,
|
||||
@@ -519,10 +549,13 @@ chainparams = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
name_map = {}
|
||||
ticker_map = {}
|
||||
|
||||
|
||||
for c, params in chainparams.items():
|
||||
name_map[params["name"].lower()] = c
|
||||
ticker_map[params["ticker"].lower()] = c
|
||||
|
||||
|
||||
@@ -530,4 +563,15 @@ def getCoinIdFromTicker(ticker: str) -> str:
|
||||
try:
|
||||
return ticker_map[ticker.lower()]
|
||||
except Exception:
|
||||
raise ValueError("Unknown coin")
|
||||
raise ValueError(f"Unknown coin {ticker}")
|
||||
|
||||
|
||||
def getCoinIdFromName(name: str) -> str:
|
||||
try:
|
||||
return name_map[name.lower()]
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin {name}")
|
||||
|
||||
|
||||
def isKnownCoinName(name: str) -> bool:
|
||||
return params["name"].lower() in name_map
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 The Basicswap developers
|
||||
# Copyright (c) 2019-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -9,6 +9,8 @@ import os
|
||||
CONFIG_FILENAME = "basicswap.json"
|
||||
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
||||
DEFAULT_ALLOW_CORS = False
|
||||
DEFAULT_RPC_POOL_ENABLED = True
|
||||
DEFAULT_RPC_POOL_MAX_CONNECTIONS = 5
|
||||
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"))
|
||||
@@ -40,13 +42,6 @@ DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
|
||||
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
|
||||
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix)
|
||||
|
||||
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"))
|
||||
)
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
# ed25519.py - Optimized version of the reference implementation of Ed25519
|
||||
#
|
||||
# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to>
|
||||
# 2013 by Donald Stufft <donald@stufft.io>
|
||||
# 2013 by Alex Gaynor <alex.gaynor@gmail.com>
|
||||
# 2013 by Greg Price <price@mit.edu>
|
||||
#
|
||||
# To the extent possible under law, the author(s) have dedicated all copyright
|
||||
# and related and neighboring rights to this software to the public domain
|
||||
# worldwide. This software is distributed without any warranty.
|
||||
#
|
||||
# You should have received a copy of the CC0 Public Domain Dedication along
|
||||
# with this software. If not, see
|
||||
# <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
"""
|
||||
NB: This code is not safe for use with secret keys or secret data.
|
||||
The only safe use of this code is for verifying signatures on public messages.
|
||||
|
||||
Functions for computing the public key of a secret key and for signing
|
||||
a message are included, namely publickey_unsafe and signature_unsafe,
|
||||
for testing purposes only.
|
||||
|
||||
The root of the problem is that Python's long-integer arithmetic is
|
||||
not designed for use in cryptography. Specifically, it may take more
|
||||
or less time to execute an operation depending on the values of the
|
||||
inputs, and its memory access patterns may also depend on the inputs.
|
||||
This opens it to timing and cache side-channel attacks which can
|
||||
disclose data to an attacker. We rely on Python's long-integer
|
||||
arithmetic, so we cannot handle secrets without risking their disclosure.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import operator
|
||||
import sys
|
||||
|
||||
|
||||
__version__ = "1.0.dev0"
|
||||
|
||||
|
||||
# Useful for very coarse version differentiation.
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
indexbytes = operator.getitem
|
||||
intlist2bytes = bytes
|
||||
int2byte = operator.methodcaller("to_bytes", 1, "big")
|
||||
else:
|
||||
int2byte = chr
|
||||
range = xrange
|
||||
|
||||
def indexbytes(buf, i):
|
||||
return ord(buf[i])
|
||||
|
||||
def intlist2bytes(l):
|
||||
return b"".join(chr(c) for c in l)
|
||||
|
||||
|
||||
b = 256
|
||||
q = 2 ** 255 - 19
|
||||
l = 2 ** 252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
def H(m):
|
||||
return hashlib.sha512(m).digest()
|
||||
|
||||
|
||||
def pow2(x, p):
|
||||
"""== pow(x, 2**p, q)"""
|
||||
while p > 0:
|
||||
x = x * x % q
|
||||
p -= 1
|
||||
return x
|
||||
|
||||
|
||||
def inv(z):
|
||||
"""$= z^{-1} \mod q$, for z != 0"""
|
||||
# Adapted from curve25519_athlon.c in djb's Curve25519.
|
||||
z2 = z * z % q # 2
|
||||
z9 = pow2(z2, 2) * z % q # 9
|
||||
z11 = z9 * z2 % q # 11
|
||||
z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0
|
||||
z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0
|
||||
z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ...
|
||||
z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
|
||||
z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
|
||||
z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
|
||||
z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
|
||||
z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0
|
||||
return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2
|
||||
|
||||
|
||||
d = -121665 * inv(121666) % q
|
||||
I = pow(2, (q - 1) // 4, q)
|
||||
|
||||
|
||||
def xrecover(y, sign=0):
|
||||
xx = (y * y - 1) * inv(d * y * y + 1)
|
||||
x = pow(xx, (q + 3) // 8, q)
|
||||
|
||||
if (x * x - xx) % q != 0:
|
||||
x = (x * I) % q
|
||||
|
||||
if x % 2 != sign:
|
||||
x = q-x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
By = 4 * inv(5)
|
||||
Bx = xrecover(By)
|
||||
B = (Bx % q, By % q, 1, (Bx * By) % q)
|
||||
ident = (0, 1, 1, 0)
|
||||
|
||||
|
||||
def edwards_add(P, Q):
|
||||
# This is formula sequence 'addition-add-2008-hwcd-3' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
(x2, y2, z2, t2) = Q
|
||||
|
||||
a = (y1-x1)*(y2-x2) % q
|
||||
b = (y1+x1)*(y2+x2) % q
|
||||
c = t1*2*d*t2 % q
|
||||
dd = z1*2*z2 % q
|
||||
e = b - a
|
||||
f = dd - c
|
||||
g = dd + c
|
||||
h = b + a
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def edwards_sub(P, Q):
|
||||
# This is formula sequence 'addition-add-2008-hwcd-3' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
(x2, y2, z2, t2) = Q
|
||||
|
||||
# https://eprint.iacr.org/2008/522.pdf
|
||||
# The negative of (X:Y:Z)is (−X:Y:Z)
|
||||
#x2 = q-x2
|
||||
"""
|
||||
doesn't work
|
||||
x2 = q-x2
|
||||
t2 = (x2*y2) % q
|
||||
"""
|
||||
|
||||
zi = inv(z2)
|
||||
x2 = q-((x2 * zi) % q)
|
||||
y2 = (y2 * zi) % q
|
||||
z2 = 1
|
||||
t2 = (x2*y2) % q
|
||||
|
||||
|
||||
a = (y1-x1)*(y2-x2) % q
|
||||
b = (y1+x1)*(y2+x2) % q
|
||||
c = t1*2*d*t2 % q
|
||||
dd = z1*2*z2 % q
|
||||
e = b - a
|
||||
f = dd - c
|
||||
g = dd + c
|
||||
h = b + a
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def edwards_double(P):
|
||||
# This is formula sequence 'dbl-2008-hwcd' from
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
(x1, y1, z1, t1) = P
|
||||
|
||||
a = x1*x1 % q
|
||||
b = y1*y1 % q
|
||||
c = 2*z1*z1 % q
|
||||
# dd = -a
|
||||
e = ((x1+y1)*(x1+y1) - a - b) % q
|
||||
g = -a + b # dd + b
|
||||
f = g - c
|
||||
h = -a - b # dd - b
|
||||
x3 = e*f
|
||||
y3 = g*h
|
||||
t3 = e*h
|
||||
z3 = f*g
|
||||
|
||||
return (x3 % q, y3 % q, z3 % q, t3 % q)
|
||||
|
||||
|
||||
def scalarmult(P, e):
|
||||
if e == 0:
|
||||
return ident
|
||||
Q = scalarmult(P, e // 2)
|
||||
Q = edwards_double(Q)
|
||||
if e & 1:
|
||||
Q = edwards_add(Q, P)
|
||||
return Q
|
||||
|
||||
|
||||
# Bpow[i] == scalarmult(B, 2**i)
|
||||
Bpow = []
|
||||
|
||||
|
||||
def make_Bpow():
|
||||
P = B
|
||||
for i in range(253):
|
||||
Bpow.append(P)
|
||||
P = edwards_double(P)
|
||||
make_Bpow()
|
||||
|
||||
|
||||
def scalarmult_B(e):
|
||||
"""
|
||||
Implements scalarmult(B, e) more efficiently.
|
||||
"""
|
||||
# scalarmult(B, l) is the identity
|
||||
e = e % l
|
||||
P = ident
|
||||
for i in range(253):
|
||||
if e & 1:
|
||||
P = edwards_add(P, Bpow[i])
|
||||
e = e // 2
|
||||
assert e == 0, e
|
||||
return P
|
||||
|
||||
|
||||
def encodeint(y):
|
||||
bits = [(y >> i) & 1 for i in range(b)]
|
||||
return b''.join([
|
||||
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
|
||||
for i in range(b//8)
|
||||
])
|
||||
|
||||
|
||||
def encodepoint(P):
|
||||
(x, y, z, t) = P
|
||||
zi = inv(z)
|
||||
x = (x * zi) % q
|
||||
y = (y * zi) % q
|
||||
bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
|
||||
return b''.join([
|
||||
int2byte(sum([bits[i * 8 + j] << j for j in range(8)]))
|
||||
for i in range(b // 8)
|
||||
])
|
||||
|
||||
|
||||
def bit(h, i):
|
||||
return (indexbytes(h, i // 8) >> (i % 8)) & 1
|
||||
|
||||
|
||||
def publickey_unsafe(sk):
|
||||
"""
|
||||
Not safe to use with secret keys or secret data.
|
||||
|
||||
See module docstring. This function should be used for testing only.
|
||||
"""
|
||||
h = H(sk)
|
||||
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
|
||||
A = scalarmult_B(a)
|
||||
return encodepoint(A)
|
||||
|
||||
|
||||
def Hint(m):
|
||||
h = H(m)
|
||||
return sum(2 ** i * bit(h, i) for i in range(2 * b))
|
||||
|
||||
|
||||
def signature_unsafe(m, sk, pk):
|
||||
"""
|
||||
Not safe to use with secret keys or secret data.
|
||||
|
||||
See module docstring. This function should be used for testing only.
|
||||
"""
|
||||
h = H(sk)
|
||||
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
|
||||
r = Hint(
|
||||
intlist2bytes([indexbytes(h, j) for j in range(b // 8, b // 4)]) + m
|
||||
)
|
||||
R = scalarmult_B(r)
|
||||
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
|
||||
return encodepoint(R) + encodeint(S)
|
||||
|
||||
|
||||
def isoncurve(P):
|
||||
(x, y, z, t) = P
|
||||
return (z % q != 0 and
|
||||
x*y % q == z*t % q and
|
||||
(y*y - x*x - z*z - d*t*t) % q == 0)
|
||||
|
||||
|
||||
def decodeint(s):
|
||||
return sum(2 ** i * bit(s, i) for i in range(0, b))
|
||||
|
||||
|
||||
def decodepoint(s):
|
||||
y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
|
||||
x = xrecover(y)
|
||||
if x & 1 != bit(s, b-1):
|
||||
x = q - x
|
||||
P = (x, y, 1, (x*y) % q)
|
||||
if not isoncurve(P):
|
||||
raise ValueError("decoding point that is not on curve")
|
||||
return P
|
||||
|
||||
|
||||
class SignatureMismatch(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def checkvalid(s, m, pk):
|
||||
"""
|
||||
Not safe to use when any argument is secret.
|
||||
|
||||
See module docstring. This function should be used only for
|
||||
verifying public signatures of public messages.
|
||||
"""
|
||||
if len(s) != b // 4:
|
||||
raise ValueError("signature length is wrong")
|
||||
|
||||
if len(pk) != b // 8:
|
||||
raise ValueError("public-key length is wrong")
|
||||
|
||||
R = decodepoint(s[:b // 8])
|
||||
A = decodepoint(pk)
|
||||
S = decodeint(s[b // 8:b // 4])
|
||||
h = Hint(encodepoint(R) + pk + m)
|
||||
|
||||
(x1, y1, z1, t1) = P = scalarmult_B(S)
|
||||
(x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h))
|
||||
|
||||
if (not isoncurve(P) or not isoncurve(Q) or
|
||||
(x1*z2 - x2*z1) % q != 0 or (y1*z2 - y2*z1) % q != 0):
|
||||
raise SignatureMismatch("signature does not pass verification")
|
||||
|
||||
|
||||
def is_identity(P):
|
||||
return True if P[0] == 0 else False
|
||||
|
||||
|
||||
def edwards_negated(P):
|
||||
(x, y, z, t) = P
|
||||
|
||||
zi = inv(z)
|
||||
x = q - ((x * zi) % q)
|
||||
y = (y * zi) % q
|
||||
z = 1
|
||||
t = (x * y) % q
|
||||
|
||||
return (x, y, z, t)
|
||||
@@ -1,486 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Implementation of elliptic curves, for cryptographic applications.
|
||||
#
|
||||
# This module doesn't provide any way to choose a random elliptic
|
||||
# curve, nor to verify that an elliptic curve was chosen randomly,
|
||||
# because one can simply use NIST's standard curves.
|
||||
#
|
||||
# Notes from X9.62-1998 (draft):
|
||||
# Nomenclature:
|
||||
# - Q is a public key.
|
||||
# The "Elliptic Curve Domain Parameters" include:
|
||||
# - q is the "field size", which in our case equals p.
|
||||
# - p is a big prime.
|
||||
# - G is a point of prime order (5.1.1.1).
|
||||
# - n is the order of G (5.1.1.1).
|
||||
# Public-key validation (5.2.2):
|
||||
# - Verify that Q is not the point at infinity.
|
||||
# - Verify that X_Q and Y_Q are in [0,p-1].
|
||||
# - Verify that Q is on the curve.
|
||||
# - Verify that nQ is the point at infinity.
|
||||
# Signature generation (5.3):
|
||||
# - Pick random k from [1,n-1].
|
||||
# Signature checking (5.4.2):
|
||||
# - Verify that r and s are in [1,n-1].
|
||||
#
|
||||
# Version of 2008.11.25.
|
||||
#
|
||||
# Revision history:
|
||||
# 2005.12.31 - Initial version.
|
||||
# 2008.11.25 - Change CurveFp.is_on to contains_point.
|
||||
#
|
||||
# Written in 2005 by Peter Pearson and placed in the public domain.
|
||||
|
||||
def inverse_mod(a, m):
|
||||
"""Inverse of a mod m."""
|
||||
|
||||
if a < 0 or m <= a:
|
||||
a = a % m
|
||||
|
||||
# From Ferguson and Schneier, roughly:
|
||||
|
||||
c, d = a, m
|
||||
uc, vc, ud, vd = 1, 0, 0, 1
|
||||
while c != 0:
|
||||
q, c, d = divmod(d, c) + (c,)
|
||||
uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc
|
||||
|
||||
# At this point, d is the GCD, and ud*a+vd*m = d.
|
||||
# If d == 1, this means that ud is a inverse.
|
||||
|
||||
assert d == 1
|
||||
if ud > 0:
|
||||
return ud
|
||||
else:
|
||||
return ud + m
|
||||
|
||||
|
||||
def modular_sqrt(a, p):
|
||||
# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/
|
||||
""" Find a quadratic residue (mod p) of 'a'. p
|
||||
must be an odd prime.
|
||||
|
||||
Solve the congruence of the form:
|
||||
x^2 = a (mod p)
|
||||
And returns x. Note that p - x is also a root.
|
||||
|
||||
0 is returned is no square root exists for
|
||||
these a and p.
|
||||
|
||||
The Tonelli-Shanks algorithm is used (except
|
||||
for some simple cases in which the solution
|
||||
is known from an identity). This algorithm
|
||||
runs in polynomial time (unless the
|
||||
generalized Riemann hypothesis is false).
|
||||
"""
|
||||
# Simple cases
|
||||
#
|
||||
if legendre_symbol(a, p) != 1:
|
||||
return 0
|
||||
elif a == 0:
|
||||
return 0
|
||||
elif p == 2:
|
||||
return p
|
||||
elif p % 4 == 3:
|
||||
return pow(a, (p + 1) // 4, p)
|
||||
|
||||
# Partition p-1 to s * 2^e for an odd s (i.e.
|
||||
# reduce all the powers of 2 from p-1)
|
||||
#
|
||||
s = p - 1
|
||||
e = 0
|
||||
while s % 2 == 0:
|
||||
s /= 2
|
||||
e += 1
|
||||
|
||||
# Find some 'n' with a legendre symbol n|p = -1.
|
||||
# Shouldn't take long.
|
||||
#
|
||||
n = 2
|
||||
while legendre_symbol(n, p) != -1:
|
||||
n += 1
|
||||
|
||||
# Here be dragons!
|
||||
# Read the paper "Square roots from 1; 24, 51,
|
||||
# 10 to Dan Shanks" by Ezra Brown for more
|
||||
# information
|
||||
#
|
||||
|
||||
# x is a guess of the square root that gets better
|
||||
# with each iteration.
|
||||
# b is the "fudge factor" - by how much we're off
|
||||
# with the guess. The invariant x^2 = ab (mod p)
|
||||
# is maintained throughout the loop.
|
||||
# g is used for successive powers of n to update
|
||||
# both a and b
|
||||
# r is the exponent - decreases with each update
|
||||
#
|
||||
x = pow(a, (s + 1) // 2, p)
|
||||
b = pow(a, s, p)
|
||||
g = pow(n, s, p)
|
||||
r = e
|
||||
|
||||
while True:
|
||||
t = b
|
||||
m = 0
|
||||
for m in range(r):
|
||||
if t == 1:
|
||||
break
|
||||
t = pow(t, 2, p)
|
||||
|
||||
if m == 0:
|
||||
return x
|
||||
|
||||
gs = pow(g, 2 ** (r - m - 1), p)
|
||||
g = (gs * gs) % p
|
||||
x = (x * gs) % p
|
||||
b = (b * g) % p
|
||||
r = m
|
||||
|
||||
|
||||
def legendre_symbol(a, p):
|
||||
""" Compute the Legendre symbol a|p using
|
||||
Euler's criterion. p is a prime, a is
|
||||
relatively prime to p (if p divides
|
||||
a, then a|p = 0)
|
||||
|
||||
Returns 1 if a has a square root modulo
|
||||
p, -1 otherwise.
|
||||
"""
|
||||
ls = pow(a, (p - 1) // 2, p)
|
||||
return -1 if ls == p - 1 else ls
|
||||
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
|
||||
class CurveFp(object):
|
||||
"""Elliptic Curve over the field of integers modulo a prime."""
|
||||
def __init__(self, p, a, b):
|
||||
"""The curve of points satisfying y^2 = x^3 + a*x + b (mod p)."""
|
||||
self.__p = p
|
||||
self.__a = a
|
||||
self.__b = b
|
||||
|
||||
def p(self):
|
||||
return self.__p
|
||||
|
||||
def a(self):
|
||||
return self.__a
|
||||
|
||||
def b(self):
|
||||
return self.__b
|
||||
|
||||
def contains_point(self, x, y):
|
||||
"""Is the point (x,y) on this curve?"""
|
||||
return (y * y - (x * x * x + self.__a * x + self.__b)) % self.__p == 0
|
||||
|
||||
|
||||
class Point(object):
|
||||
""" A point on an elliptic curve. Altering x and y is forbidding,
|
||||
but they can be read by the x() and y() methods."""
|
||||
def __init__(self, curve, x, y, order=None):
|
||||
"""curve, x, y, order; order (optional) is the order of this point."""
|
||||
self.__curve = curve
|
||||
self.__x = x
|
||||
self.__y = y
|
||||
self.__order = order
|
||||
# self.curve is allowed to be None only for INFINITY:
|
||||
if self.__curve:
|
||||
assert self.__curve.contains_point(x, y)
|
||||
if order:
|
||||
assert self * order == INFINITY
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Return 1 if the points are identical, 0 otherwise."""
|
||||
if self.__curve == other.__curve \
|
||||
and self.__x == other.__x \
|
||||
and self.__y == other.__y:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __add__(self, other):
|
||||
"""Add one point to another point."""
|
||||
|
||||
# X9.62 B.3:
|
||||
if other == INFINITY:
|
||||
return self
|
||||
if self == INFINITY:
|
||||
return other
|
||||
assert self.__curve == other.__curve
|
||||
if self.__x == other.__x:
|
||||
if (self.__y + other.__y) % self.__curve.p() == 0:
|
||||
return INFINITY
|
||||
else:
|
||||
return self.double()
|
||||
|
||||
p = self.__curve.p()
|
||||
|
||||
l = ((other.__y - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
|
||||
|
||||
x3 = (l * l - self.__x - other.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def __sub__(self, other):
|
||||
#The inverse of a point P=(xP,yP) is its reflexion across the x-axis : P′=(xP,−yP).
|
||||
#If you want to compute Q−P, just replace yP by −yP in the usual formula for point addition.
|
||||
|
||||
# X9.62 B.3:
|
||||
if other == INFINITY:
|
||||
return self
|
||||
if self == INFINITY:
|
||||
return other
|
||||
assert self.__curve == other.__curve
|
||||
|
||||
p = self.__curve.p()
|
||||
#opi = inverse_mod(other.__y, p)
|
||||
opi = -other.__y % p
|
||||
#print(opi)
|
||||
#print(-other.__y % p)
|
||||
|
||||
if self.__x == other.__x:
|
||||
if (self.__y + opi) % self.__curve.p() == 0:
|
||||
return INFINITY
|
||||
else:
|
||||
return self.double
|
||||
|
||||
l = ((opi - self.__y) * inverse_mod(other.__x - self.__x, p)) % p
|
||||
|
||||
x3 = (l * l - self.__x - other.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def __mul__(self, e):
|
||||
if self.__order:
|
||||
e %= self.__order
|
||||
if e == 0 or self == INFINITY:
|
||||
return INFINITY
|
||||
result, q = INFINITY, self
|
||||
while e:
|
||||
if e & 1:
|
||||
result += q
|
||||
e, q = e >> 1, q.double()
|
||||
return result
|
||||
|
||||
"""
|
||||
def __mul__(self, other):
|
||||
#Multiply a point by an integer.
|
||||
|
||||
def leftmost_bit( x ):
|
||||
assert x > 0
|
||||
result = 1
|
||||
while result <= x: result = 2 * result
|
||||
return result // 2
|
||||
|
||||
e = other
|
||||
if self.__order: e = e % self.__order
|
||||
if e == 0: return INFINITY
|
||||
if self == INFINITY: return INFINITY
|
||||
assert e > 0
|
||||
|
||||
# From X9.62 D.3.2:
|
||||
|
||||
e3 = 3 * e
|
||||
negative_self = Point( self.__curve, self.__x, -self.__y, self.__order )
|
||||
i = leftmost_bit( e3 ) // 2
|
||||
result = self
|
||||
# print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 )
|
||||
while i > 1:
|
||||
result = result.double()
|
||||
if ( e3 & i ) != 0 and ( e & i ) == 0: result = result + self
|
||||
if ( e3 & i ) == 0 and ( e & i ) != 0: result = result + negative_self
|
||||
# print ". . . i = %d, result = %s" % ( i, result )
|
||||
i = i // 2
|
||||
|
||||
return result
|
||||
"""
|
||||
|
||||
def __rmul__(self, other):
|
||||
"""Multiply a point by an integer."""
|
||||
|
||||
return self * other
|
||||
|
||||
def __str__(self):
|
||||
if self == INFINITY:
|
||||
return "infinity"
|
||||
return "(%d, %d)" % (self.__x, self.__y)
|
||||
|
||||
def inverse(self):
|
||||
return Point(self.__curve, self.__x, -self.__y % self.__curve.p())
|
||||
|
||||
def double(self):
|
||||
"""Return a new point that is twice the old."""
|
||||
|
||||
if self == INFINITY:
|
||||
return INFINITY
|
||||
|
||||
# X9.62 B.3:
|
||||
|
||||
p = self.__curve.p()
|
||||
a = self.__curve.a()
|
||||
|
||||
l = ((3 * self.__x * self.__x + a) * inverse_mod(2 * self.__y, p)) % p
|
||||
|
||||
x3 = (l * l - 2 * self.__x) % p
|
||||
y3 = (l * (self.__x - x3) - self.__y) % p
|
||||
|
||||
return Point(self.__curve, x3, y3)
|
||||
|
||||
def x(self):
|
||||
return self.__x
|
||||
|
||||
def y(self):
|
||||
return self.__y
|
||||
|
||||
def pair(self):
|
||||
return (self.__x, self.__y)
|
||||
|
||||
def curve(self):
|
||||
return self.__curve
|
||||
|
||||
def order(self):
|
||||
return self.__order
|
||||
|
||||
|
||||
# This one point is the Point At Infinity for all purposes:
|
||||
INFINITY = Point(None, None, None)
|
||||
|
||||
|
||||
def __main__():
|
||||
|
||||
class FailedTest(Exception):
|
||||
pass
|
||||
|
||||
def test_add(c, x1, y1, x2, y2, x3, y3):
|
||||
"""We expect that on curve c, (x1,y1) + (x2, y2 ) = (x3, y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p2 = Point(c, x2, y2)
|
||||
p3 = p1 + p2
|
||||
print("%s + %s = %s" % (p1, p2, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_double(c, x1, y1, x3, y3):
|
||||
"""We expect that on curve c, 2*(x1,y1) = (x3, y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p3 = p1.double()
|
||||
print("%s doubled = %s" % (p1, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_double_infinity(c):
|
||||
"""We expect that on curve c, 2*INFINITY = INFINITY."""
|
||||
p1 = INFINITY
|
||||
p3 = p1.double()
|
||||
print("%s doubled = %s" % (p1, p3))
|
||||
if p3.x() != INFINITY.x() or p3.y() != INFINITY.y():
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (INFINITY.x(), INFINITY.y()))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
def test_multiply(c, x1, y1, m, x3, y3):
|
||||
"""We expect that on curve c, m*(x1,y1) = (x3,y3)."""
|
||||
p1 = Point(c, x1, y1)
|
||||
p3 = p1 * m
|
||||
print("%s * %d = %s" % (p1, m, p3))
|
||||
if p3.x() != x3 or p3.y() != y3:
|
||||
raise FailedTest("Failure: should give (%d,%d)." % (x3, y3))
|
||||
else:
|
||||
print(" Good.")
|
||||
|
||||
# A few tests from X9.62 B.3:
|
||||
|
||||
c = CurveFp(23, 1, 1)
|
||||
test_add(c, 3, 10, 9, 7, 17, 20)
|
||||
test_double(c, 3, 10, 7, 12)
|
||||
test_add(c, 3, 10, 3, 10, 7, 12) # (Should just invoke double.)
|
||||
test_multiply(c, 3, 10, 2, 7, 12)
|
||||
|
||||
test_double_infinity(c)
|
||||
|
||||
# From X9.62 I.1 (p. 96):
|
||||
|
||||
g = Point(c, 13, 7, 7)
|
||||
|
||||
check = INFINITY
|
||||
for i in range(7 + 1):
|
||||
p = (i % 7) * g
|
||||
print("%s * %d = %s, expected %s . . ." % (g, i, p, check))
|
||||
if p == check:
|
||||
print(" Good.")
|
||||
else:
|
||||
raise FailedTest("Bad.")
|
||||
check = check + g
|
||||
|
||||
# NIST Curve P-192:
|
||||
p = 6277101735386680763835789423207666416083908700390324961279
|
||||
r = 6277101735386680763835789423176059013767194773182842284081
|
||||
#s = 0x3045ae6fc8422f64ed579528d38120eae12196d5L
|
||||
c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65
|
||||
b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
|
||||
Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
|
||||
Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
|
||||
|
||||
c192 = CurveFp(p, -3, b)
|
||||
p192 = Point(c192, Gx, Gy, r)
|
||||
|
||||
# Checking against some sample computations presented
|
||||
# in X9.62:
|
||||
|
||||
d = 651056770906015076056810763456358567190100156695615665659
|
||||
Q = d * p192
|
||||
if Q.x() != 0x62B12D60690CDCF330BABAB6E69763B471F994DD702D16A5:
|
||||
raise FailedTest("p192 * d came out wrong.")
|
||||
else:
|
||||
print("p192 * d came out right.")
|
||||
|
||||
k = 6140507067065001063065065565667405560006161556565665656654
|
||||
R = k * p192
|
||||
if R.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
|
||||
or R.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
|
||||
raise FailedTest("k * p192 came out wrong.")
|
||||
else:
|
||||
print("k * p192 came out right.")
|
||||
|
||||
u1 = 2563697409189434185194736134579731015366492496392189760599
|
||||
u2 = 6266643813348617967186477710235785849136406323338782220568
|
||||
temp = u1 * p192 + u2 * Q
|
||||
if temp.x() != 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD \
|
||||
or temp.y() != 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835:
|
||||
raise FailedTest("u1 * p192 + u2 * Q came out wrong.")
|
||||
else:
|
||||
print("u1 * p192 + u2 * Q came out right.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
__main__()
|
||||
@@ -1,386 +0,0 @@
|
||||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
||||
WARNING: This code is slow, uses bad randomness, does not properly protect
|
||||
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
||||
anything but tests."""
|
||||
import random
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
|
||||
"""
|
||||
t1, t2 = 0, 1
|
||||
r1, r2 = n, a
|
||||
while r2 != 0:
|
||||
q = r1 // r2
|
||||
t1, t2 = t2, t1 - q * t2
|
||||
r1, r2 = r2, r1 - q * r2
|
||||
if r1 > 1:
|
||||
return None
|
||||
if t1 < 0:
|
||||
t1 += n
|
||||
return t1
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
def modsqrt(a, p):
|
||||
"""Compute the square root of a modulo p when p % 4 = 3.
|
||||
|
||||
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
|
||||
|
||||
Limiting this function to only work for p % 4 = 3 means we don't need to
|
||||
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
|
||||
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
|
||||
|
||||
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
|
||||
"""
|
||||
if p % 4 != 3:
|
||||
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
|
||||
sqrt = pow(a, (p + 1)//4, p)
|
||||
if pow(sqrt, 2, p) == a % p:
|
||||
return sqrt
|
||||
return None
|
||||
|
||||
class EllipticCurve:
|
||||
def __init__(self, p, a, b):
|
||||
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
|
||||
self.p = p
|
||||
self.a = a % p
|
||||
self.b = b % p
|
||||
|
||||
def affine(self, p1):
|
||||
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
|
||||
|
||||
An affine point is represented as the Jacobian (x, y, 1)"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return None
|
||||
inv = modinv(z1, self.p)
|
||||
inv_2 = (inv**2) % self.p
|
||||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
return (x1, (self.p - y1) % self.p, z1)
|
||||
|
||||
def on_curve(self, p1):
|
||||
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
|
||||
x1, y1, z1 = p1
|
||||
z2 = pow(z1, 2, self.p)
|
||||
z4 = pow(z2, 2, self.p)
|
||||
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
|
||||
|
||||
def is_x_coord(self, x):
|
||||
"""Test whether x is a valid X coordinate on the curve."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return (0, 1, 0)
|
||||
y1_2 = (y1**2) % self.p
|
||||
y1_4 = (y1_2**2) % self.p
|
||||
x1_2 = (x1**2) % self.p
|
||||
s = (4*x1*y1_2) % self.p
|
||||
m = 3*x1_2
|
||||
if self.a:
|
||||
m += self.a * pow(z1, 4, self.p)
|
||||
m = m % self.p
|
||||
x2 = (m**2 - 2*s) % self.p
|
||||
y2 = (m*(s - x2) - 8*y1_4) % self.p
|
||||
z2 = (2*y1*z1) % self.p
|
||||
return (x2, y2, z2)
|
||||
|
||||
def add_mixed(self, p1, p2):
|
||||
"""Add a Jacobian tuple p1 and an affine tuple p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
assert(z2 == 1)
|
||||
# Adding to the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if x1 == u2:
|
||||
if (y1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - x1
|
||||
r = s2 - y1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (x1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
|
||||
z3 = (h*z1) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def add(self, p1, p2):
|
||||
"""Add two Jacobian tuples p1 and p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
# Adding the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
if z2 == 0:
|
||||
return p1
|
||||
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
|
||||
if z1 == 1:
|
||||
return self.add_mixed(p2, p1)
|
||||
if z2 == 1:
|
||||
return self.add_mixed(p1, p2)
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
z2_2 = (z2**2) % self.p
|
||||
z2_3 = (z2_2 * z2) % self.p
|
||||
u1 = (x1 * z2_2) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s1 = (y1 * z2_3) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if u1 == u2:
|
||||
if (s1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (u1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
|
||||
z3 = (h*z1*z2) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def mul(self, ps):
|
||||
"""Compute a (multi) point multiplication
|
||||
|
||||
ps is a list of (Jacobian tuple, scalar) pairs.
|
||||
"""
|
||||
r = (0, 1, 0)
|
||||
for i in range(255, -1, -1):
|
||||
r = self.double(r)
|
||||
for (p, n) in ps:
|
||||
if ((n >> i) & 1):
|
||||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
||||
class ECPubKey():
|
||||
"""A secp256k1 public key"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct an uninitialized public key"""
|
||||
self.valid = False
|
||||
|
||||
def set(self, data):
|
||||
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
||||
if (len(data) == 65 and data[0] == 0x04):
|
||||
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
|
||||
x = int.from_bytes(data[1:33], 'big')
|
||||
if SECP256K1.is_x_coord(x):
|
||||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
self.compressed = True
|
||||
else:
|
||||
self.valid = False
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
def get_bytes(self):
|
||||
assert(self.valid)
|
||||
p = SECP256K1.affine(self.p)
|
||||
if p is None:
|
||||
return None
|
||||
if self.compressed:
|
||||
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
|
||||
else:
|
||||
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
|
||||
|
||||
def verify_ecdsa(self, sig, msg, low_s=True):
|
||||
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA verifier algorithm"""
|
||||
assert(self.valid)
|
||||
|
||||
# Extract r and s from the DER formatted signature. Return false for
|
||||
# any DER encoding errors.
|
||||
if (sig[1] + 2 != len(sig)):
|
||||
return False
|
||||
if (len(sig) < 4):
|
||||
return False
|
||||
if (sig[0] != 0x30):
|
||||
return False
|
||||
if (sig[2] != 0x02):
|
||||
return False
|
||||
rlen = sig[3]
|
||||
if (len(sig) < 6 + rlen):
|
||||
return False
|
||||
if rlen < 1 or rlen > 33:
|
||||
return False
|
||||
if sig[4] >= 0x80:
|
||||
return False
|
||||
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
||||
return False
|
||||
r = int.from_bytes(sig[4:4+rlen], 'big')
|
||||
if (sig[4+rlen] != 0x02):
|
||||
return False
|
||||
slen = sig[5+rlen]
|
||||
if slen < 1 or slen > 33:
|
||||
return False
|
||||
if (len(sig) != 6 + rlen + slen):
|
||||
return False
|
||||
if sig[6+rlen] >= 0x80:
|
||||
return False
|
||||
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
||||
return False
|
||||
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
||||
|
||||
# Verify that r and s are within the group order
|
||||
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
|
||||
return False
|
||||
if low_s and s >= SECP256K1_ORDER_HALF:
|
||||
return False
|
||||
z = int.from_bytes(msg, 'big')
|
||||
|
||||
# Run verifier algorithm on r, s
|
||||
w = modinv(s, SECP256K1_ORDER)
|
||||
u1 = z*w % SECP256K1_ORDER
|
||||
u2 = r*w % SECP256K1_ORDER
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
|
||||
if R is None or R[0] != r:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid = False
|
||||
|
||||
def set(self, secret, compressed):
|
||||
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
||||
assert(len(secret) == 32)
|
||||
secret = int.from_bytes(secret, 'big')
|
||||
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
|
||||
if self.valid:
|
||||
self.secret = secret
|
||||
self.compressed = compressed
|
||||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
assert(self.valid)
|
||||
return self.secret.to_bytes(32, 'big')
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
def get_pubkey(self):
|
||||
"""Compute an ECPubKey object for this secret key."""
|
||||
assert(self.valid)
|
||||
ret = ECPubKey()
|
||||
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
|
||||
ret.p = p
|
||||
ret.valid = True
|
||||
ret.compressed = self.compressed
|
||||
return ret
|
||||
|
||||
def sign_ecdsa(self, msg, low_s=True):
|
||||
"""Construct a DER-encoded ECDSA signature with this key.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA signer algorithm."""
|
||||
assert(self.valid)
|
||||
z = int.from_bytes(msg, 'big')
|
||||
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
||||
k = random.randrange(1, SECP256K1_ORDER)
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
|
||||
r = R[0] % SECP256K1_ORDER
|
||||
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
|
||||
if low_s and s > SECP256K1_ORDER_HALF:
|
||||
s = SECP256K1_ORDER - s
|
||||
# Represent in DER format. The byte representations of r and s have
|
||||
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
||||
# bytes).
|
||||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
||||
64
basicswap/contrib/test_framework/descriptors.py
Normal file
64
basicswap/contrib/test_framework/descriptors.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Utility functions related to output descriptors"""
|
||||
|
||||
import re
|
||||
|
||||
INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
|
||||
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd]
|
||||
|
||||
def descsum_polymod(symbols):
|
||||
"""Internal function that computes the descriptor checksum."""
|
||||
chk = 1
|
||||
for value in symbols:
|
||||
top = chk >> 35
|
||||
chk = (chk & 0x7ffffffff) << 5 ^ value
|
||||
for i in range(5):
|
||||
chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
def descsum_expand(s):
|
||||
"""Internal function that does the character to symbol expansion"""
|
||||
groups = []
|
||||
symbols = []
|
||||
for c in s:
|
||||
if not c in INPUT_CHARSET:
|
||||
return None
|
||||
v = INPUT_CHARSET.find(c)
|
||||
symbols.append(v & 31)
|
||||
groups.append(v >> 5)
|
||||
if len(groups) == 3:
|
||||
symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
|
||||
groups = []
|
||||
if len(groups) == 1:
|
||||
symbols.append(groups[0])
|
||||
elif len(groups) == 2:
|
||||
symbols.append(groups[0] * 3 + groups[1])
|
||||
return symbols
|
||||
|
||||
def descsum_create(s):
|
||||
"""Add a checksum to a descriptor without"""
|
||||
symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
checksum = descsum_polymod(symbols) ^ 1
|
||||
return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))
|
||||
|
||||
def descsum_check(s, require=True):
|
||||
"""Verify that the checksum is correct in a descriptor"""
|
||||
if not '#' in s:
|
||||
return not require
|
||||
if s[-9] != '#':
|
||||
return False
|
||||
if not all(x in CHECKSUM_CHARSET for x in s[-8:]):
|
||||
return False
|
||||
symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]]
|
||||
return descsum_polymod(symbols) == 1
|
||||
|
||||
def drop_origins(s):
|
||||
'''Drop the key origins from a descriptor'''
|
||||
desc = re.sub(r'\[.+?\]', '', s)
|
||||
if '#' in s:
|
||||
desc = desc[:desc.index('#')]
|
||||
return descsum_create(desc)
|
||||
@@ -1,393 +0,0 @@
|
||||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
||||
WARNING: This code is slow, uses bad randomness, does not properly protect
|
||||
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
||||
anything but tests."""
|
||||
import random
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
|
||||
"""
|
||||
t1, t2 = 0, 1
|
||||
r1, r2 = n, a
|
||||
while r2 != 0:
|
||||
q = r1 // r2
|
||||
t1, t2 = t2, t1 - q * t2
|
||||
r1, r2 = r2, r1 - q * r2
|
||||
if r1 > 1:
|
||||
return None
|
||||
if t1 < 0:
|
||||
t1 += n
|
||||
return t1
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
def modsqrt(a, p):
|
||||
"""Compute the square root of a modulo p when p % 4 = 3.
|
||||
|
||||
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
|
||||
|
||||
Limiting this function to only work for p % 4 = 3 means we don't need to
|
||||
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
|
||||
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
|
||||
|
||||
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
|
||||
"""
|
||||
if p % 4 != 3:
|
||||
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
|
||||
sqrt = pow(a, (p + 1)//4, p)
|
||||
if pow(sqrt, 2, p) == a % p:
|
||||
return sqrt
|
||||
return None
|
||||
|
||||
class EllipticCurve:
|
||||
def __init__(self, p, a, b):
|
||||
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
|
||||
self.p = p
|
||||
self.a = a % p
|
||||
self.b = b % p
|
||||
|
||||
def affine(self, p1):
|
||||
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
|
||||
|
||||
An affine point is represented as the Jacobian (x, y, 1)"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return None
|
||||
inv = modinv(z1, self.p)
|
||||
inv_2 = (inv**2) % self.p
|
||||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
return (x1, (self.p - y1) % self.p, z1)
|
||||
|
||||
def on_curve(self, p1):
|
||||
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
|
||||
x1, y1, z1 = p1
|
||||
z2 = pow(z1, 2, self.p)
|
||||
z4 = pow(z2, 2, self.p)
|
||||
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
|
||||
|
||||
def is_x_coord(self, x):
|
||||
"""Test whether x is a valid X coordinate on the curve."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return (0, 1, 0)
|
||||
y1_2 = (y1**2) % self.p
|
||||
y1_4 = (y1_2**2) % self.p
|
||||
x1_2 = (x1**2) % self.p
|
||||
s = (4*x1*y1_2) % self.p
|
||||
m = 3*x1_2
|
||||
if self.a:
|
||||
m += self.a * pow(z1, 4, self.p)
|
||||
m = m % self.p
|
||||
x2 = (m**2 - 2*s) % self.p
|
||||
y2 = (m*(s - x2) - 8*y1_4) % self.p
|
||||
z2 = (2*y1*z1) % self.p
|
||||
return (x2, y2, z2)
|
||||
|
||||
def add_mixed(self, p1, p2):
|
||||
"""Add a Jacobian tuple p1 and an affine tuple p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
assert(z2 == 1)
|
||||
# Adding to the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if x1 == u2:
|
||||
if (y1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - x1
|
||||
r = s2 - y1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (x1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
|
||||
z3 = (h*z1) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def add(self, p1, p2):
|
||||
"""Add two Jacobian tuples p1 and p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
# Adding the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
if z2 == 0:
|
||||
return p1
|
||||
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
|
||||
if z1 == 1:
|
||||
return self.add_mixed(p2, p1)
|
||||
if z2 == 1:
|
||||
return self.add_mixed(p1, p2)
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
z2_2 = (z2**2) % self.p
|
||||
z2_3 = (z2_2 * z2) % self.p
|
||||
u1 = (x1 * z2_2) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s1 = (y1 * z2_3) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if u1 == u2:
|
||||
if (s1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (u1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
|
||||
z3 = (h*z1*z2) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def mul(self, ps):
|
||||
"""Compute a (multi) point multiplication
|
||||
|
||||
ps is a list of (Jacobian tuple, scalar) pairs.
|
||||
"""
|
||||
r = (0, 1, 0)
|
||||
for i in range(255, -1, -1):
|
||||
r = self.double(r)
|
||||
for (p, n) in ps:
|
||||
if ((n >> i) & 1):
|
||||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
||||
class ECPubKey():
|
||||
"""A secp256k1 public key"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct an uninitialized public key"""
|
||||
self.valid = False
|
||||
|
||||
def set_int(self, x, y):
|
||||
p = (x, y, 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
|
||||
def set(self, data):
|
||||
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
||||
if (len(data) == 65 and data[0] == 0x04):
|
||||
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
|
||||
x = int.from_bytes(data[1:33], 'big')
|
||||
if SECP256K1.is_x_coord(x):
|
||||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
self.compressed = True
|
||||
else:
|
||||
self.valid = False
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
def get_bytes(self):
|
||||
assert(self.valid)
|
||||
p = SECP256K1.affine(self.p)
|
||||
if p is None:
|
||||
return None
|
||||
if self.compressed:
|
||||
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
|
||||
else:
|
||||
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
|
||||
|
||||
def verify_ecdsa(self, sig, msg, low_s=True):
|
||||
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA verifier algorithm"""
|
||||
assert(self.valid)
|
||||
|
||||
# Extract r and s from the DER formatted signature. Return false for
|
||||
# any DER encoding errors.
|
||||
if (sig[1] + 2 != len(sig)):
|
||||
return False
|
||||
if (len(sig) < 4):
|
||||
return False
|
||||
if (sig[0] != 0x30):
|
||||
return False
|
||||
if (sig[2] != 0x02):
|
||||
return False
|
||||
rlen = sig[3]
|
||||
if (len(sig) < 6 + rlen):
|
||||
return False
|
||||
if rlen < 1 or rlen > 33:
|
||||
return False
|
||||
if sig[4] >= 0x80:
|
||||
return False
|
||||
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
||||
return False
|
||||
r = int.from_bytes(sig[4:4+rlen], 'big')
|
||||
if (sig[4+rlen] != 0x02):
|
||||
return False
|
||||
slen = sig[5+rlen]
|
||||
if slen < 1 or slen > 33:
|
||||
return False
|
||||
if (len(sig) != 6 + rlen + slen):
|
||||
return False
|
||||
if sig[6+rlen] >= 0x80:
|
||||
return False
|
||||
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
||||
return False
|
||||
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
||||
|
||||
# Verify that r and s are within the group order
|
||||
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
|
||||
return False
|
||||
if low_s and s >= SECP256K1_ORDER_HALF:
|
||||
return False
|
||||
z = int.from_bytes(msg, 'big')
|
||||
|
||||
# Run verifier algorithm on r, s
|
||||
w = modinv(s, SECP256K1_ORDER)
|
||||
u1 = z*w % SECP256K1_ORDER
|
||||
u2 = r*w % SECP256K1_ORDER
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
|
||||
if R is None or R[0] != r:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid = False
|
||||
|
||||
def set(self, secret, compressed):
|
||||
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
||||
assert(len(secret) == 32)
|
||||
secret = int.from_bytes(secret, 'big')
|
||||
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
|
||||
if self.valid:
|
||||
self.secret = secret
|
||||
self.compressed = compressed
|
||||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
assert(self.valid)
|
||||
return self.secret.to_bytes(32, 'big')
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
def get_pubkey(self):
|
||||
"""Compute an ECPubKey object for this secret key."""
|
||||
assert(self.valid)
|
||||
ret = ECPubKey()
|
||||
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
|
||||
ret.p = p
|
||||
ret.valid = True
|
||||
ret.compressed = self.compressed
|
||||
return ret
|
||||
|
||||
def sign_ecdsa(self, msg, low_s=True):
|
||||
"""Construct a DER-encoded ECDSA signature with this key.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA signer algorithm."""
|
||||
assert(self.valid)
|
||||
z = int.from_bytes(msg, 'big')
|
||||
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
||||
k = random.randrange(1, SECP256K1_ORDER)
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
|
||||
r = R[0] % SECP256K1_ORDER
|
||||
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
|
||||
if low_s and s > SECP256K1_ORDER_HALF:
|
||||
s = SECP256K1_ORDER - s
|
||||
# Represent in DER format. The byte representations of r and s have
|
||||
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
||||
# bytes).
|
||||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
||||
File diff suppressed because it is too large
Load Diff
1006
basicswap/contrib/test_framework/p2p.py
Executable file
1006
basicswap/contrib/test_framework/p2p.py
Executable file
File diff suppressed because it is too large
Load Diff
@@ -5,20 +5,22 @@
|
||||
"""Helpful routines for regression testing."""
|
||||
|
||||
from base64 import b64encode
|
||||
from binascii import unhexlify
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from subprocess import CalledProcessError
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import time
|
||||
|
||||
from . import coverage
|
||||
from .authproxy import AuthServiceProxy, JSONRPCException
|
||||
from io import BytesIO
|
||||
from collections.abc import Callable
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger("TestFramework.utils")
|
||||
|
||||
@@ -28,23 +30,46 @@ logger = logging.getLogger("TestFramework.utils")
|
||||
|
||||
def assert_approx(v, vexp, vspan=0.00001):
|
||||
"""Assert that `v` is within `vspan` of `vexp`"""
|
||||
if isinstance(v, Decimal) or isinstance(vexp, Decimal):
|
||||
v=Decimal(v)
|
||||
vexp=Decimal(vexp)
|
||||
vspan=Decimal(vspan)
|
||||
if v < vexp - vspan:
|
||||
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
|
||||
if v > vexp + vspan:
|
||||
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
|
||||
|
||||
|
||||
def assert_fee_amount(fee, tx_size, fee_per_kB):
|
||||
"""Assert the fee was in range"""
|
||||
target_fee = round(tx_size * fee_per_kB / 1000, 8)
|
||||
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
|
||||
"""Assert the fee is in range."""
|
||||
assert isinstance(tx_size, int)
|
||||
target_fee = get_fee(tx_size, feerate_BTC_kvB)
|
||||
if fee < target_fee:
|
||||
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
|
||||
# allow the wallet's estimation to be at most 2 bytes off
|
||||
if fee > (tx_size + 2) * fee_per_kB / 1000:
|
||||
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
|
||||
if fee > high_fee:
|
||||
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
|
||||
|
||||
|
||||
def summarise_dict_differences(thing1, thing2):
|
||||
if not isinstance(thing1, dict) or not isinstance(thing2, dict):
|
||||
return thing1, thing2
|
||||
d1, d2 = {}, {}
|
||||
for k in sorted(thing1.keys()):
|
||||
if k not in thing2:
|
||||
d1[k] = thing1[k]
|
||||
elif thing1[k] != thing2[k]:
|
||||
d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
|
||||
for k in sorted(thing2.keys()):
|
||||
if k not in thing1:
|
||||
d2[k] = thing2[k]
|
||||
return d1, d2
|
||||
|
||||
def assert_equal(thing1, thing2, *args):
|
||||
if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
|
||||
d1,d2 = summarise_dict_differences(thing1, thing2)
|
||||
raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2))
|
||||
if thing1 != thing2 or any(thing1 != arg for arg in args):
|
||||
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
|
||||
|
||||
@@ -79,7 +104,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds):
|
||||
raise AssertionError("No exception raised")
|
||||
|
||||
|
||||
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
|
||||
def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
|
||||
"""Execute a process and asserts the process return code and output.
|
||||
|
||||
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
|
||||
@@ -87,9 +112,9 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds):
|
||||
no CalledProcessError was raised or if the return code and output are not as expected.
|
||||
|
||||
Args:
|
||||
returncode (int): the process return code.
|
||||
output (string): [a substring of] the process output.
|
||||
fun (function): the function to call. This should execute a process.
|
||||
returncode: the process return code.
|
||||
output: [a substring of] the process output.
|
||||
fun: the function to call. This should execute a process.
|
||||
args*: positional arguments for the function.
|
||||
kwds**: named arguments for the function.
|
||||
"""
|
||||
@@ -104,7 +129,7 @@ def assert_raises_process_error(returncode, output, fun, *args, **kwds):
|
||||
raise AssertionError("No exception raised")
|
||||
|
||||
|
||||
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
|
||||
def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
|
||||
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
|
||||
|
||||
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
|
||||
@@ -112,11 +137,11 @@ def assert_raises_rpc_error(code, message, fun, *args, **kwds):
|
||||
no JSONRPCException was raised or if the error code/message are not as expected.
|
||||
|
||||
Args:
|
||||
code (int), optional: the error code returned by the RPC call (defined
|
||||
in src/rpc/protocol.h). Set to None if checking the error code is not required.
|
||||
message (string), optional: [a substring of] the error string returned by the
|
||||
RPC call. Set to None if checking the error string is not required.
|
||||
fun (function): the function to call. This should be the name of an RPC.
|
||||
code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
|
||||
Set to None if checking the error code is not required.
|
||||
message: [a substring of] the error string returned by the RPC call.
|
||||
Set to None if checking the error string is not required.
|
||||
fun: the function to call. This should be the name of an RPC.
|
||||
args*: positional arguments for the function.
|
||||
kwds**: named arguments for the function.
|
||||
"""
|
||||
@@ -203,29 +228,45 @@ def check_json_precision():
|
||||
raise RuntimeError("JSON encode/decode loses precision")
|
||||
|
||||
|
||||
def EncodeDecimal(o):
|
||||
if isinstance(o, Decimal):
|
||||
return str(o)
|
||||
raise TypeError(repr(o) + " is not JSON serializable")
|
||||
|
||||
|
||||
def count_bytes(hex_string):
|
||||
return len(bytearray.fromhex(hex_string))
|
||||
|
||||
|
||||
def hex_str_to_bytes(hex_str):
|
||||
return unhexlify(hex_str.encode('ascii'))
|
||||
|
||||
|
||||
def str_to_b64str(string):
|
||||
return b64encode(string.encode('utf-8')).decode('ascii')
|
||||
|
||||
|
||||
def ceildiv(a, b):
|
||||
"""
|
||||
Divide 2 ints and round up to next int rather than round down
|
||||
Implementation requires python integers, which have a // operator that does floor division.
|
||||
Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
|
||||
"""
|
||||
assert isinstance(a, int)
|
||||
assert isinstance(b, int)
|
||||
return -(-a // b)
|
||||
|
||||
|
||||
def get_fee(tx_size, feerate_btc_kvb):
|
||||
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
|
||||
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
|
||||
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
|
||||
return target_fee_sat / Decimal(1e8) # Return result in BTC
|
||||
|
||||
|
||||
def satoshi_round(amount):
|
||||
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||
|
||||
|
||||
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
|
||||
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
|
||||
"""Sleep until the predicate resolves to be True.
|
||||
|
||||
Warning: Note that this method is not recommended to be used in tests as it is
|
||||
not aware of the context of the test framework. Using the `wait_until()` members
|
||||
from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
|
||||
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
|
||||
`p2p.py` has a preset lock.
|
||||
"""
|
||||
if attempts == float('inf') and timeout == float('inf'):
|
||||
timeout = 60
|
||||
timeout = timeout * timeout_factor
|
||||
@@ -253,6 +294,16 @@ def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=N
|
||||
raise RuntimeError('Unreachable')
|
||||
|
||||
|
||||
def sha256sum_file(filename):
|
||||
h = hashlib.sha256()
|
||||
with open(filename, 'rb') as f:
|
||||
d = f.read(4096)
|
||||
while len(d) > 0:
|
||||
h.update(d)
|
||||
d = f.read(4096)
|
||||
return h.digest()
|
||||
|
||||
|
||||
# RPC/P2P connection constants and functions
|
||||
############################################
|
||||
|
||||
@@ -269,15 +320,15 @@ class PortSeed:
|
||||
n = None
|
||||
|
||||
|
||||
def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
|
||||
def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, coveragedir: Optional[str]=None) -> coverage.AuthServiceProxyWrapper:
|
||||
"""
|
||||
Args:
|
||||
url (str): URL of the RPC server to call
|
||||
node_number (int): the node number (or id) that this calls to
|
||||
url: URL of the RPC server to call
|
||||
node_number: the node number (or id) that this calls to
|
||||
|
||||
Kwargs:
|
||||
timeout (int): HTTP timeout in seconds
|
||||
coveragedir (str): Directory
|
||||
timeout: HTTP timeout in seconds
|
||||
coveragedir: Directory
|
||||
|
||||
Returns:
|
||||
AuthServiceProxy. convenience object for making RPC calls.
|
||||
@@ -288,11 +339,10 @@ def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
|
||||
proxy_kwargs['timeout'] = int(timeout)
|
||||
|
||||
proxy = AuthServiceProxy(url, **proxy_kwargs)
|
||||
proxy.url = url # store URL on proxy for info
|
||||
|
||||
coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None
|
||||
|
||||
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
|
||||
return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile)
|
||||
|
||||
|
||||
def p2p_port(n):
|
||||
@@ -321,38 +371,76 @@ def rpc_url(datadir, i, chain, rpchost):
|
||||
################
|
||||
|
||||
|
||||
def initialize_datadir(dirname, n, chain):
|
||||
def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
|
||||
datadir = get_datadir_path(dirname, n)
|
||||
if not os.path.isdir(datadir):
|
||||
os.makedirs(datadir)
|
||||
# Translate chain name to config name
|
||||
if chain == 'testnet3':
|
||||
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
|
||||
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
|
||||
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
|
||||
return datadir
|
||||
|
||||
|
||||
def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
|
||||
# Translate chain subdirectory name to config name
|
||||
if chain == 'testnet':
|
||||
chain_name_conf_arg = 'testnet'
|
||||
chain_name_conf_section = 'test'
|
||||
else:
|
||||
chain_name_conf_arg = chain
|
||||
chain_name_conf_section = chain
|
||||
with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
|
||||
f.write("{}=1\n".format(chain_name_conf_arg))
|
||||
f.write("[{}]\n".format(chain_name_conf_section))
|
||||
with open(config_path, 'w', encoding='utf8') as f:
|
||||
if chain_name_conf_arg:
|
||||
f.write("{}=1\n".format(chain_name_conf_arg))
|
||||
if chain_name_conf_section:
|
||||
f.write("[{}]\n".format(chain_name_conf_section))
|
||||
f.write("port=" + str(p2p_port(n)) + "\n")
|
||||
f.write("rpcport=" + str(rpc_port(n)) + "\n")
|
||||
# Disable server-side timeouts to avoid intermittent issues
|
||||
f.write("rpcservertimeout=99000\n")
|
||||
f.write("rpcdoccheck=1\n")
|
||||
f.write("fallbackfee=0.0002\n")
|
||||
f.write("server=1\n")
|
||||
f.write("keypool=1\n")
|
||||
f.write("discover=0\n")
|
||||
f.write("dnsseed=0\n")
|
||||
f.write("fixedseeds=0\n")
|
||||
f.write("listenonion=0\n")
|
||||
# Increase peertimeout to avoid disconnects while using mocktime.
|
||||
# peertimeout is measured in mock time, so setting it large enough to
|
||||
# cover any duration in mock time is sufficient. It can be overridden
|
||||
# in tests.
|
||||
f.write("peertimeout=999999999\n")
|
||||
f.write("printtoconsole=0\n")
|
||||
f.write("upnp=0\n")
|
||||
f.write("natpmp=0\n")
|
||||
f.write("shrinkdebugfile=0\n")
|
||||
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
|
||||
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
|
||||
return datadir
|
||||
f.write("deprecatedrpc=create_bdb\n") # Required to run the tests
|
||||
# To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync
|
||||
f.write("unsafesqlitesync=1\n")
|
||||
if disable_autoconnect:
|
||||
f.write("connect=0\n")
|
||||
f.write(extra_config)
|
||||
|
||||
|
||||
def get_datadir_path(dirname, n):
|
||||
return os.path.join(dirname, "node" + str(n))
|
||||
return pathlib.Path(dirname) / f"node{n}"
|
||||
|
||||
|
||||
def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]:
|
||||
"""Return os-specific environment variables that can be set to make the
|
||||
GetDefaultDataDir() function return a datadir path under the provided
|
||||
temp_dir, as well as the complete path it would return."""
|
||||
if platform.system() == "Windows":
|
||||
env = dict(APPDATA=str(temp_dir))
|
||||
datadir = temp_dir / "Particl"
|
||||
else:
|
||||
env = dict(HOME=str(temp_dir))
|
||||
if platform.system() == "Darwin":
|
||||
datadir = temp_dir / "Library/Application Support/Particl"
|
||||
else:
|
||||
datadir = temp_dir / ".particl"
|
||||
return env, datadir
|
||||
|
||||
|
||||
def append_config(datadir, options):
|
||||
@@ -395,7 +483,7 @@ def delete_cookie_file(datadir, chain):
|
||||
|
||||
def softfork_active(node, key):
|
||||
"""Return whether a softfork is active."""
|
||||
return node.getblockchaininfo()['softforks'][key]['active']
|
||||
return node.getdeploymentinfo()['deployments'][key]['active']
|
||||
|
||||
|
||||
def set_node_times(nodes, t):
|
||||
@@ -403,208 +491,51 @@ def set_node_times(nodes, t):
|
||||
node.setmocktime(t)
|
||||
|
||||
|
||||
def disconnect_nodes(from_connection, node_num):
|
||||
def get_peer_ids():
|
||||
result = []
|
||||
for peer in from_connection.getpeerinfo():
|
||||
if "testnode{}".format(node_num) in peer['subver']:
|
||||
result.append(peer['id'])
|
||||
return result
|
||||
|
||||
peer_ids = get_peer_ids()
|
||||
if not peer_ids:
|
||||
logger.warning("disconnect_nodes: {} and {} were not connected".format(
|
||||
from_connection.index,
|
||||
node_num,
|
||||
))
|
||||
return
|
||||
for peer_id in peer_ids:
|
||||
try:
|
||||
from_connection.disconnectnode(nodeid=peer_id)
|
||||
except JSONRPCException as e:
|
||||
# If this node is disconnected between calculating the peer id
|
||||
# and issuing the disconnect, don't worry about it.
|
||||
# This avoids a race condition if we're mass-disconnecting peers.
|
||||
if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
|
||||
raise
|
||||
|
||||
# wait to disconnect
|
||||
wait_until(lambda: not get_peer_ids(), timeout=5)
|
||||
|
||||
|
||||
def connect_nodes(from_connection, node_num):
|
||||
ip_port = "127.0.0.1:" + str(p2p_port(node_num))
|
||||
from_connection.addnode(ip_port, "onetry")
|
||||
# poll until version handshake complete to avoid race conditions
|
||||
# with transaction relaying
|
||||
# See comments in net_processing:
|
||||
# * Must have a version message before anything else
|
||||
# * Must have a verack message before anything else
|
||||
wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo()))
|
||||
wait_until(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo()))
|
||||
def check_node_connections(*, node, num_in, num_out):
|
||||
info = node.getnetworkinfo()
|
||||
assert_equal(info["connections_in"], num_in)
|
||||
assert_equal(info["connections_out"], num_out)
|
||||
|
||||
|
||||
# Transaction/Block functions
|
||||
#############################
|
||||
|
||||
|
||||
def find_output(node, txid, amount, *, blockhash=None):
|
||||
"""
|
||||
Return index to output of txid with value amount
|
||||
Raises exception if there is none.
|
||||
"""
|
||||
txdata = node.getrawtransaction(txid, 1, blockhash)
|
||||
for i in range(len(txdata["vout"])):
|
||||
if txdata["vout"][i]["value"] == amount:
|
||||
return i
|
||||
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))
|
||||
|
||||
|
||||
def gather_inputs(from_node, amount_needed, confirmations_required=1):
|
||||
"""
|
||||
Return a random set of unspent txouts that are enough to pay amount_needed
|
||||
"""
|
||||
assert confirmations_required >= 0
|
||||
utxo = from_node.listunspent(confirmations_required)
|
||||
random.shuffle(utxo)
|
||||
inputs = []
|
||||
total_in = Decimal("0.00000000")
|
||||
while total_in < amount_needed and len(utxo) > 0:
|
||||
t = utxo.pop()
|
||||
total_in += t["amount"]
|
||||
inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]})
|
||||
if total_in < amount_needed:
|
||||
raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in))
|
||||
return (total_in, inputs)
|
||||
|
||||
|
||||
def make_change(from_node, amount_in, amount_out, fee):
|
||||
"""
|
||||
Create change output(s), return them
|
||||
"""
|
||||
outputs = {}
|
||||
amount = amount_out + fee
|
||||
change = amount_in - amount
|
||||
if change > amount * 2:
|
||||
# Create an extra change output to break up big inputs
|
||||
change_address = from_node.getnewaddress()
|
||||
# Split change in two, being careful of rounding:
|
||||
outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
|
||||
change = amount_in - amount - outputs[change_address]
|
||||
if change > 0:
|
||||
outputs[from_node.getnewaddress()] = change
|
||||
return outputs
|
||||
|
||||
|
||||
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
|
||||
"""
|
||||
Create a random transaction.
|
||||
Returns (txid, hex-encoded-transaction-data, fee)
|
||||
"""
|
||||
from_node = random.choice(nodes)
|
||||
to_node = random.choice(nodes)
|
||||
fee = min_fee + fee_increment * random.randint(0, fee_variants)
|
||||
|
||||
(total_in, inputs) = gather_inputs(from_node, amount + fee)
|
||||
outputs = make_change(from_node, total_in, amount, fee)
|
||||
outputs[to_node.getnewaddress()] = float(amount)
|
||||
|
||||
rawtx = from_node.createrawtransaction(inputs, outputs)
|
||||
signresult = from_node.signrawtransactionwithwallet(rawtx)
|
||||
txid = from_node.sendrawtransaction(signresult["hex"], 0)
|
||||
|
||||
return (txid, signresult["hex"], fee)
|
||||
|
||||
|
||||
# Helper to create at least "count" utxos
|
||||
# Pass in a fee that is sufficient for relay and mining new transactions.
|
||||
def create_confirmed_utxos(fee, node, count):
|
||||
to_generate = int(0.5 * count) + 101
|
||||
while to_generate > 0:
|
||||
node.generate(min(25, to_generate))
|
||||
to_generate -= 25
|
||||
utxos = node.listunspent()
|
||||
iterations = count - len(utxos)
|
||||
addr1 = node.getnewaddress()
|
||||
addr2 = node.getnewaddress()
|
||||
if iterations <= 0:
|
||||
return utxos
|
||||
for i in range(iterations):
|
||||
t = utxos.pop()
|
||||
inputs = []
|
||||
inputs.append({"txid": t["txid"], "vout": t["vout"]})
|
||||
outputs = {}
|
||||
send_value = t['amount'] - fee
|
||||
outputs[addr1] = satoshi_round(send_value / 2)
|
||||
outputs[addr2] = satoshi_round(send_value / 2)
|
||||
raw_tx = node.createrawtransaction(inputs, outputs)
|
||||
signed_tx = node.signrawtransactionwithwallet(raw_tx)["hex"]
|
||||
node.sendrawtransaction(signed_tx)
|
||||
|
||||
while (node.getmempoolinfo()['size'] > 0):
|
||||
node.generate(1)
|
||||
|
||||
utxos = node.listunspent()
|
||||
assert len(utxos) >= count
|
||||
return utxos
|
||||
|
||||
|
||||
# Create large OP_RETURN txouts that can be appended to a transaction
|
||||
# to make it large (helper for constructing large transactions).
|
||||
# to make it large (helper for constructing large transactions). The
|
||||
# total serialized size of the txouts is about 66k vbytes.
|
||||
def gen_return_txouts():
|
||||
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
|
||||
# So we have big transactions (and therefore can't fit very many into each block)
|
||||
# create one script_pubkey
|
||||
script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes
|
||||
for i in range(512):
|
||||
script_pubkey = script_pubkey + "01"
|
||||
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
|
||||
txouts = []
|
||||
from .messages import CTxOut
|
||||
txout = CTxOut()
|
||||
txout.nValue = 0
|
||||
txout.scriptPubKey = hex_str_to_bytes(script_pubkey)
|
||||
for k in range(128):
|
||||
txouts.append(txout)
|
||||
from .script import CScript, OP_RETURN
|
||||
txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))]
|
||||
assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456)
|
||||
return txouts
|
||||
|
||||
|
||||
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
|
||||
# transaction to make it large. See gen_return_txouts() above.
|
||||
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
|
||||
addr = node.getnewaddress()
|
||||
def create_lots_of_big_transactions(mini_wallet, node, fee, tx_batch_size, txouts, utxos=None):
|
||||
txids = []
|
||||
from .messages import CTransaction
|
||||
for _ in range(num):
|
||||
t = utxos.pop()
|
||||
inputs = [{"txid": t["txid"], "vout": t["vout"]}]
|
||||
outputs = {}
|
||||
change = t['amount'] - fee
|
||||
outputs[addr] = satoshi_round(change)
|
||||
rawtx = node.createrawtransaction(inputs, outputs)
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(hex_str_to_bytes(rawtx)))
|
||||
for txout in txouts:
|
||||
tx.vout.append(txout)
|
||||
newtx = tx.serialize().hex()
|
||||
signresult = node.signrawtransactionwithwallet(newtx, None, "NONE")
|
||||
txid = node.sendrawtransaction(signresult["hex"], 0)
|
||||
txids.append(txid)
|
||||
use_internal_utxos = utxos is None
|
||||
for _ in range(tx_batch_size):
|
||||
tx = mini_wallet.create_self_transfer(
|
||||
utxo_to_spend=None if use_internal_utxos else utxos.pop(),
|
||||
fee=fee,
|
||||
)["tx"]
|
||||
tx.vout.extend(txouts)
|
||||
res = node.testmempoolaccept([tx.serialize().hex()])[0]
|
||||
assert_equal(res['fees']['base'], fee)
|
||||
txids.append(node.sendrawtransaction(tx.serialize().hex()))
|
||||
return txids
|
||||
|
||||
|
||||
def mine_large_block(node, utxos=None):
|
||||
def mine_large_block(test_framework, mini_wallet, node):
|
||||
# generate a 66k transaction,
|
||||
# and 14 of them is close to the 1MB block limit
|
||||
num = 14
|
||||
txouts = gen_return_txouts()
|
||||
utxos = utxos if utxos is not None else []
|
||||
if len(utxos) < num:
|
||||
utxos.clear()
|
||||
utxos.extend(node.listunspent())
|
||||
fee = 100 * node.getnetworkinfo()["relayfee"]
|
||||
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
|
||||
node.generate(1)
|
||||
create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts)
|
||||
test_framework.generate(node, 1)
|
||||
|
||||
|
||||
def find_vout_for_address(node, txid, addr):
|
||||
@@ -614,11 +545,6 @@ def find_vout_for_address(node, txid, addr):
|
||||
"""
|
||||
tx = node.getrawtransaction(txid, True)
|
||||
for i in range(len(tx["vout"])):
|
||||
scriptPubKey = tx["vout"][i]["scriptPubKey"]
|
||||
if "addresses" in scriptPubKey:
|
||||
if any([addr == a for a in scriptPubKey["addresses"]]):
|
||||
return i
|
||||
elif "address" in scriptPubKey:
|
||||
if addr == scriptPubKey["address"]:
|
||||
return i
|
||||
if addr == tx["vout"][i]["scriptPubKey"]["address"]:
|
||||
return i
|
||||
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))
|
||||
|
||||
@@ -166,6 +166,9 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API):
|
||||
def _message_received_(self, handler, msg):
|
||||
self.message_received(self.handler_to_client(handler), self, msg)
|
||||
|
||||
def _binary_message_received_(self, handler, msg):
|
||||
self.binary_message_received(self.handler_to_client(handler), self, msg)
|
||||
|
||||
def _ping_received_(self, handler, msg):
|
||||
handler.send_pong(msg)
|
||||
|
||||
@@ -309,6 +312,7 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
opcode = b1 & OPCODE
|
||||
masked = b2 & MASKED
|
||||
payload_length = b2 & PAYLOAD_LEN
|
||||
is_binary: bool = False
|
||||
|
||||
if opcode == OPCODE_CLOSE_CONN:
|
||||
logger.info("Client asked to close connection.")
|
||||
@@ -322,8 +326,8 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
logger.warning("Continuation frames are not supported.")
|
||||
return
|
||||
elif opcode == OPCODE_BINARY:
|
||||
logger.warning("Binary frames are not supported.")
|
||||
return
|
||||
is_binary = True
|
||||
opcode_handler = self.server._binary_message_received_
|
||||
elif opcode == OPCODE_TEXT:
|
||||
opcode_handler = self.server._message_received_
|
||||
elif opcode == OPCODE_PING:
|
||||
@@ -345,7 +349,8 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
for message_byte in self.read_bytes(payload_length):
|
||||
message_byte ^= masks[len(message_bytes) % 4]
|
||||
message_bytes.append(message_byte)
|
||||
opcode_handler(self, message_bytes.decode('utf8'))
|
||||
|
||||
opcode_handler(self, message_bytes if is_binary else message_bytes.decode('utf8'))
|
||||
|
||||
def send_message(self, message):
|
||||
self.send_text(message)
|
||||
@@ -375,6 +380,35 @@ class WebSocketHandler(StreamRequestHandler):
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def send_bytes(self, message, opcode=OPCODE_BINARY):
|
||||
header = bytearray()
|
||||
payload = message
|
||||
payload_length = len(payload)
|
||||
|
||||
# Normal payload
|
||||
if payload_length <= 125:
|
||||
header.append(FIN | opcode)
|
||||
header.append(payload_length)
|
||||
|
||||
# Extended payload
|
||||
elif payload_length >= 126 and payload_length <= 65535:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT16)
|
||||
header.extend(struct.pack(">H", payload_length))
|
||||
|
||||
# Huge extended payload
|
||||
elif payload_length < 18446744073709551616:
|
||||
header.append(FIN | opcode)
|
||||
header.append(PAYLOAD_LEN_EXT64)
|
||||
header.extend(struct.pack(">Q", payload_length))
|
||||
|
||||
else:
|
||||
raise Exception("Message is too big. Consider breaking it into chunks.")
|
||||
return
|
||||
|
||||
with self._send_lock:
|
||||
self.request.send(header + payload)
|
||||
|
||||
def send_text(self, message, opcode=OPCODE_TEXT):
|
||||
"""
|
||||
Important: Fragmented(=continuation) messages are not supported since
|
||||
|
||||
384
basicswap/db.py
384
basicswap/db.py
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 25
|
||||
CURRENT_DB_DATA_VERSION = 5
|
||||
CURRENT_DB_VERSION = 32
|
||||
CURRENT_DB_DATA_VERSION = 7
|
||||
|
||||
|
||||
class Concepts(IntEnum):
|
||||
@@ -174,6 +174,7 @@ class Offer(Table):
|
||||
secret_hash = Column("blob")
|
||||
|
||||
addr_from = Column("string")
|
||||
pk_from = Column("blob")
|
||||
addr_to = Column("string")
|
||||
created_at = Column("integer")
|
||||
expire_at = Column("integer")
|
||||
@@ -183,6 +184,8 @@ class Offer(Table):
|
||||
|
||||
amount_negotiable = Column("bool")
|
||||
rate_negotiable = Column("bool")
|
||||
auto_accept_type = Column("integer")
|
||||
message_nets = Column("string")
|
||||
|
||||
# Local fields
|
||||
auto_accept_bids = Column("bool")
|
||||
@@ -192,6 +195,7 @@ class Offer(Table):
|
||||
) # Address to spend lock tx to - address from wallet if empty TODO
|
||||
security_token = Column("blob")
|
||||
bid_reversed = Column("bool")
|
||||
smsg_payload_version = Column("integer")
|
||||
|
||||
state = Column("integer")
|
||||
states = Column("blob") # Packed states and times
|
||||
@@ -215,7 +219,9 @@ class Bid(Table):
|
||||
created_at = Column("integer")
|
||||
expire_at = Column("integer")
|
||||
bid_addr = Column("string")
|
||||
pk_bid_addr = Column("blob")
|
||||
proof_address = Column("string")
|
||||
proof_signature = Column("blob")
|
||||
proof_utxos = Column("blob")
|
||||
# Address to spend lock tx to - address from wallet if empty TODO
|
||||
withdraw_to_addr = Column("string")
|
||||
@@ -229,6 +235,7 @@ class Bid(Table):
|
||||
rate = Column("integer")
|
||||
|
||||
pkhash_seller = Column("blob")
|
||||
message_nets = Column("string")
|
||||
|
||||
initiate_txn_redeem = Column("blob")
|
||||
initiate_txn_refund = Column("blob")
|
||||
@@ -377,6 +384,8 @@ class SmsgAddress(Table):
|
||||
use_type = Column("integer")
|
||||
note = Column("string")
|
||||
|
||||
index = Index("smsgaddresses_address_index", "addr")
|
||||
|
||||
|
||||
class Action(Table):
|
||||
__tablename__ = "actions"
|
||||
@@ -482,6 +491,14 @@ class XmrSwap(Table):
|
||||
|
||||
b_lock_tx_id = Column("blob")
|
||||
|
||||
msg_split_info = Column("string")
|
||||
|
||||
def getMsgSplitInfo(self):
|
||||
if self.msg_split_info is None:
|
||||
return 16000, 17000
|
||||
msg_split_info = self.msg_split_info.split(":")
|
||||
return int(msg_split_info[0]), int(msg_split_info[1])
|
||||
|
||||
|
||||
class XmrSplitData(Table):
|
||||
__tablename__ = "xmr_split_data"
|
||||
@@ -602,6 +619,8 @@ class BidState(Table):
|
||||
swap_failed = Column("integer")
|
||||
swap_ended = Column("integer")
|
||||
can_accept = Column("integer")
|
||||
can_expire = Column("integer")
|
||||
can_timeout = Column("integer")
|
||||
|
||||
note = Column("string")
|
||||
created_at = Column("integer")
|
||||
@@ -644,82 +663,268 @@ class CheckedBlock(Table):
|
||||
block_time = Column("integer")
|
||||
|
||||
|
||||
class CoinRates(Table):
|
||||
__tablename__ = "coinrates"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
currency_from = Column("integer")
|
||||
currency_to = Column("integer")
|
||||
rate = Column("string")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class CoinVolume(Table):
|
||||
__tablename__ = "coinvolume"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_id = Column("integer")
|
||||
volume_24h = Column("string")
|
||||
price_change_24h = Column("string")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class CoinHistory(Table):
|
||||
__tablename__ = "coinhistory"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
coin_id = Column("integer")
|
||||
days = Column("integer")
|
||||
price_data = Column("blob")
|
||||
source = Column("string")
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class MessageNetworks(Table):
|
||||
__tablename__ = "message_networks"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
name = Column("string")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class MessageNetworkLink(Table):
|
||||
__tablename__ = "message_network_links"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
|
||||
network_id = Column("string")
|
||||
link_type = Column("integer") # MessageNetworkLinkTypes
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRoute(Table):
|
||||
__tablename__ = "direct_message_routes"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
network_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
smsg_addr_local = Column("string")
|
||||
smsg_addr_remote = Column("string")
|
||||
# smsg_addr_id_local = Column("integer") # SmsgAddress
|
||||
# smsg_addr_id_remote = Column("integer") # KnownIdentity
|
||||
route_data = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRouteLink(Table):
|
||||
__tablename__ = "direct_message_route_links"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
direct_message_route_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class NetworkPortal(Table):
|
||||
__tablename__ = "network_portals"
|
||||
|
||||
def set(
|
||||
self, time_start, time_valid, network_from, network_to, address_from, address_to
|
||||
):
|
||||
super().__init__()
|
||||
self.active_ind = 1
|
||||
self.time_start = time_start
|
||||
self.time_valid = time_valid
|
||||
self.network_from = network_from
|
||||
self.network_to = network_to
|
||||
self.address_from = address_from
|
||||
self.address_to = address_to
|
||||
|
||||
self.smsg_difficulty = 0x1EFFFFFF
|
||||
|
||||
self.num_refreshes = 0
|
||||
self.messages_sent = 0
|
||||
self.responses_seen = 0
|
||||
self.time_last_used = 0
|
||||
self.num_issues = 0
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
own_portal = Column("integer")
|
||||
|
||||
address_from = Column("string", unique=True)
|
||||
address_to = Column("string")
|
||||
|
||||
network_from = Column("integer")
|
||||
network_to = Column("integer")
|
||||
|
||||
time_start = Column("integer")
|
||||
time_valid = Column("integer")
|
||||
smsg_difficulty = Column("integer")
|
||||
num_refreshes = Column("integer")
|
||||
|
||||
messages_sent = Column("integer")
|
||||
responses_seen = Column("integer")
|
||||
time_last_used = Column("integer")
|
||||
num_issues = Column("integer")
|
||||
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
def extract_schema() -> dict:
|
||||
g = globals().copy()
|
||||
tables = {}
|
||||
for name, obj in g.items():
|
||||
if not inspect.isclass(obj):
|
||||
continue
|
||||
if not hasattr(obj, "__sqlite3_table__"):
|
||||
continue
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
continue
|
||||
|
||||
table_name: str = obj.__tablename__
|
||||
table = {}
|
||||
columns = {}
|
||||
primary_key = None
|
||||
constraints = []
|
||||
indices = []
|
||||
for m in inspect.getmembers(obj):
|
||||
m_name, m_obj = m
|
||||
if hasattr(m_obj, "__sqlite3_primary_key__"):
|
||||
primary_key = m_obj
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_unique__"):
|
||||
constraints.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_index__"):
|
||||
indices.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_column__"):
|
||||
col_type: str = m_obj.column_type.upper()
|
||||
if col_type == "BOOL":
|
||||
col_type = "INTEGER"
|
||||
columns[m_name] = {
|
||||
"type": col_type,
|
||||
"primary_key": m_obj.primary_key,
|
||||
"unique": m_obj.unique,
|
||||
}
|
||||
table["columns"] = columns
|
||||
if primary_key is not None:
|
||||
table["primary_key"] = {"column_1": primary_key.column_1}
|
||||
if primary_key.column_2:
|
||||
table["primary_key"]["column_2"] = primary_key.column_2
|
||||
if primary_key.column_3:
|
||||
table["primary_key"]["column_3"] = primary_key.column_3
|
||||
|
||||
for constraint in constraints:
|
||||
if "constraints" not in table:
|
||||
table["constraints"] = []
|
||||
table_constraint = {"column_1": constraint.column_1}
|
||||
if constraint.column_2:
|
||||
table_constraint["column_2"] = constraint.column_2
|
||||
if constraint.column_3:
|
||||
table_constraint["column_3"] = constraint.column_3
|
||||
table["constraints"].append(table_constraint)
|
||||
|
||||
for i in indices:
|
||||
if "indices" not in table:
|
||||
table["indices"] = []
|
||||
table_index = {"index_name": i.name, "column_1": i.column_1}
|
||||
if i.column_2 is not None:
|
||||
table_index["column_2"] = i.column_2
|
||||
if i.column_3 is not None:
|
||||
table_index["column_3"] = i.column_3
|
||||
table["indices"].append(table_index)
|
||||
|
||||
tables[table_name] = table
|
||||
return tables
|
||||
|
||||
|
||||
def create_table(c, table_name, table) -> None:
|
||||
query: str = f"CREATE TABLE {table_name} ("
|
||||
|
||||
for i, (colname, column) in enumerate(table["columns"].items()):
|
||||
col_type = column["type"]
|
||||
query += ("," if i > 0 else "") + f" {colname} {col_type} "
|
||||
if column["primary_key"]:
|
||||
query += "PRIMARY KEY ASC "
|
||||
if column["unique"]:
|
||||
query += "UNIQUE "
|
||||
|
||||
if "primary_key" in table:
|
||||
column_1 = table["primary_key"]["column_1"]
|
||||
column_2 = table["primary_key"].get("column_2", None)
|
||||
column_3 = table["primary_key"].get("column_3", None)
|
||||
query += f", PRIMARY KEY ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ") "
|
||||
|
||||
constraints = table.get("constraints", [])
|
||||
for constraint in constraints:
|
||||
column_1 = constraint["column_1"]
|
||||
column_2 = constraint.get("column_2", None)
|
||||
column_3 = constraint.get("column_3", None)
|
||||
query += f", UNIQUE ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ") "
|
||||
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
indices = table.get("indices", [])
|
||||
for index in indices:
|
||||
index_name = index["index_name"]
|
||||
column_1 = index["column_1"]
|
||||
column_2 = index.get("column_2", None)
|
||||
column_3 = index.get("column_3", None)
|
||||
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
|
||||
def create_db_(con, log) -> None:
|
||||
db_schema = extract_schema()
|
||||
c = con.cursor()
|
||||
for table_name, table in db_schema.items():
|
||||
create_table(c, table_name, table)
|
||||
|
||||
|
||||
def create_db(db_path: str, log) -> None:
|
||||
con = None
|
||||
try:
|
||||
con = sqlite3.connect(db_path)
|
||||
c = con.cursor()
|
||||
|
||||
g = globals().copy()
|
||||
for name, obj in g.items():
|
||||
if not inspect.isclass(obj):
|
||||
continue
|
||||
if not hasattr(obj, "__sqlite3_table__"):
|
||||
continue
|
||||
if not hasattr(obj, "__tablename__"):
|
||||
continue
|
||||
|
||||
table_name: str = obj.__tablename__
|
||||
query: str = f"CREATE TABLE {table_name} ("
|
||||
|
||||
primary_key = None
|
||||
constraints = []
|
||||
indices = []
|
||||
num_columns: int = 0
|
||||
for m in inspect.getmembers(obj):
|
||||
m_name, m_obj = m
|
||||
|
||||
if hasattr(m_obj, "__sqlite3_primary_key__"):
|
||||
primary_key = m_obj
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_unique__"):
|
||||
constraints.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_index__"):
|
||||
indices.append(m_obj)
|
||||
continue
|
||||
if hasattr(m_obj, "__sqlite3_column__"):
|
||||
if num_columns > 0:
|
||||
query += ","
|
||||
|
||||
col_type: str = m_obj.column_type.upper()
|
||||
if col_type == "BOOL":
|
||||
col_type = "INTEGER"
|
||||
query += f" {m_name} {col_type} "
|
||||
|
||||
if m_obj.primary_key:
|
||||
query += "PRIMARY KEY ASC "
|
||||
if m_obj.unique:
|
||||
query += "UNIQUE "
|
||||
num_columns += 1
|
||||
|
||||
if primary_key is not None:
|
||||
query += f", PRIMARY KEY ({primary_key.column_1}"
|
||||
if primary_key.column_2:
|
||||
query += f", {primary_key.column_2}"
|
||||
if primary_key.column_3:
|
||||
query += f", {primary_key.column_3}"
|
||||
query += ") "
|
||||
|
||||
for constraint in constraints:
|
||||
query += f", UNIQUE ({constraint.column_1}"
|
||||
if constraint.column_2:
|
||||
query += f", {constraint.column_2}"
|
||||
if constraint.column_3:
|
||||
query += f", {constraint.column_3}"
|
||||
query += ") "
|
||||
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
for i in indices:
|
||||
query: str = f"CREATE INDEX {i.name} ON {table_name} ({i.column_1}"
|
||||
if i.column_2 is not None:
|
||||
query += f", {i.column_2}"
|
||||
if i.column_3 is not None:
|
||||
query += f", {i.column_3}"
|
||||
query += ")"
|
||||
c.execute(query)
|
||||
|
||||
create_db_(con, log)
|
||||
con.commit()
|
||||
finally:
|
||||
if con:
|
||||
@@ -898,6 +1103,7 @@ class DBMethods:
|
||||
query += f"{key}=:{key}"
|
||||
|
||||
cursor.execute(query, values)
|
||||
return cursor.lastrowid
|
||||
|
||||
def query(
|
||||
self,
|
||||
@@ -915,15 +1121,12 @@ class DBMethods:
|
||||
table_name: str = table_class.__tablename__
|
||||
|
||||
query: str = "SELECT "
|
||||
|
||||
columns = []
|
||||
|
||||
for mc in inspect.getmembers(table_class):
|
||||
mc_name, mc_obj = mc
|
||||
|
||||
if not hasattr(mc_obj, "__sqlite3_column__"):
|
||||
continue
|
||||
|
||||
if len(columns) > 0:
|
||||
query += ", "
|
||||
query += mc_name
|
||||
@@ -931,10 +1134,32 @@ class DBMethods:
|
||||
|
||||
query += f" FROM {table_name} WHERE 1=1 "
|
||||
|
||||
query_data = {}
|
||||
for ck in constraints:
|
||||
if not validColumnName(ck):
|
||||
raise ValueError(f"Invalid constraint column: {ck}")
|
||||
query += f" AND {ck} = :{ck} "
|
||||
|
||||
constraint_value = constraints[ck]
|
||||
if isinstance(constraint_value, tuple) or isinstance(
|
||||
constraint_value, list
|
||||
):
|
||||
if len(constraint_value) < 2:
|
||||
raise ValueError(f"Too few constraint values for list: {ck}")
|
||||
query += f" AND {ck} IN ("
|
||||
|
||||
for i, cv in enumerate(constraint_value):
|
||||
cv_name: str = f"{ck}_{i}"
|
||||
if i > 0:
|
||||
query += ","
|
||||
query += ":" + cv_name
|
||||
query_data[cv_name] = cv
|
||||
query += ") "
|
||||
else:
|
||||
if constraint_value is None:
|
||||
query += f" AND {ck} IS NULL "
|
||||
else:
|
||||
query += f" AND {ck} = :{ck} "
|
||||
query_data[ck] = constraint_value
|
||||
|
||||
for order_col, order_dir in order_by.items():
|
||||
if validColumnName(order_col) is False:
|
||||
@@ -947,7 +1172,6 @@ class DBMethods:
|
||||
if query_suffix:
|
||||
query += query_suffix
|
||||
|
||||
query_data = constraints.copy()
|
||||
query_data.update(extra_query_data)
|
||||
rows = cursor.execute(query, query_data)
|
||||
for row in rows:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -12,13 +12,17 @@ from .db import (
|
||||
AutomationStrategy,
|
||||
BidState,
|
||||
Concepts,
|
||||
create_table,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
CURRENT_DB_VERSION,
|
||||
extract_schema,
|
||||
)
|
||||
|
||||
from .basicswap_util import (
|
||||
BidStates,
|
||||
canAcceptBidState,
|
||||
canExpireBidState,
|
||||
canTimeoutBidState,
|
||||
isActiveBidState,
|
||||
isErrorBidState,
|
||||
isFailingBidState,
|
||||
@@ -37,6 +41,8 @@ def addBidState(self, state, now, cursor):
|
||||
swap_failed=isFailingBidState(state),
|
||||
swap_ended=isFinalBidState(state),
|
||||
can_accept=canAcceptBidState(state),
|
||||
can_expire=canExpireBidState(state),
|
||||
can_timeout=canTimeoutBidState(state),
|
||||
label=strBidState(state),
|
||||
created_at=now,
|
||||
),
|
||||
@@ -49,10 +55,9 @@ def upgradeDatabaseData(self, data_version):
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
"Upgrading database records from version %d to %d.",
|
||||
data_version,
|
||||
CURRENT_DB_DATA_VERSION,
|
||||
f"Upgrading database records from version {data_version} to {CURRENT_DB_DATA_VERSION}."
|
||||
)
|
||||
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
now = int(time.time())
|
||||
@@ -64,7 +69,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept All",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=False,
|
||||
created_at=now,
|
||||
@@ -77,7 +82,7 @@ def upgradeDatabaseData(self, data_version):
|
||||
label="Accept Known",
|
||||
type_ind=Concepts.OFFER,
|
||||
data=json.dumps(
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 5}
|
||||
{"exact_rate_only": True, "max_concurrent_bids": 1}
|
||||
).encode("utf-8"),
|
||||
only_known_identities=True,
|
||||
note="Accept bids from identities with previously successful swaps only",
|
||||
@@ -104,17 +109,23 @@ def upgradeDatabaseData(self, data_version):
|
||||
),
|
||||
cursor,
|
||||
)
|
||||
if data_version > 0 and data_version < 3:
|
||||
if data_version > 0 and data_version < 7:
|
||||
for state in BidStates:
|
||||
in_error = isErrorBidState(state)
|
||||
swap_failed = isFailingBidState(state)
|
||||
swap_ended = isFinalBidState(state)
|
||||
can_accept = canAcceptBidState(state)
|
||||
can_expire = canExpireBidState(state)
|
||||
can_timeout = canTimeoutBidState(state)
|
||||
cursor.execute(
|
||||
"UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
|
||||
"UPDATE bidstates SET can_accept = :can_accept, can_expire = :can_expire, can_timeout = :can_timeout, 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,
|
||||
"can_accept": can_accept,
|
||||
"can_expire": can_expire,
|
||||
"can_timeout": can_timeout,
|
||||
"state_id": int(state),
|
||||
},
|
||||
)
|
||||
@@ -136,292 +147,137 @@ def upgradeDatabaseData(self, data_version):
|
||||
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)
|
||||
)
|
||||
self.log.info(f"Upgraded database records to version {self.db_data_version}")
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
|
||||
def upgradeDatabase(self, db_version):
|
||||
if db_version >= CURRENT_DB_VERSION:
|
||||
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
# db_version, tablename, oldcolumnname, newcolumnname
|
||||
rename_columns = [
|
||||
(13, "actions", "event_id", "action_id"),
|
||||
(13, "actions", "event_type", "action_type"),
|
||||
(13, "actions", "event_data", "action_data"),
|
||||
(
|
||||
14,
|
||||
"xmr_swaps",
|
||||
"coin_a_lock_refund_spend_tx_msg_id",
|
||||
"coin_a_lock_spend_tx_msg_id",
|
||||
),
|
||||
]
|
||||
|
||||
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")
|
||||
expect_schema = extract_schema()
|
||||
have_tables = {}
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
|
||||
for rename_column in rename_columns:
|
||||
dbv, table_name, colname_from, colname_to = rename_column
|
||||
if db_version < dbv:
|
||||
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"
|
||||
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
|
||||
)
|
||||
|
||||
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))"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE automationlinks (
|
||||
record_id INTEGER NOT NULL,
|
||||
active_ind INTEGER,
|
||||
|
||||
linked_type INTEGER,
|
||||
linked_id BLOB,
|
||||
strategy_id INTEGER,
|
||||
|
||||
data BLOB,
|
||||
repeat_limit INTEGER,
|
||||
repeat_count INTEGER,
|
||||
|
||||
note VARCHAR,
|
||||
created_at BIGINT,
|
||||
PRIMARY KEY (record_id))"""
|
||||
)
|
||||
|
||||
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))"""
|
||||
)
|
||||
|
||||
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))"""
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
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))
|
||||
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
||||
tables = cursor.execute(query).fetchall()
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
if table_name in ("sqlite_sequence",):
|
||||
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.")
|
||||
have_table = {}
|
||||
have_columns = {}
|
||||
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
|
||||
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
|
||||
for column in columns:
|
||||
cid, name, data_type, notnull, default_value, primary_key = column
|
||||
have_columns[name] = {"type": data_type, "primary_key": primary_key}
|
||||
|
||||
have_table["columns"] = have_columns
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
|
||||
indices = cursor.fetchall()
|
||||
for index in indices:
|
||||
seq, index_name, unique, origin, partial = index
|
||||
|
||||
if origin == "pk": # Created by a PRIMARY KEY constraint
|
||||
continue
|
||||
|
||||
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
|
||||
index_info = cursor.fetchall()
|
||||
|
||||
add_index = {"index_name": index_name}
|
||||
for index_columns in index_info:
|
||||
seqno, cid, name = index_columns
|
||||
if origin == "u": # Created by a UNIQUE constraint
|
||||
have_columns[name]["unique"] = 1
|
||||
else:
|
||||
if "column_1" not in add_index:
|
||||
add_index["column_1"] = name
|
||||
elif "column_2" not in add_index:
|
||||
add_index["column_2"] = name
|
||||
elif "column_3" not in add_index:
|
||||
add_index["column_3"] = name
|
||||
else:
|
||||
raise RuntimeError("Add more index columns.")
|
||||
if origin == "c":
|
||||
if "indices" not in table:
|
||||
have_table["indices"] = []
|
||||
have_table["indices"].append(add_index)
|
||||
|
||||
have_tables[table_name] = have_table
|
||||
|
||||
for table_name, table in expect_schema.items():
|
||||
if table_name not in have_tables:
|
||||
self.log.info(f"Creating table {table_name}.")
|
||||
create_table(cursor, table_name, table)
|
||||
continue
|
||||
|
||||
have_table = have_tables[table_name]
|
||||
have_columns = have_table["columns"]
|
||||
for colname, column in table["columns"].items():
|
||||
if colname not in have_columns:
|
||||
col_type = column["type"]
|
||||
self.log.info(f"Adding column {colname} to table {table_name}.")
|
||||
cursor.execute(
|
||||
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
|
||||
)
|
||||
indices = table.get("indices", [])
|
||||
have_indices = have_table.get("indices", [])
|
||||
for index in indices:
|
||||
index_name = index["index_name"]
|
||||
if not any(
|
||||
have_idx.get("index_name") == index_name
|
||||
for have_idx in have_indices
|
||||
):
|
||||
self.log.info(f"Adding index {index_name} to table {table_name}.")
|
||||
column_1 = index["column_1"]
|
||||
column_2 = index.get("column_2", None)
|
||||
column_3 = index.get("column_3", None)
|
||||
query: str = (
|
||||
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
|
||||
)
|
||||
if column_2:
|
||||
query += f", {column_2}"
|
||||
if column_3:
|
||||
query += f", {column_3}"
|
||||
query += ")"
|
||||
cursor.execute(query)
|
||||
|
||||
if CURRENT_DB_VERSION != db_version:
|
||||
self.db_version = CURRENT_DB_VERSION
|
||||
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
|
||||
self.log.info(f"Upgraded database to version {self.db_version}")
|
||||
self.commitDB()
|
||||
except Exception as e:
|
||||
self.log.error(f"Upgrade failed {e}")
|
||||
self.rollbackDB()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023-2024 The Basicswap Developers
|
||||
# Copyright (c) 2023-2025 The Basicswap Developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -26,93 +26,46 @@ def remove_expired_data(self, time_offset: int = 0):
|
||||
)
|
||||
for offer_row in offer_rows:
|
||||
num_offers += 1
|
||||
offer_query_data = {
|
||||
"type_ind": int(Concepts.OFFER),
|
||||
"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]},
|
||||
offer_query_data,
|
||||
)
|
||||
for bid_row in bid_rows:
|
||||
num_bids += 1
|
||||
cursor.execute(
|
||||
bid_query_data = {"type_ind": int(Concepts.BID), "bid_id": bid_row[0]}
|
||||
for query_str in [
|
||||
"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]},
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :bid_id",
|
||||
]:
|
||||
cursor.execute(query_str, bid_query_data)
|
||||
for query_str in [
|
||||
"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]},
|
||||
)
|
||||
"DELETE FROM message_network_links WHERE linked_type = :type_ind AND linked_id = :offer_id",
|
||||
]:
|
||||
cursor.execute(query_str, offer_query_data)
|
||||
|
||||
if num_offers > 0 or num_bids > 0:
|
||||
self.log.info(
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import secrets
|
||||
import hashlib
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
|
||||
|
||||
def get_secret():
|
||||
return 9 + secrets.randbelow(edf.l - 9)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def hashToEd25519(bytes_in):
|
||||
hashed = hashlib.sha256(bytes_in).digest()
|
||||
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")
|
||||
x = edf.xrecover(y, x_sign)
|
||||
if x == 0 and y == 1: # Skip infinity point
|
||||
continue
|
||||
|
||||
P = [x, y, 1, (x * y) % edf.q]
|
||||
# Keep trying until the point is in the correct subgroup
|
||||
if edf.isoncurve(P) and edf.is_identity(edf.scalarmult(P, edf.l)):
|
||||
return P
|
||||
hashed = hashlib.sha256(hashed).digest()
|
||||
raise ValueError("hashToEd25519 failed")
|
||||
@@ -1,12 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2023 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
|
||||
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
class Explorer:
|
||||
def __init__(self, swapclient, coin_type, base_url):
|
||||
self.swapclient = swapclient
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
import secrets
|
||||
import traceback
|
||||
import threading
|
||||
import http.client
|
||||
from urllib import parse
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import base64
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from socket import error as SocketError
|
||||
from urllib import parse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from . import __version__
|
||||
from .util import (
|
||||
dumpj,
|
||||
toBool,
|
||||
LockedCoinError,
|
||||
format_timestamp,
|
||||
)
|
||||
from .chainparams import (
|
||||
@@ -30,6 +35,7 @@ from .basicswap_util import (
|
||||
strTxState,
|
||||
strBidState,
|
||||
)
|
||||
from .util.rfc2440 import verify_rfc2440_password
|
||||
|
||||
from .js_server import (
|
||||
js_error,
|
||||
@@ -47,6 +53,7 @@ from .ui.page_automation import (
|
||||
page_automation_strategy_new,
|
||||
)
|
||||
|
||||
from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api
|
||||
from .ui.page_bids import page_bids, page_bid
|
||||
from .ui.page_offers import page_offers, page_offer, page_newoffer
|
||||
from .ui.page_tor import page_tor, get_tor_established_state
|
||||
@@ -57,6 +64,9 @@ from .ui.page_identity import page_identity
|
||||
from .ui.page_smsgaddresses import page_smsgaddresses
|
||||
from .ui.page_debug import page_debug
|
||||
|
||||
SESSION_COOKIE_NAME = "basicswap_session_id"
|
||||
SESSION_DURATION_MINUTES = 60
|
||||
|
||||
env = Environment(loader=PackageLoader("basicswap", "templates"))
|
||||
env.filters["formatts"] = format_timestamp
|
||||
|
||||
@@ -119,6 +129,58 @@ def parse_cmd(cmd: str, type_map: str):
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
def _get_session_cookie(self):
|
||||
if "Cookie" in self.headers:
|
||||
cookie = SimpleCookie(self.headers["Cookie"])
|
||||
if SESSION_COOKIE_NAME in cookie:
|
||||
return cookie[SESSION_COOKIE_NAME].value
|
||||
return None
|
||||
|
||||
def _set_session_cookie(self, session_id):
|
||||
cookie = SimpleCookie()
|
||||
cookie[SESSION_COOKIE_NAME] = session_id
|
||||
cookie[SESSION_COOKIE_NAME]["path"] = "/"
|
||||
cookie[SESSION_COOKIE_NAME]["httponly"] = True
|
||||
cookie[SESSION_COOKIE_NAME]["samesite"] = "Lax"
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
cookie[SESSION_COOKIE_NAME]["expires"] = expires.strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT"
|
||||
)
|
||||
return ("Set-Cookie", cookie.output(header="").strip())
|
||||
|
||||
def _clear_session_cookie(self):
|
||||
cookie = SimpleCookie()
|
||||
cookie[SESSION_COOKIE_NAME] = ""
|
||||
cookie[SESSION_COOKIE_NAME]["path"] = "/"
|
||||
cookie[SESSION_COOKIE_NAME]["httponly"] = True
|
||||
cookie[SESSION_COOKIE_NAME]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
return ("Set-Cookie", cookie.output(header="").strip())
|
||||
|
||||
def is_authenticated(self):
|
||||
swap_client = self.server.swap_client
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
|
||||
if not client_auth_hash:
|
||||
return True
|
||||
|
||||
session_id = self._get_session_cookie()
|
||||
if not session_id:
|
||||
return False
|
||||
|
||||
with self.server.session_lock:
|
||||
session_data = self.server.active_sessions.get(session_id)
|
||||
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
||||
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
return True
|
||||
|
||||
if session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
return False
|
||||
|
||||
def log_error(self, format, *args):
|
||||
super().log_message(format, *args)
|
||||
|
||||
@@ -134,14 +196,20 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
form_data = parse.parse_qs(post_string)
|
||||
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))
|
||||
return None
|
||||
self.server.last_form_id[name] = form_id
|
||||
with self.server.form_id_lock:
|
||||
if self.server.last_form_id.get(name, None) == 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__
|
||||
self,
|
||||
template,
|
||||
args_dict,
|
||||
status_code=200,
|
||||
version=__version__,
|
||||
extra_headers=None,
|
||||
):
|
||||
swap_client = self.server.swap_client
|
||||
if swap_client.ws_server:
|
||||
@@ -150,48 +218,86 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["debug_mode"] = True
|
||||
if swap_client.debug_ui:
|
||||
args_dict["debug_ui_mode"] = True
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
# TODO: Cache value?
|
||||
try:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
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()
|
||||
is_authenticated = self.is_authenticated() or not swap_client.settings.get(
|
||||
"client_auth_hash"
|
||||
)
|
||||
|
||||
if is_authenticated:
|
||||
if swap_client.use_tor_proxy:
|
||||
args_dict["use_tor_proxy"] = True
|
||||
try:
|
||||
tor_state = get_tor_established_state(swap_client)
|
||||
args_dict["tor_established"] = True if tor_state == "1" else False
|
||||
except Exception:
|
||||
args_dict["tor_established"] = False
|
||||
|
||||
from .ui.page_amm import get_amm_status, get_amm_active_count
|
||||
|
||||
try:
|
||||
args_dict["current_status"] = get_amm_status()
|
||||
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
|
||||
except Exception:
|
||||
args_dict["current_status"] = "stopped"
|
||||
args_dict["amm_active_count"] = 0
|
||||
|
||||
if swap_client._show_notifications:
|
||||
args_dict["notifications"] = swap_client.getNotifications()
|
||||
else:
|
||||
args_dict["current_status"] = "unknown"
|
||||
args_dict["amm_active_count"] = 0
|
||||
|
||||
if "messages" in args_dict:
|
||||
messages_with_ids = []
|
||||
for msg in args_dict["messages"]:
|
||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
with self.server.msg_id_lock:
|
||||
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:
|
||||
err_messages_with_ids = []
|
||||
for msg in args_dict["err_messages"]:
|
||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||
self.server.msg_id_counter += 1
|
||||
with self.server.msg_id_lock:
|
||||
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
|
||||
|
||||
if self.path:
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split("/")
|
||||
if len(url_split) > 1 and url_split[1]:
|
||||
args_dict["current_page"] = url_split[1]
|
||||
else:
|
||||
args_dict["current_page"] = "index"
|
||||
else:
|
||||
args_dict["current_page"] = "index"
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
with self.server.session_lock:
|
||||
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
|
||||
if is_authenticated:
|
||||
try:
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
except Exception as e:
|
||||
args_dict["encrypted"] = False
|
||||
args_dict["locked"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.warning(f"Could not get wallet locked state: {e}")
|
||||
else:
|
||||
args_dict["encrypted"] = args_dict.get("encrypted", False)
|
||||
args_dict["locked"] = args_dict.get("locked", False)
|
||||
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
with self.server.msg_id_lock:
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
|
||||
args_dict["version"] = version
|
||||
|
||||
self.putHeaders(status_code, "text/html")
|
||||
self.putHeaders(status_code, "text/html", extra_headers=extra_headers)
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
@@ -203,6 +309,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
|
||||
def render_simple_template(self, template, args_dict):
|
||||
self.putHeaders(200, "text/html")
|
||||
return bytes(
|
||||
template.render(
|
||||
title=self.server.title,
|
||||
@@ -211,7 +318,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
def page_info(self, info_str, post_string=None):
|
||||
def page_info(self, info_str, post_string=None, extra_headers=None):
|
||||
template = env.get_template("info.html")
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
@@ -222,6 +329,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
"message_str": info_str,
|
||||
"summary": summary,
|
||||
},
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
def page_error(self, error_str, post_string=None):
|
||||
@@ -237,6 +345,100 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
},
|
||||
)
|
||||
|
||||
def page_login(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
template = env.get_template("login.html")
|
||||
err_messages = []
|
||||
extra_headers = []
|
||||
is_json_request = "application/json" in self.headers.get("Content-Type", "")
|
||||
security_warning = None
|
||||
if self.server.host_name not in ("127.0.0.1", "localhost"):
|
||||
security_warning = "WARNING: Server is accessible on the network. Sending password over plain HTTP is insecure. Use HTTPS (e.g., via reverse proxy) for non-local access."
|
||||
if not is_json_request:
|
||||
err_messages.append(security_warning)
|
||||
|
||||
if post_string:
|
||||
password = None
|
||||
if is_json_request:
|
||||
try:
|
||||
json_data = json.loads(post_string.decode("utf-8"))
|
||||
password = json_data.get("password")
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error parsing JSON login data: {e}")
|
||||
else:
|
||||
try:
|
||||
form_data = parse.parse_qs(post_string.decode("utf-8"))
|
||||
password = form_data.get("password", [None])[0]
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error parsing form login data: {e}")
|
||||
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
|
||||
if (
|
||||
client_auth_hash
|
||||
and password is not None
|
||||
and verify_rfc2440_password(client_auth_hash, password)
|
||||
):
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
with self.server.session_lock:
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
cookie_header = self._set_session_cookie(session_id)
|
||||
|
||||
if is_json_request:
|
||||
response_data = {"success": True, "session_id": session_id}
|
||||
if security_warning:
|
||||
response_data["warning"] = security_warning
|
||||
self.putHeaders(
|
||||
200, "application/json", extra_headers=[cookie_header]
|
||||
)
|
||||
return json.dumps(response_data).encode("utf-8")
|
||||
else:
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/offers")
|
||||
self.send_header(cookie_header[0], cookie_header[1])
|
||||
self.end_headers()
|
||||
return b""
|
||||
else:
|
||||
if is_json_request:
|
||||
self.putHeaders(401, "application/json")
|
||||
return json.dumps({"error": "Invalid password"}).encode("utf-8")
|
||||
else:
|
||||
err_messages.append("Invalid password.")
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
extra_headers.append(clear_cookie_header)
|
||||
|
||||
if (
|
||||
not is_json_request
|
||||
and swap_client.settings.get("client_auth_hash")
|
||||
and self.is_authenticated()
|
||||
):
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/offers")
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"title_str": "Login",
|
||||
"err_messages": err_messages,
|
||||
"summary": {},
|
||||
"encrypted": False,
|
||||
"locked": False,
|
||||
},
|
||||
status_code=401 if post_string and not is_json_request else 200,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
def page_shutdown_ping(self, url_split, post_string):
|
||||
if not self.server.stop_event.is_set():
|
||||
raise ValueError("Unexpected shutdown ping.")
|
||||
self.putHeaders(401, "application/json")
|
||||
return json.dumps({"ack": True}).encode("utf-8")
|
||||
|
||||
def page_explorers(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -250,14 +452,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
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 = get_data_entry(form_data, "explorer")
|
||||
action = get_data_entry(form_data, "action")
|
||||
args = get_data_entry_or(form_data, "args", "")
|
||||
|
||||
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)]
|
||||
@@ -410,7 +608,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"refresh": 30,
|
||||
"active_swaps": [
|
||||
(
|
||||
s[0].hex(),
|
||||
@@ -447,16 +644,52 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def page_shutdown(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
extra_headers = []
|
||||
|
||||
if len(url_split) > 2:
|
||||
token = url_split[2]
|
||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||
with self.server.session_lock:
|
||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||
if token != expect_token:
|
||||
return self.page_info("Unexpected token, still running.")
|
||||
|
||||
session_id = self._get_session_cookie()
|
||||
with self.server.session_lock:
|
||||
if session_id and session_id in self.server.active_sessions:
|
||||
del self.server.active_sessions[session_id]
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
extra_headers.append(clear_cookie_header)
|
||||
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
swap_client.log.info("Web shutdown stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
swap_client.log.info(f"AMM web shutdown: {msg}")
|
||||
else:
|
||||
swap_client.log.warning(f"AMM web shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error stopping AMM in web shutdown: {e}")
|
||||
|
||||
swap_client.stopRunning()
|
||||
|
||||
return self.page_info("Shutting down")
|
||||
return self.page_info("Shutting down", extra_headers=extra_headers)
|
||||
|
||||
def page_donation(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
template = env.get_template("donation.html")
|
||||
return self.render_template(
|
||||
template,
|
||||
{
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
def page_index(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
@@ -477,41 +710,109 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
},
|
||||
)
|
||||
|
||||
def putHeaders(self, status_code, content_type):
|
||||
def putHeaders(self, status_code, content_type, extra_headers=None):
|
||||
self.send_response(status_code)
|
||||
if self.server.allow_cors:
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Content-Type", content_type)
|
||||
if extra_headers:
|
||||
for header_tuple in extra_headers:
|
||||
self.send_header(header_tuple[0], header_tuple[1])
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string="", is_json=False):
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
swap_client = self.server.swap_client
|
||||
parsed = parse.urlparse(self.path)
|
||||
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":
|
||||
page = url_split[1] if len(url_split) > 1 else ""
|
||||
|
||||
exempt_pages = ["login", "static", "error", "info"]
|
||||
auth_header = self.headers.get("Authorization")
|
||||
basic_auth_ok = False
|
||||
|
||||
if auth_header and auth_header.startswith("Basic "):
|
||||
try:
|
||||
self.putHeaders(status_code, "text/plain")
|
||||
encoded_creds = auth_header.split(" ", 1)[1]
|
||||
decoded_creds = base64.b64decode(encoded_creds).decode("utf-8")
|
||||
_, password = decoded_creds.split(":", 1)
|
||||
|
||||
client_auth_hash = swap_client.settings.get("client_auth_hash")
|
||||
if client_auth_hash and verify_rfc2440_password(
|
||||
client_auth_hash, password
|
||||
):
|
||||
basic_auth_ok = True
|
||||
else:
|
||||
self.send_response(401)
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Invalid Basic Auth credentials"}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
)
|
||||
return b""
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error processing Basic Auth header: {e}")
|
||||
self.send_response(401)
|
||||
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Malformed Basic Auth header"}).encode("utf-8")
|
||||
)
|
||||
return b""
|
||||
|
||||
if not basic_auth_ok and page not in exempt_pages:
|
||||
if not self.is_authenticated():
|
||||
if page == "json":
|
||||
self.putHeaders(401, "application/json")
|
||||
self.wfile.write(
|
||||
json.dumps({"error": "Unauthorized"}).encode("utf-8")
|
||||
)
|
||||
return b""
|
||||
else:
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/login")
|
||||
clear_cookie_header = self._clear_session_cookie()
|
||||
self.send_header(clear_cookie_header[0], clear_cookie_header[1])
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
if not post_string and len(parsed.query) > 0:
|
||||
post_string = parsed.query
|
||||
|
||||
if page == "json":
|
||||
try:
|
||||
self.putHeaders(status_code, "json")
|
||||
func = js_url_to_function(url_split)
|
||||
return func(self, url_split, post_string, is_json)
|
||||
except Exception as ex:
|
||||
if swap_client.debug is True:
|
||||
if isinstance(ex, LockedCoinError):
|
||||
clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked"
|
||||
swap_client.log.warning(clean_msg)
|
||||
return js_error(self, clean_msg)
|
||||
elif swap_client.debug is True:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
return js_error(self, str(ex))
|
||||
|
||||
if len(url_split) > 1 and url_split[1] == "static":
|
||||
if page == "static":
|
||||
try:
|
||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||
content = None
|
||||
mime_type = ""
|
||||
filepath = ""
|
||||
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()
|
||||
filepath = os.path.join(
|
||||
static_path, "sequence_diagrams", url_split[3]
|
||||
)
|
||||
mime_type = "image/svg+xml"
|
||||
elif len(url_split) > 3 and url_split[2] == "images":
|
||||
filename = os.path.join(*url_split[3:])
|
||||
filepath = os.path.join(static_path, "images", filename)
|
||||
_, extension = os.path.splitext(filename)
|
||||
mime_type = {
|
||||
".svg": "image/svg+xml",
|
||||
@@ -520,25 +821,25 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
".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":
|
||||
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")
|
||||
return fp.read()
|
||||
filepath = os.path.join(static_path, "css", filename)
|
||||
mime_type = "text/css; charset=utf-8"
|
||||
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")
|
||||
return fp.read()
|
||||
filepath = os.path.join(static_path, "js", filename)
|
||||
mime_type = "application/javascript"
|
||||
else:
|
||||
return self.page_404(url_split)
|
||||
|
||||
if mime_type == "" or not filepath:
|
||||
raise ValueError("Unknown file type or path")
|
||||
|
||||
with open(filepath, "rb") as fp:
|
||||
content = fp.read()
|
||||
self.putHeaders(status_code, mime_type)
|
||||
return content
|
||||
|
||||
except FileNotFoundError:
|
||||
return self.page_404(url_split)
|
||||
except Exception as ex:
|
||||
@@ -550,6 +851,10 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if len(url_split) > 1:
|
||||
page = url_split[1]
|
||||
|
||||
if page == "login":
|
||||
return self.page_login(url_split, post_string)
|
||||
if page == "shutdown_ping":
|
||||
return self.page_shutdown_ping(url_split, post_string)
|
||||
if page == "active":
|
||||
return self.page_active(url_split, post_string)
|
||||
if page == "wallets":
|
||||
@@ -578,14 +883,14 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return page_offers(self, url_split, post_string, sent=True)
|
||||
if page == "bid":
|
||||
return page_bid(self, url_split, post_string)
|
||||
if page == "receivedbids":
|
||||
return page_bids(self, url_split, post_string, received=True)
|
||||
if page == "sentbids":
|
||||
return page_bids(self, url_split, post_string, sent=True)
|
||||
if page == "bids":
|
||||
return page_bids(self, url_split, post_string)
|
||||
if page == "availablebids":
|
||||
return page_bids(self, url_split, post_string, available=True)
|
||||
if page == "watched":
|
||||
return self.page_watched(url_split, post_string)
|
||||
if page == "donation":
|
||||
return self.page_donation(url_split, post_string)
|
||||
if page == "smsgaddresses":
|
||||
return page_smsgaddresses(self, url_split, post_string)
|
||||
if page == "identity":
|
||||
@@ -598,6 +903,41 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return page_automation_strategy(self, url_split, post_string)
|
||||
if page == "newautomationstrategy":
|
||||
return page_automation_strategy_new(self, url_split, post_string)
|
||||
if page == "amm":
|
||||
if len(url_split) > 2 and url_split[2] == "status":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
status_data = amm_status_api(
|
||||
swap_client, self.path, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(status_data).encode("utf-8")
|
||||
elif len(url_split) > 2 and url_split[2] == "autostart":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
autostart_data = amm_autostart_api(
|
||||
swap_client, post_string, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(autostart_data).encode("utf-8")
|
||||
elif len(url_split) > 2 and url_split[2] == "debug":
|
||||
query_params = {}
|
||||
if parsed.query:
|
||||
query_params = {
|
||||
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
|
||||
}
|
||||
debug_data = amm_debug_api(
|
||||
swap_client, post_string, query_params
|
||||
)
|
||||
self.putHeaders(200, "application/json")
|
||||
return json.dumps(debug_data).encode("utf-8")
|
||||
return page_amm(self, url_split, post_string)
|
||||
if page == "shutdown":
|
||||
return self.page_shutdown(url_split, post_string)
|
||||
if page == "changepassword":
|
||||
@@ -617,15 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.page_error(str(ex))
|
||||
|
||||
def do_GET(self):
|
||||
response = self.handle_http(200, self.path)
|
||||
self.wfile.write(response)
|
||||
try:
|
||||
response = self.handle_http(200, self.path)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
|
||||
try:
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(content_length)
|
||||
|
||||
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)
|
||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
try:
|
||||
self.wfile.write(response)
|
||||
except SocketError as e:
|
||||
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
||||
finally:
|
||||
pass
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, "text/html")
|
||||
@@ -638,12 +991,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
def __init__(self, fp, host_name, port_no, allow_cors, swap_client):
|
||||
class HttpThread(threading.Thread, ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
def __init__(self, host_name, port_no, allow_cors, swap_client):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.stop_event = threading.Event()
|
||||
self.fp = fp
|
||||
self.host_name = host_name
|
||||
self.port_no = port_no
|
||||
self.allow_cors = allow_cors
|
||||
@@ -651,27 +1005,36 @@ class HttpThread(threading.Thread, HTTPServer):
|
||||
self.title = "BasicSwap - " + __version__
|
||||
self.last_form_id = dict()
|
||||
self.session_tokens = dict()
|
||||
self.active_sessions = {}
|
||||
self.env = env
|
||||
self.msg_id_counter = 0
|
||||
|
||||
self.session_lock = threading.Lock()
|
||||
self.form_id_lock = threading.Lock()
|
||||
self.msg_id_lock = threading.Lock()
|
||||
|
||||
self.timeout = 60
|
||||
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
|
||||
if swap_client.debug:
|
||||
swap_client.log.info("HTTP server initialized with threading support")
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
# Send fake request
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
||||
conn.connect()
|
||||
conn.request("GET", "/none")
|
||||
response = conn.getresponse()
|
||||
_ = response.read()
|
||||
conn.close()
|
||||
try:
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no, timeout=0.5)
|
||||
conn.request("GET", "/shutdown_ping")
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def serve_forever(self):
|
||||
self.timeout = 1
|
||||
while not self.stop_event.is_set():
|
||||
self.handle_request()
|
||||
self.socket.close()
|
||||
self.swap_client.log.info("HTTP server stopped.")
|
||||
|
||||
def run(self):
|
||||
self.serve_forever()
|
||||
|
||||
@@ -53,6 +53,7 @@ class CoinInterface:
|
||||
self._network = network
|
||||
self._mx_wallet = threading.Lock()
|
||||
self._altruistic = True
|
||||
self._core_version = None # Set in getDaemonVersion()
|
||||
|
||||
def interface_type(self) -> int:
|
||||
# coin_type() returns the base coin type, interface_type() returns the coin+balance type.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from typing import Union
|
||||
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn
|
||||
from basicswap.util import b2h, b2i, ensure, i2h
|
||||
from basicswap.util import b2i, ensure, i2b
|
||||
from basicswap.util.script import decodePushData, decodeScriptNum
|
||||
from .btc import BTCInterface, ensure_op, findOutput
|
||||
from basicswap.rpc import make_rpc_func
|
||||
@@ -79,6 +79,7 @@ class BCHInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def has_segwit(self) -> bool:
|
||||
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
|
||||
@@ -106,6 +107,31 @@ class BCHInterface(BTCInterface):
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
def createWallet(self, wallet_name: str, password: str = ""):
|
||||
self.rpc("createwallet", [wallet_name, False])
|
||||
if password != "":
|
||||
self.rpc(
|
||||
"encryptwallet",
|
||||
[
|
||||
password,
|
||||
],
|
||||
override_wallet=wallet_name,
|
||||
)
|
||||
|
||||
def newKeypool(self) -> None:
|
||||
self._log.debug("Refreshing keypool.")
|
||||
|
||||
# Use up current keypool
|
||||
wi = self.rpc_wallet("getwalletinfo")
|
||||
keypool_size: int = wi["keypoolsize"]
|
||||
for i in range(keypool_size):
|
||||
_ = self.rpc_wallet("getnewaddress")
|
||||
keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"]
|
||||
for i in range(keypoolsize_hd_internal):
|
||||
_ = self.rpc_wallet("getrawchangeaddress")
|
||||
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
# returns pkh
|
||||
def decodeAddress(self, address: str) -> bytes:
|
||||
return bytes(Address.from_string(address).payload)
|
||||
@@ -454,11 +480,14 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
size,
|
||||
pay_fee,
|
||||
"createSCLockSpendTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {size}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize_without_witness()
|
||||
@@ -506,11 +535,14 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockRefundTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue
|
||||
@@ -582,11 +614,14 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockRefundSpendToFTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize_without_witness()
|
||||
@@ -780,10 +815,18 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "Bad nLockTime") # TODO match txns created by cores
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: Locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if tx.nLockTime != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if tx.nLockTime > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {tx.nLockTime}, current height {current_height}"
|
||||
)
|
||||
|
||||
script_pk = self.getScriptDest(script_out)
|
||||
locked_n = findOutput(tx, script_pk)
|
||||
@@ -835,7 +878,7 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock refund tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock refund tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "nLockTime not 0")
|
||||
@@ -881,7 +924,7 @@ class BCHInterface(BTCInterface):
|
||||
size = self.getTxSize(tx)
|
||||
vsize = size
|
||||
|
||||
self._log.info(
|
||||
self._log.info_s(
|
||||
"tx amount, vsize, fee: %ld, %ld, %ld", locked_coin, vsize, fee_paid
|
||||
)
|
||||
|
||||
@@ -905,7 +948,7 @@ class BCHInterface(BTCInterface):
|
||||
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock refund spend tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock refund spend tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "nLockTime not 0")
|
||||
@@ -947,9 +990,7 @@ class BCHInterface(BTCInterface):
|
||||
size = self.getTxSize(tx)
|
||||
vsize = size
|
||||
|
||||
self._log.info(
|
||||
"tx amount, vsize, fee: %ld, %ld, %ld", tx_value, vsize, fee_paid
|
||||
)
|
||||
self._log.info_s(f"tx amount, vsize, fee: {tx_value}, {vsize}, {fee_paid}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -962,7 +1003,7 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock spend tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock spend tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.nVersion == self.txVersion(), "Bad version")
|
||||
ensure(tx.nLockTime == 0, "nLockTime not 0")
|
||||
@@ -995,7 +1036,7 @@ class BCHInterface(BTCInterface):
|
||||
size = self.getTxSize(tx)
|
||||
vsize = size
|
||||
|
||||
self._log.info(
|
||||
self._log.info_s(
|
||||
"tx amount, vsize, fee: %ld, %ld, %ld", tx.vout[0].nValue, vsize, fee_paid
|
||||
)
|
||||
|
||||
@@ -1115,11 +1156,14 @@ class BCHInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createMercyTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
1,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createMercyTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {1}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
txHex = tx.serialize_without_witness()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -521,7 +521,7 @@ class CTransaction(object):
|
||||
self.hash = tx.hash
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
ver32bit = struct.unpack("<i", f.read(4))[0]
|
||||
self.nVersion = ver32bit & 0xffff
|
||||
self.nType = (ver32bit >> 16) & 0xffff
|
||||
|
||||
@@ -455,12 +455,12 @@ class CTransaction(object):
|
||||
self.wit = copy.deepcopy(tx.wit)
|
||||
self.strDZeel = copy.deepcopy(tx.strDZeel)
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<i", f.read(4))[0]
|
||||
self.nTime = struct.unpack("<i", f.read(4))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
flags = 0
|
||||
if len(self.vin) == 0:
|
||||
if len(self.vin) == 0 and allow_witness:
|
||||
flags = struct.unpack("<B", f.read(1))[0]
|
||||
# Not sure why flags can't be zero, but this
|
||||
# matches the implementation in bitcoind
|
||||
|
||||
@@ -505,7 +505,7 @@ class CTransaction:
|
||||
self.sha256 = tx.sha256
|
||||
self.hash = tx.hash
|
||||
|
||||
def deserialize(self, f):
|
||||
def deserialize(self, f, allow_witness: bool = True):
|
||||
self.nVersion = struct.unpack("<h", f.read(2))[0]
|
||||
self.nType = struct.unpack("<h", f.read(2))[0]
|
||||
self.vin = deser_vector(f, CTxIn)
|
||||
|
||||
@@ -47,7 +47,7 @@ class DASHInterface(BTCInterface):
|
||||
def entropyToMnemonic(self, key: bytes) -> None:
|
||||
return Mnemonic("english").to_mnemonic(key)
|
||||
|
||||
def initialiseWallet(self, key_bytes: bytes) -> None:
|
||||
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
|
||||
self._have_checked_seed = False
|
||||
if self._wallet_v20_compatible:
|
||||
self._log.warning("Generating wallet compatible with v20 seed.")
|
||||
@@ -66,7 +66,11 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
def checkExpectedSeed(self, expect_seedid: str) -> bool:
|
||||
self._expect_seedid_hex = expect_seedid
|
||||
rv = self.rpc_wallet("dumphdinfo")
|
||||
try:
|
||||
rv = self.rpc_wallet("dumphdinfo")
|
||||
except Exception as e:
|
||||
self._log.debug(f"DASH dumphdinfo failed {e}.")
|
||||
return False
|
||||
if rv["mnemonic"] != "":
|
||||
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
|
||||
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
|
||||
@@ -111,18 +115,45 @@ class DASHInterface(BTCInterface):
|
||||
|
||||
return None
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
super().unlockWallet(password)
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
super().unlockWallet(password, check_seed)
|
||||
if self._wallet_v20_compatible:
|
||||
# Store password for initialiseWallet
|
||||
self._wallet_passphrase = password
|
||||
if not self._have_checked_seed:
|
||||
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 = ""
|
||||
|
||||
def encryptWallet(
|
||||
self, old_password: str, new_password: str, check_seed: bool = True
|
||||
):
|
||||
if old_password != "":
|
||||
self.unlockWallet(old_password, check_seed=False)
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [new_password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
self.unlockWallet(new_password, check_seed=False)
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
self.lockWallet()
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
|
||||
def changeWalletPassword(
|
||||
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
if old_password == "":
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
|
||||
@@ -13,6 +13,8 @@ import logging
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from typing import List
|
||||
|
||||
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
@@ -27,7 +29,6 @@ from basicswap.interface.btc import (
|
||||
)
|
||||
from basicswap.util import (
|
||||
ensure,
|
||||
b2h,
|
||||
b2i,
|
||||
i2b,
|
||||
i2h,
|
||||
@@ -211,6 +212,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
def txoType():
|
||||
return CTxOut
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
return 224
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 327
|
||||
@@ -273,6 +278,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
self._connection_type = coin_settings["connection_type"]
|
||||
self._altruistic = coin_settings.get("altruistic", True)
|
||||
|
||||
if "wallet_name" in coin_settings:
|
||||
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||
|
||||
def open_rpc(self):
|
||||
return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host)
|
||||
|
||||
@@ -326,14 +334,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
|
||||
def testDaemonRPC(self, with_wallet=True) -> None:
|
||||
if with_wallet:
|
||||
self.rpc_wallet("getinfo")
|
||||
self.rpc_wallet("walletislocked")
|
||||
else:
|
||||
self.rpc("getblockchaininfo")
|
||||
|
||||
def getChainHeight(self) -> int:
|
||||
return self.rpc("getblockcount")
|
||||
|
||||
def initialiseWallet(self, key: bytes) -> None:
|
||||
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
|
||||
# Load with --create
|
||||
pass
|
||||
|
||||
@@ -348,7 +356,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
walletislocked = self.rpc_wallet("walletislocked")
|
||||
return True, walletislocked
|
||||
|
||||
def changeWalletPassword(self, old_password: str, new_password: str):
|
||||
def changeWalletPassword(
|
||||
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
if old_password == "":
|
||||
# Read initial pwd from settings
|
||||
@@ -362,14 +372,15 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Clear initial password
|
||||
self._sc.editSettings(self.coin_name().lower(), {"wallet_pwd": ""})
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
if password == "":
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self):
|
||||
self._log.info("lockWallet - {}".format(self.ticker()))
|
||||
@@ -1076,22 +1087,21 @@ class DCRInterface(Secp256k1Interface):
|
||||
return self.fundTx(tx_bytes, feerate)
|
||||
|
||||
def genScriptLockRefundTxScript(self, Kal, Kaf, csv_val) -> bytes:
|
||||
|
||||
Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal)
|
||||
Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
script = bytearray()
|
||||
script += bytes((OP_IF,))
|
||||
push_script_data(script, bytes((2,)))
|
||||
push_script_data(script, Kal_enc)
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kal)
|
||||
push_script_data(script, Kaf)
|
||||
push_script_data(script, bytes((2,)))
|
||||
script += bytes((OP_CHECKMULTISIG,))
|
||||
script += bytes((OP_ELSE,))
|
||||
script += CScriptNum.encode(CScriptNum(csv_val))
|
||||
script += bytes((OP_CHECKSEQUENCEVERIFY,))
|
||||
script += bytes((OP_DROP,))
|
||||
push_script_data(script, Kaf_enc)
|
||||
push_script_data(script, Kaf)
|
||||
script += bytes((OP_CHECKSIG,))
|
||||
script += bytes((OP_ENDIF,))
|
||||
|
||||
@@ -1123,11 +1133,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
fee_info["size"] = size
|
||||
|
||||
self._log.info(
|
||||
"createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
|
||||
tx.TxHash().hex(),
|
||||
tx_fee_rate,
|
||||
size,
|
||||
pay_fee,
|
||||
"createSCLockSpendTx {}{}.".format(
|
||||
self._log.id(tx.TxHash()),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize(TxSerializeType.NoWitness)
|
||||
@@ -1167,11 +1180,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
tx.vout[0].value = locked_coin - pay_fee
|
||||
|
||||
self._log.info(
|
||||
"createSCLockRefundTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
|
||||
tx.TxHash().hex(),
|
||||
tx_fee_rate,
|
||||
size,
|
||||
pay_fee,
|
||||
"createSCLockRefundTx {}{}.".format(
|
||||
self._log.id(tx.TxHash()),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize(TxSerializeType.NoWitness), refund_script, tx.vout[0].value
|
||||
@@ -1215,11 +1231,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
tx.vout[0].value = locked_coin - pay_fee
|
||||
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
|
||||
tx.TxHash().hex(),
|
||||
tx_fee_rate,
|
||||
size,
|
||||
pay_fee,
|
||||
"createSCLockRefundSpendTx {}{}.".format(
|
||||
self._log.id(tx.TxHash()),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize(TxSerializeType.NoWitness)
|
||||
@@ -1244,7 +1263,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.version == self.txVersion(), "Bad version")
|
||||
ensure(tx.locktime == 0, "Bad locktime")
|
||||
@@ -1320,7 +1339,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock spend tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock spend tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.version == self.txVersion(), "Bad version")
|
||||
ensure(tx.locktime == 0, "Bad locktime")
|
||||
@@ -1390,7 +1409,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock refund tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock refund tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.version == self.txVersion(), "Bad version")
|
||||
ensure(tx.locktime == 0, "locktime not 0")
|
||||
@@ -1453,7 +1472,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
|
||||
tx = self.loadTx(tx_bytes)
|
||||
txid = self.getTxid(tx)
|
||||
self._log.info("Verifying lock refund spend tx: {}.".format(b2h(txid)))
|
||||
self._log.info("Verifying lock refund spend tx: {}.".format(self._log.id(txid)))
|
||||
|
||||
ensure(tx.version == self.txVersion(), "Bad version")
|
||||
ensure(tx.locktime == 0, "locktime not 0")
|
||||
@@ -1539,11 +1558,14 @@ class DCRInterface(Secp256k1Interface):
|
||||
tx.vout[0].value = locked_amount - pay_fee
|
||||
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendToFTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.",
|
||||
tx.TxHash().hex(),
|
||||
tx_fee_rate,
|
||||
size,
|
||||
pay_fee,
|
||||
"createSCLockRefundSpendToFTx {}{}.".format(
|
||||
self._log.id(tx.TxHash()),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, size, fee: {tx_fee_rate}, {size}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize(TxSerializeType.NoWitness)
|
||||
@@ -1588,11 +1610,11 @@ class DCRInterface(Secp256k1Interface):
|
||||
script_pk = self.getScriptDest(script)
|
||||
return findOutput(tx, script_pk)
|
||||
|
||||
def getScriptLockTxDummyWitness(self, script: bytes):
|
||||
def getScriptLockTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes):
|
||||
return [bytes(72), bytes(72), bytes((1,)), bytes(len(script))]
|
||||
def getScriptLockRefundSpendTxDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
return [bytes(72), bytes(72), bytes(len(script))]
|
||||
|
||||
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
|
||||
tx = self.loadTx(tx_bytes)
|
||||
@@ -1712,7 +1734,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
witness_bytes = 115
|
||||
size = len(tx.serialize()) + witness_bytes
|
||||
pay_fee = round(fee_rate * size / 1000)
|
||||
self._log.info(
|
||||
self._log.info_s(
|
||||
f"BLockSpendTx fee_rate, vsize, fee: {fee_rate}, {size}, {pay_fee}."
|
||||
)
|
||||
return pay_fee
|
||||
|
||||
@@ -89,7 +89,7 @@ class CTransaction:
|
||||
self.locktime = tx.locktime
|
||||
self.expiry = tx.expiry
|
||||
|
||||
def deserialize(self, data: bytes) -> None:
|
||||
def deserialize(self, data: bytes, allow_witness: bool = True) -> None:
|
||||
|
||||
version = int.from_bytes(data[:4], "little")
|
||||
self.version = version & 0xFFFF
|
||||
|
||||
@@ -42,7 +42,6 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None):
|
||||
nonlocal port, auth, host
|
||||
return callrpc(port, auth, method, params, host)
|
||||
|
||||
return rpc_func
|
||||
|
||||
@@ -24,6 +24,10 @@ class DOGEInterface(BTCInterface):
|
||||
def coin_type():
|
||||
return Coins.DOGE
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
return 192
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 192
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -44,17 +44,40 @@ class FIROInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
if "wallet_name" in coin_settings:
|
||||
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||
|
||||
def getExchangeName(self, exchange_name: str) -> str:
|
||||
return "zcoin"
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
def initialiseWallet(self, key, restore_time: int = -1):
|
||||
# load with -hdseed= parameter
|
||||
pass
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
# Firo shuts down after encryptwallet
|
||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
|
||||
def getNewAddress(self, use_segwit, label="swap_receive"):
|
||||
return self.rpc("getnewaddress", [label])
|
||||
# addr_plain = self.rpc('getnewaddress', [label])
|
||||
@@ -102,7 +125,9 @@ class FIROInterface(BTCInterface):
|
||||
|
||||
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(
|
||||
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
|
||||
)
|
||||
self._log.info(
|
||||
"Rescanning {} chain from height: {}".format(
|
||||
self.coin_name(), rescan_from
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 = coin_settings.get("mweb_wallet_name", "mweb")
|
||||
self.rpc_wallet_mweb = make_rpc_func(
|
||||
self._rpcport,
|
||||
self._rpcauth,
|
||||
@@ -94,10 +94,11 @@ 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 = coin_settings.get("mweb_wallet_name", "mweb")
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def chainparams(self):
|
||||
return chainparams[Coins.LTC]
|
||||
@@ -128,7 +129,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
self._log.info("init_wallet - {}".format(self.ticker()))
|
||||
|
||||
self._log.info("Creating mweb wallet for {}.".format(self.coin_name()))
|
||||
self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.")
|
||||
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
|
||||
self.rpc("createwallet", ["mweb", False, True, password, False, False, True])
|
||||
|
||||
@@ -137,7 +138,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
|
||||
if self.getWalletSeedID() == "Not found":
|
||||
self._sc.initialiseWallet(self.coin_type())
|
||||
self._sc.initialiseWallet(self.interface_type())
|
||||
|
||||
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
|
||||
self.rpc("unloadwallet", ["mweb"])
|
||||
@@ -146,7 +147,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
def unlockWallet(self, password: str):
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
if password == "":
|
||||
return
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
@@ -156,5 +157,5 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
else:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -41,7 +41,6 @@ from basicswap.util.address import (
|
||||
from basicswap.util import (
|
||||
b2i,
|
||||
i2b,
|
||||
i2h,
|
||||
ensure,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
@@ -80,12 +79,16 @@ class NAVInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
if "wallet_name" in coin_settings:
|
||||
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
|
||||
|
||||
def use_p2shp2wsh(self) -> bool:
|
||||
# p2sh-p2wsh
|
||||
return True
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
def initialiseWallet(self, key, restore_time: int = -1):
|
||||
# Load with -importmnemonic= parameter
|
||||
pass
|
||||
|
||||
@@ -549,7 +552,9 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
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(
|
||||
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
|
||||
)
|
||||
self._log.info(
|
||||
"Rescanning {} chain from height: {}".format(
|
||||
self.coin_name(), rescan_from
|
||||
@@ -813,11 +818,14 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockRefundTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize(), refund_script, tx.vout[0].nValue
|
||||
@@ -868,11 +876,14 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockRefundSpendTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
@@ -925,11 +936,14 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockRefundSpendToFTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
@@ -972,11 +986,14 @@ class NAVInterface(BTCInterface):
|
||||
|
||||
tx.rehash()
|
||||
self._log.info(
|
||||
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
i2h(tx.sha256),
|
||||
tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockSpendTx {}{}.".format(
|
||||
self._log.id(i2b(tx.sha256)),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return tx.serialize()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -13,39 +14,3 @@ class NMCInterface(BTCInterface):
|
||||
@staticmethod
|
||||
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")
|
||||
return_txid = True if txid is None else False
|
||||
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"],
|
||||
)
|
||||
continue
|
||||
|
||||
rv = {"depth": 0, "height": o["height"]}
|
||||
if o["height"] > 0:
|
||||
rv["depth"] = ro["height"] - o["height"]
|
||||
if find_index:
|
||||
rv["index"] = o["vout"]
|
||||
if return_txid:
|
||||
rv["txid"] = o["txid"]
|
||||
return rv
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
import hashlib
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOutPart,
|
||||
)
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_DUP,
|
||||
OP_HASH160,
|
||||
OP_EQUALVERIFY,
|
||||
@@ -25,7 +25,6 @@ from basicswap.util import (
|
||||
TemporaryError,
|
||||
)
|
||||
from basicswap.util.script import (
|
||||
getP2WSH,
|
||||
getCompactSizeLen,
|
||||
getWitnessElementLen,
|
||||
)
|
||||
@@ -66,6 +65,10 @@ class PARTInterface(BTCInterface):
|
||||
def txVersion() -> int:
|
||||
return 0xA0
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
return 138
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 200
|
||||
@@ -106,7 +109,7 @@ class PARTInterface(BTCInterface):
|
||||
)
|
||||
return index_info["spentindex"]
|
||||
|
||||
def initialiseWallet(self, key: bytes) -> None:
|
||||
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
|
||||
raise ValueError("TODO")
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
@@ -132,6 +135,11 @@ class PARTInterface(BTCInterface):
|
||||
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
|
||||
def getScriptDummyWitness(self, script: bytes) -> List[bytes]:
|
||||
if self.isScriptP2WPKH(script) or self.isScriptP2PKH(script):
|
||||
return [bytes(72), bytes(33)]
|
||||
raise ValueError("Unknown script type")
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]["stealth_key_prefix"]
|
||||
|
||||
@@ -185,6 +193,27 @@ class PARTInterface(BTCInterface):
|
||||
) + self.make_int(u["amount"], r=1)
|
||||
return unspent_addr
|
||||
|
||||
def combine_non_segwit_prevouts(self):
|
||||
raise RuntimeError("No non-segwit outputs found.")
|
||||
|
||||
def signMessage(self, address: str, message: str) -> str:
|
||||
args = [address, message]
|
||||
if self.getDaemonVersion() > 23020700:
|
||||
message_magic: str = self.chainparams()["message_magic"]
|
||||
args += [
|
||||
message_magic,
|
||||
]
|
||||
return self.rpc_wallet("signmessage", args)
|
||||
|
||||
def signMessageWithKey(self, key_wif: str, message: str) -> str:
|
||||
args = [key_wif, message]
|
||||
if self.getDaemonVersion() > 23020700:
|
||||
message_magic: str = self.chainparams()["message_magic"]
|
||||
args += [
|
||||
message_magic,
|
||||
]
|
||||
return self.rpc("signmessagewithprivkey", args)
|
||||
|
||||
|
||||
class PARTInterfaceBlind(PARTInterface):
|
||||
|
||||
@@ -195,6 +224,10 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def balance_type():
|
||||
return BalanceTypes.BLIND
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
return 980
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 1032
|
||||
@@ -203,6 +236,15 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 980
|
||||
|
||||
@staticmethod
|
||||
def compareFeeRates(actual: int, expected: int) -> bool:
|
||||
# Allow the fee to be up to 10% larger than expected
|
||||
if actual < expected - 20:
|
||||
return False
|
||||
if actual > expected + expected * 0.1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def coin_name(self) -> str:
|
||||
return super().coin_name() + " Blind"
|
||||
|
||||
@@ -248,7 +290,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||
assert len(ephemeral_pubkey) == 33
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
|
||||
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(script))
|
||||
inputs = []
|
||||
outputs = [
|
||||
{
|
||||
@@ -322,7 +364,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
locked_coin = input_blinded_info["amount"]
|
||||
tx_lock_id = lock_tx_obj["txid"]
|
||||
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
|
||||
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(refund_script))
|
||||
|
||||
inputs = [
|
||||
{
|
||||
@@ -469,10 +511,19 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
):
|
||||
lock_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
|
||||
lock_txid_hex = lock_tx_obj["txid"]
|
||||
self._log.info("Verifying lock tx: {}.".format(lock_txid_hex))
|
||||
self._log.info("Verifying lock tx: {}.".format(self._log.id(lock_txid_hex)))
|
||||
|
||||
ensure(lock_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
ensure(lock_tx_obj["locktime"] == 0, "Bad nLockTime")
|
||||
lock_time: int = lock_tx_obj["locktime"]
|
||||
# locktime must be <= chainheight + 2
|
||||
# TODO: locktime is set to 0 to keep compaitibility with older nodes.
|
||||
# Set locktime to current chainheight in createSCLockTx.
|
||||
if lock_time != 0:
|
||||
current_height: int = self.getChainHeight()
|
||||
if lock_time > current_height + 2:
|
||||
raise ValueError(
|
||||
f"{self.coin_name()} - Bad nLockTime {lock_time}, current height {current_height}"
|
||||
)
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
@@ -487,7 +538,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_txo_scriptpk = bytes.fromhex(
|
||||
lock_tx_obj["vout"][lock_output_n]["scriptPubKey"]["hex"]
|
||||
)
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
script_pk = self.getP2WSHScriptDest(script_out)
|
||||
ensure(lock_txo_scriptpk == script_pk, "Bad output script")
|
||||
A, B = extractScriptLockScriptValues(script_out)
|
||||
ensure(A == Kal, "Bad script leader pubkey")
|
||||
@@ -533,7 +584,9 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
):
|
||||
lock_refund_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
|
||||
lock_refund_txid_hex = lock_refund_tx_obj["txid"]
|
||||
self._log.info("Verifying lock refund tx: {}.".format(lock_refund_txid_hex))
|
||||
self._log.info(
|
||||
"Verifying lock refund tx: {}.".format(self._log.id(lock_refund_txid_hex))
|
||||
)
|
||||
|
||||
ensure(lock_refund_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
ensure(lock_refund_tx_obj["locktime"] == 0, "Bad nLockTime")
|
||||
@@ -562,7 +615,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_refund_txo_scriptpk = bytes.fromhex(
|
||||
lock_refund_tx_obj["vout"][lock_refund_output_n]["scriptPubKey"]["hex"]
|
||||
)
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
script_pk = self.getP2WSHScriptDest(script_out)
|
||||
ensure(lock_refund_txo_scriptpk == script_pk, "Bad output script")
|
||||
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
|
||||
ensure(A == Kal, "Bad script pubkey")
|
||||
@@ -622,7 +675,9 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
lock_refund_spend_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
|
||||
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj["txid"]
|
||||
self._log.info(
|
||||
"Verifying lock refund spend tx: {}.".format(lock_refund_spend_txid_hex)
|
||||
"Verifying lock refund spend tx: {}.".format(
|
||||
self._log.id(lock_refund_spend_txid_hex)
|
||||
)
|
||||
)
|
||||
|
||||
ensure(lock_refund_spend_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
@@ -668,6 +723,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_paid = self.make_int(lock_refund_spend_tx_obj["vout"][0]["ct_fee"])
|
||||
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
ensure(
|
||||
self.compareFeeRates(fee_rate_paid, feerate),
|
||||
@@ -781,11 +837,14 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
)
|
||||
actual_tx_fee_rate = pay_fee * 1000 // vsize
|
||||
self._log.info(
|
||||
"createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.",
|
||||
lock_spend_tx_obj["txid"],
|
||||
actual_tx_fee_rate,
|
||||
vsize,
|
||||
pay_fee,
|
||||
"createSCLockSpendTx {}{}.".format(
|
||||
self._log.id(lock_spend_tx_obj["txid"]),
|
||||
(
|
||||
""
|
||||
if self._log.safe_logs
|
||||
else f":\n fee_rate, vsize, fee: {actual_tx_fee_rate}, {vsize}, {pay_fee}"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
fee_info["vsize"] = vsize
|
||||
@@ -800,7 +859,9 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
):
|
||||
lock_spend_tx_obj = self.rpc("decoderawtransaction", [tx_bytes.hex()])
|
||||
lock_spend_txid_hex = lock_spend_tx_obj["txid"]
|
||||
self._log.info("Verifying lock spend tx: {}.".format(lock_spend_txid_hex))
|
||||
self._log.info(
|
||||
"Verifying lock spend tx: {}.".format(self._log.id(lock_spend_txid_hex))
|
||||
)
|
||||
|
||||
ensure(lock_spend_tx_obj["version"] == self.txVersion(), "Bad version")
|
||||
ensure(lock_spend_tx_obj["locktime"] == 0, "Bad nLockTime")
|
||||
@@ -1014,10 +1075,11 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value,
|
||||
cb_block_confirmed,
|
||||
cb_swap_value: int,
|
||||
cb_block_confirmed: int,
|
||||
restore_height: int,
|
||||
bid_sender: bool,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
@@ -1048,7 +1110,10 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
) # Should not be possible
|
||||
ensure(tx["outputs"][0]["type"] == "blind", "Output is not anon")
|
||||
|
||||
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
|
||||
if (
|
||||
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
|
||||
or check_amount is False
|
||||
):
|
||||
height = 0
|
||||
if tx["confirmations"] > 0:
|
||||
chain_height = self.rpc("getblockcount")
|
||||
@@ -1220,6 +1285,10 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
def balance_type():
|
||||
return BalanceTypes.ANON
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
return 1153
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
raise ValueError("Not possible")
|
||||
@@ -1266,7 +1335,14 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
return bytes.fromhex(txid)
|
||||
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value,
|
||||
cb_block_confirmed,
|
||||
restore_height,
|
||||
bid_sender,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
@@ -1298,7 +1374,10 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
) # Should not be possible
|
||||
ensure(tx["outputs"][0]["type"] == "anon", "Output is not anon")
|
||||
|
||||
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
|
||||
if (
|
||||
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
|
||||
or check_amount is False
|
||||
):
|
||||
height = 0
|
||||
if tx["confirmations"] > 0:
|
||||
chain_height = self.rpc("getblockcount")
|
||||
|
||||
@@ -33,9 +33,36 @@ class PIVXInterface(BTCInterface):
|
||||
self.rpc_wallet = make_rpc_func(
|
||||
self._rpcport, self._rpcauth, host=self._rpc_host
|
||||
)
|
||||
self.rpc_wallet_watch = self.rpc_wallet
|
||||
|
||||
def checkWallets(self) -> int:
|
||||
return 1
|
||||
def encryptWallet(self, password: str, check_seed: bool = True):
|
||||
# Watchonly wallets are not encrypted
|
||||
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
seed_id_after: str = self.getWalletSeedID()
|
||||
|
||||
if seed_id_before == seed_id_after:
|
||||
return
|
||||
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
|
||||
self._log.debug(
|
||||
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
|
||||
)
|
||||
self.setWalletSeedWarning(True)
|
||||
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
|
||||
chain_client_settings = self._sc.getChainClientSettings(
|
||||
self.coin_type()
|
||||
) # basicswap.json
|
||||
|
||||
if chain_client_settings.get("manage_daemon", False) is False:
|
||||
self._log.warning(
|
||||
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
|
||||
)
|
||||
return
|
||||
|
||||
def signTxWithWallet(self, tx):
|
||||
rv = self.rpc("signrawtransaction", [tx.hex()])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -28,4 +29,28 @@ class WOWInterface(XMRInterface):
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
# below only needed until wow is rebased to monero v0.18.4.0+
|
||||
def openWallet(self, filename):
|
||||
params = {"filename": filename}
|
||||
if self._wallet_password is not None:
|
||||
params["password"] = self._wallet_password
|
||||
|
||||
try:
|
||||
self.rpc_wallet("open_wallet", params)
|
||||
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
|
||||
|
||||
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)
|
||||
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import basicswap.contrib.ed25519_fast as edf
|
||||
import basicswap.ed25519_fast_util as edu
|
||||
import basicswap.util_xmr as xmr_util
|
||||
from coincurve.ed25519 import (
|
||||
ed25519_add,
|
||||
@@ -34,6 +35,9 @@ from basicswap.chainparams import XMR_COIN, Coins
|
||||
from basicswap.interface.base import CoinInterface
|
||||
|
||||
|
||||
ed25519_l = 2**252 + 27742317777372353535851937790883648493
|
||||
|
||||
|
||||
class XMRInterface(CoinInterface):
|
||||
@staticmethod
|
||||
def curve_type():
|
||||
@@ -71,15 +75,28 @@ class XMRInterface(CoinInterface):
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
raise ValueError("Not possible")
|
||||
|
||||
@staticmethod
|
||||
def est_lock_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 1604
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 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
|
||||
str_error: str = str(ex).lower()
|
||||
if any(
|
||||
response in str_error
|
||||
for response in [
|
||||
"failed to get earliest fork height",
|
||||
"failed to get output distribution",
|
||||
"request-sent",
|
||||
"idle",
|
||||
]
|
||||
):
|
||||
return True
|
||||
return super().is_transient_error(ex)
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
@@ -94,6 +111,7 @@ class XMRInterface(CoinInterface):
|
||||
self._log = self._sc.log if self._sc and self._sc.log else logging
|
||||
self._wallet_password = None
|
||||
self._have_checked_seed = False
|
||||
self._wallet_filename = coin_settings.get("wallet_name", "swap_wallet")
|
||||
|
||||
daemon_login = None
|
||||
if coin_settings.get("rpcuser", "") != "":
|
||||
@@ -139,6 +157,8 @@ class XMRInterface(CoinInterface):
|
||||
self._walletrpctimeout = coin_settings.get("walletrpctimeout", 120)
|
||||
# walletrpctimeoutlong likely unneeded
|
||||
self._walletrpctimeoutlong = coin_settings.get("walletrpctimeoutlong", 600)
|
||||
self._num_chaininfo_retries = coin_settings.get("numchaininforetries", 20)
|
||||
self._chaininfo_retry_delay = coin_settings.get("chaininforetrydelay", 1)
|
||||
|
||||
self.rpc = make_xmr_rpc_func(
|
||||
coin_settings["rpcport"],
|
||||
@@ -170,14 +190,23 @@ class XMRInterface(CoinInterface):
|
||||
ensure(new_priority >= 0 and new_priority < 4, "Invalid fee_priority value")
|
||||
self._fee_priority = new_priority
|
||||
|
||||
def setWalletFilename(self, wallet_filename):
|
||||
self._wallet_filename = wallet_filename
|
||||
|
||||
def createWallet(self, params):
|
||||
if self._wallet_password is not None:
|
||||
params["password"] = self._wallet_password
|
||||
rv = self.rpc_wallet("generate_from_keys", params)
|
||||
self._log.info("generate_from_keys %s", dumpj(rv))
|
||||
if "address" in rv:
|
||||
new_address: str = rv["address"]
|
||||
is_watch_only: bool = "Watch-only" in rv.get("info", "")
|
||||
self._log.info(
|
||||
"Generated{} {} wallet: {}".format(
|
||||
" watch-only" if is_watch_only else "",
|
||||
self.coin_name(),
|
||||
self._log.addr(new_address),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._log.debug("generate_from_keys %s", dumpj(rv))
|
||||
raise ValueError("generate_from_keys failed")
|
||||
|
||||
def openWallet(self, filename):
|
||||
params = {"filename": filename}
|
||||
@@ -186,25 +215,57 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
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
|
||||
return # Bypass refresh error to allow startup with a busy daemon
|
||||
if any(
|
||||
x in str(e)
|
||||
for x in (
|
||||
"invalid signature",
|
||||
"std::bad_alloc",
|
||||
"basic_string::_M_replace_aux",
|
||||
)
|
||||
):
|
||||
self._log.error(f"{self.coin_name()} wallet is corrupt.")
|
||||
chain_client_settings = self._sc.getChainClientSettings(
|
||||
self.coin_type()
|
||||
) # basicswap.json
|
||||
if chain_client_settings.get("manage_wallet_daemon", False):
|
||||
self._log.info(f"Renaming {self.coin_name()} wallet cache file.")
|
||||
walletpath = os.path.join(
|
||||
chain_client_settings.get("datadir", "none"),
|
||||
"wallets",
|
||||
filename,
|
||||
)
|
||||
if not os.path.isfile(walletpath):
|
||||
self._log.warning(
|
||||
f"Could not find {self.coin_name()} wallet cache file."
|
||||
)
|
||||
raise
|
||||
bkp_path = walletpath + ".corrupt"
|
||||
for i in range(100):
|
||||
if not os.path.exists(bkp_path):
|
||||
break
|
||||
bkp_path = walletpath + f".corrupt{i}"
|
||||
if os.path.exists(bkp_path):
|
||||
self._log.error(
|
||||
f"Could not find backup path for {self.coin_name()} wallet."
|
||||
)
|
||||
raise
|
||||
os.rename(walletpath, bkp_path)
|
||||
# Drop through to open_wallet
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self.rpc_wallet("close_wallet")
|
||||
self._log.debug(f"Closing {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")
|
||||
self._log.debug(f"Attempting to open {self.coin_name()} wallet")
|
||||
|
||||
def initialiseWallet(
|
||||
self, key_view: bytes, key_spend: bytes, restore_height=None
|
||||
@@ -239,10 +300,13 @@ class XMRInterface(CoinInterface):
|
||||
self.rpc_wallet("get_languages")
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet("get_version")["version"]
|
||||
# Returns wallet version
|
||||
if self._core_version is None:
|
||||
self._core_version = self.rpc_wallet("get_version")["version"]
|
||||
return self._core_version
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
get_height = self.getChainHeight(full_output=True)
|
||||
rv = {
|
||||
"blocks": get_height["height"],
|
||||
"verificationprogress": 0.0,
|
||||
@@ -271,8 +335,16 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc2("get_height", timeout=self._rpctimeout)["height"]
|
||||
def getChainHeight(self, full_output: bool = False):
|
||||
for i in range(self._num_chaininfo_retries):
|
||||
try:
|
||||
get_height = self.rpc2("get_height", timeout=self._rpctimeout)
|
||||
return get_height if full_output else get_height["height"]
|
||||
except Exception as e:
|
||||
if i < self._num_chaininfo_retries - 1 and self.is_transient_error(e):
|
||||
time.sleep(self._chaininfo_retry_delay)
|
||||
continue
|
||||
raise (e)
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
@@ -290,6 +362,8 @@ class XMRInterface(CoinInterface):
|
||||
raise e
|
||||
|
||||
rv = {}
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
|
||||
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
|
||||
@@ -328,10 +402,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def getNewRandomKey(self) -> bytes:
|
||||
# Note: Returned bytes are in big endian order
|
||||
return i2b(edu.get_secret())
|
||||
|
||||
def pubkey(self, key: bytes) -> bytes:
|
||||
return edf.scalarmult_B(key)
|
||||
return i2b(9 + secrets.randbelow(ed25519_l - 9))
|
||||
|
||||
def encodeKey(self, vk: bytes) -> str:
|
||||
return vk[::-1].hex()
|
||||
@@ -339,12 +410,6 @@ class XMRInterface(CoinInterface):
|
||||
def decodeKey(self, k_hex: str) -> bytes:
|
||||
return bytes.fromhex(k_hex)[::-1]
|
||||
|
||||
def encodePubkey(self, pk: bytes) -> str:
|
||||
return edu.encodepoint(pk)
|
||||
|
||||
def decodePubkey(self, pke):
|
||||
return edf.decodepoint(pke)
|
||||
|
||||
def getPubkey(self, privkey):
|
||||
return ed25519_get_pubkey(privkey)
|
||||
|
||||
@@ -355,7 +420,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
def verifyKey(self, k: int) -> bool:
|
||||
i = b2i(k)
|
||||
return i < edf.l and i > 8
|
||||
return i < ed25519_l and i > 8
|
||||
|
||||
def verifyPubkey(self, pubkey_bytes):
|
||||
# Calls ed25519_decode_check_point() in secp256k1
|
||||
@@ -391,6 +456,8 @@ class XMRInterface(CoinInterface):
|
||||
) -> bytes:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
Kbv = self.getPubkey(kbv)
|
||||
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
|
||||
@@ -403,14 +470,24 @@ class XMRInterface(CoinInterface):
|
||||
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
|
||||
"publishBLockTx {} to address_b58 {}".format(
|
||||
self._log.id(rv["tx_hash"]),
|
||||
self._log.addr(shared_addr),
|
||||
)
|
||||
)
|
||||
tx_hash = bytes.fromhex(rv["tx_hash"])
|
||||
|
||||
return tx_hash
|
||||
|
||||
def findTxB(
|
||||
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
|
||||
self,
|
||||
kbv,
|
||||
Kbs,
|
||||
cb_swap_value: int,
|
||||
cb_block_confirmed: int,
|
||||
restore_height: int,
|
||||
bid_sender: bool,
|
||||
check_amount: bool = True,
|
||||
):
|
||||
with self._mx_wallet:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
@@ -430,6 +507,9 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
"""
|
||||
# Debug
|
||||
try:
|
||||
@@ -459,7 +539,7 @@ class XMRInterface(CoinInterface):
|
||||
)
|
||||
rv = -1
|
||||
continue
|
||||
if transfer["amount"] == cb_swap_value:
|
||||
if transfer["amount"] == cb_swap_value or check_amount is False:
|
||||
return {
|
||||
"txid": transfer["tx_hash"],
|
||||
"amount": transfer["amount"],
|
||||
@@ -478,14 +558,14 @@ class XMRInterface(CoinInterface):
|
||||
rv = -1
|
||||
return rv
|
||||
|
||||
def findTxnByHash(self, txid):
|
||||
def findTxnByHash(self, txid: str):
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
try:
|
||||
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
|
||||
"height"
|
||||
]
|
||||
current_height: int = self.getChainHeight()
|
||||
self._log.info(
|
||||
f"findTxnByHash {self.ticker_str()} current_height {current_height}\nhash: {txid}"
|
||||
)
|
||||
@@ -549,6 +629,8 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(wallet_filename)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
rv = self.rpc_wallet("get_balance")
|
||||
if rv["balance"] < cb_swap_value:
|
||||
self._log.warning("Balance is too low, checking for existing spend.")
|
||||
@@ -603,6 +685,8 @@ class XMRInterface(CoinInterface):
|
||||
) -> str:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
if sweepall:
|
||||
balance = self.rpc_wallet("get_balance")
|
||||
@@ -682,6 +766,9 @@ class XMRInterface(CoinInterface):
|
||||
self.createWallet(params)
|
||||
self.openWallet(address_b58)
|
||||
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
rv = self.rpc_wallet(
|
||||
"get_transfers",
|
||||
{"in": True, "out": True, "pending": True, "failed": True},
|
||||
@@ -694,11 +781,15 @@ class XMRInterface(CoinInterface):
|
||||
def getSpendableBalance(self) -> int:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet("refresh")
|
||||
self._log.debug(f"Refreshing {self.coin_name()} wallet")
|
||||
|
||||
balance_info = self.rpc_wallet("get_balance")
|
||||
return balance_info["unlocked_balance"]
|
||||
|
||||
def changeWalletPassword(self, old_password, new_password):
|
||||
def changeWalletPassword(
|
||||
self, old_password, new_password, check_seed_if_encrypt: bool = True
|
||||
):
|
||||
self._log.info("changeWalletPassword - {}".format(self.ticker()))
|
||||
orig_password = self._wallet_password
|
||||
if old_password != "":
|
||||
@@ -713,11 +804,11 @@ class XMRInterface(CoinInterface):
|
||||
self._wallet_password = orig_password
|
||||
raise e
|
||||
|
||||
def unlockWallet(self, password: str) -> None:
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
self._wallet_password = password
|
||||
|
||||
if not self._have_checked_seed:
|
||||
if check_seed and not self._have_checked_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
def lockWallet(self) -> None:
|
||||
|
||||
@@ -14,13 +14,18 @@ from .util import (
|
||||
toBool,
|
||||
)
|
||||
from .basicswap_util import (
|
||||
fiatFromTicker,
|
||||
strBidState,
|
||||
strTxState,
|
||||
SwapTypes,
|
||||
NotificationTypes as NT,
|
||||
)
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
chainparams,
|
||||
Fiat,
|
||||
getCoinIdFromTicker,
|
||||
getCoinIdFromName,
|
||||
)
|
||||
from .ui.util import (
|
||||
PAGE_LIMIT,
|
||||
@@ -32,12 +37,12 @@ from .ui.util import (
|
||||
get_data_entry,
|
||||
get_data_entry_or,
|
||||
have_data_entry,
|
||||
tickerToCoinId,
|
||||
listOldBidStates,
|
||||
checkAddressesOwned,
|
||||
)
|
||||
from .ui.page_offers import postNewOffer
|
||||
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
|
||||
from .db import Concepts
|
||||
|
||||
|
||||
def getFormData(post_string: str, is_json: bool):
|
||||
@@ -118,12 +123,151 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(coins), "UTF-8")
|
||||
|
||||
|
||||
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
try:
|
||||
|
||||
swap_client.updateWalletsInfo()
|
||||
wallets = swap_client.getCachedWalletsInfo()
|
||||
coins_with_balances = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
|
||||
balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "balance" in w and "error" not in w and "no_data" not in w:
|
||||
raw_balance = w["balance"]
|
||||
if isinstance(raw_balance, float):
|
||||
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
balance = str(raw_balance)
|
||||
else:
|
||||
balance = raw_balance
|
||||
|
||||
pending = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
ci = swap_client.ci(k)
|
||||
pending_amount = 0
|
||||
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["unconfirmed"])
|
||||
if "immature" in w and float(w["immature"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["immature"])
|
||||
if pending_amount > 0:
|
||||
pending = ci.format_amount(pending_amount)
|
||||
|
||||
coin_entry = {
|
||||
"id": int(k),
|
||||
"name": getCoinName(k),
|
||||
"balance": balance,
|
||||
"pending": pending,
|
||||
"ticker": chainparams[k]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(coin_entry)
|
||||
|
||||
if k == Coins.PART:
|
||||
variants = [
|
||||
{
|
||||
"coin": Coins.PART_ANON,
|
||||
"balance_field": "anon_balance",
|
||||
"pending_field": "anon_pending",
|
||||
},
|
||||
{
|
||||
"coin": Coins.PART_BLIND,
|
||||
"balance_field": "blind_balance",
|
||||
"pending_field": "blind_unconfirmed",
|
||||
},
|
||||
]
|
||||
|
||||
for variant_info in variants:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if variant_info["balance_field"] in w:
|
||||
raw_balance = w[variant_info["balance_field"]]
|
||||
if isinstance(raw_balance, float):
|
||||
variant_balance = f"{raw_balance:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
variant_balance = str(raw_balance)
|
||||
else:
|
||||
variant_balance = raw_balance
|
||||
|
||||
if (
|
||||
variant_info["pending_field"] in w
|
||||
and float(w[variant_info["pending_field"]]) > 0.0
|
||||
):
|
||||
variant_pending = str(
|
||||
w[variant_info["pending_field"]]
|
||||
)
|
||||
|
||||
variant_entry = {
|
||||
"id": int(variant_info["coin"]),
|
||||
"name": getCoinName(variant_info["coin"]),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.PART]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
elif k == Coins.LTC:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if "mweb_balance" in w:
|
||||
variant_balance = w["mweb_balance"]
|
||||
|
||||
pending_amount = 0
|
||||
if (
|
||||
"mweb_unconfirmed" in w
|
||||
and float(w["mweb_unconfirmed"]) > 0.0
|
||||
):
|
||||
pending_amount += float(w["mweb_unconfirmed"])
|
||||
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
|
||||
pending_amount += float(w["mweb_immature"])
|
||||
if pending_amount > 0:
|
||||
variant_pending = f"{pending_amount:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
|
||||
variant_entry = {
|
||||
"id": int(Coins.LTC_MWEB),
|
||||
"name": getCoinName(Coins.LTC_MWEB),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.LTC]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
return bytes(json.dumps(coins_with_balances), "UTF-8")
|
||||
|
||||
except Exception as e:
|
||||
error_data = {"error": str(e)}
|
||||
return bytes(json.dumps(error_data), "UTF-8")
|
||||
|
||||
|
||||
def js_wallets(self, url_split, post_string, is_json):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
if len(url_split) > 3:
|
||||
ticker_str = url_split[3]
|
||||
coin_type = tickerToCoinId(ticker_str)
|
||||
coin_type = getCoinIdFromTicker(ticker_str)
|
||||
|
||||
if len(url_split) > 4:
|
||||
cmd = url_split[4]
|
||||
@@ -162,12 +306,13 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
return bytes(
|
||||
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
|
||||
)
|
||||
|
||||
raise ValueError("Unknown command")
|
||||
|
||||
if coin_type == Coins.LTC_MWEB:
|
||||
coin_type = Coins.LTC
|
||||
rv = swap_client.getWalletInfo(coin_type)
|
||||
if not rv:
|
||||
raise ValueError(f"getWalletInfo failed for coin: {coin_type}")
|
||||
rv.update(swap_client.getBlockchainInfo(coin_type))
|
||||
ci = swap_client.ci(coin_type)
|
||||
checkAddressesOwned(swap_client, ci, rv)
|
||||
@@ -178,7 +323,19 @@ def js_wallets(self, url_split, post_string, is_json):
|
||||
|
||||
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
offer_id = None
|
||||
if len(url_split) > 3:
|
||||
if url_split[3] == "new":
|
||||
@@ -201,6 +358,12 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
if offer_id:
|
||||
filters["offer_id"] = offer_id
|
||||
|
||||
parsed_url = urllib.parse.urlparse(self.path)
|
||||
query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {}
|
||||
|
||||
if "with_extra_info" in query_params:
|
||||
with_extra_info = toBool(query_params["with_extra_info"][0])
|
||||
|
||||
if post_string != "":
|
||||
post_data = getFormData(post_string, is_json)
|
||||
filters["coin_from"] = setCoinFilter(post_data, "coin_from")
|
||||
@@ -253,7 +416,9 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
"is_revoked": True if o.active_ind == 2 else False,
|
||||
"is_public": o.addr_to == swap_client.network_addr
|
||||
or o.addr_to.strip() == "",
|
||||
"message_nets": o.message_nets,
|
||||
}
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
if with_extra_info:
|
||||
offer_data["amount_negotiable"] = o.amount_negotiable
|
||||
offer_data["rate_negotiable"] = o.rate_negotiable
|
||||
@@ -268,6 +433,24 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
offer_data["feerate_from"] = o.from_feerate
|
||||
offer_data["feerate_to"] = o.to_feerate
|
||||
|
||||
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
|
||||
|
||||
if o.was_sent:
|
||||
try:
|
||||
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id)
|
||||
if strategy:
|
||||
offer_data["local_automation_strat_id"] = strategy[0]
|
||||
swap_client.log.debug(
|
||||
f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}"
|
||||
)
|
||||
else:
|
||||
offer_data["local_automation_strat_id"] = 0
|
||||
except Exception as e:
|
||||
swap_client.log.debug(
|
||||
f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}"
|
||||
)
|
||||
offer_data["local_automation_strat_id"] = 0
|
||||
|
||||
rv.append(offer_data)
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
|
||||
@@ -320,18 +503,35 @@ def formatBids(swap_client, bids, filters) -> bytes:
|
||||
with_extra_info = filters.get("with_extra_info", False)
|
||||
rv = []
|
||||
for b in bids:
|
||||
ci_from = swap_client.ci(b[9])
|
||||
offer = swap_client.getOffer(b[3])
|
||||
ci_to = swap_client.ci(offer.coin_to) if offer else None
|
||||
|
||||
bid_rate: int = 0 if b[10] is None else b[10]
|
||||
amount_to = None
|
||||
if ci_to:
|
||||
amount_to_int = (b[4] * bid_rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
|
||||
bid_data = {
|
||||
"bid_id": b[2].hex(),
|
||||
"offer_id": b[3].hex(),
|
||||
"created_at": b[0],
|
||||
"expire_at": b[1],
|
||||
"coin_from": b[9],
|
||||
"amount_from": swap_client.ci(b[9]).format_amount(b[4]),
|
||||
"bid_rate": swap_client.ci(b[14]).format_amount(b[10]),
|
||||
"coin_from": ci_from.coin_name(),
|
||||
"coin_to": ci_to.coin_name() if ci_to else "Unknown",
|
||||
"amount_from": ci_from.format_amount(b[4]),
|
||||
"amount_to": amount_to,
|
||||
"bid_rate": swap_client.ci(b[14]).format_amount(bid_rate),
|
||||
"bid_state": strBidState(b[5]),
|
||||
"addr_from": b[11],
|
||||
"addr_to": offer.addr_to if offer else None,
|
||||
}
|
||||
|
||||
if with_extra_info:
|
||||
bid_data["addr_from"] = b[11]
|
||||
bid_data.update(
|
||||
{"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])}
|
||||
)
|
||||
rv.append(bid_data)
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
|
||||
@@ -493,7 +693,19 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
|
||||
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
post_data = getFormData(post_string, is_json)
|
||||
offer_id, filters = parseBidFilters(post_data)
|
||||
|
||||
@@ -589,16 +801,14 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
if amt_from_str is not None:
|
||||
rate = ci_to.make_int(rate, r=1)
|
||||
amt_from = inputAmount(amt_from_str, ci_from)
|
||||
amount_to = ci_to.format_amount(
|
||||
int((amt_from * rate) // ci_from.COIN()), r=1
|
||||
)
|
||||
amount_to_int = (amt_from * rate + ci_from.COIN() - 1) // ci_from.COIN()
|
||||
amount_to = ci_to.format_amount(amount_to_int)
|
||||
return bytes(json.dumps({"amount_to": amount_to}), "UTF-8")
|
||||
if amt_to_str is not None:
|
||||
rate = ci_from.make_int(1.0 / float(rate), r=1)
|
||||
amt_to = inputAmount(amt_to_str, ci_to)
|
||||
amount_from = ci_from.format_amount(
|
||||
int((amt_to * rate) // ci_to.COIN()), r=1
|
||||
)
|
||||
amount_from_int = (amt_to * rate + ci_to.COIN() - 1) // ci_to.COIN()
|
||||
amount_from = ci_from.format_amount(amount_from_int)
|
||||
return bytes(json.dumps({"amount_from": amount_from}), "UTF-8")
|
||||
|
||||
amt_from: int = inputAmount(get_data_entry(post_data, "amt_from"), ci_from)
|
||||
@@ -610,7 +820,19 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
|
||||
|
||||
def js_index(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
try:
|
||||
swap_client.checkSystemStatus()
|
||||
except Exception as e:
|
||||
from basicswap.util import LockedCoinError
|
||||
from basicswap.ui.util import getCoinName
|
||||
|
||||
if isinstance(e, LockedCoinError):
|
||||
error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet."
|
||||
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
|
||||
else:
|
||||
return bytes(json.dumps({"error": str(e)}), "UTF-8")
|
||||
|
||||
return bytes(json.dumps(swap_client.getSummary()), "UTF-8")
|
||||
|
||||
|
||||
@@ -620,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
if not swap_client.debug:
|
||||
raise ValueError("Debug mode not active.")
|
||||
|
||||
r = random.randint(0, 3)
|
||||
r = random.randint(0, 4)
|
||||
if r == 0:
|
||||
swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()})
|
||||
swap_client.notify(
|
||||
NT.OFFER_RECEIVED,
|
||||
{
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 1:
|
||||
swap_client.notify(
|
||||
NT.BID_RECEIVED,
|
||||
@@ -630,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "atomic",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 2,
|
||||
"coin_to": 6,
|
||||
"amount_from": 100000000,
|
||||
"amount_to": 15500000000000,
|
||||
"bid_amount": 50000000,
|
||||
"bid_amount_to": 7750000000000,
|
||||
"rate": 15500000000000,
|
||||
},
|
||||
)
|
||||
elif r == 2:
|
||||
@@ -641,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
||||
"type": "ads",
|
||||
"bid_id": random.randbytes(28).hex(),
|
||||
"offer_id": random.randbytes(28).hex(),
|
||||
"coin_from": 1,
|
||||
"coin_to": 3,
|
||||
"amount_from": 500000000,
|
||||
"amount_to": 100000000,
|
||||
"bid_amount": 250000000,
|
||||
"bid_amount_to": 50000000,
|
||||
"rate": 20000000,
|
||||
},
|
||||
)
|
||||
elif r == 4:
|
||||
swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()})
|
||||
|
||||
return bytes(json.dumps({"type": r}), "UTF-8")
|
||||
|
||||
|
||||
def js_checkupdates(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
if not swap_client.settings.get("check_updates", True):
|
||||
return bytes(
|
||||
json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8"
|
||||
)
|
||||
|
||||
import time
|
||||
|
||||
now = time.time()
|
||||
last_manual_check = getattr(swap_client, "_last_manual_update_check", 0)
|
||||
|
||||
if not swap_client.debug and (now - last_manual_check) < 3600:
|
||||
remaining = int(3600 - (now - last_manual_check))
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"error": f"Please wait {remaining // 60} minutes before checking again"
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
swap_client._last_manual_update_check = now
|
||||
swap_client.log.info("Manual update check requested via web interface")
|
||||
|
||||
swap_client.checkForUpdates()
|
||||
|
||||
if swap_client._update_available:
|
||||
swap_client.log.info(
|
||||
f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})"
|
||||
)
|
||||
else:
|
||||
swap_client.log.info(f"Manual check result: Up to date (v{__version__})")
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"message": "Update check completed",
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"update_available": swap_client._update_available,
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -654,6 +952,32 @@ def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(swap_client.getNotifications()), "UTF-8")
|
||||
|
||||
|
||||
def js_updatestatus(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
from basicswap import __version__
|
||||
|
||||
return bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"update_available": swap_client._update_available,
|
||||
"current_version": __version__,
|
||||
"latest_version": swap_client._latest_version,
|
||||
"release_url": (
|
||||
f"https://github.com/basicswap/basicswap/releases/tag/v{swap_client._latest_version}"
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
"release_notes": (
|
||||
f"New version v{swap_client._latest_version} is available. Click to view details on GitHub."
|
||||
if swap_client._latest_version
|
||||
else None
|
||||
),
|
||||
}
|
||||
),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -776,7 +1100,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
|
||||
"label": strat_data.label,
|
||||
"type_ind": strat_data.type_ind,
|
||||
"only_known_identities": strat_data.only_known_identities,
|
||||
"data": json.loads(strat_data.data.decode("utf-8")),
|
||||
"data": json.loads(strat_data.data.decode("UTF-8")),
|
||||
"note": "" if strat_data.note is None else strat_data.note,
|
||||
}
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
@@ -804,7 +1128,7 @@ def js_validateamount(self, url_split, post_string: str, is_json: bool) -> bytes
|
||||
f"Unknown rounding method, must be one of {valid_round_methods}"
|
||||
)
|
||||
|
||||
coin_type = tickerToCoinId(ticker_str)
|
||||
coin_type = getCoinIdFromTicker(ticker_str)
|
||||
ci = swap_client.ci(coin_type)
|
||||
|
||||
r = 0
|
||||
@@ -830,7 +1154,21 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client.checkSystemStatus()
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
coin = getCoinType(get_data_entry(post_data, "coin"))
|
||||
coin_in = get_data_entry(post_data, "coin")
|
||||
extkey_prefix = get_data_entry_or(
|
||||
post_data, "extkey_prefix", 0x04B2430C
|
||||
) # default, zprv for P2WPKH in electrum
|
||||
if isinstance(extkey_prefix, str):
|
||||
if extkey_prefix.isdigit():
|
||||
extkey_prefix = int(extkey_prefix)
|
||||
else:
|
||||
extkey_prefix = int(extkey_prefix, 16) # Try hex
|
||||
|
||||
try:
|
||||
coin = getCoinIdFromName(coin_in)
|
||||
except Exception:
|
||||
coin = getCoinType(coin_in)
|
||||
|
||||
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
|
||||
raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.")
|
||||
|
||||
@@ -858,14 +1196,22 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
expect_seedid = swap_client.getStringKV(
|
||||
"main_wallet_seedid_" + ci.coin_name().lower()
|
||||
)
|
||||
|
||||
try:
|
||||
wallet_seed_id = ci.getWalletSeedID()
|
||||
except Exception as e:
|
||||
wallet_seed_id = f"Error: {e}"
|
||||
rv.update(
|
||||
{
|
||||
"seed": seed_key.hex(),
|
||||
"seed_id": seed_id.hex(),
|
||||
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
|
||||
"current_seed_id": wallet_seed_id,
|
||||
}
|
||||
)
|
||||
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
|
||||
rv.update(
|
||||
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)}
|
||||
) # Master key can be imported into electrum (Must set prefix for P2WPKH)
|
||||
|
||||
return bytes(
|
||||
json.dumps(rv),
|
||||
@@ -900,7 +1246,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
password = get_data_entry(post_data, "password")
|
||||
password: str = get_data_entry(post_data, "password")
|
||||
|
||||
if have_data_entry(post_data, "coin"):
|
||||
coin = getCoinType(str(get_data_entry(post_data, "coin")))
|
||||
@@ -935,7 +1281,7 @@ def js_404(self, url_split, post_string, is_json) -> bytes:
|
||||
def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||
# TODO: Add details and examples
|
||||
commands = []
|
||||
for k in pages:
|
||||
for k in endpoints:
|
||||
commands.append(k)
|
||||
return bytes(json.dumps({"commands": commands}), "UTF-8")
|
||||
|
||||
@@ -943,26 +1289,289 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
|
||||
def js_readurl(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if have_data_entry(post_data, "url"):
|
||||
url = get_data_entry(post_data, "url")
|
||||
default_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
}
|
||||
response = swap_client.readURL(url, headers=default_headers)
|
||||
try:
|
||||
error = json.loads(response.decode())
|
||||
if "Error" in error:
|
||||
return json.dumps({"Error": error["Error"]}).encode()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return response
|
||||
raise ValueError("Requires URL.")
|
||||
if not have_data_entry(post_data, "url"):
|
||||
raise ValueError("Requires URL.")
|
||||
url = get_data_entry(post_data, "url")
|
||||
default_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
}
|
||||
response = swap_client.readURL(url, headers=default_headers)
|
||||
try:
|
||||
error = json.loads(response.decode())
|
||||
if "Error" in error:
|
||||
return json.dumps({"Error": error["Error"]}).encode()
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return response
|
||||
|
||||
|
||||
pages = {
|
||||
def js_active(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
all_bids = []
|
||||
try:
|
||||
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
|
||||
try:
|
||||
ci_from = swap_client.ci(offer.coin_from)
|
||||
ci_to = swap_client.ci(offer.coin_to)
|
||||
if offer.bid_reversed:
|
||||
amount_from: int = bid.amount_to
|
||||
amount_to: int = bid.amount
|
||||
bid_rate: int = ci_from.make_int(amount_to / amount_from, r=1)
|
||||
else:
|
||||
amount_from: int = bid.amount
|
||||
amount_to: int = bid.amount_to
|
||||
bid_rate: int = bid.rate
|
||||
swap_data = {
|
||||
"bid_id": bid_id.hex(),
|
||||
"offer_id": offer.offer_id.hex(),
|
||||
"created_at": bid.created_at,
|
||||
"expire_at": bid.expire_at,
|
||||
"bid_state": strBidState(bid.state),
|
||||
"tx_state_a": None,
|
||||
"tx_state_b": None,
|
||||
"coin_from": ci_from.coin_name(),
|
||||
"coin_to": ci_to.coin_name(),
|
||||
"amount_from": ci_from.format_amount(amount_from),
|
||||
"amount_to": ci_to.format_amount(amount_to),
|
||||
"rate": bid_rate,
|
||||
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
|
||||
"was_sent": bid.was_sent,
|
||||
}
|
||||
|
||||
if offer.swap_type == SwapTypes.XMR_SWAP:
|
||||
swap_data["tx_state_a"] = (
|
||||
strTxState(bid.xmr_a_lock_tx.state)
|
||||
if bid.xmr_a_lock_tx
|
||||
else None
|
||||
)
|
||||
swap_data["tx_state_b"] = (
|
||||
strTxState(bid.xmr_b_lock_tx.state)
|
||||
if bid.xmr_b_lock_tx
|
||||
else None
|
||||
)
|
||||
else:
|
||||
swap_data["tx_state_a"] = bid.getITxState()
|
||||
swap_data["tx_state_b"] = bid.getPTxState()
|
||||
|
||||
all_bids.append(swap_data)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
return bytes(json.dumps([]), "UTF-8")
|
||||
return bytes(json.dumps(all_bids), "UTF-8")
|
||||
|
||||
|
||||
def js_coinprices(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
currency_to = Fiat.USD
|
||||
if have_data_entry(post_data, "currency_to"):
|
||||
currency_to = fiatFromTicker(get_data_entry(post_data, "currency_to"))
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
coinprices = swap_client.lookupFiatRates(
|
||||
coin_ids, currency_to=currency_to, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in coinprices.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"currency": currency_to.name, "source": rate_source, "rates": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
volume_data = swap_client.lookupVolume(
|
||||
coin_ids, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in volume_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
if not have_data_entry(post_data, "coins"):
|
||||
raise ValueError("Requires coins list.")
|
||||
|
||||
rate_source: str = "coingecko.com"
|
||||
if have_data_entry(post_data, "source"):
|
||||
rate_source = get_data_entry(post_data, "source")
|
||||
|
||||
match_input_key: bool = toBool(
|
||||
get_data_entry_or(post_data, "match_input_key", "true")
|
||||
)
|
||||
ttl: int = int(get_data_entry_or(post_data, "ttl", 3600))
|
||||
days: int = int(get_data_entry_or(post_data, "days", 1))
|
||||
|
||||
coins = get_data_entry(post_data, "coins")
|
||||
coins_list = coins.split(",")
|
||||
coin_ids = []
|
||||
input_id_map = {}
|
||||
for coin in coins_list:
|
||||
if coin.isdigit():
|
||||
try:
|
||||
coin_id = Coins(int(coin))
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
else:
|
||||
try:
|
||||
coin_id = getCoinIdFromTicker(coin)
|
||||
except Exception:
|
||||
try:
|
||||
coin_id = getCoinIdFromName(coin)
|
||||
except Exception:
|
||||
raise ValueError(f"Unknown coin type {coin}")
|
||||
coin_ids.append(coin_id)
|
||||
input_id_map[coin_id] = coin
|
||||
|
||||
historical_data = swap_client.lookupHistoricalData(
|
||||
coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
|
||||
)
|
||||
|
||||
rv = {}
|
||||
for k, v in historical_data.items():
|
||||
if match_input_key:
|
||||
rv[input_id_map[k]] = v
|
||||
else:
|
||||
rv[int(k)] = v
|
||||
return bytes(
|
||||
json.dumps({"source": rate_source, "days": days, "data": rv}),
|
||||
"UTF-8",
|
||||
)
|
||||
|
||||
|
||||
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
|
||||
filters = {
|
||||
"page_no": 1,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
}
|
||||
|
||||
if have_data_entry(post_data, "sort_by"):
|
||||
sort_by = get_data_entry(post_data, "sort_by")
|
||||
ensure(
|
||||
sort_by
|
||||
in [
|
||||
"created_at",
|
||||
],
|
||||
"Invalid sort by",
|
||||
)
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(post_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(post_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
|
||||
if have_data_entry(post_data, "offset"):
|
||||
filters["offset"] = int(get_data_entry(post_data, "offset"))
|
||||
if have_data_entry(post_data, "limit"):
|
||||
filters["limit"] = int(get_data_entry(post_data, "limit"))
|
||||
ensure(filters["limit"] > 0, "Invalid limit")
|
||||
|
||||
if have_data_entry(post_data, "address_from"):
|
||||
filters["address_from"] = get_data_entry(post_data, "address_from")
|
||||
if have_data_entry(post_data, "address_to"):
|
||||
filters["address_to"] = get_data_entry(post_data, "address_to")
|
||||
|
||||
action = get_data_entry_or(post_data, "action", None)
|
||||
|
||||
message_routes = swap_client.listMessageRoutes(filters, action)
|
||||
return bytes(json.dumps(message_routes), "UTF-8")
|
||||
|
||||
|
||||
endpoints = {
|
||||
"coins": js_coins,
|
||||
"walletbalances": js_walletbalances,
|
||||
"wallets": js_wallets,
|
||||
"offers": js_offers,
|
||||
"sentoffers": js_sentoffers,
|
||||
@@ -975,6 +1584,8 @@ pages = {
|
||||
"rates": js_rates,
|
||||
"rateslist": js_rates_list,
|
||||
"generatenotification": js_generatenotification,
|
||||
"checkupdates": js_checkupdates,
|
||||
"updatestatus": js_updatestatus,
|
||||
"notifications": js_notifications,
|
||||
"identities": js_identities,
|
||||
"automationstrategies": js_automationstrategies,
|
||||
@@ -986,10 +1597,15 @@ pages = {
|
||||
"lock": js_lock,
|
||||
"help": js_help,
|
||||
"readurl": js_readurl,
|
||||
"active": js_active,
|
||||
"coinprices": js_coinprices,
|
||||
"coinvolume": js_coinvolume,
|
||||
"coinhistory": js_coinhistory,
|
||||
"messageroutes": js_messageroutes,
|
||||
}
|
||||
|
||||
|
||||
def js_url_to_function(url_split):
|
||||
if len(url_split) > 2:
|
||||
return pages.get(url_split[2], js_404)
|
||||
return endpoints.get(url_split[2], js_404)
|
||||
return js_index
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -23,6 +24,13 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||
from basicswap.util.integer import encode_varint, decode_varint
|
||||
|
||||
|
||||
NPBW_INT = 0
|
||||
NPBW_BYTES = 2
|
||||
|
||||
NPBF_STR = 1
|
||||
NPBF_BOOL = 2
|
||||
|
||||
|
||||
class NonProtobufClass:
|
||||
def __init__(self, init_all: bool = True, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
@@ -34,7 +42,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()
|
||||
@@ -117,150 +125,182 @@ 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", NPBW_INT, 0),
|
||||
2: ("coin_from", NPBW_INT, 0),
|
||||
3: ("coin_to", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("min_bid_amount", NPBW_INT, 0),
|
||||
7: ("time_valid", NPBW_INT, 0),
|
||||
8: ("lock_type", NPBW_INT, 0),
|
||||
9: ("lock_value", NPBW_INT, 0),
|
||||
10: ("swap_type", NPBW_INT, 0),
|
||||
11: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
12: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
13: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
14: ("secret_hash", NPBW_BYTES, 0),
|
||||
15: ("fee_rate_from", NPBW_INT, 0),
|
||||
16: ("fee_rate_to", NPBW_INT, 0),
|
||||
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
19: ("proof_utxos", NPBW_BYTES, 0),
|
||||
20: ("auto_accept_type", NPBW_INT, 0),
|
||||
21: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
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", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkhash_buyer", NPBW_BYTES, 0),
|
||||
7: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
9: ("proof_utxos", NPBW_BYTES, 0),
|
||||
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
|
||||
11: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
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", NPBW_BYTES, 0),
|
||||
2: ("initiate_txid", NPBW_BYTES, 0),
|
||||
3: ("contract_script", NPBW_BYTES, 0),
|
||||
4: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class OfferRevokeMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("offer_msg_id", 2, 0),
|
||||
2: ("signature", 2, 0),
|
||||
1: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
2: ("signature", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidRejectMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("reject_code", 0, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("reject_code", NPBW_INT, 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", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkaf", NPBW_BYTES, 0),
|
||||
7: ("kbvf", NPBW_BYTES, 0),
|
||||
8: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
9: ("dest_af", NPBW_BYTES, 0),
|
||||
10: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
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", NPBW_BYTES, 0),
|
||||
2: ("msg_type", NPBW_INT, 0),
|
||||
3: ("sequence", NPBW_INT, 0),
|
||||
4: ("dleag", NPBW_BYTES, 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", NPBW_BYTES, 0),
|
||||
2: ("pkal", NPBW_BYTES, 0),
|
||||
3: ("kbvl", NPBW_BYTES, 0),
|
||||
4: ("kbsl_dleag", NPBW_BYTES, 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", NPBW_BYTES, 0),
|
||||
6: ("a_lock_tx_script", NPBW_BYTES, 0),
|
||||
7: ("a_lock_refund_tx", NPBW_BYTES, 0),
|
||||
8: ("a_lock_refund_tx_script", NPBW_BYTES, 0),
|
||||
9: ("a_lock_refund_spend_tx", NPBW_BYTES, 0),
|
||||
10: ("al_lock_refund_tx_sig", NPBW_BYTES, 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", NPBW_BYTES, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", NPBW_BYTES, 0),
|
||||
3: ("af_lock_refund_tx_sig", NPBW_BYTES, 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", NPBW_BYTES, 0),
|
||||
2: ("a_lock_spend_tx", NPBW_BYTES, 0),
|
||||
3: ("kal_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||
# MSG5F
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("al_lock_spend_tx_esig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("al_lock_spend_tx_esig", NPBW_BYTES, 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", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("message_nets", NPBW_BYTES, NPBF_STR),
|
||||
}
|
||||
|
||||
|
||||
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", NPBW_BYTES, 0),
|
||||
2: ("pkaf", NPBW_BYTES, 0),
|
||||
3: ("kbvf", NPBW_BYTES, 0),
|
||||
4: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
5: ("dest_af", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class ConnectReqMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("network_type", NPBW_INT, 0),
|
||||
2: ("network_data", NPBW_BYTES, 0),
|
||||
3: ("request_type", NPBW_INT, 0),
|
||||
4: ("request_data", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class MessagePortalOffer(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("network_type_from", NPBW_INT, 0),
|
||||
2: ("network_type_to", NPBW_INT, 0),
|
||||
3: ("portal_address_from", NPBW_BYTES, 0),
|
||||
4: ("portal_address_to", NPBW_BYTES, 0),
|
||||
5: ("time_valid", NPBW_INT, 0),
|
||||
6: ("smsg_difficulty", NPBW_INT, 0),
|
||||
}
|
||||
|
||||
|
||||
class MessagePortalSend(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("forward_address", NPBW_BYTES, 0), # pubkey, 33 bytes
|
||||
2: ("message_bytes", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
0
basicswap/network/__init__.py
Normal file
0
basicswap/network/__init__.py
Normal file
1185
basicswap/network/bsx_network.py
Normal file
1185
basicswap/network/bsx_network.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,17 +6,17 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
"""
|
||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||
|
||||
Handshake procedure:
|
||||
node0 connecting to node1
|
||||
node0 send_handshake
|
||||
node1 process_handshake
|
||||
node1 send_ping - With a version field
|
||||
node0 recv_ping
|
||||
Both nodes are initialised
|
||||
Handshake procedure:
|
||||
node0 connecting to node1
|
||||
node0 send_handshake
|
||||
node1 process_handshake
|
||||
node1 send_ping - With a version field
|
||||
node0 recv_ping
|
||||
Both nodes are initialised
|
||||
|
||||
XChaCha20_Poly1305 mac is 16bytes
|
||||
XChaCha20_Poly1305 mac is 16bytes
|
||||
"""
|
||||
|
||||
import time
|
||||
516
basicswap/network/simplex.py
Normal file
516
basicswap/network/simplex.py
Normal file
@@ -0,0 +1,516 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import websocket
|
||||
|
||||
|
||||
from queue import Queue, Empty
|
||||
|
||||
from basicswap.util.smsg import (
|
||||
smsgEncrypt,
|
||||
smsgDecrypt,
|
||||
smsgGetID,
|
||||
)
|
||||
from basicswap.chainparams import (
|
||||
Coins,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
decodeWif,
|
||||
)
|
||||
from basicswap.basicswap_util import AddressTypes
|
||||
|
||||
|
||||
def encode_base64(data: bytes) -> str:
|
||||
return base64.b64encode(data).decode("utf-8")
|
||||
|
||||
|
||||
def decode_base64(encoded_data: str) -> bytes:
|
||||
return base64.b64decode(encoded_data)
|
||||
|
||||
|
||||
class WebSocketThread(threading.Thread):
|
||||
def __init__(self, url: str, tag: str = None, logger=None):
|
||||
super().__init__()
|
||||
self.url: str = url
|
||||
self.tag = tag
|
||||
self.logger = logger
|
||||
self.ws = None
|
||||
self.mutex = threading.Lock()
|
||||
self.corrId: int = 0
|
||||
self.connected: bool = False
|
||||
self.delay_event = threading.Event()
|
||||
|
||||
self.recv_queue = Queue()
|
||||
self.cmd_recv_queue = Queue()
|
||||
self.delayed_events_queue = Queue()
|
||||
|
||||
self.ignore_events: bool = False
|
||||
|
||||
self.num_messages_received: int = 0
|
||||
|
||||
def disable_debug_mode(self):
|
||||
self.ignore_events = False
|
||||
for i in range(100):
|
||||
try:
|
||||
message = self.delayed_events_queue.get(block=False)
|
||||
except Empty:
|
||||
break
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def on_message(self, ws, message):
|
||||
if self.logger:
|
||||
self.logger.debug("Simplex received msg")
|
||||
else:
|
||||
print(f"{self.tag} - Received msg")
|
||||
|
||||
if message.startswith('{"corrId"'):
|
||||
self.cmd_recv_queue.put(message)
|
||||
else:
|
||||
self.num_messages_received += 1
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def queue_get(self):
|
||||
try:
|
||||
return self.recv_queue.get(block=False)
|
||||
except Empty:
|
||||
return None
|
||||
|
||||
def cmd_queue_get(self):
|
||||
try:
|
||||
return self.cmd_recv_queue.get(block=False)
|
||||
except Empty:
|
||||
return None
|
||||
|
||||
def on_error(self, ws, error):
|
||||
if self.logger:
|
||||
self.logger.error(f"Simplex ws - {error}")
|
||||
else:
|
||||
print(f"{self.tag} - Error: {error}")
|
||||
|
||||
def on_close(self, ws, close_status_code, close_msg):
|
||||
if self.logger:
|
||||
self.logger.info(f"Simplex ws - Closed: {close_status_code}, {close_msg}")
|
||||
else:
|
||||
print(f"{self.tag} - Closed: {close_status_code}, {close_msg}")
|
||||
|
||||
def on_open(self, ws):
|
||||
if self.logger:
|
||||
self.logger.info("Simplex ws - Connection opened")
|
||||
else:
|
||||
print(f"{self.tag}: WebSocket connection opened")
|
||||
self.connected = True
|
||||
|
||||
def send_command(self, cmd_str: str):
|
||||
with self.mutex:
|
||||
self.corrId += 1
|
||||
if self.logger:
|
||||
self.logger.debug(f"Simplex sent command {self.corrId}")
|
||||
else:
|
||||
print(f"{self.tag}: sent command {self.corrId}")
|
||||
cmd = json.dumps({"corrId": str(self.corrId), "cmd": cmd_str})
|
||||
self.ws.send(cmd)
|
||||
return self.corrId
|
||||
|
||||
def wait_for_command_response(self, cmd_id, num_tries: int = 200):
|
||||
cmd_id = str(cmd_id)
|
||||
for i in range(num_tries):
|
||||
message = self.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == cmd_id:
|
||||
return data
|
||||
self.delay_event.wait(0.5)
|
||||
raise ValueError(
|
||||
f"wait_for_command_response timed-out waiting for ID: {cmd_id}"
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.ws = websocket.WebSocketApp(
|
||||
self.url,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_open=self.on_open,
|
||||
on_close=self.on_close,
|
||||
)
|
||||
while not self.delay_event.is_set():
|
||||
self.ws.run_forever()
|
||||
self.delay_event.wait(0.5)
|
||||
|
||||
def stop(self):
|
||||
self.delay_event.set()
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
|
||||
|
||||
def waitForResponse(ws_thread, sent_id, delay_event):
|
||||
sent_id = str(sent_id)
|
||||
for i in range(200):
|
||||
message = ws_thread.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == sent_id:
|
||||
return data
|
||||
delay_event.wait(0.5)
|
||||
raise ValueError(f"waitForResponse timed-out waiting for ID: {sent_id}")
|
||||
|
||||
|
||||
def waitForConnected(ws_thread, delay_event):
|
||||
for i in range(100):
|
||||
if ws_thread.connected:
|
||||
return True
|
||||
delay_event.wait(0.5)
|
||||
raise ValueError("waitForConnected timed-out.")
|
||||
|
||||
|
||||
def encryptMsg(
|
||||
self,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp=None,
|
||||
deterministic=False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> bytes:
|
||||
self.log.debug("encryptMsg")
|
||||
|
||||
pubkey_to = self.getPubkeyForAddress(cursor, addr_to)
|
||||
privkey_from = self.getPrivkeyForAddress(cursor, addr_from)
|
||||
|
||||
smsg_msg: bytes = smsgEncrypt(
|
||||
privkey_from,
|
||||
pubkey_to,
|
||||
payload,
|
||||
timestamp,
|
||||
deterministic,
|
||||
msg_valid,
|
||||
difficulty_target=difficulty_target,
|
||||
)
|
||||
|
||||
return smsg_msg
|
||||
|
||||
|
||||
def sendSimplexMsg(
|
||||
self,
|
||||
network,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp: int = None,
|
||||
deterministic: bool = False,
|
||||
to_user_name: str = None,
|
||||
return_msg: bool = False,
|
||||
difficulty_target=0x1EFFFFFF,
|
||||
) -> bytes:
|
||||
self.log.debug("sendSimplexMsg")
|
||||
|
||||
smsg_msg: bytes = encryptMsg(
|
||||
self,
|
||||
addr_from,
|
||||
addr_to,
|
||||
payload,
|
||||
msg_valid,
|
||||
cursor,
|
||||
timestamp,
|
||||
deterministic,
|
||||
difficulty_target,
|
||||
)
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
|
||||
ws_thread = network["ws_thread"]
|
||||
if to_user_name is not None:
|
||||
to = "@" + to_user_name + " "
|
||||
else:
|
||||
to = "#bsx "
|
||||
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if getResponseData(response, "type") != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
self.log.debug(f"Response {json_str}")
|
||||
raise ValueError("Send failed")
|
||||
if to_user_name is not None:
|
||||
self.num_direct_simplex_messages_sent += 1
|
||||
else:
|
||||
self.num_group_simplex_messages_sent += 1
|
||||
|
||||
if return_msg:
|
||||
return smsg_id, smsg_msg
|
||||
return smsg_id
|
||||
|
||||
|
||||
def forwardSimplexMsg(self, network, smsg_msg, to_user_name: str = None):
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
ws_thread = network["ws_thread"]
|
||||
if to_user_name is not None:
|
||||
to = "@" + to_user_name + " "
|
||||
else:
|
||||
to = "#bsx "
|
||||
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if getResponseData(response, "type") != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
self.log.debug(f"Response {json_str}")
|
||||
raise ValueError("Send failed")
|
||||
if to_user_name is not None:
|
||||
self.num_direct_simplex_messages_sent += 1
|
||||
else:
|
||||
self.num_group_simplex_messages_sent += 1
|
||||
|
||||
return smsg_id
|
||||
|
||||
|
||||
def decryptSimplexMsg(self, msg_data):
|
||||
ci_part = self.ci(Coins.PART)
|
||||
|
||||
# Try with the network key first
|
||||
network_key: bytes = decodeWif(self.network_key)
|
||||
try:
|
||||
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pubkey_from"])
|
||||
)
|
||||
decrypted["to"] = self.network_addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
return decrypted
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
|
||||
# Try with all active bid/offer addresses
|
||||
query: str = """SELECT DISTINCT address FROM (
|
||||
SELECT b.bid_addr AS address FROM bids b
|
||||
JOIN bidstates s ON b.state = s.state_id
|
||||
WHERE b.active_ind = 1
|
||||
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
|
||||
UNION
|
||||
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
|
||||
UNION
|
||||
SELECT addr AS address FROM smsgaddresses WHERE active_ind = 1 AND use_type = :local_portal
|
||||
)"""
|
||||
|
||||
now: int = self.getTime()
|
||||
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
addr_rows = cursor.execute(
|
||||
query, {"now": now, "local_portal": AddressTypes.PORTAL_LOCAL}
|
||||
).fetchall()
|
||||
decrypted = None
|
||||
for row in addr_rows:
|
||||
addr = row[0]
|
||||
try:
|
||||
vk_addr = self.getPrivkeyForAddress(cursor, addr)
|
||||
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
|
||||
decrypted["from"] = ci_part.pubkey_to_address(
|
||||
bytes.fromhex(decrypted["pubkey_from"])
|
||||
)
|
||||
decrypted["to"] = addr
|
||||
decrypted["msg_net"] = "simplex"
|
||||
return decrypted
|
||||
except Exception as e: # noqa: F841
|
||||
pass
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
return decrypted
|
||||
|
||||
|
||||
def parseSimplexMsg(self, chat_item):
|
||||
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
|
||||
dir_type = item_status["type"]
|
||||
if dir_type not in ("sndRcvd", "rcvNew"):
|
||||
return None
|
||||
|
||||
snd_progress = item_status.get("sndProgress", None)
|
||||
if snd_progress and snd_progress != "complete":
|
||||
item_id = chat_item["chatItem"]["meta"]["itemId"]
|
||||
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
|
||||
return None
|
||||
|
||||
conn_id = None
|
||||
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
|
||||
chat_type: str = chat_item["chatInfo"]["type"]
|
||||
if chat_type == "group":
|
||||
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
|
||||
self.num_group_simplex_messages_received += 1
|
||||
elif chat_type == "direct":
|
||||
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
|
||||
self.num_direct_simplex_messages_received += 1
|
||||
else:
|
||||
return None
|
||||
|
||||
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
|
||||
try:
|
||||
msg_data: bytes = decode_base64(msg_content)
|
||||
decrypted_msg = decryptSimplexMsg(self, msg_data)
|
||||
if decrypted_msg is None:
|
||||
return None
|
||||
decrypted_msg["chat_type"] = chat_type
|
||||
decrypted_msg["chat_name"] = chat_name
|
||||
decrypted_msg["conn_id"] = conn_id
|
||||
decrypted_msg["msg_dir"] = msg_dir
|
||||
return decrypted_msg
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
|
||||
if ws_thread.ignore_events:
|
||||
if msg_type not in ("contactConnected", "contactDeletedByContact"):
|
||||
return False
|
||||
ws_thread.delayed_events_queue.put(json.dumps(data))
|
||||
return True
|
||||
|
||||
if msg_type == "contactConnected":
|
||||
self.processContactConnected(data)
|
||||
elif msg_type == "contactDeletedByContact":
|
||||
self.processContactDisconnected(data)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def readSimplexMsgs(self, network):
|
||||
ws_thread = network["ws_thread"]
|
||||
for i in range(100):
|
||||
message = ws_thread.queue_get()
|
||||
if message is None:
|
||||
break
|
||||
if self.delay_event.is_set():
|
||||
break
|
||||
|
||||
data = json.loads(message)
|
||||
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
|
||||
try:
|
||||
msg_type: str = getResponseData(data, "type")
|
||||
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
|
||||
for chat_item in getResponseData(data, "chatItems"):
|
||||
decrypted_msg = parseSimplexMsg(self, chat_item)
|
||||
if decrypted_msg is None:
|
||||
continue
|
||||
self.processMsg(decrypted_msg)
|
||||
elif msg_type == "chatError":
|
||||
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
|
||||
pass
|
||||
elif processEvent(self, ws_thread, msg_type, data):
|
||||
pass
|
||||
else:
|
||||
self.log.debug(f"simplex: Unknown msg_type: {msg_type}")
|
||||
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
|
||||
except Exception as e:
|
||||
self.log.debug(f"readSimplexMsgs error: {e}")
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
self.delay_event.wait(0.05)
|
||||
|
||||
|
||||
def getResponseData(data, tag=None):
|
||||
for pretag in ("Right", "Left"):
|
||||
if pretag in data["resp"]:
|
||||
if tag:
|
||||
return data["resp"][pretag][tag]
|
||||
return data["resp"][pretag]
|
||||
if tag:
|
||||
return data["resp"][tag]
|
||||
return data["resp"]
|
||||
|
||||
|
||||
def getNewSimplexLink(data):
|
||||
response_data = getResponseData(data)
|
||||
if "connLinkContact" in response_data:
|
||||
return response_data["connLinkContact"]["connFullLink"]
|
||||
return response_data["connReqContact"]
|
||||
|
||||
|
||||
def getJoinedSimplexLink(data):
|
||||
response_data = getResponseData(data)
|
||||
if "connLinkInvitation" in response_data:
|
||||
return response_data["connLinkInvitation"]["connFullLink"]
|
||||
return response_data["connReqInvitation"]
|
||||
|
||||
|
||||
def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
self.log.debug("initialiseSimplexNetwork")
|
||||
|
||||
client_host: str = network_config.get("client_host", "127.0.0.1")
|
||||
ws_port: str = network_config.get("ws_port")
|
||||
|
||||
ws_thread = WebSocketThread(f"ws://{client_host}:{ws_port}", logger=self.log)
|
||||
self.threads.append(ws_thread)
|
||||
ws_thread.start()
|
||||
waitForConnected(ws_thread, self.delay_event)
|
||||
|
||||
sent_id = ws_thread.send_command("/groups")
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
|
||||
if len(getResponseData(response, "groups")) < 1:
|
||||
sent_id = ws_thread.send_command("/c " + network_config["group_link"])
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
assert "groupLinkId" in getResponseData(response, "connection")
|
||||
|
||||
add_network = {
|
||||
"type": "simplex",
|
||||
"ws_thread": ws_thread,
|
||||
}
|
||||
if "bridged" in network_config:
|
||||
add_network["bridged"] = network_config["bridged"]
|
||||
|
||||
self.active_networks.append(add_network)
|
||||
|
||||
|
||||
def closeSimplexChat(self, net_i, connId) -> bool:
|
||||
try:
|
||||
cmd_id = net_i.send_command("/chats")
|
||||
response = net_i.wait_for_command_response(cmd_id, num_tries=500)
|
||||
remote_name = None
|
||||
for chat in getResponseData(response, "chats"):
|
||||
if (
|
||||
"chatInfo" not in chat
|
||||
or "type" not in chat["chatInfo"]
|
||||
or chat["chatInfo"]["type"] != "direct"
|
||||
):
|
||||
continue
|
||||
try:
|
||||
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
|
||||
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error parsing chat: {e}")
|
||||
|
||||
if remote_name is None:
|
||||
self.log.warning(
|
||||
f"Unable to find remote name for simplex direct chat, ID: {connId}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
|
||||
cmd_id = net_i.send_command(f"/delete @{remote_name}")
|
||||
cmd_response = net_i.wait_for_command_response(cmd_id)
|
||||
|
||||
if getResponseData(cmd_response, "type") != "contactDeleted":
|
||||
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
|
||||
self.log.debug(
|
||||
"cmd_response: {}".format(json.dumps(cmd_response, indent=4))
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error deleting simplex chat, ID: {connId} - {e}")
|
||||
return False
|
||||
return True
|
||||
158
basicswap/network/simplex_chat.py
Normal file
158
basicswap/network/simplex_chat.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import select
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from basicswap.util.daemon import Daemon
|
||||
|
||||
|
||||
def serverExistsInDatabase(simplex_db_path: str, server_address: str, logger) -> bool:
|
||||
try:
|
||||
# Extract hostname from SMP URL format: smp://fingerprint@hostname
|
||||
if server_address.startswith("smp://") and "@" in server_address:
|
||||
host = server_address.split("@")[-1]
|
||||
elif ":" in server_address:
|
||||
host = server_address.split(":", 1)[0]
|
||||
else:
|
||||
host = server_address
|
||||
|
||||
with sqlite3.connect(simplex_db_path) as con:
|
||||
c = con.cursor()
|
||||
|
||||
# Check for any server entry with this hostname
|
||||
query = (
|
||||
"SELECT COUNT(*) FROM protocol_servers WHERE host LIKE ? OR host = ?"
|
||||
)
|
||||
host_pattern = f"%{host}%"
|
||||
count = c.execute(query, (host_pattern, host)).fetchone()[0]
|
||||
|
||||
if count > 0:
|
||||
logger.debug(
|
||||
f"Server {host} already exists in database ({count} entries)"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Server {host} not found in database")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database check failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def initSimplexClient(args, logger, delay_event):
|
||||
# Need to set initial profile through CLI
|
||||
# TODO: Must be a better way?
|
||||
logger.info("Initialising Simplex client")
|
||||
|
||||
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
|
||||
|
||||
if os.name == "nt":
|
||||
str_args = " ".join(args)
|
||||
p = subprocess.Popen(
|
||||
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
|
||||
)
|
||||
else:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
|
||||
|
||||
def readOutput():
|
||||
buf = os.read(pipe_r, 1024).decode("utf-8")
|
||||
response = None
|
||||
# logger.debug(f"simplex-chat output: {buf}")
|
||||
if "display name:" in buf:
|
||||
logger.debug("Setting display name")
|
||||
response = b"user\n"
|
||||
else:
|
||||
logger.debug(f"Unexpected output: {buf}")
|
||||
return
|
||||
if response is not None:
|
||||
p.stdin.write(response)
|
||||
p.stdin.flush()
|
||||
|
||||
try:
|
||||
start_time: int = time.time()
|
||||
max_wait_seconds: int = 60
|
||||
while p.poll() is None:
|
||||
if time.time() > start_time + max_wait_seconds:
|
||||
raise RuntimeError("Timed out")
|
||||
if os.name == "nt":
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
continue
|
||||
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
|
||||
readOutput()
|
||||
delay_event.wait(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"initSimplexClient: {e}")
|
||||
finally:
|
||||
if p.poll() is None:
|
||||
p.terminate()
|
||||
os.close(pipe_r)
|
||||
os.close(pipe_w)
|
||||
p.stdin.close()
|
||||
|
||||
|
||||
def startSimplexClient(
|
||||
bin_path: str,
|
||||
data_path: str,
|
||||
server_address: str,
|
||||
websocket_port: int,
|
||||
logger,
|
||||
delay_event,
|
||||
socks_proxy=None,
|
||||
log_level: str = "debug",
|
||||
) -> Daemon:
|
||||
logger.info("Starting Simplex client")
|
||||
if not os.path.exists(data_path):
|
||||
os.makedirs(data_path)
|
||||
|
||||
simplex_data_prefix = os.path.join(data_path, "simplex_client_data")
|
||||
simplex_db_path = simplex_data_prefix + "_chat.db"
|
||||
args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)]
|
||||
|
||||
if socks_proxy:
|
||||
args += ["--socks-proxy", socks_proxy]
|
||||
|
||||
if not os.path.exists(simplex_db_path):
|
||||
# Database doesn't exist - safe to add server during initialization
|
||||
logger.info("Database not found, initializing Simplex client")
|
||||
init_args = args + ["-e", "/help"] # Run command to exit client
|
||||
init_args += ["-s", server_address]
|
||||
initSimplexClient(init_args, logger, delay_event)
|
||||
else:
|
||||
# Database exists - only add server if it's not already there
|
||||
if not serverExistsInDatabase(simplex_db_path, server_address, logger):
|
||||
logger.debug(f"Adding server to Simplex CLI args: {server_address}")
|
||||
args += ["-s", server_address]
|
||||
else:
|
||||
logger.debug("Server already exists, not adding to CLI args")
|
||||
|
||||
args += ["-l", log_level]
|
||||
|
||||
opened_files = []
|
||||
stdout_dest = open(
|
||||
os.path.join(data_path, "simplex_stdout.log"),
|
||||
"w",
|
||||
)
|
||||
opened_files.append(stdout_dest)
|
||||
stderr_dest = stdout_dest
|
||||
return Daemon(
|
||||
subprocess.Popen(
|
||||
args,
|
||||
shell=False,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=stdout_dest,
|
||||
stderr=stderr_dest,
|
||||
cwd=data_path,
|
||||
),
|
||||
opened_files,
|
||||
"simplex-chat",
|
||||
)
|
||||
20
basicswap/network/util.py
Normal file
20
basicswap/network/util.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from basicswap.util.address import b58decode
|
||||
|
||||
|
||||
def getMsgPubkey(self, msg) -> bytes:
|
||||
if "pubkey_from" in msg:
|
||||
return bytes.fromhex(msg["pubkey_from"])
|
||||
rv = self.callrpc(
|
||||
"smsggetpubkey",
|
||||
[
|
||||
msg["from"],
|
||||
],
|
||||
)
|
||||
return b58decode(rv["publickey"])
|
||||
54
basicswap/pgp/keys/SimpleX_Chat.pgp
Normal file
54
basicswap/pgp/keys/SimpleX_Chat.pgp
Normal file
@@ -0,0 +1,54 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC
|
||||
Comment: SimpleX Chat <chat@simplex.chat>
|
||||
|
||||
xsFNBGRDvZkBEACsxFENFWj5hMS1dCPCOXIJTNnWClVarltfUOESy5q0Ar84WJaj
|
||||
hmAcc8j1Qw7uiLxVq/j+tMxcZOy79jnmhWpV5KrYA6H/E3I5NNlZOyT23rvah9mg
|
||||
KtxfMHnhz/jJSwSXifYN2mmAYetQ1TQBSdLZayC7aW6BFhUaaQsaFABGli5abRUW
|
||||
KArmnSfVEHI0f7TthLerPZ0hCoK06ZOPxEKCWt5CSqrC3J2d+8Cyb6j2jxkkB3GN
|
||||
JXr9kI4JebivqrFNwvGw15xEDbSXIZf9I/+B/t9EA4Ebs+qrbLFRH5Drha50RIhu
|
||||
LNYCkVnpKbrO6Y90KkJibm4ZtdUeNTFXjfXxT81Gi5lDmsvIyIMkFC78ePK68knM
|
||||
dnESnIzEEwDtniV+ZvY0L9t/Ig1tGYggqPGVTVp9672bHKTGdiL3eXEzwv0FROD2
|
||||
0HaZORXj2UZkAJTQO2ia7aS3hWdJL/iVBf4yIYARr+6NjPxv/sUMCaeuPYXTqCOB
|
||||
Ykl6Lv3SPoSkEyPfVJY+12STtHH1ZofxJKYwo6Xe7EvmCiC9DK0KKVbeakZZ6wfd
|
||||
5LO/tArDkqT2YjT3DUsfGqxQOoQvGCmk9yUuCm0s0vLwTHdJhSVgn9dxrEuK4FYL
|
||||
IM3tGENAPAcK3e1VEbncgBMRikxvECKIz+YZyQVtoYzX2HDlT4D2HrbgXQARAQAB
|
||||
zSBTaW1wbGVYIENoYXQgPGNoYXRAc2ltcGxleC5jaGF0PsLBlAQTAQgAPhYhBPtE
|
||||
r4GkW94ycxl5fIUQfjV9Shf8BQJkQ72ZAhsDBQkHhh9CBQsJCAcCBhUKCQgLAgQW
|
||||
AgMBAh4BAheAAAoJEIUQfjV9Shf8GekP/jpZYGJrna7467Qe82KV+qtwu+p2cRIy
|
||||
IsoOmCje2p0D9DmmmDQH1IdxlJhvHZ8uEu21QwDK03r5y4iaXhz9bx4CDSDB5JPp
|
||||
fMIDfOdc1V1GDT8Q2f/sYd5DX9kwpW6LdWOQZf6hwRDAeWDa+BQVhwo3E0WsPvRK
|
||||
o5fqrbJzfWj8pz+JMlT8RGGt0ZxEyUjnD9C6XfqGckLdubBycs9CipPKV+3X4cY/
|
||||
ix0zM2Nb3oSJ27VWMIFxi7lnBGtyUY69bE248Xhj0nJ79twPwzvk94+3e5tLQvyt
|
||||
NIZcWEZEu+eYthyKcGDo/aA6lIvt1Bqp8eeFMogRxs5GJI4L/wQGwIDckemtLb45
|
||||
gUdjpufEfPEfxuYWuuHuQ8W7Yvd2/ndiRkir4k+r8ypXx8yeCgocxnuUm1+4s+Wv
|
||||
h64Op+M+l56cTjVCaEn25kv/T+4ll/RBplzKdNe4ClcH4NXppwXFkAHXq/j3RX++
|
||||
64gRzIEC33TGheTo3btowUW+0/6iOi7Jy1RDsNvigzWwpm0p+pje54+d7hTxDmLR
|
||||
bHxOZ9QiauO/HnlqNw/MezZLYL18hyEsghD3ns6QIHcUsHf17u/tRfLgN11x9tiE
|
||||
ADqORsNgQ8FIRGdJcxGIt8lUlSe5vKPArsjpiomoA9CeAqepU27haIesl2QGe/jI
|
||||
5OuS7CsRVDlOzsFNBGRDvZkBEADGYf7E+bzYgORnlSY3TZgS5UvkMIGswlw6GW7j
|
||||
Vx6hAsMbiCwoKCVdzl/J4BImbJIJg2Pxvn/k7tYS2Jqb1q/EcpBmOZU9BRiTw49A
|
||||
TiK8UfeH9aIPNFwuiatmA29dGxPH2RgSCwa3f4l2RsnQl301UdNlXj6mmWngD6mj
|
||||
ae5/COUgH6CbKptfLp0Xw0WpPfKV1GK9+/X8Hv7W6RDA6xoWFlgzTyuy96rMmXJ1
|
||||
3E7P/50ebIOundVzCni10dZyn7+W13cJGOyzxQnbR6PEMVHsgi4uZB/Gt6PxF0dC
|
||||
s56IUi05hr9uH++p7ps2G8iIwvqXDu8VOvwN9hvt1fpxnRC2+Zv0lHwpDrnSBvjY
|
||||
8er2tJxXlybXwEpk1nzctmDDWrgbBgQugOxTu4rkqIvAGwq7U98aLUb3vEqlyWSp
|
||||
YDufsiLbGYC5owCli36yDzjfm48W0DwaOA5Ne5yVCih1f4ocF3RXVU6o1TEW1pfL
|
||||
DEDZOXDT9sj0qef9NW0Nz+x/EiCT2k2Bkwt0ETf4TralsJ7smCcbhqfJbu1NG22g
|
||||
oLNXZcZgTUxmOWmU+nlrFk8Hk7EK2KDeMKSgiX6jrAGpwbphrYYBZ3NLpvJ311l2
|
||||
d56ZgmUt8gb1O5tLNiD2ySCvWKnpG0A5WoKZ2329nlnX2R30otYdpP1vcAEvA3GU
|
||||
7fw5lQARAQABwsF8BBgBCAAmFiEE+0SvgaRb3jJzGXl8hRB+NX1KF/wFAmRDvZkC
|
||||
GwwFCQeGH0IACgkQhRB+NX1KF/wNbw//bi4RcxEOVJpT37pyx6wSlq6urHopuZA5
|
||||
duy0fGYxRXt4w/WR0UMH9i7iSU8J2E/UKgE7OMZg3oJqVt7g70zQDiT8ez+ep9d0
|
||||
YvPAqgRnT1VDmAyMO8FOTPQPIrPMsQTnmtmxf9qrdoxW8HVqiyK+7mCGqd9ldcer
|
||||
XGplALTugRWABY7iYyRyfpDSid+xMKV7KLHabv/0WdcT41HpZuUt0gmH0sMDDiJt
|
||||
XrWW01LDqEZTdfaZ1xXPPp7oXUYGY6U7cH5CdLS6D38tPKR9x0ttgM83/SOx/hOO
|
||||
XApcA+g113eMOyh4udowGYEkpxT26V3u8cLzCBOPDNSFx/H8ggFbfMsCWNBYV2Nx
|
||||
EmAmciHvPMNLR7Hjfvn018/Q+lo1J6snoEhT9zFwpL15Lwurkqy5Z4n1D9BUyZ7m
|
||||
hS/Wg7LDpaEeJCkSkOvQEPKz8YsnMpsbPc44ZZf0yuTUsWwJkZCVEqN8qByKXRdI
|
||||
28zGBBJr5/rjaSJJ7+VGbh/FGUzaEkLONybzKcxazwjSASBNZXmasgStngOGWGpM
|
||||
GKDnIuXs/Z7vljkKF2YoNT9bvGr7yoY74PCKrMkWdVSA1cQBj+cJ4OOojVvOGJaR
|
||||
Gdpp/2r7me5UKImmUw2dhHf0KdM1iYwjzztCO72hi5Fw7vFlNS7QoadmYDzAgWkk
|
||||
0oXYKNS+x2w=
|
||||
=68E9
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
25
basicswap/pgp/keys/bitcoincash_Calin_Culianu.pgp
Normal file
25
basicswap/pgp/keys/bitcoincash_Calin_Culianu.pgp
Normal file
@@ -0,0 +1,25 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQMuBFmZ6L4RCACuqDDCIe2bzKznyKVN1aInzRQnSxdGTXuw0mcDz5HYudAhBjR8
|
||||
gY6sxCRPNxvZCJVDZDpCygXMhWZlJtWLR8KMTCXxC4HLXXOY4RxQ5KGnYWxEAcKY
|
||||
deq1ymmuOuMUp7ltRTSyWcBKbR9xTd2vW/+0W7GQIOxUW/aiT1V0x3cky+6kqaec
|
||||
BorP3+uxJcx0Q8WdlS/6N4x3pBv/lfsdrZSaDD8fU/29pQGMDUEnupKoWJVVei6r
|
||||
G+vxLHEtIFYYO8VWjZntymw3dl+aogrjyuxqWzl8mfPi9M/DgiRb4pJnH2yOGDI6
|
||||
Lvg+oo9E79Vwi98UjYSicsB1dtcptKiA96UXAQD/hDB+dil7/SX/SDTlaw/+uTdd
|
||||
Xg0No63dbN++iY4k3Qf/Xk1ZzbuDviLhe+zEhlJOw6TaMlxfwwQOtxEJXILS5uIL
|
||||
jYlGcDbBtJh3p4qUoUduDOgjumJ9m47XqIq81rQ0pqzzGMbK1Y82NQjX5Sn8yTm9
|
||||
p1hmOZ/uX9vCrUSbYBjxJXyQ1OXlerlLRLfBf5WQ0+LO+0cmgtCyX0zV4oGK7vph
|
||||
XEm7lar7AezOOXaSrWAB+CTPUdJF1E7lcJiUuMVcqMx8pphrH+rfcsqPtN6tkyUD
|
||||
TmPDpc5ViqFFelEEQnKSlmAY+3iCNZ3y/VdPPhuJ2lAsL3tm9MMh2JGV378LG45a
|
||||
6SOkQrC977Qq1dhgJA+PGJxQvL2RJWsYlJwp79+Npgf9EfFaJVNzbdjGVq1XmNie
|
||||
MZYqHRfABkyK0ooDxSyzJrq4vvuhWKInS4JhpKSabgNSsNiiaoDR+YYMHb0H8GRR
|
||||
Za6JCmfU8w97R41UTI32N7dhul4xCDs5OV6maOIoNts20oigNGb7TKH9b5N7sDJB
|
||||
zh3Of/fHCChO9Y2chbzU0bERfcn+evrWBf/9XdQGQ3ggoLbOtGpcUQuB/7ofTcBZ
|
||||
awL6K4VJ2Qlb8DPlRgju6uU9AR/KTYeAlVFC8FX7R0FGgPRcJ3GNkNHGqrbuQ72q
|
||||
AOhYOPx9nRrU5u+E2J325vOabLnLbOazze3j6LFPSFV4vfmTO9exYlwhz3g+lFAd
|
||||
CrQ2Q2FsaW4gQ3VsaWFudSAoTmlsYWNUaGVHcmltKSA8Y2FsaW4uY3VsaWFudUBn
|
||||
bWFpbC5jb20+iHoEExEIACIFAlmZ6L4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B
|
||||
AheAAAoJECGBClQgMcAsU5cBAO/ngONpHsxny2uTV4ge2f+5V2ajTjcIfN2jUZtg
|
||||
31jJAQCl1NcrwcIu98+aM2IyjB1UFXkoaMANpr8L9jBopivRGQ==
|
||||
=cf8I
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
52
basicswap/pgp/keys/namecoin_RoseTuring.pgp
Normal file
52
basicswap/pgp/keys/namecoin_RoseTuring.pgp
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGdeBqoBEADuBizUBhm1m34OQ0rnqUONvkfL3tGsriWuX0n3Bq9rhf3I3kZk
|
||||
5fi+R0Jj6jmz+sbUYRULU35S6eeIY77eYiveWl81H+3JAu8kmo/S6gegINnsPM/g
|
||||
F7X2P757gQdHMAE0olME3iGfXNpLg/e0OBv3j45eimjvUejgE7eI0e4gjajq8hyf
|
||||
bizMrGT+5I2PE0g3h07NqN3OuI5xVYujuZp41EgxY99QgYm5qEoU0wMGy8+F7gXV
|
||||
0htjhvUZcSGGpixP5+kaJJXFAP1TkZ/jqya6vy7LLeEEEuU8eMWhViOmzIjqoOFW
|
||||
Mq+2rJUrzNEk43tXW5LU+DdGl90HQcXPmQP3aWL27Dx/4AcTMYPDB/0bJrU9qF9Y
|
||||
9zfJV2HcNMnkhEb9XKDwkA6m3Jx2gfYG6HoMKp6bWSWsODItEgL1taoy35OnaVSM
|
||||
NWb857DC6p6n+eQUXUNx/1ct4LWmf4lN4Uf61i4mD+hkc4cWmRLAh7vTqMGG4xmb
|
||||
8Tb3wss8mEXzJvWVP4+bE6EkNPMCVAQleD4ePItaDg3lSJH/cIueIz6NDl5ik07r
|
||||
AZOZTxhhGU1CD8NkxQKoZLZ6GgjHDEwiUbxaCoD0FAzqtG5/at+jiwyDmCsJ96aE
|
||||
f0tPLXKOOc62BbqsAUuEOIooGwX/swXrhS4Xvfh8GxBYFBlRponoWXG7XQARAQAB
|
||||
tEhSb3NlIFR1cmluZyAoUm9zZSBUdXJpbmcncyBzaWduaW5nIGtleSBmb3IgZGV2
|
||||
IHdvcmsuKSA8cm9zZXR1cmluZ0BwbS5tZT6JAk4EEwEKADgWIQT9g2aoB6mfon/Z
|
||||
zOqf47/dpsU0lQUCZ14GqgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCf
|
||||
47/dpsU0lTjfD/9WkMBWlbYhJwRU6JrdZdIPsj2jlMIDYEHXxFo+h1lNn1SLKKrE
|
||||
4c/+9+H0YGM03pL5ZTtydsxdPMTbAP5l24hBFpokySds3abOcKaPuNcct5BDWiiL
|
||||
UxsnV3SxCAsN3QcBt+0tYFYP9yIMkko9BRwsY7pSpjZOSCx26jeTKj7M4XQGdcpT
|
||||
4KMtzXe2s8ss1jLyuaDP2B5ikrFI+IZ5dHVBhohK3ug1y0SzHjfSYeskOEYSgJ/4
|
||||
uRUJCItWxrkSh16qRz+NFxwsewqIKz8Q0EmpHx4WpAii8z29IFPYKJEqdwcuPyF3
|
||||
7SiqAow4tY+CtnLAUYEbSiL52e8W/U8KSnrxqhkpMd5wZ28z+k682A5uEQn5YjOy
|
||||
7dBRjytSC2S87FJ+3zp4OtToDio8Wi0zpZWj/BD5K9raE2ct6Uiw3NG6JI8A7yaJ
|
||||
pEENfMpxMgKc8G5t8NfiZdDFDw+P+bd6sMAk3q7ZFe/o0zJcsbhtYacBFvwBpeIp
|
||||
HZnLdUQlKrZoASku7biTZyt7BBJZuNdVv6Q/K+pigJxTYCZNbbx9s/lzS6KGUKuD
|
||||
yi7n/1qYFXVFktomR+Cm045btVNeAQpnfIKiJS77FNeB5saSWEAOcCMtUkoR74lA
|
||||
9MGYdeWrPjvdeBu+Muvo/y1h57sVMwvStrXjGrJNs6KBcmvITXrek0osbrkCDQRn
|
||||
XgaqARAAu8bgP9AbeNatYshdG1xoYv20FeC0MUz0oYu+FvVuhvaAePl/VFFBlh3O
|
||||
CsCzJ+a+/hyeW22ZGZl62yblvlZcSTw1/WOv5zboFVVLD58/iiz3dCYAUUTQ2OaI
|
||||
+oMLTCmZ/+GIcuVM1ZZMEohvR9eLcyzY89CgOi8R9+agqTXxNg7Uj43tPkgY2vc0
|
||||
v66od1SrOAisduXVDAiqTbc6nax9d9aYt27zQlGfuVo5J//rnteHiGA7VphDLlCR
|
||||
+dra1ZGjbdOieSyhxiEAkBPY2js6UqO/CoRn9uHaTSv4MJqzzMOzLfPni+6y3FqH
|
||||
qaUoe3vr07Ehf85gBEL4IBiux/WL3Vi1WceqvNkS9aC0MVnnEgHbyAy2R6pWrtN5
|
||||
vlxdrkqQcnnnYHvOupG5KPsgT/CFK0jGfA23I/dBPuI372EcqFLFpAB4q14cSLQE
|
||||
ZER81pK7Q445vTv9qQIPu34oq0mg7GWlunduI4v7uGN+oSYIW0kfNLRnM4QjNhTP
|
||||
07LJZLZoCRW2MyPqTbk8cM0UQDGFOozcjlSgSZSABLdHpnudArl6fzkMi4VH8WNS
|
||||
JNXvtL2yX8cnOWXuOgK5pFuhr6zeRaHsjlMXgR5ZPSCiq0aMR4upk5n/Mn64qGVm
|
||||
EnxDEBiGfgL1sl+GGl+rYxvH8vYEEX3fjTtlsaImUzKByfLaY60AEQEAAYkCNgQY
|
||||
AQoAIBYhBP2DZqgHqZ+if9nM6p/jv92mxTSVBQJnXgaqAhsMAAoJEJ/jv92mxTSV
|
||||
+0wP/itANwrdF+9kolUUVJg8Vkx7IgIGlcdIiUTxPAu9c8JdTKpziy9q7oVVpzLf
|
||||
zo+4qgzXGUGuGtcHdM8XSFYQ8CAuuOdvPUvtKbNQiZ1DVjoS/wk4vrzIvLTS1VVd
|
||||
f4jTgOImx3Tk75/8KX3EpCk26orMMBCHk7nWWia1KF8X2K2Hu1DZ9GqsWlE/uAPN
|
||||
tS/+ONlbn6tlk1XWDvFC8DkDkRWNRPva++GP5ACylybOHy2rqWKNEtetYflDuMIc
|
||||
5tkrXZ/rdZgzASKzSrNlEjN2DEBjl15WjUppOPkSc4QPK+SVza6UZJaE7oOrIOqs
|
||||
tQRchspkyDFreCuK/WZLZC8SUwZ5rzbOsFMLUHeZtFtNkJGxwF1ZUNHbNPPCEaCN
|
||||
oqNu/nkjxFqeydJfqDM8K8An9dQE2GkUm1nACpuLNgpILXebdG7ItVbbkjosx7HI
|
||||
0i3BXHeQzT+xY1gmuFFGEVCf9bZVmYspXJaiRGFRfGVyc6mMtdow7urb/A9g5Jqb
|
||||
Dkc+p29y9hCeOAVZfTY2C/GlWu9X/E64WJ2mQ3ujhtJmSgLM4ieYJU+lxosOC6BW
|
||||
EjFrTOeLa+myW7qm+/R6Mo/545s1qXvXnDL5Z4aVkSHtUu+fiWBa4f4WaH3mxAAg
|
||||
XLVwKhulQ3wPaCehbbMPbsQ+091iAOo+hn9s2BPfehM0ltgI
|
||||
=atlH
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
161
basicswap/pgp/keys/nicolasdorier.asc
Normal file
161
basicswap/pgp/keys/nicolasdorier.asc
Normal file
@@ -0,0 +1,161 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: https://keybase.io/nicolasdorier
|
||||
Version: Keybase Go 2.6.0 (linux)
|
||||
|
||||
xsFNBFuPQQEBEADWe0DHzPvxOuiRAlUyvoQm/+P6jiCqZ4XjFfPIthPh4lnj9ZC6
|
||||
oK4XfFgU5Z1YLcXWg/3Ven5GZzcz/V82Q8MoDAuf2cNjmG+hHuoLMCwECGE8GcoN
|
||||
gqBhNGcUp8UykEUjMx6B+B1kBH/Z563Id82y4MssIWwVZA2roGvrLZKSTA0m7rhu
|
||||
JHLmO8rOsBZymEtRvGFhnVBTrSw13RIgUpr0D+nYU8s/ahnLwf5EAA0l9AgQcMQ+
|
||||
VQFMV3zPMnhVHIXpcw1dmfiLMiOHhonQ9uu4x/kLroq2zGRHqetV0Ix9pbx4cxKw
|
||||
idXt0KbFi2lNX+Xh2s47mC3oJSJyOTLxoIyj073nMPwFE+fZrByop+qYYmLvq9BM
|
||||
q75ocJIr+O41/IdL0/R4l3rwD+dfwYDHITfwcYMfrI0GZYC8igoeBtQiHx+9bHyV
|
||||
spmAH6W4pJeo8jkEdWvu8xbBHP37+ELVrabz4DpYnGga1fBGoHGVwTOlIzmtOCJ7
|
||||
hIS5tpjC0njfiJJRq15bwFeUoWhzr4fngA2pqE5LX1bvH9HwoYJ7nbNZcsXhYFoW
|
||||
0lXxYJA/6wPoxC5FWFBZ2goq/qPiVLfnp7XPgDJu3UkYn9Mqi1MTJk4nDviUb5iZ
|
||||
1wFoEFw9QZIpBpIaQKeRCVOa88FGQxP3Ud8CRMsGy1TyOiN/ZkiWxvB1/wARAQAB
|
||||
zSlOaWNvbGFzIERvcmllciA8bmljb2xhcy5kb3JpZXJAZ21haWwuY29tPsLBeAQT
|
||||
AQgALAUCW49BAQkQZhh2PvCRhv4CGwMFCR4TOAACGQEECwcJAwUVCAoCAwQWAAEC
|
||||
AAAmRBAANTErDJqg7Qh2gIEJFS+LVOBF427Bmj+DNTEb/XeMDB1QAbVw/ItM5LEa
|
||||
WW499HFgG+jBMohIVNcmtKIOGdrQSBc2B8Ox4KUnDLO2TXrzMW+EveMIDjBGjxSZ
|
||||
n2QAVaeemY19cENZfqmYkBTF2kcJzpzlTLsN9FpjOWYjdebjA/plM8W29rUqLE7R
|
||||
RRqkayXhkkkou6m3diblDiboWj26V+79Rd4iXYE/S/nzbJfNIUjUTj1geVWVgW+7
|
||||
Gh26H1c5IkeNrsTx/oSA6PN1Zk8/B8q6ftpt6tN1ksrvW6ErxivaxKQJsxM1RO0f
|
||||
9tfZlUPCuf6Qsjg/IFayZhzi3U+5KBTpJeupBUPqTDtF8byD/iSi0/s0s3ogEFu7
|
||||
ibMkmGnPu3W3n74qZpl7dNJysu1J7X1bzbeUb4CTgYl/hmsEu+nj7E82knckNXiI
|
||||
cqSUlHTGsEywGiEkuGTP2N7qikWdggvDsBVE18OfQnBnzOxEXAVe0rCbRSqtgrqc
|
||||
CSAG/pXdTfNTAo3ScTJ34DYTrZ3EohUwYuSc77e4nkec6+CdUg/IIGX7rB+Iz6RY
|
||||
Py/24lRp9AJOG6Pzb3K8evE1o3kZjrU/vYyWEo1kiyJJmQa1toBnvJBVIUrcjk7A
|
||||
603GGU0yFNXfGG31WxudDNMXaIbFG+s6SUC5H+eA+A9HHMM9/vHOwU0EW49BAQEQ
|
||||
ALDfCek420s6nTWd0lqhJxpaYbGzw44KekwIyOqiA9BZ9W6/DJ4VJoHHK0tBplhQ
|
||||
J9yrpfuIPTx+TG/2qShNShWv3zLjtGc1JIjYlJGzofmglo/zXP4HdXIfq5bhC2pP
|
||||
9F0gVmnVNdSN4nA1/FuMJ3raST23F0Q5hieM2znPRoCxNdy6eGo5+Pn8Hssyvr/1
|
||||
rRjRmTUIEyB4v5uVlPbqfvEMBtVOy8AS8+sWiW9PCojWV/NQpJ8DEP4NPfZG4sNu
|
||||
rhUN6wTYTc1YpqHp2ZjSCFgscgXOBXpbhj8wRvfuOR7PQjBMW5Trz1yFvaOXIRHN
|
||||
Srtoldmt8QyHXwIPVn1Z6byULWGsWw2hSKV4kgCep0djb4cncY04f1hCFHKtycv/
|
||||
32pKdzya3nd8455wS755L2cQBMRs5tS71EpjkZwiwAHdQ8csXLZ3F+JwveavNp+K
|
||||
cn4eYhfFx0TejQuryvrPx4le51iH6ozVOM37gIUftNGx537yWYBTBTsspz3fau13
|
||||
s7NicSKc00GNfdGw2CP5NfcLOosUntk5CK/ZMQcnY2YT2FPdmIdX2iF100Ai+be6
|
||||
xbbYB3tWbRbnvI5JUIuOPuNeZcFQUEd4mr+XRpGLhzkGi5XqTPaAXiwjfZie7tYO
|
||||
/ZCuAWmpNo2VWOlBJO/QvN/sHyHwIBAkJ123fQtUystPABEBAAHCwXUEGAEIACkF
|
||||
AluPQQEJEGYYdj7wkYb+AhsMBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAiKIQANI2
|
||||
RDk4L33EjOS0abxB8h5tR9ca1P2BIKCnXb/IfiqlDcoKR0RVAy1dOHlmyH/5K7lh
|
||||
5cp9LsqY3/XuPZoN9MRcWmav6HWWvWKdtpg0RbRqDyiqh0uiwwB8QZ7Hf4uWmLPj
|
||||
V+tficTqyFhNn7RdU5DrcVhvuueh1fJrTqaizB88QMvYW+xGuuIBYIFrkibH3UFS
|
||||
/L8Qj7CBgfWNAsC47t8DtBKKX/i07bJnlFyv+0dOpxNAFIROlXw33sbTM8SkZ7jR
|
||||
jIeKhS+fEowjA8R3rSJLBEadIwUaD+uIACaFVh+o/ogssXWZX3GZ2IgwPhiAFcJT
|
||||
qDzDu5nsIu8/QwN+TH0zPLoVjfg56HqPAsJHYLOSqO5xCE8lhyQuMh3PPF47kUoS
|
||||
6QGNkASgSAGEq5RMBpUWqS8TYkYU/mk+b94nJnhhvXQPAEUHIqY7R7EPduHldyBh
|
||||
e9eF6GZLUj9iA7uUY8m5CrLNl+axKxRhyMqUNOAos58z5bg6pqvrJIy7J26pWjnF
|
||||
qNj7ylvjGakY3WR+EjPmgU2KGdcKloZLMOOSLq+4kwWPr0+q3dBI0qqXssVPZAtJ
|
||||
b+lEWZtwBM0n3d8RcNEGywqeZIiAfgvyUQ6rNosDhE51q9nWoJW1i3r9X0ATe+aV
|
||||
avYCWTKM5AQ7bEIvuVW/4M8PLFClJ2GmI7+YY7gl
|
||||
=sNb2
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: https://keybase.io/nicolasdorier
|
||||
Version: Keybase Go 5.0.0 (windows)
|
||||
|
||||
xsFNBF3clT4BEAC65tyMgP9NWzaUyNlbvbT8LlFRd/QsbxTElVILwdlypB/HInSt
|
||||
18P0d5Px381cTN6QQnfRaE5cvbghqL94qVg4Ycc/tW71XxS4GT/xujzbNfol0unC
|
||||
DAo1NqYWESrIAlosvgZBU2L4M88ASE2psHVdo2Dc6NRmdcit7G/RD9Js4MgGi9Kf
|
||||
8bu4Xwk+vwGDvHDjPbDjlyx+djkGenQeuBVsIwJqXyFrr4WYkpFfBcGtMiBM986Z
|
||||
lCMZ/Y8+WeGMHoq16uOuauIiE10RCAjSMkpLbqNcAFY5/qIImaHlQFpUxRewX/04
|
||||
RQ00QrKYmToMB4VT+b0JSMVpHZAKaITFfSB3QbOSJrblZXyC1cTSGaDnTzhuvVeF
|
||||
0S1eD1v4ZPDW5egxEKe/ckCxq4O/j39oj3oiYWcVmS+kceiIyETuXlgWyB2meG69
|
||||
AAFfPisv0jUN/xrQJ7+TNBD86Cs53GvlghqHHWOZyLEDrNlkFOd/f7uN08cYJcCH
|
||||
HLWwysLxBFhFUE9PXBT+83EkgsU1nCysB7kvodXkAS7rjCtrXuBuE3z3HOyfrQVZ
|
||||
geOAlyAlLdbL/IQeQWe2k4Mz1ej90k4kqjfzZxSS8zBN3kvBW56/4W1LSA5pPhjl
|
||||
5BSRUxk/nSrNMfc2u8ZmcD//mNZJ2d9yVJfOAjXJPEDQXAebWRZaWJw/hwARAQAB
|
||||
zSlOaWNvbGFzIERvcmllciA8bmljb2xhcy5kb3JpZXJAZ21haWwuY29tPsLBeAQT
|
||||
AQgALAUCXdyVPgkQIj/aad6+qC0CGwMFCR4TOAACGQEECwcJAwUVCAoCAwQWAAEC
|
||||
AABCERAAFi2eSIRh9kpkERD1NYCMf6NfuPC1y6vf0xNYnIodPkAyv4xthEl4esdJ
|
||||
xeltVIQ5BcPNUrHitcwO6TmtQa/a/4E8RgFzKDbGo/Wgr7shVAs0YUnQ6Tk07fL6
|
||||
OVuwRCc1uTpUAgcv8ESNUyUgMeThcTmPChDRhhWn2Imy7pi8NPzM0X+/QCA0yj3p
|
||||
Fa6Y+03WrqWbv9+OdqRysCwNPtOSAfbT4XXifn4efkOtBk4vx2oGr/NxxUOw5CgR
|
||||
DAp8hEL76b5yZzvex75JFjCUwKqeYf2GjZrv94XgWXWZderlW2MHM+R/ON2K60/Y
|
||||
SkafrGg4GdorwJIaLR8OVGV2nuBeUJXg75taOEzTtm8siEmiF1cvlfyEO15lTUuZ
|
||||
7rIb9CILwCJ79nlON21MFax3bMqWP55GuC8Z79dSl3uSHaJg28NiB1iFVO0xAOlT
|
||||
wQ++qeWQXpWUviNbHJ57+jgK80PLn6alXvfGSDovNZfO2UvRD5lpDmN6VyqrDB5z
|
||||
ibPZmfR5SR+G9XqR03i5mG6/ynjWmXDzL4t3trrBPwLeyppvRXA9QY444Tm9OdH/
|
||||
yj06mNGcQMLqsbd+9KS/veKDl9yJDxhqJe/nauq4vV0a+oMjFGKM+7waLc2n851N
|
||||
yqdToaKfwt9FocDy4Xh54WPx+xaCfi9tDJMmKPjJP87oys2EdlXOwU0EXdyVPgEQ
|
||||
AOyufiiUouX9yBrfeLOt3vLMVY3swP1KEosa/EZn+7zNJ+VZzfQFcmrNJ6lfzoIk
|
||||
WNTYhqhCwPWLyw89wYhXNHEedICzRuOsET2CMP9bYXe0GcMi5vXCOs3QZDD5bNau
|
||||
VnqnjM/sT25GHJb5IPdE/jOtAO3/WnwtlclfqNBgI1n0UUak4QZM03B7fFmVldXg
|
||||
G1FydusZ0cH5vn2O8yQkvY7IcgNhgsQRPahrrpfDnfRd/CuX1yP4xbgULrgMjs3P
|
||||
98HW+vwsx3IS8uFfxMUOftjXBUvCWoz+rc6fNqCS9lUIKdmpN0J+wtvbgcwXlde/
|
||||
C2j3gzHBg8uGnRyVgygTUZceLeIxYjfwgCoRuGK70EfV4TAKkT9ODivA00D4mQm1
|
||||
Bkh39hl4dCZ3xMVlVthT4BK1nEEM5DtwRAkVjR7wrv+fHR90yoHH/zDA/wFCGaD+
|
||||
ML4v3578bctkJcmIJq32pbiP2jS36xnjxSRsDhQcbJjfeSm9qtMAOwF36GyGRVF6
|
||||
fgxkRh04gzpE7d+fugRM9aTaaSBvr4oU5OmR9Aw066SC0nGGSnGehuvH5Ov/QtpC
|
||||
Wl95tCviMaW28MSudwdYAfwgzKpCbe6sRi9tH0D6z2ZSLsykwby29wVfdPKVqUZt
|
||||
LLSHhlRdw/eJDt7vCoxHR/TOJxOQZWCzJma+idz3NBkXABEBAAHCwXUEGAEIACkF
|
||||
Al3clT4JECI/2mnevqgtAhsMBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAersQAKm/
|
||||
I45krs/U4OWfru8FA5auuGgdiFThzk2Z+iE3XZ/TcJDSZfcECil8eFvjycL7JSRy
|
||||
VUDY8GOmxL9oZyW9YY7EuvpsSBq6b7x6r8Cz40hBuP59DD+V1qtIokvc+kh2XJlS
|
||||
GYKjggKaKTwrUazFtLur+XipPEL6yLYabaJaOiM5sMPmGc8raovIrh5IsVsEgEA2
|
||||
bLbtaBiQqSR8Czh8pznijT/qw2ZLKqHkD+YQWf0xxwt/jMj/eG0yWzBam7YoqzM9
|
||||
9GX411vmJNImNnLLrwA+LhN5A+m9oyf2KINHhq9xmyP2cRmXUcLDejMIIaISFWxT
|
||||
aBrcmDSdztzsDzGaAz389bPUheSnOE6iK3zxbaUx67Tcmt1UjIWEZW1jyO4zmeXI
|
||||
JG+0rdxZJU+wxa0jZcjF4C4IjgV6mXm+hN8F9jKBXu42ayqBHH2FAQLJQkD7mGSy
|
||||
YJKo6eiJUfwI6DfDTlYF3QCWGi9bpdKZsaWj6+sgzhsHrENEEd1UnXm3W31wzYew
|
||||
YtnmykETkCW0tnYf6tW5zJqpH6Y1zTS2+oSE2CRLjIPhWqRw6gfIk7g54mgNXf4D
|
||||
ppHvGVduPErEE5WWH8iUVWYtk/yA7LhyRfvRjAezjtK7uzqQNqZirQjf6coqrV+Q
|
||||
/+7CvHSsc6GjkqB7bFx5phZPRpt7OLzVKszDroyv
|
||||
=ut7t
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: https://keybase.io/nicolasdorier
|
||||
Version: Keybase Go 5.0.0 (windows)
|
||||
|
||||
xsFNBF3ec/EBEAC5sbWmzhP1hoLQ2/gm8Tds+v/p6DmY+vVNIgiBz1/XG+glRkna
|
||||
qqwmVe71CE+nYtrxlzzc70PfxvrfWzfoavYGMgIkIQhEcst3ST6Qqo7IglAcXL0z
|
||||
Vwqq5QcmCfyz2kr9wxUUrwofznKQch/7dZATkTl18ci5bzKTgENzHFKJx6EHN6aF
|
||||
0meUW6tmIVSxva/tmkQK+dZtjfYHZvlDC0AUTNv8nWGEVNtvJvN+KKrXpHjiSjp4
|
||||
lHGXp6QZEA4Xmbo/5RMoy7FtHAjT8QXG3kmmWAQSN8TYrI0KMWoSIfZMVhytTgqc
|
||||
1S2G4nmUmkLVJgJ1p2/plLwY3ORpmQHgTrmttYnh/y9h3wNEje/8QQKlLncCLP4b
|
||||
GVfIfBjuKSoYAU6UqDBV8wgyCbgysdhDDxlt6hkF1lMljc9xlj1pUlYqdMCn8Nvt
|
||||
rQ21mpaMOcyAKu0qZPgSBJR9W15hdAS7Y3RCHBDi8TraLnl+pvhRy4q2e9qYsMIO
|
||||
w8kmrRVtXHTdPCyAfVKU93mn8A1MUbISr3f4AmP623NOK8MVP/J0Khx3tHpJ1Hdr
|
||||
L5Erg0N4n7lA+eUiYthwdxG1JaGQaCRVeqUZJ/TwuLvAsknDOCdZAn/jrjjaxRJ8
|
||||
EwVnu8kJUuxYIix4CuydLKCS3QXey3jbRccEn8Ybzz4nPcoZoWmJianrRQARAQAB
|
||||
zS1CVENQYXlTZXJ2ZXIgVmF1bHQgPG5pY29sYXMuZG9yaWVyQGdtYWlsLmNvbT7C
|
||||
wXgEEwEIACwFAl3ec/EJEGL+hWR97douAhsDBQkeEzgAAhkBBAsHCQMFFQgKAgME
|
||||
FgABAgAAVGkQABOWW9mCyBOdWaJ7JBFGraUv9qQ3Q9EXFfOCXHDJdiY6WSWyvhMG
|
||||
0KluY6h0kVMGkc5MXl5D04+UuCrVIn7ucQ3FR5E3pkROJ/ZqGuXXBY/G7JVJsJz2
|
||||
TGjRD5PxQD2SkfLQ/ZscqhmwcZPtmyVcyfKsLrtSPmDp25xYo/InJ0BDh2M6jvs7
|
||||
WNRX4O/jQNl2WnAx8e8W/BtTQr23PC5+y6jsi2GVo+ePubqS+nz+O5MD0+0FJ2ov
|
||||
2i9MAwJZUez4z7w11SRO2QT1MX4FzgIe+YcnnU5DeO+WTQci6cuv2+l1heDysRto
|
||||
oZlWFL8bNNCKtGC46ZyJ4jmsMUp2eP5st32bpHQPf0yIhFvvKzPkm7u1fZIPPbXM
|
||||
bmREBJWNiCNWOnCLr7yiO9ATVIzvvnK713oQYHpAHRoIuYgUiVxLVveBSY4ERE8F
|
||||
IfOu2VUXyi+c/ottTd07dDrLpy8DJ25891ovE883NZcFR/rW1+0ymTDFyl/fPEDM
|
||||
DNq/NxVKFfrIaGFvRoDLpOJPGbUgHsU3+xxndorFnrWIiOpLk9dIGxKSdVs67Hmx
|
||||
YiRDuw/2j1QhR4dk1l8ySD75Hs7FFrLrUDfDWbipFHjrKti/V7zgUsgWYxmscAGs
|
||||
cRd1Q/59vX7GFyyWYvMsEAMob1oIfSA+2SgpVDP55AXoqbo9iWUfJePYzsFNBF3e
|
||||
c/EBEADQCD6OD21aTYARADbEfnCysxD1l/tDbhmjbJNgw5v5YzvVs2GCovhPzQmC
|
||||
aLybwzuOvsh+dh2cnOjlWoYaQK/8JXolH0ZAh4z3oJca9UUdcOcBt6poYjPUYCjA
|
||||
NLNFIS4CH05yr4CECu/GBGM9dSbizmbl/tJ7EcZO8xlxg85XOFT8fz/KhEhElyb8
|
||||
KrCC46gtWnXYSBQ1XljfcZOUXRhv7ROAe1BAw3j9sdZ34RZ79xXx4rMyna2BBbzn
|
||||
Gki4hV2qVAgXwcn8gq8Qhux/Y6XeZuJhjFCS6FCk8JgK7BFrThZi2z6FTHFM+7HR
|
||||
eAkoJBcg/JoqyBauZx0UJ+JckxQb8dqImDiPc+2WJ8ENCTU8xobWAZUT0Hj8HhJi
|
||||
kQ6URScpty1VushBtU4GHsPfLJoU2mLI7YQQ6b0VJD3ZT3eQuYchNjE44eSGx8M5
|
||||
XVZjunbrrZjq2gzxd8+iK7vj9mnQ5M/kiFA2ptwPUVHjGmVS/omOI89AtPpLENwC
|
||||
yFwKqOgOGPy92tVF/FFqKveFnic6U1M/3FWZamU0A3BxUFHrXrY9MWFul9AVLTud
|
||||
lbrNluOIxmSsRAJXkkTs0JLam4ubgoSAg4XOHe1Y9w/BRC6huIRs72HBNUuDtACS
|
||||
oMWfPOgt66rl0CW6/qBDh4gSLxxni2PhGehJOEc+ls6K6k+b4QARAQABwsF1BBgB
|
||||
CAApBQJd3nPxCRBi/oVkfe3aLgIbDAUJHhM4AAQLBwkDBRUICgIDBBYAAQIAACWW
|
||||
EAB510r8zce3r4bspcj/A/WFAPHgoGlMUeJQkoxsgE3tfcZBLPWkInTGnUHsLPMw
|
||||
olE+pmqbS3XV3FjC4yGOGPOQYLeF+o/64+EabTzDomi9Hs0rV7GzpuYqSRQ/j8/j
|
||||
H1qo5iuWwJnvvr5rGy3+mN1O6I88AZDRGHiLS1oG+mFXhNVp0dXPeDMsbGnztgNJ
|
||||
zmIAWMeWqsC852ZmXa0VosTEE1Jb3s48otblwBwOWzNXBs+J+amuA71DridQYNWR
|
||||
l3ixirH9/D+tpXOd+zOXwyczoYgf14Yz/lgKT+wlSfOQeMRbqTY5oijIxeLDJbeX
|
||||
eYZoCss6gX1ue5yqgT0+haI9FAPrnJ/Jq9cPmwXuBmjQ7869JvDWUNgoQ8sP5GoH
|
||||
vRGjaEzKkH8ibQTLtP2VKPENKsNjikKCaLsmWGvfC1CzAuw0JHQ8fNgwuqIXGs0L
|
||||
MBCOUgynVqhHQKnApGcbnkCrRjr1wuAydPCQ7xbIaKdhbN3qzj1rUcvkG0GEjs9C
|
||||
R4VB8G0zcLXMoqwKxPLAeR2cnSiIUW0JEcjxxBb+6poj9kQKaee97cxXP1qq2D8d
|
||||
hsZHpy1Q/HSyaKYK4gId5/eZ7IsbPH60L61OJ2NC7xRcM9P09/EDz08dbt8IKqrq
|
||||
bhogEBf9UyDmPn6DW8jC1nkVbE8ODYDaOuLW3PKrthoKVQ==
|
||||
=n82A
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
29
basicswap/pgp/keys/particl_tecnovert.pgp
Normal file
29
basicswap/pgp/keys/particl_tecnovert.pgp
Normal file
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFlLyYIBEADW3oJnVMDC94g+7OB1/IBUYNevCqJIOAtC0eeFS5g0XGvvqRLx
|
||||
2NLUqn5te+R7deoGElkZFJLLxFUwEnhqGCRH50Iou5aanUzvgI5fVAbK3k0fp9vc
|
||||
LKCR0fQVIidcLyqMpkLZo8BSE3+BWxFp/r2OHvh2dYtJC+BZVwblkDS3cqwKvUZx
|
||||
IocvDs47Wo3tzZfEsqUavbbiGx+Dm0fCV7TVHdVLU7q3bZsHSRiyTUZ2EAApoAmT
|
||||
ir9csVxv2IM8yf6/HwWi6/Lp7dgSG1+qBZ1lUPPTY+dFLPZyt/kul+vuOj6GLZaU
|
||||
s3D66d7TaPCHKWAOnP9RHpic/iXODXVXo1KHJfa0x8fW7I+y7/Gb+5x/m4O0Bz2T
|
||||
BivdrSAuFpXkPqwawlw4CPgI9fc801g83+ZFzD2jJ6qxkEgfnlmf+zGNn5tC4N5j
|
||||
NRTQ+GyHo1w4824SXcSN590wgz8goGJC3QPJxbifvOA8GzQIVzpxHckofOVyqIEq
|
||||
qSnkP2xn4mELqD7HcFnoojZBqFbF2cN+oWQ+niLN+v4qrUncpQI9SVWlyp66S+1T
|
||||
BhBQj2QuX+3B+K27EiDbhNV7EX6xEbGsnB1poMc2aMiz6veybW3GnoM+2ppr8Ko/
|
||||
12Ij7l+ZA44t6PWUfQQbNSbUk/0Yhd9QJ8VQVck+TaS6gtarTbURlSdmHwARAQAB
|
||||
tCl0ZWNub3ZlcnQgKGdpdGlhbikgPHRlY25vdmVydEBwYXJ0aWNsLmlvPokCVAQT
|
||||
AQgAPhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mCAhsDBQkSzAMABQsJCAcC
|
||||
BhUICQoLAgQWAgMBAh4BAheAAAoJEBPxNlHJzw1rtdYP/22iRX8O2Q++MayPHNx5
|
||||
XHAlMk9mfi5FB1qJwshtlhda7P9U/hOTi227wH+Mzh5dBje4t2DkoHzxlz/Wr4cQ
|
||||
QUJMOYd0OEZY6kpAQkvtyYobIb6zlRQK1koAfNMxewmfZZGTlr16IUVCovGSFvZ+
|
||||
hdYRDEjuHqXjpwBfrxFAy/HCnfY10qSRkJc5w1ypj1IkzlanS+xeRJSDvRTQDAEr
|
||||
zv3xKcMGjCCHaaCP+tyAaViBaUOlvmZdWwg0gwQCuPLqIh0cfDbcg0quciRIpnyp
|
||||
zINmfwngZCwXdIYfAzmCzMHw1J3iOiqfqK0EcpHMsL689VyQSPgsoEHtcOGHYjRL
|
||||
pMPGvRFHtICrnCHENK3IcwFWDGXW+i3zgOlA7g48yYWWvSup+t8I6YT+FeeFlxSO
|
||||
dj1GdeMA0O7gXZ7znLVduokL2Ef4dZjc+3NwBlFov52vwCZwQMAGsMriwEDB2rDZ
|
||||
B7YOvAxlUB/kavtx/oE8fV7mZcuwYg02lq4bozF9xlhjOFaRit6xXnLVi0TuI76c
|
||||
uz67nB9VkWczSLIzCyjNyFpWbx1BMxTYfehZX3+YNajXwG6HdEp9CAYK0u46Guz/
|
||||
Pth67bbNVYyP/cuOIrv/hqQ6xo4mOBMDDxcCEAXx3rwxfbxNM8vlwrMcpITrtNON
|
||||
r41bcxUIfMEDefPm5wnXep8W
|
||||
=szpX
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
29
basicswap/pgp/keys/wownero_wowario.pgp
Normal file
29
basicswap/pgp/keys/wownero_wowario.pgp
Normal file
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGROehcBEACXWWc6dHqCos1PmKI32iHi0jP3mYM3jU57YxbjwT78QEtEwSqf
|
||||
YklpXkgTYq7jexx2JElfegM6w1sPYarq1y051RjnCgzl32da5I506SMvcJTmXumV
|
||||
Rw6erPeDxAO74PflDSlALgtGOgbKhwwWRudbWgT5hKGkl62qy0mI6GStul0rbT+3
|
||||
gq77DCGyURfe1PG1pymhO5XVz3WGtOa12NvRA+3wGIcqIji2MbtXuOhGMg//kVI5
|
||||
m2vcfHyMMuQ01xUXRu57WxRujYaJ1RB4p86JCbDX3YU2XlzTxGAhqChDLuJGqo54
|
||||
AZMUWDceftXsAoOqH8Hwmm5gFkYSpMt86ZT+umvWygmxohD5k85MuRj4AGagFj/u
|
||||
CMcQjI/SN1UU/Qozg6VL/5FO8aH9IybDzX7eE3j0V/jTweStw1CIUajYgfemWOWl
|
||||
whLPBDflRz/8EEqTN0CaSSaiYiULZUiawBO/bRIiCO2Q6QrAi3KpPUhCwiw/Yecd
|
||||
rAMLH7bytpECDdbNonQ/VMxWwtWJQ87qBtWvHFQxXBKjyuANsKL9X7v3KcYOUdd2
|
||||
fSt7eqE9GDT4DbK6sTmuTpq2TgHXET0cA39+N2zxTh5xFupI/pi2iAHJ6hgIiQnn
|
||||
662TngjGOSFvrTV/51Ua0Vx8OCMJJOcRdOVaYzuzg9DsjVcJin3aRqUh4wARAQAB
|
||||
tCBXb3dhcmlvIDx3b3dhcmlvQHByb3Rvbm1haWwuY29tPokCVAQTAQgAPhYhBKs6
|
||||
L3JYGPz/J5SEHHk1BLRJxpIgBQJkTnoXAhsDBQkHhh77BQsJCAcCBhUKCQgLAgQW
|
||||
AgMBAh4BAheAAAoJEHk1BLRJxpIgwgEP/109vw1WXRh9EaRr3Y1+sBi+/PRQ5TCx
|
||||
UEcP9ru5sQPJ0BsZK8RYw0BNIfDQX9OB1k/AoiBelL+0EoDKvjXmwz9fPUmSVk5r
|
||||
3RzfClXTnxn4HXPKkSGMt4WBUnvohTexK7CPkb9xy+K0Jtx8XF1XiQLDFg2a9lBj
|
||||
IIX2H6aHn4VjdUBv7TrTCAI2Vg0cQUpeJUwyHH+lk0r2WM3zAxzS3Iy2yDDstNT8
|
||||
audXEX4BtJhyEU1m57jwgscrbTtgwYOAsaRLcnUaAFWhbov3IiGInk7N1fkMsuW5
|
||||
HE5RcegSZRS3X4o6O/nmwdSjCEB9weydOCPrtfdbvfvuTiMg/jZBikOk/Sj7FM/D
|
||||
eZKghSHpLbT/V3S76FyIcc/xFkUmR+2fGvCNjJ1Qn2lXTS8xcbyzqR4LZPeUGppV
|
||||
hvriilLnXSjyc60wuD3kmCCo1Zw4tNL8pr09BtVmScUy6eiwca8LLzvbbivqxF1g
|
||||
Mrkkv8yQE0ZwO1kgNSn+PSzUPbwAoklcyN5Rhr5DxZh0UudiH5Jt5WWYeE8O2Uc1
|
||||
si13X575kymGkkeiUcp9WtBkh2uial+RVmTrUTDUTIR2HzT6MAR84/DHlC5dsW8a
|
||||
h4uDUhzeG2cTxuIfZC881UHKL+xT/I3PPuFdLbU5uoWJpXYpxKYulYWd7LA/k4bi
|
||||
JWBrQo7VDvvP
|
||||
=H3wS
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,16 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmeP/40ACgkQE/E2UcnP
|
||||
DWuraw/9HCuAZG+D6xSLWmjA9Dtj9OZMEOIxqvxw+1e2KQ5ek4d1waL63NWFQfMi
|
||||
fDlKKeFbZoL6Dfjbx0GoUJKTfrIVKog6DlVzIi5PuUwPOCBFuLl0g5kHlC20jbPw
|
||||
nu7T6fj6/oD/lqo0rzFDkbsX7Fk4GGC7rYLKfdtYhDgMq9ro7QhSxAOJanRyqzXL
|
||||
dvPNxlyksOyttJLSAZI9BOkrpTWoyb3asOli5oHgdcheHd/2fjby69huS3UWEjdO
|
||||
9Bm73UFlxF2hxCTc2Fqvvb3SBDmNCLlFM0f+DDJNMJGUQViVCar0YRw3R+/NBo83
|
||||
ptutp3bpabHijQFEEpIx/19nh9RQMJjaHHHqdPcTeg8bU/Yeq36TI7gsCenK0mQT
|
||||
75MscvJAG0enoKVrTZez5ner9ZwLOevAKzRe4huRJZZjM8gM6sb2OKslJLqTxEVt
|
||||
G3b8BLB9IUAxCeyuvGSG/3RV3MgZLnLy5MLYjh72+Kmo6HpuajJwPuvUck5ZYcGE
|
||||
jjeRFZmqZj0FtCrcfStau/0liyAxU5k/43RwMvujO1uTTgOVHw1QhhMEkZ9bYhhO
|
||||
JgeCEkwL1Bjjved1NSySjZbt2sFbG89as14ezHxgc4HaujJ6bGkINnkPOPWM1tk4
|
||||
DjjEO/0PY9i0m/ivQUXf5ZPSnlkAR8x6Ve2S2MvQd7nFoS/YfLs=
|
||||
=0pTn
|
||||
-----END PGP SIGNATURE-----
|
||||
21
basicswap/pgp/sigs/utxo-snapshot-bitcoin-mainnet-hashes.asc
Normal file
21
basicswap/pgp/sigs/utxo-snapshot-bitcoin-mainnet-hashes.asc
Normal file
@@ -0,0 +1,21 @@
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
725a049bc5a9fd60b05bba4d4825d35115d99f05ab5b7716d4507c295d05172d utxo-snapshot-bitcoin-mainnet-820852.tar
|
||||
744c42885df700513331a978b289d9c9d5b27e0cf1147f2f5a287b4492ff940c utxo-snapshot-bitcoin-mainnet-867690.tar
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmedUAYACgkQE/E2UcnP
|
||||
DWs1Vw/+P3CGP9LLVv2deNocBFunUz+7aDZsQiykSI8ws50ssJ5PsAg5VSl4CbCl
|
||||
owWOdQVJiDUh7daP0jr+bt3X2FY5ORBb1TGlvfCHE+vLfEFDnTpLXouSCclP0cv8
|
||||
Ci8zQFKSI5Pf6uSMpALgQZxBgNU/0IegAQbpuJI4nrQXTKHJcMqtw1LtnmcreESO
|
||||
MsSiGCXnC1R+xGQjptfvbzXaQVrin7ctYA9zjN4CGbjNChzr+ywT8dht2RKoLYyP
|
||||
OrEys7d8EIaw/ktRvRmyk6O7KmnvUhf0uuFlDq+eTiBIpQoUEovCow1YYKaWkIRB
|
||||
r4JBJJ34AB+XC2hgi5jpJNub/wKgVBm0iy79zZOSILP3ymbn3iJGg4ifUF0YeZCU
|
||||
ufYkYi3iTJDpwYr0tylZmBiwsWNcbUhB+WTNX7ogCW70ZuhrF0PJQRPmhI34vsE/
|
||||
qg3n0/hNNsypy0epRd33KSOvrSmaoTKLtCax9Osnt+F+yTYjD5EPqkQuzlJl+fDe
|
||||
VvjWO5XHuaRvzijBrJQz6r5V4e/0ioNa8FTRqWmMTO1wHmxF5glpozyKycv9+bsB
|
||||
IL9F1IQjhPkSVI7Hw8bsURpfH4mV+9eZJJDIvBf1/0gDctsBdsI5+5jxZjup769Q
|
||||
AmMsGeZoplm/eUofQ9hItWcVitPhisDmC3wDR71UKM0b9FF6IUY=
|
||||
=YUjt
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -44,7 +44,7 @@ def addLockRefundSigs(self, xmr_swap, ci):
|
||||
|
||||
|
||||
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
|
||||
self.log.info(f"Manually recovering {bid_id.hex()}")
|
||||
self.log.info(f"Manually recovering {self.log.id(bid_id)}")
|
||||
# Manually recover txn if other key is known
|
||||
try:
|
||||
use_cursor = self.openDB(cursor)
|
||||
@@ -119,10 +119,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
|
||||
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(),
|
||||
f"Submitted lock B spend txn {self.log.id(txid)} to {ci_follower.coin_name()} chain for bid {self.log.id(bid_id)}."
|
||||
)
|
||||
self.logBidEvent(
|
||||
bid.bid_id,
|
||||
@@ -218,10 +215,10 @@ class XmrSwapInterface(ProtocolInterface):
|
||||
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)
|
||||
assert len(Kal) == 33
|
||||
assert len(Kaf) == 33
|
||||
|
||||
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
|
||||
|
||||
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
|
||||
addr_to = self.getMockAddrTo(ci)
|
||||
|
||||
175
basicswap/rpc.py
175
basicswap/rpc.py
@@ -1,15 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
import urllib
|
||||
import logging
|
||||
import traceback
|
||||
import subprocess
|
||||
import urllib
|
||||
import http.client
|
||||
from xmlrpc.client import (
|
||||
Fault,
|
||||
Transport,
|
||||
@@ -17,6 +17,35 @@ from xmlrpc.client import (
|
||||
)
|
||||
from .util import jsonDecimal
|
||||
|
||||
_use_rpc_pooling = False
|
||||
_rpc_pool_settings = {}
|
||||
|
||||
|
||||
def enable_rpc_pooling(settings):
|
||||
global _use_rpc_pooling, _rpc_pool_settings
|
||||
_use_rpc_pooling = settings.get("enabled", False)
|
||||
_rpc_pool_settings = settings
|
||||
|
||||
|
||||
class TimeoutTransport(Transport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class TimeoutSafeTransport(SafeTransport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class Jsonrpc:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
@@ -31,22 +60,40 @@ class Jsonrpc:
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
timeout=10,
|
||||
):
|
||||
# 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")
|
||||
self.__host = parsed.netloc
|
||||
|
||||
self.__auth = None
|
||||
if "@" in parsed.netloc:
|
||||
auth_part, host_port = parsed.netloc.rsplit("@", 1)
|
||||
self.__host = host_port
|
||||
if ":" in auth_part:
|
||||
import base64
|
||||
|
||||
auth_bytes = auth_part.encode("utf-8")
|
||||
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
|
||||
self.__auth = f"Basic {auth_b64}"
|
||||
else:
|
||||
self.__host = parsed.netloc
|
||||
|
||||
if not self.__host:
|
||||
raise ValueError(f"Invalid or empty hostname in URI: {uri}")
|
||||
self.__handler = parsed.path
|
||||
if not self.__handler:
|
||||
self.__handler = "/RPC2"
|
||||
|
||||
if transport is None:
|
||||
handler = SafeTransport if parsed.scheme == "https" else Transport
|
||||
handler = (
|
||||
TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
|
||||
)
|
||||
extra_kwargs = {}
|
||||
transport = handler(
|
||||
timeout=timeout,
|
||||
use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs,
|
||||
@@ -64,6 +111,7 @@ class Jsonrpc:
|
||||
self.__transport.close()
|
||||
|
||||
def json_request(self, method, params):
|
||||
connection = None
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
headers = self.__transport._extra_headers[:]
|
||||
@@ -73,6 +121,10 @@ class Jsonrpc:
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", "jsonrpc"))
|
||||
|
||||
if self.__auth:
|
||||
headers.append(("Authorization", self.__auth))
|
||||
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(
|
||||
connection,
|
||||
@@ -81,18 +133,29 @@ class Jsonrpc:
|
||||
self.__request_id += 1
|
||||
|
||||
resp = connection.getresponse()
|
||||
return resp.read()
|
||||
result = resp.read()
|
||||
|
||||
connection.close()
|
||||
|
||||
return result
|
||||
|
||||
except Fault:
|
||||
raise
|
||||
except Exception:
|
||||
# All unexpected errors leave connection in
|
||||
# a strange state, so we clear it.
|
||||
self.__transport.close()
|
||||
raise
|
||||
finally:
|
||||
if connection is not None:
|
||||
try:
|
||||
connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
if _use_rpc_pooling:
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
|
||||
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
@@ -103,8 +166,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
x.close()
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
@@ -112,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
return r["result"]
|
||||
|
||||
|
||||
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
from .rpc_pool import get_rpc_pool
|
||||
import http.client
|
||||
import socket
|
||||
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
|
||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||
pool = get_rpc_pool(url, max_connections)
|
||||
|
||||
max_retries = 2
|
||||
|
||||
for attempt in range(max_retries):
|
||||
conn = pool.get_connection()
|
||||
|
||||
try:
|
||||
v = conn.json_request(method, params)
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
|
||||
if "error" in r and r["error"] is not None:
|
||||
pool.discard_connection(conn)
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
|
||||
pool.return_connection(conn)
|
||||
return r["result"]
|
||||
|
||||
except (
|
||||
http.client.RemoteDisconnected,
|
||||
http.client.IncompleteRead,
|
||||
http.client.BadStatusLine,
|
||||
ConnectionError,
|
||||
ConnectionResetError,
|
||||
ConnectionAbortedError,
|
||||
BrokenPipeError,
|
||||
TimeoutError,
|
||||
socket.timeout,
|
||||
socket.error,
|
||||
OSError,
|
||||
) as ex:
|
||||
pool.discard_connection(conn)
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
logging.warning(
|
||||
f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
|
||||
)
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
pool.discard_connection(conn)
|
||||
logging.error(f"Unexpected RPC error: {ex}, method: {method}")
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
|
||||
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
@@ -120,36 +238,7 @@ def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||
return Jsonrpc(url)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
raise ValueError("RPC error " + str(ex))
|
||||
|
||||
|
||||
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)
|
||||
if wallet is not None:
|
||||
args.append("-rpcwallet=" + wallet)
|
||||
args += shlex.split(cmd)
|
||||
|
||||
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]))
|
||||
|
||||
r = out[0].decode("utf-8").strip()
|
||||
try:
|
||||
r = json.loads(r)
|
||||
except Exception:
|
||||
pass
|
||||
return r
|
||||
raise ValueError(f"RPC error: {ex}")
|
||||
|
||||
|
||||
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
@@ -159,7 +248,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None, wallet_override=None):
|
||||
nonlocal port, auth, wallet, host
|
||||
return callrpc(
|
||||
port,
|
||||
auth,
|
||||
@@ -174,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
|
||||
def escape_rpcauth(auth_str: str) -> str:
|
||||
username, password = auth_str.split(":", 1)
|
||||
username = urllib.parse.quote(username, safe="")
|
||||
password = urllib.parse.quote(password, safe="")
|
||||
return f"{username}:{password}"
|
||||
|
||||
131
basicswap/rpc_pool.py
Normal file
131
basicswap/rpc_pool.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from basicswap.rpc import Jsonrpc
|
||||
|
||||
|
||||
class RPCConnectionPool:
|
||||
def __init__(
|
||||
self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300
|
||||
):
|
||||
self.url = url
|
||||
self.max_connections = max_connections
|
||||
self.timeout = timeout
|
||||
self.logger = logger
|
||||
self.max_idle_time = max_idle_time
|
||||
self._pool = queue.Queue(maxsize=max_connections)
|
||||
self._lock = threading.Lock()
|
||||
self._created_connections = 0
|
||||
self._connection_timestamps = {}
|
||||
|
||||
def get_connection(self):
|
||||
try:
|
||||
conn_data = self._pool.get(block=False)
|
||||
conn, timestamp = (
|
||||
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||
)
|
||||
|
||||
if time.time() - timestamp > self.max_idle_time:
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||
)
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
return self._create_new_connection()
|
||||
|
||||
return conn
|
||||
except queue.Empty:
|
||||
return self._create_new_connection()
|
||||
|
||||
def _create_new_connection(self):
|
||||
with self._lock:
|
||||
if self._created_connections < self.max_connections:
|
||||
self._created_connections += 1
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
try:
|
||||
conn_data = self._pool.get(block=True, timeout=self.timeout)
|
||||
conn, timestamp = (
|
||||
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||
)
|
||||
|
||||
if time.time() - timestamp > self.max_idle_time:
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||
)
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
return conn
|
||||
except queue.Empty:
|
||||
if self.logger:
|
||||
self.logger.warning(
|
||||
f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
|
||||
)
|
||||
return Jsonrpc(self.url)
|
||||
|
||||
def return_connection(self, conn):
|
||||
try:
|
||||
self._pool.put((conn, time.time()), block=False)
|
||||
except queue.Full:
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
|
||||
def discard_connection(self, conn):
|
||||
conn.close()
|
||||
with self._lock:
|
||||
if self._created_connections > 0:
|
||||
self._created_connections -= 1
|
||||
|
||||
def close_all(self):
|
||||
while not self._pool.empty():
|
||||
try:
|
||||
conn_data = self._pool.get(block=False)
|
||||
conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
|
||||
conn.close()
|
||||
except queue.Empty:
|
||||
break
|
||||
with self._lock:
|
||||
self._created_connections = 0
|
||||
self._connection_timestamps.clear()
|
||||
|
||||
|
||||
_rpc_pools = {}
|
||||
_pool_lock = threading.Lock()
|
||||
_pool_logger = None
|
||||
|
||||
|
||||
def set_pool_logger(logger):
|
||||
global _pool_logger
|
||||
_pool_logger = logger
|
||||
|
||||
|
||||
def get_rpc_pool(url, max_connections=5):
|
||||
with _pool_lock:
|
||||
if url not in _rpc_pools:
|
||||
_rpc_pools[url] = RPCConnectionPool(
|
||||
url, max_connections, logger=_pool_logger
|
||||
)
|
||||
return _rpc_pools[url]
|
||||
|
||||
|
||||
def close_all_pools():
|
||||
with _pool_lock:
|
||||
for pool in _rpc_pools.values():
|
||||
pool.close_all()
|
||||
_rpc_pools.clear()
|
||||
@@ -309,7 +309,6 @@ def make_xmr_rpc2_func(
|
||||
transport.set_proxy(proxy_host, proxy_port)
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr2(
|
||||
port,
|
||||
method,
|
||||
@@ -345,7 +344,6 @@ def make_xmr_rpc_func(
|
||||
transport.set_proxy(proxy_host, proxy_port)
|
||||
|
||||
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
|
||||
nonlocal port, auth, host, transport, tag
|
||||
return callrpc_xmr(
|
||||
port,
|
||||
method,
|
||||
|
||||
@@ -1,153 +1,213 @@
|
||||
/* General Styles */
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.floatright {
|
||||
position: fixed;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Toast Notification Animations */
|
||||
.toast-slide-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-slide-out {
|
||||
animation: slideOutRight 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast Container Styles */
|
||||
#ul_updates {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
#ul_updates li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Toast Hover Effects */
|
||||
#ul_updates .bg-white:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.dark #ul_updates .dark\:bg-gray-800:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.padded_row td {
|
||||
padding-top: 1.5em;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-highest {
|
||||
z-index: 9999;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
#hide {
|
||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
-moz-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-o-animation: cssAnimation 0s ease-in 15s forwards;
|
||||
animation: cssAnimation 0s ease-in 15s forwards;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes cssAnimation {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Select Styles */
|
||||
.custom-select .select {
|
||||
appearance: none;
|
||||
background-image: url('/static/images/other/coin.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
appearance: none;
|
||||
background-image: url('/static/images/other/coin.png');
|
||||
background-position: 10px center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-select select::-webkit-scrollbar {
|
||||
width: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.custom-select .select option {
|
||||
padding-left: 0;
|
||||
text-indent: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
padding-left: 0;
|
||||
text-indent: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
}
|
||||
|
||||
.custom-select .select option.no-space {
|
||||
padding-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.custom-select .select option[data-image] {
|
||||
background-image: url('');
|
||||
background-image: url('');
|
||||
}
|
||||
|
||||
.custom-select .select-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-select .select-image {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.custom-select .select:focus + .select-dropdown .select-image {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Blur and Overlay Styles */
|
||||
.blurred {
|
||||
filter: blur(3px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
filter: blur(3px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error-overlay.non-blurred {
|
||||
filter: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
filter: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* Form Element Styles */
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
select:disabled,
|
||||
input:disabled,
|
||||
textarea:disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
select:disabled,
|
||||
input:disabled,
|
||||
textarea:disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
border: 1px solid red !important;
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
|
||||
/* Active Container Styles */
|
||||
.active-container {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.active-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid rgb(77, 132, 240);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid rgb(77, 132, 240);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Center Spin Animation */
|
||||
.center-spin {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover Container Styles */
|
||||
@@ -155,215 +215,353 @@
|
||||
.hover-container:hover #coin_to,
|
||||
.hover-container:hover #coin_from_button,
|
||||
.hover-container:hover #coin_from {
|
||||
border-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
#coin_to_button, #coin_from_button {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 20px 20px;
|
||||
#coin_to_button,
|
||||
#coin_from_button {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Input-like Container Styles */
|
||||
.input-like-container {
|
||||
max-width: 100%;
|
||||
background-color: #ffffff;
|
||||
width: 360px;
|
||||
padding: 1rem;
|
||||
color: #374151;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
height: auto;
|
||||
min-height: 90px;
|
||||
max-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
max-width: 100%;
|
||||
background-color: #ffffff;
|
||||
width: 360px;
|
||||
padding: 1rem;
|
||||
color: #374151;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
outline: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
height: auto;
|
||||
min-height: 90px;
|
||||
max-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-like-container.dark {
|
||||
background-color: #374151;
|
||||
color: #ffffff;
|
||||
background-color: #374151;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.input-like-container.copying {
|
||||
width: inherit;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
/* QR Code Styles */
|
||||
.qrcode {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qrcode-border {
|
||||
border: 2px solid;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0px;
|
||||
border: 2px solid;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.qrcode img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#showQR {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
margin-top: 25px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* Disabled Element Styles */
|
||||
select.select-disabled,
|
||||
.disabled-input-enabled,
|
||||
select.disabled-select-enabled {
|
||||
opacity: 0.40 !important;
|
||||
opacity: 0.40 !important;
|
||||
}
|
||||
|
||||
/* Shutdown Modal Styles */
|
||||
#shutdownModal {
|
||||
z-index: 50;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div:first-child {
|
||||
z-index: 40;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
#shutdownModal > div:last-child {
|
||||
z-index: 50;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
#shutdownModal > div {
|
||||
transition: opacity 0.3s ease-out;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
#shutdownModal.hidden > div {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#shutdownModal:not(.hidden) > div {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shutdown-button {
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #a0aec0;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled:hover {
|
||||
background-color: #4a5568;
|
||||
background-color: #4a5568;
|
||||
}
|
||||
|
||||
.shutdown-button.shutdown-disabled svg {
|
||||
opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
/* Loading line animation */
|
||||
/* Loading Line Animation */
|
||||
.loading-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-line::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent, #007bff, transparent);
|
||||
animation: loading 1.5s infinite;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent, #007bff, transparent);
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
/* Hide the loading line once data is loaded */
|
||||
|
||||
.usd-value:not(.loading) .loading-line,
|
||||
.profit-loss:not(.loading) .loading-line {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resolution-button {
|
||||
/* Resolution Button Styles */
|
||||
.resolution-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4B5563; /* gray-600 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
color: #4B5563;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.resolution-button:hover {
|
||||
color: #1F2937; /* gray-800 */
|
||||
}
|
||||
.resolution-button:hover {
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.resolution-button:focus {
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
.resolution-button:focus {
|
||||
outline: 2px solid #3B82F6;
|
||||
}
|
||||
|
||||
.resolution-button.active {
|
||||
color: #3B82F6; /* blue-500 */
|
||||
outline: 2px solid #3B82F6; /* blue-500 */
|
||||
}
|
||||
.resolution-button.active {
|
||||
color: #3B82F6;
|
||||
outline: 2px solid #3B82F6;
|
||||
}
|
||||
|
||||
.dark .resolution-button {
|
||||
color: #9CA3AF; /* gray-400 */
|
||||
}
|
||||
.dark .resolution-button {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.dark .resolution-button:hover {
|
||||
color: #F3F4F6; /* gray-100 */
|
||||
}
|
||||
.dark .resolution-button:hover {
|
||||
color: #F3F4F6;
|
||||
}
|
||||
|
||||
.dark .resolution-button.active {
|
||||
color: #60A5FA; /* blue-400 */
|
||||
outline-color: #60A5FA; /* blue-400 */
|
||||
.dark .resolution-button.active {
|
||||
color: #60A5FA;
|
||||
outline-color: #60A5FA;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#toggle-volume.active {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
#toggle-auto-refresh[data-enabled="true"] {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
[data-popper-placement] {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
/* Toggle Button Styles */
|
||||
#toggle-volume.active {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
#toggle-auto-refresh[data-enabled="true"] {
|
||||
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
|
||||
}
|
||||
|
||||
/* Multi-select dropdown styles */
|
||||
.multi-select-dropdown::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.multi-select-dropdown input[type="checkbox"]:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
.multi-select-dropdown label:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#coin_to_button:focus,
|
||||
#coin_from_button:focus {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
.coin-badge {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
.coin-badge .remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.coin-badge .remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.multi-select-dropdown {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999 !important;
|
||||
position: fixed !important;
|
||||
min-width: 200px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
}
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.dropdown-container.open {
|
||||
z-index: 9999;
|
||||
}
|
||||
.filter-button-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.multi-select-dropdown input[type="checkbox"] {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.multi-select-dropdown input[type="checkbox"]:focus {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.multi-select-dropdown input[type="checkbox"]:checked {
|
||||
background-color: #3b82f6 !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"] {
|
||||
border-color: #6b7280;
|
||||
background-color: #374151;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"]:focus {
|
||||
border-color: #3b82f6 !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
|
||||
}
|
||||
.dark .multi-select-dropdown input[type="checkbox"]:checked {
|
||||
background-color: #3b82f6 !important;
|
||||
border-color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
.multi-select-dropdown label {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
BIN
basicswap/static/images/coins/Namecoin-20.png
Normal file
BIN
basicswap/static/images/coins/Namecoin-20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 743 B |
BIN
basicswap/static/images/coins/Namecoin.png
Normal file
BIN
basicswap/static/images/coins/Namecoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -1,68 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const selectCache = {};
|
||||
|
||||
function updateSelectCache(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image');
|
||||
const name = selectedOption.textContent.trim();
|
||||
selectCache[select.id] = { image, name };
|
||||
}
|
||||
|
||||
function setSelectData(select) {
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const image = selectedOption.getAttribute('data-image') || '';
|
||||
const name = selectedOption.textContent.trim();
|
||||
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
|
||||
|
||||
const selectImage = select.nextElementSibling.querySelector('.select-image');
|
||||
if (selectImage) {
|
||||
selectImage.src = image;
|
||||
}
|
||||
|
||||
const selectNameElement = select.nextElementSibling.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
}
|
||||
|
||||
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
|
||||
const selectImages = document.querySelectorAll('.custom-select .select-image');
|
||||
const selectNames = document.querySelectorAll('.custom-select .select-name');
|
||||
|
||||
selectIcons.forEach(icon => icon.style.display = 'none');
|
||||
selectImages.forEach(image => image.style.display = 'none');
|
||||
selectNames.forEach(name => name.style.display = 'none');
|
||||
|
||||
function setupCustomSelect(select) {
|
||||
const options = select.querySelectorAll('option');
|
||||
const selectIcon = select.parentElement.querySelector('.select-icon');
|
||||
const selectImage = select.parentElement.querySelector('.select-image');
|
||||
|
||||
options.forEach(option => {
|
||||
const image = option.getAttribute('data-image');
|
||||
if (image) {
|
||||
option.style.backgroundImage = `url(${image})`;
|
||||
}
|
||||
});
|
||||
|
||||
const storedValue = localStorage.getItem(select.name);
|
||||
if (storedValue && select.value == '-1') {
|
||||
select.value = storedValue;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
setSelectData(select);
|
||||
localStorage.setItem(select.name, select.value);
|
||||
});
|
||||
|
||||
setSelectData(select);
|
||||
selectIcon.style.display = 'none';
|
||||
selectImage.style.display = 'none';
|
||||
}
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select select');
|
||||
customSelects.forEach(setupCustomSelect);
|
||||
});
|
||||
199
basicswap/static/js/global.js
Normal file
199
basicswap/static/js/global.js
Normal file
@@ -0,0 +1,199 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const burger = document.querySelectorAll('.navbar-burger');
|
||||
const menu = document.querySelectorAll('.navbar-menu');
|
||||
|
||||
if (burger.length && menu.length) {
|
||||
for (var i = 0; i < burger.length; i++) {
|
||||
burger[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const close = document.querySelectorAll('.navbar-close');
|
||||
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
||||
|
||||
if (close.length) {
|
||||
for (var k = 0; k < close.length; k++) {
|
||||
close[k].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (backdrop.length) {
|
||||
for (var l = 0; l < backdrop.length; l++) {
|
||||
backdrop[l].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipManager = TooltipManager.initialize();
|
||||
tooltipManager.initializeTooltips();
|
||||
setupShutdownModal();
|
||||
setupDarkMode();
|
||||
toggleImages();
|
||||
});
|
||||
|
||||
function setupShutdownModal() {
|
||||
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 closeAllDropdowns() {
|
||||
|
||||
const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
|
||||
openDropdowns.forEach(dropdown => {
|
||||
if (dropdown.style.display !== 'none') {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (window.Dropdown && window.Dropdown.instances) {
|
||||
window.Dropdown.instances.forEach(instance => {
|
||||
if (instance._visible) {
|
||||
instance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showShutdownModal() {
|
||||
closeAllDropdowns();
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
if (shutdownButtons.length) {
|
||||
shutdownButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
showShutdownModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (closeModalButton) {
|
||||
closeModalButton.addEventListener('click', hideShutdownModal);
|
||||
}
|
||||
|
||||
if (confirmShutdownButton) {
|
||||
confirmShutdownButton.addEventListener('click', function() {
|
||||
const shutdownToken = document.querySelector('.shutdown-button')
|
||||
.getAttribute('href').split('/').pop();
|
||||
window.location.href = '/shutdown/' + shutdownToken;
|
||||
});
|
||||
}
|
||||
|
||||
if (shutdownModal) {
|
||||
shutdownModal.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideShutdownModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (shutdownButtons.length) {
|
||||
updateShutdownButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function setupDarkMode() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (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');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
if (localStorage.getItem('color-theme') === 'dark') {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
}
|
||||
|
||||
if (themeToggleDarkIcon && themeToggleLightIcon) {
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
toggleImages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImages() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImageDisplay(images, display) {
|
||||
images.forEach(function(img) {
|
||||
img.style.display = display;
|
||||
});
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1825
basicswap/static/js/libs/popper.js
Normal file
1825
basicswap/static/js/libs/popper.js
Normal file
File diff suppressed because it is too large
Load Diff
2516
basicswap/static/js/libs/tippy.js
Normal file
2516
basicswap/static/js/libs/tippy.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
||||
// Burger menus
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// open
|
||||
const burger = document.querySelectorAll('.navbar-burger');
|
||||
const menu = document.querySelectorAll('.navbar-menu');
|
||||
|
||||
if (burger.length && menu.length) {
|
||||
for (var i = 0; i < burger.length; i++) {
|
||||
burger[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// close
|
||||
const close = document.querySelectorAll('.navbar-close');
|
||||
const backdrop = document.querySelectorAll('.navbar-backdrop');
|
||||
|
||||
if (close.length) {
|
||||
for (var i = 0; i < close.length; i++) {
|
||||
close[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (backdrop.length) {
|
||||
for (var i = 0; i < backdrop.length; i++) {
|
||||
backdrop[i].addEventListener('click', function() {
|
||||
for (var j = 0; j < menu.length; j++) {
|
||||
menu[j].classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
376
basicswap/static/js/modules/api-manager.js
Normal file
376
basicswap/static/js/modules/api-manager.js
Normal file
@@ -0,0 +1,376 @@
|
||||
const ApiManager = (function() {
|
||||
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
function getConfig() {
|
||||
return window.config || window.ConfigManager || {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: { requestsPerMinute: 50, minInterval: 1200 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
requestQueue: {},
|
||||
|
||||
getMinInterval: function(apiName) {
|
||||
const config = getConfig();
|
||||
return config.rateLimits?.[apiName]?.minInterval || 1200;
|
||||
},
|
||||
|
||||
getRetryDelays: function() {
|
||||
const config = getConfig();
|
||||
return config.retryDelays || [5000, 15000, 30000];
|
||||
},
|
||||
|
||||
canMakeRequest: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return (now - lastRequest) >= this.getMinInterval(apiName);
|
||||
},
|
||||
|
||||
updateLastRequestTime: function(apiName) {
|
||||
this.lastRequestTime[apiName] = Date.now();
|
||||
},
|
||||
|
||||
getWaitTime: function(apiName) {
|
||||
const now = Date.now();
|
||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
|
||||
},
|
||||
|
||||
queueRequest: async function(apiName, requestFn, retryCount = 0) {
|
||||
if (!this.requestQueue[apiName]) {
|
||||
this.requestQueue[apiName] = Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.requestQueue[apiName];
|
||||
|
||||
const executeRequest = async () => {
|
||||
const waitTime = this.getWaitTime(apiName);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateLastRequestTime(apiName);
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
const retryDelays = this.getRetryDelays();
|
||||
if (error.message.includes('429') && retryCount < retryDelays.length) {
|
||||
const delay = retryDelays[retryCount];
|
||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
||||
retryCount < retryDelays.length) {
|
||||
const delay = retryDelays[retryCount];
|
||||
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||
apiName,
|
||||
retryCount,
|
||||
error: error.message
|
||||
});
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
this.requestQueue[apiName] = executeRequest();
|
||||
return await this.requestQueue[apiName];
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'NetworkError') {
|
||||
const cacheKey = `coinData_${apiName}`;
|
||||
try {
|
||||
const cachedData = JSON.parse(localStorage.getItem(cacheKey));
|
||||
if (cachedData && cachedData.value) {
|
||||
return cachedData.value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing cached data:', e);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
config,
|
||||
rateLimiter,
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ApiManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options.config) {
|
||||
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
state.isInitialized = true;
|
||||
console.log('ApiManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
|
||||
if (window.ErrorHandler) {
|
||||
return window.ErrorHandler.safeExecuteAsync(async () => {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}, `ApiManager.makeRequest(${url})`, null);
|
||||
}
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
makePostRequest: async function(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/json/readurl', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
headers: headers
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.Error) {
|
||||
reject(new Error(data.Error));
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Request failed for ${url}:`, error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
|
||||
if (!coins) {
|
||||
throw new Error('No coins specified for price lookup');
|
||||
}
|
||||
let coinsParam;
|
||||
if (Array.isArray(coins)) {
|
||||
coinsParam = coins.filter(c => c && c.trim() !== '').join(',');
|
||||
} else if (typeof coins === 'object' && coins.coins) {
|
||||
coinsParam = coins.coins;
|
||||
} else {
|
||||
coinsParam = coins;
|
||||
}
|
||||
if (!coinsParam || coinsParam.trim() === '') {
|
||||
throw new Error('No valid coins to fetch prices for');
|
||||
}
|
||||
|
||||
return this.makeRequest('/json/coinprices', 'POST', {}, {
|
||||
coins: coinsParam,
|
||||
source: source,
|
||||
ttl: ttl
|
||||
});
|
||||
},
|
||||
|
||||
fetchCoinGeckoData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coins = (window.config && window.config.coins) ?
|
||||
window.config.coins
|
||||
.filter(coin => coin.usesCoinGecko)
|
||||
.map(coin => coin.name)
|
||||
.join(',') :
|
||||
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
||||
|
||||
const response = await this.fetchCoinPrices(coins);
|
||||
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error('Invalid response type');
|
||||
}
|
||||
|
||||
if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
|
||||
throw new Error('No valid rates found in response');
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchCoinGeckoData:', {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchVolumeData: async function() {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
const coinSymbols = window.CoinManager
|
||||
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||
: (window.config.coins
|
||||
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
|
||||
|
||||
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
|
||||
coins: coinSymbols.join(','),
|
||||
source: 'coingecko.com',
|
||||
ttl: 300
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.error('No response from backend');
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
console.error('Response missing data field:', response);
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
const volumeData = {};
|
||||
|
||||
Object.entries(response.data).forEach(([coinSymbol, data]) => {
|
||||
const coinKey = coinSymbol.toLowerCase();
|
||||
volumeData[coinKey] = {
|
||||
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
|
||||
price_change_percentage_24h: data.price_change_24h || 0
|
||||
};
|
||||
});
|
||||
|
||||
return volumeData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching volume data:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
|
||||
if (!Array.isArray(coinSymbols)) {
|
||||
coinSymbols = [coinSymbols];
|
||||
}
|
||||
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
let days;
|
||||
if (resolution === 'day') {
|
||||
days = 1;
|
||||
} else if (resolution === 'year') {
|
||||
days = 365;
|
||||
} else {
|
||||
days = 180;
|
||||
}
|
||||
|
||||
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
|
||||
coins: coinSymbols.join(','),
|
||||
days: days,
|
||||
source: 'coingecko.com',
|
||||
ttl: 3600
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.error('No response from backend');
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
console.error('Response missing data field:', response);
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching historical data:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
rateLimiter.requestQueue = {};
|
||||
rateLimiter.lastRequestTime = {};
|
||||
state.isInitialized = false;
|
||||
console.log('ApiManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.Api = ApiManager;
|
||||
window.ApiManager = ApiManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.apiManagerInitialized) {
|
||||
ApiManager.initialize();
|
||||
window.apiManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('ApiManager initialized');
|
||||
244
basicswap/static/js/modules/balance-updates.js
Normal file
244
basicswap/static/js/modules/balance-updates.js
Normal file
@@ -0,0 +1,244 @@
|
||||
const BalanceUpdatesManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const config = {
|
||||
balanceUpdateDelay: 2000,
|
||||
swapEventDelay: 5000,
|
||||
periodicRefreshInterval: 120000,
|
||||
walletPeriodicRefreshInterval: 60000,
|
||||
};
|
||||
|
||||
const state = {
|
||||
handlers: new Map(),
|
||||
timeouts: new Map(),
|
||||
intervals: new Map(),
|
||||
initialized: false
|
||||
};
|
||||
|
||||
async function fetchBalanceData() {
|
||||
if (window.ApiManager) {
|
||||
const data = await window.ApiManager.makeRequest('/json/walletbalances', 'GET');
|
||||
|
||||
if (data && data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return fetch('/json/walletbalances', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(balanceData => {
|
||||
if (balanceData.error) {
|
||||
throw new Error(balanceData.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(balanceData)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
return balanceData;
|
||||
});
|
||||
}
|
||||
|
||||
function clearTimeoutByKey(key) {
|
||||
if (state.timeouts.has(key)) {
|
||||
const timeoutId = state.timeouts.get(key);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(timeoutId);
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
state.timeouts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutByKey(key, callback, delay) {
|
||||
clearTimeoutByKey(key);
|
||||
const timeoutId = window.CleanupManager
|
||||
? window.CleanupManager.setTimeout(callback, delay)
|
||||
: setTimeout(callback, delay);
|
||||
state.timeouts.set(key, timeoutId);
|
||||
}
|
||||
|
||||
function clearIntervalByKey(key) {
|
||||
if (state.intervals.has(key)) {
|
||||
const intervalId = state.intervals.get(key);
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearInterval(intervalId);
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
state.intervals.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setIntervalByKey(key, callback, interval) {
|
||||
clearIntervalByKey(key);
|
||||
const intervalId = window.CleanupManager
|
||||
? window.CleanupManager.setInterval(callback, interval)
|
||||
: setInterval(callback, interval);
|
||||
state.intervals.set(key, intervalId);
|
||||
}
|
||||
|
||||
function handleBalanceUpdate(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
setTimeoutByKey(`${contextKey}_balance_update`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via WebSocket:`, error);
|
||||
});
|
||||
}, config.balanceUpdateDelay);
|
||||
}
|
||||
|
||||
function handleSwapEvent(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
setTimeoutByKey(`${contextKey}_swap_event`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via swap event:`, error);
|
||||
});
|
||||
}, config.swapEventDelay);
|
||||
}
|
||||
|
||||
function setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext) {
|
||||
const handlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'coin_balance_updated') {
|
||||
handleBalanceUpdate(contextKey, balanceUpdateCallback, errorContext);
|
||||
}
|
||||
|
||||
if (swapEventCallback) {
|
||||
const swapEvents = ['new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (swapEvents.includes(data.event)) {
|
||||
handleSwapEvent(contextKey, swapEventCallback, errorContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.handlers.set(contextKey, handlerId);
|
||||
return handlerId;
|
||||
}
|
||||
|
||||
function setupPeriodicRefresh(contextKey, updateCallback, errorContext, interval) {
|
||||
const refreshInterval = interval || config.periodicRefreshInterval;
|
||||
|
||||
setIntervalByKey(`${contextKey}_periodic`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error in periodic ${errorContext} balance refresh:`, error);
|
||||
});
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
function cleanup(contextKey) {
|
||||
if (state.handlers.has(contextKey)) {
|
||||
const handlerId = state.handlers.get(contextKey);
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
state.handlers.delete(contextKey);
|
||||
}
|
||||
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
|
||||
clearIntervalByKey(`${contextKey}_periodic`);
|
||||
}
|
||||
|
||||
function cleanupAll() {
|
||||
state.handlers.forEach((handlerId) => {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
});
|
||||
state.handlers.clear();
|
||||
|
||||
state.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
||||
state.timeouts.clear();
|
||||
|
||||
state.intervals.forEach(intervalId => clearInterval(intervalId));
|
||||
state.intervals.clear();
|
||||
|
||||
state.initialized = false;
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: function() {
|
||||
if (state.initialized) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('balanceUpdatesManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', cleanupAll);
|
||||
|
||||
state.initialized = true;
|
||||
console.log('BalanceUpdatesManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
setup: function(options) {
|
||||
const {
|
||||
contextKey,
|
||||
balanceUpdateCallback,
|
||||
swapEventCallback,
|
||||
errorContext,
|
||||
enablePeriodicRefresh = false,
|
||||
periodicInterval
|
||||
} = options;
|
||||
|
||||
if (!contextKey || !balanceUpdateCallback || !errorContext) {
|
||||
throw new Error('Missing required options: contextKey, balanceUpdateCallback, errorContext');
|
||||
}
|
||||
|
||||
setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext);
|
||||
|
||||
if (enablePeriodicRefresh) {
|
||||
setupPeriodicRefresh(contextKey, balanceUpdateCallback, errorContext, periodicInterval);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchBalanceData: fetchBalanceData,
|
||||
|
||||
cleanup: cleanup,
|
||||
|
||||
dispose: cleanupAll,
|
||||
|
||||
isInitialized: function() {
|
||||
return state.initialized;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.BalanceUpdatesManager = BalanceUpdatesManager;
|
||||
}
|
||||
552
basicswap/static/js/modules/cache-manager.js
Normal file
552
basicswap/static/js/modules/cache-manager.js
Normal file
@@ -0,0 +1,552 @@
|
||||
const CacheManager = (function() {
|
||||
function getDefaults() {
|
||||
if (window.config?.cacheConfig?.storage) {
|
||||
return window.config.cacheConfig.storage;
|
||||
}
|
||||
if (window.ConfigManager?.cacheConfig?.storage) {
|
||||
return window.ConfigManager.cacheConfig.storage;
|
||||
}
|
||||
return {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200,
|
||||
defaultTTL: 5 * 60 * 1000
|
||||
};
|
||||
}
|
||||
|
||||
const defaults = getDefaults();
|
||||
|
||||
const PRICES_CACHE_KEY = 'crypto_prices_unified';
|
||||
|
||||
const CACHE_KEY_PATTERNS = [
|
||||
'coinData_',
|
||||
'chartData_',
|
||||
'historical_',
|
||||
'rates_',
|
||||
'prices_',
|
||||
'offers_',
|
||||
'fallback_',
|
||||
'volumeData'
|
||||
];
|
||||
|
||||
const isCacheKey = (key) => {
|
||||
return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
|
||||
key === 'coinGeckoOneLiner' ||
|
||||
key === PRICES_CACHE_KEY;
|
||||
};
|
||||
|
||||
const isLocalStorageAvailable = () => {
|
||||
try {
|
||||
const testKey = '__storage_test__';
|
||||
localStorage.setItem(testKey, testKey);
|
||||
localStorage.removeItem(testKey);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
const memoryCache = new Map();
|
||||
|
||||
if (!storageAvailable) {
|
||||
console.warn('localStorage is not available. Using in-memory cache instead.');
|
||||
}
|
||||
|
||||
const cacheAPI = {
|
||||
getTTL: function(resourceType) {
|
||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
|
||||
window.ConfigManager?.cacheConfig?.ttlSettings || {};
|
||||
const defaultTTL = window.config?.cacheConfig?.defaultTTL ||
|
||||
window.ConfigManager?.cacheConfig?.defaultTTL ||
|
||||
defaults.defaultTTL;
|
||||
return ttlConfig[resourceType] || defaultTTL;
|
||||
},
|
||||
|
||||
set: function(key, value, resourceTypeOrCustomTtl = null) {
|
||||
try {
|
||||
this.cleanup();
|
||||
|
||||
if (!value) {
|
||||
console.warn('Attempted to cache null/undefined value for key:', key);
|
||||
return false;
|
||||
}
|
||||
|
||||
let ttl;
|
||||
if (typeof resourceTypeOrCustomTtl === 'string') {
|
||||
ttl = this.getTTL(resourceTypeOrCustomTtl);
|
||||
} else if (typeof resourceTypeOrCustomTtl === 'number') {
|
||||
ttl = resourceTypeOrCustomTtl;
|
||||
} else {
|
||||
ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
||||
}
|
||||
|
||||
const item = {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + ttl
|
||||
};
|
||||
|
||||
const serializedItem = window.ErrorHandler
|
||||
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
|
||||
: (() => {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!serializedItem) return false;
|
||||
|
||||
const itemSize = new Blob([serializedItem]).size;
|
||||
if (itemSize > defaults.maxSizeBytes) {
|
||||
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (storageError) {
|
||||
if (storageError.name === 'QuotaExceededError') {
|
||||
this.cleanup(true);
|
||||
try {
|
||||
localStorage.setItem(key, serializedItem);
|
||||
return true;
|
||||
} catch (retryError) {
|
||||
console.error('Storage quota exceeded even after cleanup:', retryError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to quota issues');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.error('localStorage error:', storageError);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
memoryCache.set(key, item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memoryCache.set(key, item);
|
||||
if (memoryCache.size > defaults.maxItems) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(k => isCacheKey(k))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.2));
|
||||
|
||||
keysToDelete.forEach(k => memoryCache.delete(k));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Cache set error:', error);
|
||||
try {
|
||||
memoryCache.set(key, {
|
||||
value: value,
|
||||
timestamp: Date.now(),
|
||||
expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Memory cache set error:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
if (itemStr) {
|
||||
let item;
|
||||
try {
|
||||
item = JSON.parse(itemStr);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse cached item:', parseError);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
|
||||
console.warn('Invalid cache item structure for key:', key);
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
}
|
||||
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("localStorage access error:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryCache.has(key)) {
|
||||
const item = memoryCache.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
if (now < item.expiresAt) {
|
||||
return {
|
||||
value: item.value,
|
||||
remainingTime: item.expiresAt - now
|
||||
};
|
||||
} else {
|
||||
|
||||
memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Cache retrieval error:", error);
|
||||
try {
|
||||
if (storageAvailable) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
memoryCache.delete(key);
|
||||
} catch (removeError) {
|
||||
console.error("Failed to remove invalid cache entry:", removeError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
isValid: function(key) {
|
||||
return this.get(key) !== null;
|
||||
},
|
||||
|
||||
cleanup: function(aggressive = false) {
|
||||
const now = Date.now();
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
const items = [];
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
localStorage.removeItem(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
key,
|
||||
size,
|
||||
expiresAt: item.expiresAt,
|
||||
timestamp: item.timestamp
|
||||
});
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
} catch (error) {
|
||||
console.error("Error processing cache item:", error);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
|
||||
items.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
|
||||
const item = items.pop();
|
||||
try {
|
||||
localStorage.removeItem(item.key);
|
||||
totalSize -= item.size;
|
||||
itemCount--;
|
||||
} catch (error) {
|
||||
console.error("Error removing cache item:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during localStorage cleanup:", error);
|
||||
storageAvailable = false;
|
||||
console.warn('Switching to in-memory cache due to localStorage error');
|
||||
}
|
||||
}
|
||||
|
||||
const expiredKeys = [];
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (now >= item.expiresAt) {
|
||||
expiredKeys.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
expiredKeys.forEach(key => memoryCache.delete(key));
|
||||
|
||||
if (aggressive && memoryCache.size > defaults.maxItems / 2) {
|
||||
const keysToDelete = Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
||||
.slice(0, Math.floor(memoryCache.size * 0.3));
|
||||
|
||||
keysToDelete.forEach(key => memoryCache.delete(key));
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
itemCount,
|
||||
memoryCacheSize: memoryCache.size,
|
||||
cleaned: items.length,
|
||||
storageAvailable
|
||||
};
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
const keys = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (isCacheKey(key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache item:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing localStorage cache:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(memoryCache.keys())
|
||||
.filter(key => isCacheKey(key))
|
||||
.forEach(key => memoryCache.delete(key));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
let totalSize = 0;
|
||||
let itemCount = 0;
|
||||
let expiredCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (storageAvailable) {
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!isCacheKey(key)) continue;
|
||||
|
||||
try {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
const size = new Blob([itemStr]).size;
|
||||
const item = JSON.parse(itemStr);
|
||||
|
||||
totalSize += size;
|
||||
itemCount++;
|
||||
|
||||
if (now >= item.expiresAt) {
|
||||
expiredCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting cache stats:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting localStorage stats:", error);
|
||||
storageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
let memoryCacheSize = 0;
|
||||
let memoryCacheItems = 0;
|
||||
let memoryCacheExpired = 0;
|
||||
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
memoryCacheItems++;
|
||||
if (now >= item.expiresAt) {
|
||||
memoryCacheExpired++;
|
||||
}
|
||||
try {
|
||||
memoryCacheSize += new Blob([JSON.stringify(item)]).size;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
||||
itemCount,
|
||||
expiredCount,
|
||||
utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
|
||||
memoryCacheItems,
|
||||
memoryCacheExpired,
|
||||
memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
|
||||
storageType: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
},
|
||||
|
||||
checkStorage: function() {
|
||||
const wasAvailable = storageAvailable;
|
||||
storageAvailable = isLocalStorageAvailable();
|
||||
|
||||
if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
|
||||
console.log('localStorage is now available. Migrating memory cache...');
|
||||
let migratedCount = 0;
|
||||
memoryCache.forEach((item, key) => {
|
||||
if (isCacheKey(key)) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
memoryCache.delete(key);
|
||||
migratedCount++;
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
|
||||
}
|
||||
|
||||
return {
|
||||
available: storageAvailable,
|
||||
type: storageAvailable ? 'localStorage' : 'memory'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...cacheAPI,
|
||||
|
||||
setPrices: function(priceData, customTtl = null) {
|
||||
return this.set(PRICES_CACHE_KEY, priceData,
|
||||
customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
|
||||
},
|
||||
|
||||
getPrices: function() {
|
||||
return this.get(PRICES_CACHE_KEY);
|
||||
},
|
||||
|
||||
getCoinPrice: function(symbol) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedSymbol = symbol.toLowerCase();
|
||||
return prices.value[normalizedSymbol] || null;
|
||||
},
|
||||
|
||||
getCompatiblePrices: function(format) {
|
||||
const prices = this.getPrices();
|
||||
if (!prices || !prices.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch(format) {
|
||||
case 'rates':
|
||||
const ratesFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const coinKey = coin.replace(/-/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.replace(' ', '-');
|
||||
|
||||
ratesFormat[coinKey] = {
|
||||
usd: data.price || data.usd,
|
||||
btc: data.price_btc || data.btc
|
||||
};
|
||||
});
|
||||
return {
|
||||
value: ratesFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
case 'coinGecko':
|
||||
const geckoFormat = {};
|
||||
Object.entries(prices.value).forEach(([coin, data]) => {
|
||||
const symbol = this.getSymbolFromCoinId(coin);
|
||||
if (symbol) {
|
||||
geckoFormat[symbol.toLowerCase()] = {
|
||||
current_price: data.price || data.usd,
|
||||
price_btc: data.price_btc || data.btc,
|
||||
total_volume: data.total_volume,
|
||||
price_change_percentage_24h: data.price_change_percentage_24h,
|
||||
displayName: symbol
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
value: geckoFormat,
|
||||
remainingTime: prices.remainingTime
|
||||
};
|
||||
|
||||
default:
|
||||
return prices;
|
||||
}
|
||||
},
|
||||
|
||||
getSymbolFromCoinId: function(coinId) {
|
||||
const symbolMap = {
|
||||
'bitcoin': 'BTC',
|
||||
'litecoin': 'LTC',
|
||||
'monero': 'XMR',
|
||||
'wownero': 'WOW',
|
||||
'particl': 'PART',
|
||||
'pivx': 'PIVX',
|
||||
'firo': 'FIRO',
|
||||
'zcoin': 'FIRO',
|
||||
'dash': 'DASH',
|
||||
'decred': 'DCR',
|
||||
'namecoin': 'NMR',
|
||||
'bitcoin-cash': 'BCH',
|
||||
'dogecoin': 'DOGE'
|
||||
};
|
||||
|
||||
return symbolMap[coinId] || null;
|
||||
}
|
||||
};
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
|
||||
cm.clear();
|
||||
});
|
||||
}
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.CacheManager = CacheManager;
|
||||
|
||||
console.log('CacheManager initialized');
|
||||
508
basicswap/static/js/modules/cleanup-manager.js
Normal file
508
basicswap/static/js/modules/cleanup-manager.js
Normal file
@@ -0,0 +1,508 @@
|
||||
const CleanupManager = (function() {
|
||||
const state = {
|
||||
eventListeners: [],
|
||||
timeouts: [],
|
||||
intervals: [],
|
||||
animationFrames: [],
|
||||
resources: new Map(),
|
||||
debug: false,
|
||||
memoryOptimizationInterval: null
|
||||
};
|
||||
|
||||
function log(message, ...args) {
|
||||
if (state.debug) {
|
||||
console.log(`[CleanupManager] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
addListener: function(element, type, handler, options = false) {
|
||||
if (!element) {
|
||||
log('Warning: Attempted to add listener to null/undefined element');
|
||||
return handler;
|
||||
}
|
||||
|
||||
element.addEventListener(type, handler, options);
|
||||
state.eventListeners.push({ element, type, handler, options });
|
||||
log(`Added ${type} listener to`, element);
|
||||
return handler;
|
||||
},
|
||||
|
||||
setTimeout: function(callback, delay) {
|
||||
const id = window.setTimeout(callback, delay);
|
||||
state.timeouts.push(id);
|
||||
log(`Created timeout ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
setInterval: function(callback, delay) {
|
||||
const id = window.setInterval(callback, delay);
|
||||
state.intervals.push(id);
|
||||
log(`Created interval ${id} with ${delay}ms delay`);
|
||||
return id;
|
||||
},
|
||||
|
||||
requestAnimationFrame: function(callback) {
|
||||
const id = window.requestAnimationFrame(callback);
|
||||
state.animationFrames.push(id);
|
||||
log(`Requested animation frame ${id}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
registerResource: function(type, resource, cleanupFn) {
|
||||
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
state.resources.set(id, { resource, cleanupFn });
|
||||
log(`Registered custom resource ${id} of type ${type}`);
|
||||
return id;
|
||||
},
|
||||
|
||||
unregisterResource: function(id) {
|
||||
const resourceInfo = state.resources.get(id);
|
||||
if (resourceInfo) {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
state.resources.delete(id);
|
||||
log(`Unregistered and cleaned up resource ${id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
log(`Resource ${id} not found`);
|
||||
return false;
|
||||
},
|
||||
|
||||
clearTimeout: function(id) {
|
||||
const index = state.timeouts.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearTimeout(id);
|
||||
state.timeouts.splice(index, 1);
|
||||
log(`Cleared timeout ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
clearInterval: function(id) {
|
||||
const index = state.intervals.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.clearInterval(id);
|
||||
state.intervals.splice(index, 1);
|
||||
log(`Cleared interval ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
cancelAnimationFrame: function(id) {
|
||||
const index = state.animationFrames.indexOf(id);
|
||||
if (index !== -1) {
|
||||
window.cancelAnimationFrame(id);
|
||||
state.animationFrames.splice(index, 1);
|
||||
log(`Cancelled animation frame ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener: function(element, type, handler, options = false) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => !(listener.element === element &&
|
||||
listener.type === type &&
|
||||
listener.handler === handler)
|
||||
);
|
||||
},
|
||||
|
||||
removeListenersByElement: function(element) {
|
||||
if (!element) return;
|
||||
|
||||
const listenersToRemove = state.eventListeners.filter(
|
||||
listener => listener.element === element
|
||||
);
|
||||
|
||||
listenersToRemove.forEach(({ element, type, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
log(`Removed ${type} listener from`, element);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
state.eventListeners = state.eventListeners.filter(
|
||||
listener => listener.element !== element
|
||||
);
|
||||
},
|
||||
|
||||
clearAllTimeouts: function() {
|
||||
state.timeouts.forEach(id => {
|
||||
window.clearTimeout(id);
|
||||
});
|
||||
const count = state.timeouts.length;
|
||||
state.timeouts = [];
|
||||
log(`Cleared all timeouts (${count})`);
|
||||
},
|
||||
|
||||
clearAllIntervals: function() {
|
||||
state.intervals.forEach(id => {
|
||||
window.clearInterval(id);
|
||||
});
|
||||
const count = state.intervals.length;
|
||||
state.intervals = [];
|
||||
log(`Cleared all intervals (${count})`);
|
||||
},
|
||||
|
||||
clearAllAnimationFrames: function() {
|
||||
state.animationFrames.forEach(id => {
|
||||
window.cancelAnimationFrame(id);
|
||||
});
|
||||
const count = state.animationFrames.length;
|
||||
state.animationFrames = [];
|
||||
log(`Cancelled all animation frames (${count})`);
|
||||
},
|
||||
|
||||
clearAllResources: function() {
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
state.resources.forEach((resourceInfo, id) => {
|
||||
try {
|
||||
resourceInfo.cleanupFn(resourceInfo.resource);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
state.resources.clear();
|
||||
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
|
||||
},
|
||||
|
||||
clearAllListeners: function() {
|
||||
state.eventListeners.forEach(({ element, type, handler, options }) => {
|
||||
if (element) {
|
||||
try {
|
||||
element.removeEventListener(type, handler, options);
|
||||
} catch (error) {
|
||||
console.error(`[CleanupManager] Error removing event listener:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
const count = state.eventListeners.length;
|
||||
state.eventListeners = [];
|
||||
log(`Removed all event listeners (${count})`);
|
||||
},
|
||||
|
||||
clearAll: function() {
|
||||
const counts = {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size
|
||||
};
|
||||
|
||||
this.clearAllListeners();
|
||||
this.clearAllTimeouts();
|
||||
this.clearAllIntervals();
|
||||
this.clearAllAnimationFrames();
|
||||
this.clearAllResources();
|
||||
|
||||
log(`All resources cleaned up:`, counts);
|
||||
return counts;
|
||||
},
|
||||
|
||||
getResourceCounts: function() {
|
||||
return {
|
||||
listeners: state.eventListeners.length,
|
||||
timeouts: state.timeouts.length,
|
||||
intervals: state.intervals.length,
|
||||
animationFrames: state.animationFrames.length,
|
||||
resources: state.resources.size,
|
||||
total: state.eventListeners.length +
|
||||
state.timeouts.length +
|
||||
state.intervals.length +
|
||||
state.animationFrames.length +
|
||||
state.resources.size
|
||||
};
|
||||
},
|
||||
|
||||
setupMemoryOptimization: function(options = {}) {
|
||||
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
|
||||
const maxCacheSize = options.maxCacheSize || 100;
|
||||
const maxDataSize = options.maxDataSize || 1000;
|
||||
|
||||
if (state.memoryOptimizationInterval) {
|
||||
this.clearInterval(state.memoryOptimizationInterval);
|
||||
}
|
||||
|
||||
this.addListener(document, 'visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
log('Tab hidden - running memory optimization');
|
||||
this.optimizeMemory({
|
||||
maxCacheSize: maxCacheSize,
|
||||
maxDataSize: maxDataSize
|
||||
});
|
||||
} else if (window.TooltipManager) {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
state.memoryOptimizationInterval = this.setInterval(() => {
|
||||
if (document.hidden) {
|
||||
log('Periodic memory optimization');
|
||||
this.optimizeMemory({
|
||||
maxCacheSize: maxCacheSize,
|
||||
maxDataSize: maxDataSize
|
||||
});
|
||||
}
|
||||
}, memoryCheckInterval);
|
||||
|
||||
log('Memory optimization setup complete');
|
||||
return state.memoryOptimizationInterval;
|
||||
},
|
||||
|
||||
optimizeMemory: function(options = {}) {
|
||||
log('Running memory optimization');
|
||||
|
||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||
window.TooltipManager.cleanup();
|
||||
}
|
||||
|
||||
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
|
||||
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
|
||||
}
|
||||
|
||||
this.cleanupOrphanedResources();
|
||||
|
||||
if (window.gc) {
|
||||
try {
|
||||
window.gc();
|
||||
log('Forced garbage collection');
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('memoryOptimized', {
|
||||
detail: {
|
||||
timestamp: Date.now(),
|
||||
maxDataSize: options.maxDataSize || 1000
|
||||
}
|
||||
}));
|
||||
|
||||
log('Memory optimization complete');
|
||||
},
|
||||
|
||||
cleanupOrphanedResources: function() {
|
||||
let removedListeners = 0;
|
||||
const validListeners = [];
|
||||
|
||||
for (let i = 0; i < state.eventListeners.length; i++) {
|
||||
const listener = state.eventListeners[i];
|
||||
if (!listener.element) {
|
||||
removedListeners++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const isDetached = !(listener.element instanceof Node) ||
|
||||
!document.body.contains(listener.element) ||
|
||||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
|
||||
(listener.element.style && listener.element.style.display === 'none');
|
||||
|
||||
if (isDetached) {
|
||||
try {
|
||||
if (listener.element instanceof Node) {
|
||||
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
|
||||
}
|
||||
removedListeners++;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} else {
|
||||
validListeners.push(listener);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
log(`Error checking listener (removing): ${e.message}`);
|
||||
removedListeners++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedListeners > 0) {
|
||||
state.eventListeners = validListeners;
|
||||
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
|
||||
}
|
||||
|
||||
let removedResources = 0;
|
||||
const resourcesForRemoval = [];
|
||||
|
||||
state.resources.forEach((info, id) => {
|
||||
const resource = info.resource;
|
||||
|
||||
try {
|
||||
|
||||
if (resource instanceof Element && !document.body.contains(resource)) {
|
||||
resourcesForRemoval.push(id);
|
||||
}
|
||||
|
||||
if (resource && resource.element) {
|
||||
|
||||
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
|
||||
resourcesForRemoval.push(id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error checking resource ${id}: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
resourcesForRemoval.forEach(id => {
|
||||
this.unregisterResource(id);
|
||||
removedResources++;
|
||||
});
|
||||
|
||||
if (removedResources > 0) {
|
||||
log(`Removed ${removedResources} orphaned resources`);
|
||||
}
|
||||
|
||||
if (window.TooltipManager) {
|
||||
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanupOrphanedTooltips();
|
||||
} catch (e) {
|
||||
|
||||
if (typeof window.TooltipManager.cleanup === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanup();
|
||||
} catch (err) {
|
||||
log(`Error cleaning up tooltips: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof window.TooltipManager.cleanup === 'function') {
|
||||
try {
|
||||
window.TooltipManager.cleanup();
|
||||
} catch (e) {
|
||||
log(`Error cleaning up tooltips: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.cleanupTooltipDOM();
|
||||
} catch (e) {
|
||||
log(`Error in cleanupTooltipDOM: ${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
cleanupTooltipDOM: function() {
|
||||
let removedElements = 0;
|
||||
|
||||
try {
|
||||
|
||||
const tooltipSelectors = [
|
||||
'[role="tooltip"]',
|
||||
'[id^="tooltip-"]',
|
||||
'.tippy-box',
|
||||
'[data-tippy-root]'
|
||||
];
|
||||
|
||||
tooltipSelectors.forEach(selector => {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
|
||||
elements.forEach(element => {
|
||||
try {
|
||||
|
||||
if (!(element instanceof Element)) return;
|
||||
|
||||
const isDetached = !element.parentElement ||
|
||||
!document.body.contains(element.parentElement) ||
|
||||
element.classList.contains('hidden') ||
|
||||
element.style.display === 'none' ||
|
||||
element.style.visibility === 'hidden';
|
||||
|
||||
if (isDetached) {
|
||||
try {
|
||||
element.remove();
|
||||
removedElements++;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
log(`Error querying for ${selector}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log(`Error in tooltip DOM cleanup: ${e.message}`);
|
||||
}
|
||||
|
||||
if (removedElements > 0) {
|
||||
log(`Removed ${removedElements} detached tooltip elements`);
|
||||
}
|
||||
},
|
||||
|
||||
setDebugMode: function(enabled) {
|
||||
state.debug = Boolean(enabled);
|
||||
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
|
||||
return state.debug;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
this.clearAll();
|
||||
log('CleanupManager disposed');
|
||||
},
|
||||
|
||||
initialize: function(options = {}) {
|
||||
if (options.debug !== undefined) {
|
||||
this.setDebugMode(options.debug);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
|
||||
this.addListener(window, 'beforeunload', () => {
|
||||
this.clearAll();
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
|
||||
this.setupMemoryOptimization(options.memoryOptions || {});
|
||||
}
|
||||
|
||||
log('CleanupManager initialized');
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CleanupManager;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.CleanupManager = CleanupManager;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
CleanupManager.initialize({ debug: false });
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
CleanupManager.initialize({ debug: false });
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
256
basicswap/static/js/modules/coin-manager.js
Normal file
256
basicswap/static/js/modules/coin-manager.js
Normal file
@@ -0,0 +1,256 @@
|
||||
const CoinManager = (function() {
|
||||
const coinRegistry = [
|
||||
{
|
||||
symbol: 'BTC',
|
||||
name: 'bitcoin',
|
||||
displayName: 'Bitcoin',
|
||||
aliases: ['btc', 'bitcoin'],
|
||||
coingeckoId: 'bitcoin',
|
||||
cryptocompareId: 'BTC',
|
||||
usesCryptoCompare: false,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Bitcoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'XMR',
|
||||
name: 'monero',
|
||||
displayName: 'Monero',
|
||||
aliases: ['xmr', 'monero'],
|
||||
coingeckoId: 'monero',
|
||||
cryptocompareId: 'XMR',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Monero.png'
|
||||
},
|
||||
{
|
||||
symbol: 'PART',
|
||||
name: 'particl',
|
||||
displayName: 'Particl',
|
||||
aliases: ['part', 'particl', 'particl anon', 'particl blind'],
|
||||
variants: ['Particl', 'Particl Blind', 'Particl Anon'],
|
||||
coingeckoId: 'particl',
|
||||
cryptocompareId: 'PART',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Particl.png'
|
||||
},
|
||||
{
|
||||
symbol: 'BCH',
|
||||
name: 'bitcoin-cash',
|
||||
displayName: 'Bitcoin Cash',
|
||||
aliases: ['bch', 'bitcoincash', 'bitcoin cash'],
|
||||
coingeckoId: 'bitcoin-cash',
|
||||
cryptocompareId: 'BCH',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Bitcoin%20Cash.png'
|
||||
},
|
||||
{
|
||||
symbol: 'PIVX',
|
||||
name: 'pivx',
|
||||
displayName: 'PIVX',
|
||||
aliases: ['pivx'],
|
||||
coingeckoId: 'pivx',
|
||||
cryptocompareId: 'PIVX',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'PIVX.png'
|
||||
},
|
||||
{
|
||||
symbol: 'FIRO',
|
||||
name: 'firo',
|
||||
displayName: 'Firo',
|
||||
aliases: ['firo', 'zcoin'],
|
||||
coingeckoId: 'firo',
|
||||
cryptocompareId: 'FIRO',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Firo.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DASH',
|
||||
name: 'dash',
|
||||
displayName: 'Dash',
|
||||
aliases: ['dash'],
|
||||
coingeckoId: 'dash',
|
||||
cryptocompareId: 'DASH',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Dash.png'
|
||||
},
|
||||
{
|
||||
symbol: 'LTC',
|
||||
name: 'litecoin',
|
||||
displayName: 'Litecoin',
|
||||
aliases: ['ltc', 'litecoin'],
|
||||
variants: ['Litecoin', 'Litecoin MWEB'],
|
||||
coingeckoId: 'litecoin',
|
||||
cryptocompareId: 'LTC',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Litecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DOGE',
|
||||
name: 'dogecoin',
|
||||
displayName: 'Dogecoin',
|
||||
aliases: ['doge', 'dogecoin'],
|
||||
coingeckoId: 'dogecoin',
|
||||
cryptocompareId: 'DOGE',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Dogecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'DCR',
|
||||
name: 'decred',
|
||||
displayName: 'Decred',
|
||||
aliases: ['dcr', 'decred'],
|
||||
coingeckoId: 'decred',
|
||||
cryptocompareId: 'DCR',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Decred.png'
|
||||
},
|
||||
{
|
||||
symbol: 'NMC',
|
||||
name: 'namecoin',
|
||||
displayName: 'Namecoin',
|
||||
aliases: ['nmc', 'namecoin'],
|
||||
coingeckoId: 'namecoin',
|
||||
cryptocompareId: 'NMC',
|
||||
usesCryptoCompare: true,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Namecoin.png'
|
||||
},
|
||||
{
|
||||
symbol: 'WOW',
|
||||
name: 'wownero',
|
||||
displayName: 'Wownero',
|
||||
aliases: ['wow', 'wownero'],
|
||||
coingeckoId: 'wownero',
|
||||
cryptocompareId: 'WOW',
|
||||
usesCryptoCompare: false,
|
||||
usesCoinGecko: true,
|
||||
historicalDays: 30,
|
||||
icon: 'Wownero.png'
|
||||
}
|
||||
];
|
||||
const symbolToInfo = {};
|
||||
const nameToInfo = {};
|
||||
const displayNameToInfo = {};
|
||||
const coinAliasesMap = {};
|
||||
|
||||
function buildLookupMaps() {
|
||||
coinRegistry.forEach(coin => {
|
||||
symbolToInfo[coin.symbol.toLowerCase()] = coin;
|
||||
nameToInfo[coin.name.toLowerCase()] = coin;
|
||||
displayNameToInfo[coin.displayName.toLowerCase()] = coin;
|
||||
if (coin.aliases && Array.isArray(coin.aliases)) {
|
||||
coin.aliases.forEach(alias => {
|
||||
coinAliasesMap[alias.toLowerCase()] = coin;
|
||||
});
|
||||
}
|
||||
coinAliasesMap[coin.symbol.toLowerCase()] = coin;
|
||||
coinAliasesMap[coin.name.toLowerCase()] = coin;
|
||||
coinAliasesMap[coin.displayName.toLowerCase()] = coin;
|
||||
if (coin.variants && Array.isArray(coin.variants)) {
|
||||
coin.variants.forEach(variant => {
|
||||
coinAliasesMap[variant.toLowerCase()] = coin;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildLookupMaps();
|
||||
|
||||
function getCoinByAnyIdentifier(identifier) {
|
||||
if (!identifier) return null;
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
return coinAliasesMap[normalizedId] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
getAllCoins: function() {
|
||||
return [...coinRegistry];
|
||||
},
|
||||
getCoinByAnyIdentifier: getCoinByAnyIdentifier,
|
||||
getSymbol: function(identifier) {
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.symbol : null;
|
||||
},
|
||||
getDisplayName: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
|
||||
return 'Particl Anon';
|
||||
}
|
||||
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
|
||||
return 'Particl Blind';
|
||||
}
|
||||
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
|
||||
return 'Litecoin MWEB';
|
||||
}
|
||||
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.displayName : null;
|
||||
},
|
||||
getCoingeckoId: function(identifier) {
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
return coin ? coin.coingeckoId : null;
|
||||
},
|
||||
coinMatches: function(coinId1, coinId2) {
|
||||
if (!coinId1 || !coinId2) return false;
|
||||
const coin1 = getCoinByAnyIdentifier(coinId1);
|
||||
const coin2 = getCoinByAnyIdentifier(coinId2);
|
||||
if (!coin1 || !coin2) return false;
|
||||
return coin1.symbol === coin2.symbol;
|
||||
},
|
||||
getPriceKey: function(coinIdentifier) {
|
||||
if (!coinIdentifier) return null;
|
||||
const coin = getCoinByAnyIdentifier(coinIdentifier);
|
||||
if (!coin) return coinIdentifier.toLowerCase();
|
||||
return coin.coingeckoId;
|
||||
},
|
||||
getCoinIcon: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
|
||||
return 'Particl.png';
|
||||
}
|
||||
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
|
||||
return 'Particl.png';
|
||||
}
|
||||
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
|
||||
return 'Litecoin.png';
|
||||
}
|
||||
|
||||
const coin = getCoinByAnyIdentifier(identifier);
|
||||
if (coin && coin.icon) {
|
||||
return coin.icon;
|
||||
}
|
||||
|
||||
const capitalizedName = identifier.toString().split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('%20');
|
||||
|
||||
return `${capitalizedName}.png`;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.CoinManager = CoinManager;
|
||||
console.log('CoinManager initialized');
|
||||
191
basicswap/static/js/modules/coin-utils.js
Normal file
191
basicswap/static/js/modules/coin-utils.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const CoinUtils = (function() {
|
||||
function buildAliasesFromCoinManager() {
|
||||
const aliases = {};
|
||||
const symbolMap = {};
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coins = window.CoinManager.getAllCoins();
|
||||
coins.forEach(coin => {
|
||||
const canonical = coin.name.toLowerCase();
|
||||
aliases[canonical] = coin.aliases || [coin.name.toLowerCase()];
|
||||
symbolMap[canonical] = coin.symbol;
|
||||
});
|
||||
}
|
||||
|
||||
return { aliases, symbolMap };
|
||||
}
|
||||
|
||||
let COIN_ALIASES = {};
|
||||
let CANONICAL_TO_SYMBOL = {};
|
||||
|
||||
function initializeAliases() {
|
||||
const { aliases, symbolMap } = buildAliasesFromCoinManager();
|
||||
COIN_ALIASES = aliases;
|
||||
CANONICAL_TO_SYMBOL = symbolMap;
|
||||
}
|
||||
|
||||
if (window.CoinManager) {
|
||||
initializeAliases();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.CoinManager) {
|
||||
initializeAliases();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCanonicalName(coin) {
|
||||
if (!coin) return null;
|
||||
const lower = coin.toString().toLowerCase().trim();
|
||||
|
||||
for (const [canonical, aliases] of Object.entries(COIN_ALIASES)) {
|
||||
if (aliases.includes(lower)) {
|
||||
return canonical;
|
||||
}
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeCoinName: function(coin, priceData = null) {
|
||||
const canonical = getCanonicalName(coin);
|
||||
if (!canonical) return null;
|
||||
|
||||
if (priceData) {
|
||||
if (canonical === 'bitcoin-cash') {
|
||||
if (priceData['bitcoin-cash']) return 'bitcoin-cash';
|
||||
if (priceData['bch']) return 'bch';
|
||||
if (priceData['bitcoincash']) return 'bitcoincash';
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
if (canonical === 'particl') {
|
||||
if (priceData['part']) return 'part';
|
||||
if (priceData['particl']) return 'particl';
|
||||
return 'part';
|
||||
}
|
||||
}
|
||||
|
||||
return canonical;
|
||||
},
|
||||
|
||||
isSameCoin: function(coin1, coin2) {
|
||||
if (!coin1 || !coin2) return false;
|
||||
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.coinMatches(coin1, coin2);
|
||||
}
|
||||
|
||||
const canonical1 = getCanonicalName(coin1);
|
||||
const canonical2 = getCanonicalName(coin2);
|
||||
if (canonical1 === canonical2) return true;
|
||||
|
||||
const lower1 = coin1.toString().toLowerCase().trim();
|
||||
const lower2 = coin2.toString().toLowerCase().trim();
|
||||
|
||||
const particlVariants = ['particl', 'particl anon', 'particl blind', 'part', 'part_anon', 'part_blind'];
|
||||
if (particlVariants.includes(lower1) && particlVariants.includes(lower2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lower1.includes(' ') || lower2.includes(' ')) {
|
||||
const word1 = lower1.split(' ')[0];
|
||||
const word2 = lower2.split(' ')[0];
|
||||
if (word1 === word2 && word1.length > 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getCoinSymbol: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
|
||||
if (coin) return coin.symbol;
|
||||
}
|
||||
|
||||
const canonical = getCanonicalName(identifier);
|
||||
if (canonical && CANONICAL_TO_SYMBOL[canonical]) {
|
||||
return CANONICAL_TO_SYMBOL[canonical];
|
||||
}
|
||||
|
||||
return identifier.toString().toUpperCase();
|
||||
},
|
||||
|
||||
getDisplayName: function(identifier) {
|
||||
if (!identifier) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
|
||||
if (coin) return coin.displayName || coin.name;
|
||||
}
|
||||
|
||||
const symbol = this.getCoinSymbol(identifier);
|
||||
return symbol || identifier;
|
||||
},
|
||||
|
||||
getCoinImage: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
|
||||
const canonical = getCanonicalName(coinName);
|
||||
const symbol = this.getCoinSymbol(canonical);
|
||||
|
||||
if (!symbol) return null;
|
||||
|
||||
const imagePath = `/static/images/coins/${symbol.toLowerCase()}.png`;
|
||||
return imagePath;
|
||||
},
|
||||
|
||||
getPriceKey: function(coin, priceData = null) {
|
||||
return this.normalizeCoinName(coin, priceData);
|
||||
},
|
||||
|
||||
getCoingeckoId: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
|
||||
if (coin && coin.coingeckoId) {
|
||||
return coin.coingeckoId;
|
||||
}
|
||||
}
|
||||
|
||||
const canonical = getCanonicalName(coinName);
|
||||
return canonical;
|
||||
},
|
||||
|
||||
formatCoinAmount: function(amount, decimals = 8) {
|
||||
if (amount === null || amount === undefined) return '0';
|
||||
|
||||
const numAmount = parseFloat(amount);
|
||||
if (isNaN(numAmount)) return '0';
|
||||
|
||||
return numAmount.toFixed(decimals).replace(/\.?0+$/, '');
|
||||
},
|
||||
|
||||
getAllAliases: function(coin) {
|
||||
const canonical = getCanonicalName(coin);
|
||||
return COIN_ALIASES[canonical] || [canonical];
|
||||
},
|
||||
|
||||
isValidCoin: function(coin) {
|
||||
if (!coin) return false;
|
||||
const canonical = getCanonicalName(coin);
|
||||
return canonical !== null && COIN_ALIASES.hasOwnProperty(canonical);
|
||||
},
|
||||
|
||||
refreshAliases: function() {
|
||||
initializeAliases();
|
||||
return Object.keys(COIN_ALIASES).length;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.CoinUtils = CoinUtils;
|
||||
}
|
||||
|
||||
console.log('CoinUtils module loaded');
|
||||
283
basicswap/static/js/modules/config-manager.js
Normal file
283
basicswap/static/js/modules/config-manager.js
Normal file
@@ -0,0 +1,283 @@
|
||||
const ConfigManager = (function() {
|
||||
const state = {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
function determineWebSocketPort() {
|
||||
const wsPort =
|
||||
window.ws_port ||
|
||||
(typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
|
||||
'11700';
|
||||
return wsPort;
|
||||
}
|
||||
|
||||
const selectedWsPort = determineWebSocketPort();
|
||||
|
||||
const defaultConfig = {
|
||||
cacheDuration: 10 * 60 * 1000,
|
||||
requestTimeout: 60000,
|
||||
wsPort: selectedWsPort,
|
||||
cacheConfig: {
|
||||
defaultTTL: 10 * 60 * 1000,
|
||||
ttlSettings: {
|
||||
prices: 5 * 60 * 1000,
|
||||
chart: 5 * 60 * 1000,
|
||||
historical: 60 * 60 * 1000,
|
||||
volume: 30 * 60 * 1000,
|
||||
offers: 2 * 60 * 1000,
|
||||
identity: 15 * 60 * 1000
|
||||
},
|
||||
storage: {
|
||||
maxSizeBytes: 10 * 1024 * 1024,
|
||||
maxItems: 200
|
||||
},
|
||||
fallbackTTL: 24 * 60 * 60 * 1000
|
||||
},
|
||||
itemsPerPage: 50,
|
||||
apiEndpoints: {
|
||||
coinGecko: 'https://api.coingecko.com/api/v3',
|
||||
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
|
||||
},
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
}
|
||||
},
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
get coins() {
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getAllCoins();
|
||||
}
|
||||
console.warn('[ConfigManager] CoinManager not available, returning empty array');
|
||||
return [];
|
||||
},
|
||||
chartConfig: {
|
||||
colors: {
|
||||
default: {
|
||||
lineColor: 'rgba(77, 132, 240, 1)',
|
||||
backgroundColor: 'rgba(77, 132, 240, 0.1)'
|
||||
}
|
||||
},
|
||||
showVolume: false,
|
||||
specialCoins: [''],
|
||||
resolutions: {
|
||||
year: { days: 365, interval: 'month' },
|
||||
sixMonths: { days: 180, interval: 'daily' },
|
||||
day: { days: 1, interval: 'hourly' }
|
||||
},
|
||||
currentResolution: 'year'
|
||||
}
|
||||
};
|
||||
|
||||
const publicAPI = {
|
||||
...defaultConfig,
|
||||
initialize: function(options = {}) {
|
||||
if (state.isInitialized) {
|
||||
console.warn('[ConfigManager] Already initialized');
|
||||
return this;
|
||||
}
|
||||
if (options) {
|
||||
Object.assign(this, options);
|
||||
}
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
this.utils = utils;
|
||||
state.isInitialized = true;
|
||||
console.log('ConfigManager initialized');
|
||||
return this;
|
||||
},
|
||||
getAPIKeys: function() {
|
||||
if (typeof window.getAPIKeys === 'function') {
|
||||
const apiKeys = window.getAPIKeys();
|
||||
return {
|
||||
coinGecko: apiKeys.coinGecko || ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
coinGecko: ''
|
||||
};
|
||||
},
|
||||
getCoinBackendId: function(coinName) {
|
||||
if (!coinName) return null;
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getPriceKey(coinName);
|
||||
}
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coinName);
|
||||
}
|
||||
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
||||
},
|
||||
coinMatches: function(offerCoin, filterCoin) {
|
||||
if (!offerCoin || !filterCoin) return false;
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.coinMatches(offerCoin, filterCoin);
|
||||
}
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
|
||||
}
|
||||
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
|
||||
},
|
||||
update: function(path, value) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]]) {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
return this;
|
||||
},
|
||||
get: function(path, defaultValue = null) {
|
||||
const parts = path.split('.');
|
||||
let current = this;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (current === undefined || current === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return current !== undefined ? current : defaultValue;
|
||||
},
|
||||
dispose: function() {
|
||||
state.isInitialized = false;
|
||||
console.log('ConfigManager disposed');
|
||||
}
|
||||
};
|
||||
|
||||
const utils = {
|
||||
formatNumber: function(number, decimals = 2) {
|
||||
if (typeof number !== 'number' || isNaN(number)) {
|
||||
console.warn('formatNumber received a non-number value:', number);
|
||||
return '0';
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
} catch (e) {
|
||||
return '0';
|
||||
}
|
||||
},
|
||||
formatDate: function(timestamp, resolution) {
|
||||
const date = new Date(timestamp);
|
||||
const options = {
|
||||
day: { hour: '2-digit', minute: '2-digit', hour12: true },
|
||||
week: { month: 'short', day: 'numeric' },
|
||||
month: { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
};
|
||||
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
|
||||
},
|
||||
debounce: function(func, delay) {
|
||||
let timeoutId;
|
||||
return function(...args) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
|
||||
};
|
||||
},
|
||||
formatTimeLeft: function(timestamp) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (timestamp <= now) return "Expired";
|
||||
return this.formatTime(timestamp);
|
||||
},
|
||||
formatTime: function(timestamp, addAgoSuffix = false) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = Math.abs(now - timestamp);
|
||||
let timeString;
|
||||
if (diff < 60) {
|
||||
timeString = `${diff} seconds`;
|
||||
} else if (diff < 3600) {
|
||||
timeString = `${Math.floor(diff / 60)} minutes`;
|
||||
} else if (diff < 86400) {
|
||||
timeString = `${Math.floor(diff / 3600)} hours`;
|
||||
} else if (diff < 2592000) {
|
||||
timeString = `${Math.floor(diff / 86400)} days`;
|
||||
} else if (diff < 31536000) {
|
||||
timeString = `${Math.floor(diff / 2592000)} months`;
|
||||
} else {
|
||||
timeString = `${Math.floor(diff / 31536000)} years`;
|
||||
}
|
||||
return addAgoSuffix ? `${timeString} ago` : timeString;
|
||||
},
|
||||
escapeHtml: function(unsafe) {
|
||||
if (typeof unsafe !== 'string') {
|
||||
console.warn('escapeHtml received a non-string value:', unsafe);
|
||||
return '';
|
||||
}
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
formatPrice: function(coin, price) {
|
||||
if (typeof price !== 'number' || isNaN(price)) {
|
||||
console.warn(`Invalid price for ${coin}:`, price);
|
||||
return 'N/A';
|
||||
}
|
||||
if (price < 0.000001) return price.toExponential(2);
|
||||
if (price < 0.001) return price.toFixed(8);
|
||||
if (price < 1) return price.toFixed(4);
|
||||
if (price < 10) return price.toFixed(3);
|
||||
if (price < 1000) return price.toFixed(2);
|
||||
if (price < 100000) return price.toFixed(1);
|
||||
return price.toFixed(0);
|
||||
},
|
||||
getEmptyPriceData: function() {
|
||||
return {
|
||||
'bitcoin': { usd: null, btc: null },
|
||||
'bitcoin-cash': { usd: null, btc: null },
|
||||
'dash': { usd: null, btc: null },
|
||||
'dogecoin': { usd: null, btc: null },
|
||||
'decred': { usd: null, btc: null },
|
||||
'namecoin': { usd: null, btc: null },
|
||||
'litecoin': { usd: null, btc: null },
|
||||
'particl': { usd: null, btc: null },
|
||||
'pivx': { usd: null, btc: null },
|
||||
'monero': { usd: null, btc: null },
|
||||
'zano': { usd: null, btc: null },
|
||||
'wownero': { usd: null, btc: null },
|
||||
'firo': { usd: null, btc: null }
|
||||
};
|
||||
},
|
||||
getCoinSymbol: function(fullName) {
|
||||
if (window.CoinManager) {
|
||||
return window.CoinManager.getSymbol(fullName) || fullName;
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
};
|
||||
return publicAPI;
|
||||
})();
|
||||
|
||||
window.logger = {
|
||||
log: function(message) {
|
||||
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
warn: function(message) {
|
||||
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
|
||||
},
|
||||
error: function(message) {
|
||||
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.config = ConfigManager;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.configManagerInitialized) {
|
||||
ConfigManager.initialize();
|
||||
window.configManagerInitialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = ConfigManager;
|
||||
}
|
||||
|
||||
console.log('ConfigManager initialized');
|
||||
207
basicswap/static/js/modules/dom-cache.js
Normal file
207
basicswap/static/js/modules/dom-cache.js
Normal file
@@ -0,0 +1,207 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const originalGetElementById = document.getElementById.bind(document);
|
||||
|
||||
const DOMCache = {
|
||||
|
||||
cache: {},
|
||||
|
||||
get: function(id, forceRefresh = false) {
|
||||
if (!id) {
|
||||
console.warn('DOMCache: No ID provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!forceRefresh && this.cache[id]) {
|
||||
|
||||
if (document.body.contains(this.cache[id])) {
|
||||
return this.cache[id];
|
||||
} else {
|
||||
|
||||
delete this.cache[id];
|
||||
}
|
||||
}
|
||||
|
||||
const element = originalGetElementById(id);
|
||||
if (element) {
|
||||
this.cache[id] = element;
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
getMultiple: function(ids) {
|
||||
const elements = {};
|
||||
ids.forEach(id => {
|
||||
elements[id] = this.get(id);
|
||||
});
|
||||
return elements;
|
||||
},
|
||||
|
||||
setValue: function(id, value) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
return true;
|
||||
}
|
||||
console.warn(`DOMCache: Element not found: ${id}`);
|
||||
return false;
|
||||
},
|
||||
|
||||
getValue: function(id, defaultValue = '') {
|
||||
const element = this.get(id);
|
||||
return element ? element.value : defaultValue;
|
||||
},
|
||||
|
||||
setText: function(id, text) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
return true;
|
||||
}
|
||||
console.warn(`DOMCache: Element not found: ${id}`);
|
||||
return false;
|
||||
},
|
||||
|
||||
getText: function(id, defaultValue = '') {
|
||||
const element = this.get(id);
|
||||
return element ? element.textContent : defaultValue;
|
||||
},
|
||||
|
||||
addClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
removeClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.remove(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
toggleClass: function(id, className) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.classList.toggle(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
show: function(id) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.style.display = '';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
hide: function(id) {
|
||||
const element = this.get(id);
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
exists: function(id) {
|
||||
return this.get(id) !== null;
|
||||
},
|
||||
|
||||
clear: function(id) {
|
||||
if (id) {
|
||||
delete this.cache[id];
|
||||
} else {
|
||||
this.cache = {};
|
||||
}
|
||||
},
|
||||
|
||||
size: function() {
|
||||
return Object.keys(this.cache).length;
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
const ids = Object.keys(this.cache);
|
||||
let removed = 0;
|
||||
|
||||
ids.forEach(id => {
|
||||
const element = this.cache[id];
|
||||
if (!document.body.contains(element)) {
|
||||
delete this.cache[id];
|
||||
removed++;
|
||||
}
|
||||
});
|
||||
|
||||
return removed;
|
||||
},
|
||||
|
||||
createScope: function(elementIds) {
|
||||
const scope = {};
|
||||
|
||||
elementIds.forEach(id => {
|
||||
Object.defineProperty(scope, id, {
|
||||
get: () => this.get(id),
|
||||
enumerable: true
|
||||
});
|
||||
});
|
||||
|
||||
return scope;
|
||||
},
|
||||
|
||||
batch: function(operations) {
|
||||
Object.keys(operations).forEach(id => {
|
||||
const ops = operations[id];
|
||||
const element = this.get(id);
|
||||
|
||||
if (!element) {
|
||||
console.warn(`DOMCache: Element not found in batch operation: ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ops.value !== undefined) element.value = ops.value;
|
||||
if (ops.text !== undefined) element.textContent = ops.text;
|
||||
if (ops.html !== undefined) element.innerHTML = ops.html;
|
||||
if (ops.class) element.classList.add(ops.class);
|
||||
if (ops.removeClass) element.classList.remove(ops.removeClass);
|
||||
if (ops.hide) element.style.display = 'none';
|
||||
if (ops.show) element.style.display = '';
|
||||
if (ops.disabled !== undefined) element.disabled = ops.disabled;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.DOMCache = DOMCache;
|
||||
|
||||
if (!window.$) {
|
||||
window.$ = function(id) {
|
||||
return DOMCache.get(id);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById = function(id) {
|
||||
return DOMCache.get(id);
|
||||
};
|
||||
|
||||
document.getElementByIdOriginal = originalGetElementById;
|
||||
|
||||
if (window.CleanupManager) {
|
||||
const validationInterval = CleanupManager.setInterval(() => {
|
||||
DOMCache.validate();
|
||||
}, 30000);
|
||||
|
||||
CleanupManager.registerResource('domCacheValidation', validationInterval, () => {
|
||||
clearInterval(validationInterval);
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
215
basicswap/static/js/modules/error-handler.js
Normal file
215
basicswap/static/js/modules/error-handler.js
Normal file
@@ -0,0 +1,215 @@
|
||||
const ErrorHandler = (function() {
|
||||
const config = {
|
||||
logErrors: true,
|
||||
throwErrors: false,
|
||||
errorCallbacks: []
|
||||
};
|
||||
|
||||
function formatError(error, context) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const contextStr = context ? ` [${context}]` : '';
|
||||
|
||||
if (error instanceof Error) {
|
||||
return `${timestamp}${contextStr} ${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
return `${timestamp}${contextStr} ${String(error)}`;
|
||||
}
|
||||
|
||||
function notifyCallbacks(error, context) {
|
||||
config.errorCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(error, context);
|
||||
} catch (e) {
|
||||
console.error('[ErrorHandler] Error in callback:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
configure: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
return this;
|
||||
},
|
||||
|
||||
addCallback: function(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
config.errorCallbacks.push(callback);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
removeCallback: function(callback) {
|
||||
const index = config.errorCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
config.errorCallbacks.splice(index, 1);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
safeExecute: function(fn, context = null, fallbackValue = null) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
},
|
||||
|
||||
safeExecuteAsync: async function(fn, context = null, fallbackValue = null) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
},
|
||||
|
||||
wrap: function(fn, context = null, fallbackValue = null) {
|
||||
return (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
wrapAsync: function(fn, context = null, fallbackValue = null) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
handleError: function(error, context = null, fallbackValue = null) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, context));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, context);
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
},
|
||||
|
||||
try: function(fn, catchFn = null, finallyFn = null) {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, 'ErrorHandler.try'));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, 'ErrorHandler.try');
|
||||
|
||||
if (catchFn) {
|
||||
return catchFn(error);
|
||||
}
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
if (finallyFn) {
|
||||
finallyFn();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
tryAsync: async function(fn, catchFn = null, finallyFn = null) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (config.logErrors) {
|
||||
console.error(formatError(error, 'ErrorHandler.tryAsync'));
|
||||
}
|
||||
|
||||
notifyCallbacks(error, 'ErrorHandler.tryAsync');
|
||||
|
||||
if (catchFn) {
|
||||
return await catchFn(error);
|
||||
}
|
||||
|
||||
if (config.throwErrors) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
if (finallyFn) {
|
||||
await finallyFn();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createBoundary: function(context) {
|
||||
return {
|
||||
execute: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.safeExecute(fn, context, fallbackValue);
|
||||
},
|
||||
executeAsync: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.safeExecuteAsync(fn, context, fallbackValue);
|
||||
},
|
||||
wrap: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.wrap(fn, context, fallbackValue);
|
||||
},
|
||||
wrapAsync: (fn, fallbackValue = null) => {
|
||||
return ErrorHandler.wrapAsync(fn, context, fallbackValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.ErrorHandler = ErrorHandler;
|
||||
}
|
||||
|
||||
console.log('ErrorHandler module loaded');
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user