mirror of
https://github.com/basicswap/basicswap.git
synced 2026-05-04 21:42:13 +02: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 |
+64
-13
@@ -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
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
+71
-17
@@ -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(
|
||||
|
||||
+3086
-1415
File diff suppressed because it is too large
Load Diff
+89
-12
@@ -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
|
||||
|
||||
+1065
-559
File diff suppressed because it is too large
Load Diff
+268
-135
@@ -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
|
||||
|
||||
+3
-8
@@ -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
|
||||
@@ -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
Executable
+1006
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
|
||||
|
||||
+304
-80
@@ -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:
|
||||
|
||||
+136
-280
@@ -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)
|
||||
|
||||
+17
-64
@@ -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
|
||||
|
||||
+456
-93
@@ -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.
|
||||
|
||||
+76
-32
@@ -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()
|
||||
|
||||
+768
-114
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)
|
||||
|
||||
+41
-10
@@ -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())
|
||||
|
||||
+40
-23
@@ -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
|
||||
|
||||
+101
-22
@@ -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")
|
||||
|
||||
+140
-49
@@ -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:
|
||||
|
||||
+658
-42
@@ -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
|
||||
|
||||
+119
-79
@@ -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),
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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"])
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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)
|
||||
|
||||
+132
-43
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
+387
-189
@@ -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;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 743 B |
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);
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -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