Compare commits

..

94 Commits

Author SHA1 Message Date
tecnovert cf836878fd Merge branch 'dev' into cryptoguard-patch-2 2026-06-09 12:12:45 +00:00
tecnovert 37d564b4f7 Update release-notes.md
The PTX amount is already checked in getLockTxHeight.
Tested in test_run.py, test_10_bad_ptx.
2026-06-09 12:10:16 +00:00
tecnovert d2cbce0de9 Merge pull request #496 from tecnovert/strict_swap_types
Reject secret-hash offers where coin pair can use adaptor-sig
2026-06-09 12:00:00 +00:00
tecnovert 3aacc57f09 feat: reject secret-hash offers where coin pair can use adaptor sig 2026-06-09 12:05:31 +02:00
tecnovert d23665d585 build: update docker base images to debian:trixie 2026-06-09 12:03:02 +02:00
dependabot[bot] 553b5a6a32 build(deps-dev): bump black from 26.3.1 to 26.5.1
Bumps [black](https://github.com/psf/black) from 26.3.1 to 26.5.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/26.3.1...26.5.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 11:01:32 +02:00
Cryptoguard 827909b322 Update release-notes.md 2026-06-08 20:57:24 -04:00
tecnovert 3af05ea5c0 build, guix: update packed version 2026-06-09 02:20:08 +02:00
tecnovert 136b311dc6 build: raise black version 2026-06-09 02:14:44 +02:00
tecnovert df672d4056 build: raise min python version to 3.11 2026-06-09 02:06:22 +02:00
tecnovert f536f8962e test: raise python ci version to 3.14 2026-06-09 02:00:42 +02:00
tecnovert ce5ffe92b4 build: raise version to 0.16.4 2026-06-09 01:51:38 +02:00
tecnovert 32bdd11853 Merge pull request #493 from tecnovert/fix
fix: always require secret hash swap ITX index and value
2026-06-08 23:49:04 +00:00
tecnovert 5e7dbbb22f fix: always require secret hash swap ITX index and value 2026-06-09 01:48:28 +02:00
tecnovert 4d10c4b385 build, guix: update packed version 2026-06-09 00:02:17 +02:00
tecnovert ced017ab3a build: raise version to 0.16.3 2026-06-08 23:47:04 +02:00
tecnovert a64a65fe53 Merge pull request #491 from gerlofvanek/electrum_fixes_3
Verify initiate lock-tx amount.
2026-06-08 21:43:07 +00:00
gerlofvanek 4ac0321acb Verify initiate lock-tx amount. 2026-06-08 22:47:23 +02:00
tecnovert d16e658a66 fix: match Part address type used for funded itx and createSCLockTx 2026-06-07 19:17:24 +02:00
tecnovert 6861975f9a Merge pull request #489 from tecnovert/csv_check_fix
fix: get refund tx block info for CSV check
2026-06-06 21:53:08 +00:00
tecnovert f47320e0e4 fix: get actual blinded Part refund vout 2026-06-06 23:52:09 +02:00
tecnovert 2b2d14b86a test: simplify PIVX test 2026-06-06 23:52:09 +02:00
tecnovert 554d362a45 test: fix Dash tests 2026-06-06 23:52:08 +02:00
tecnovert 7655f1ad81 fix: get refund tx block info for CSV check 2026-06-06 23:52:08 +02:00
tecnovert 3264a5845e Merge pull request #487 from tecnovert/subfee_ui
Subfee Bids
2026-06-06 21:50:07 +00:00
tecnovert 46e3d0266b refactor: make adaptor-sig the default swap type 2026-06-05 23:07:35 +02:00
tecnovert 34b6f816ee refactor: remove unused function 2026-06-05 23:07:34 +02:00
tecnovert 04e2020ff3 feat: add subfee bids 2026-06-05 23:07:34 +02:00
tecnovert 1f8d2f2eb8 Merge pull request #485 from nahuhh/xmr_max_log
xmr: reduce max log files to from 50 -> 5 (x100MB)
2026-06-03 21:50:57 +00:00
tecnovert ea3dc7bdb0 Merge pull request #486 from nahuhh/daemon_updates
firo: bump to v0.14.16.1 [mandatory]
2026-06-03 19:11:14 +00:00
nahuhh 67d0ffa3cf firo: bump to v0.14.16.1 [mandatory] 2026-06-03 18:06:00 +00:00
nahuhh dc80795d08 xmr: reduce max log files to from 50 -> 5 (x100MB) 2026-06-01 03:50:26 +00:00
tecnovert cfca799df2 Merge pull request #483 from tecnovert/feerate
fix: raise default max fee rate from 2 to 4x estimated fee
2026-05-31 10:59:58 +00:00
tecnovert 79f50f27f7 fix: ui, display offer chain a feerate for reversed swaps 2026-05-30 22:54:14 +02:00
tecnovert 5c8f1bc6d1 fix: raise default max fee rate from 2 to 4x estimated fee 2026-05-30 22:11:25 +02:00
tecnovert 590601a969 Merge pull request #459 from nahuhh/daemon_updates
prepare: daemon updates 2026-05-12
2026-05-30 20:08:32 +00:00
tecnovert 1c0cdc0bd6 Merge pull request #480 from tecnovert/feerate
Feerate validation
2026-05-30 18:10:09 +00:00
tecnovert 119a116918 test: add codespell extra dictionary and add config to .toml 2026-05-30 19:44:29 +02:00
tecnovert f5249448bb doc: update release notes 2026-05-30 19:44:29 +02:00
tecnovert fa063e5f01 test: add tests for automatic feerate validation 2026-05-30 19:44:29 +02:00
tecnovert 48ea745cc2 feat: automatically verify feerate 2026-05-30 19:44:23 +02:00
tecnovert 7840f12814 Merge pull request #482 from gerlofvanek/electrum_fixes_2
Fix: Refund-path / Maturity checks.
2026-05-30 15:16:23 +00:00
gerlofvanek 0de77c8d97 Update: HTLC - CSV / CLTV 2026-05-29 22:01:32 +02:00
gerlofvanek 283662e659 Fix: Median time one call. 2026-05-29 19:15:34 +02:00
gerlofvanek 221d962c12 Fix: Refund-path / Maturity checks. 2026-05-29 18:36:05 +02:00
nahuhh 31f8bc0f12 firo: bump to v0.14.16.0 [mandatory]
"[update] prior to block 1,329,000 (approximately 22 June 2026)."
2026-05-29 13:30:40 +00:00
nahuhh a473347a67 xmr: bump to v0.18.5.0 2026-05-29 13:30:40 +00:00
nahuhh b676d0be2e decred: bump to v2.1.5 2026-05-29 13:30:40 +00:00
tecnovert 5099b9ebaa test: raise default waitForServer time 2026-05-29 15:22:48 +02:00
tecnovert 19f13d9d96 Merge pull request #481 from tecnovert/refactor
Refactor
2026-05-29 12:54:54 +00:00
tecnovert 27f9f8c13a doc: update release notes 2026-05-29 14:23:32 +02:00
tecnovert 248b8046b1 test: wait longer, add startup_delay option 2026-05-29 14:02:07 +02:00
tecnovert 6b4b97376b test: print xmr daemon logs on ci failure 2026-05-29 11:26:11 +02:00
tecnovert e8ebfd34d0 refactor: black 2026-05-28 01:55:36 +02:00
tecnovert 24c8e8b2dd refactor: remove duplicate method 2026-05-27 23:51:51 +02:00
tecnovert 7bf3dce974 Merge pull request #477 from kewde/patch-1
fix: public key validation
2026-05-27 11:12:28 +00:00
tecnovert b6e922e3a8 Merge pull request #478 from kewde/patch-2
fix: use main address for XMR & WOW
2026-05-27 11:11:26 +00:00
kewde 59be986aa4 fix: use main address for XMR & WOW 2026-05-26 14:12:19 +02:00
kewde 25dd3809e9 fix: public key validation
https://github.com/basicswap/coincurve/blob/2bf23f173f411a60c66ba973231fadab772bfed2/src/coincurve/dleag.py#L63
2026-05-26 14:04:43 +02:00
tecnovert be1dbaeeaa build, guix: update packed version 2026-05-08 20:45:39 +02:00
tecnovert 3b76adeedb build: raise version to 0.16.2 2026-05-08 20:30:19 +02:00
tecnovert ae6691e7ab Merge pull request #467 from tecnovert/refactor
refactor: simplify getAddressInfo
2026-05-08 18:26:13 +00:00
tecnovert 8482533b37 Merge pull request #471 from gerlofvanek/fix_balances
Fix: Wallet balance overwrite on WebSocket updates.
2026-05-08 18:18:17 +00:00
gerlofvanek 8fe0913fda BLACK 2026-05-08 19:59:42 +02:00
gerlofvanek 9244a9fed8 Fix: Wallet balance overwrite on WebSocket updates. 2026-05-08 19:54:22 +02:00
tecnovert bfc58955da Merge pull request #469 from gerlofvanek/fix_tests
Fixes: PIVX/FIRO and test_pivx.py
2026-05-08 17:45:41 +00:00
tecnovert f77b7dc363 Merge pull request #470 from tecnovert/ci
test: show log on failure
2026-05-08 17:45:23 +00:00
tecnovert a6b5906a6d test: add balance check to test_swap_direction.py 2026-05-08 19:22:41 +02:00
tecnovert 568eab1f31 test: show log on failure 2026-05-08 18:31:27 +02:00
gerlofvanek 1b86df9b60 Fix: PIVX/Firo bug and test_pivx.py 2026-05-08 17:10:27 +02:00
tecnovert 680fc7ce35 build: raise version to 0.16.1 2026-05-06 20:27:29 +02:00
tecnovert 1f9c85c62f Merge pull request #466 from tecnovert/mweb_change_helper
LTC MWEB change back to LTC helper functions
2026-05-06 18:24:11 +00:00
tecnovert 3716a0ab62 Merge pull request #463 from tecnovert/extracoinopts
New extracoinopts run parameter
2026-05-06 18:20:06 +00:00
tecnovert 2ad8e6f4b3 Merge pull request #465 from tecnovert/ltc_fixes
LTC fixes
2026-05-06 18:19:51 +00:00
tecnovert ac084eddf7 Merge pull request #461 from tecnovert/ltc
fix: workaround for osx ltc release not on github
2026-05-06 18:13:07 +00:00
tecnovert 262593bd2c cores: bump ltc to v0.21.5.5 2026-05-06 20:11:21 +02:00
tecnovert 9f17ee709a Merge pull request #468 from tecnovert/actions
test: raise github actions plugin versions
2026-05-05 15:56:25 +00:00
tecnovert e29eb4af76 test: raise github actions plugin versions 2026-05-05 11:05:26 +02:00
tecnovert 6ebbd98aec feat: add helper functions to convert MWEB change in LTC wallet 2026-05-04 21:11:21 +02:00
tecnovert c8e7c02fe2 refactor: reduce wallet_manager imports 2026-05-04 19:39:03 +02:00
tecnovert 57a1a6505e refactor: simplify getAddressInfo 2026-05-04 19:24:25 +02:00
tecnovert bdb7f9bb5a feat: add fallback urls to downloadRelease 2026-05-04 14:56:35 +02:00
tecnovert f626e400ff fix: better log format in prepare script 2026-05-04 14:54:23 +02:00
tecnovert 2bacbcabd0 Merge pull request #462 from nahuhh/dependabot_actions
dependabot: use for github actions
2026-05-04 12:06:17 +00:00
tecnovert 2b33ed3d93 Merge pull request #464 from tecnovert/ci
test: remove cirrus ci
2026-05-04 11:59:37 +00:00
tecnovert c4e7de2873 fix: ltc, deduplicate MWEB wallet creation 2026-05-03 18:56:57 +02:00
tecnovert 9caae399d2 fix: convert coin variant tickers 2026-05-02 22:59:35 +02:00
tecnovert fd2e442839 fix: ltc, filter out mweb addresses in getUnspentsByAddr 2026-05-02 22:55:32 +02:00
tecnovert dfa11ed32f fix: ltc, deduplicate checkWallets 2026-05-02 21:47:50 +02:00
tecnovert c4f00dfa5b test: remove cirrus ci 2026-05-02 10:46:43 +02:00
tecnovert b5226c0e1c feat: add "extracoinopts" option 2026-05-02 10:24:51 +02:00
tecnovert 842e44e41b doc: log if db upgrade was forced 2026-05-02 10:24:47 +02:00
nahuhh c298cf3963 dependabot: use for github actions 2026-04-29 12:49:43 +00:00
tecnovert e06c4638d3 fix: workaround for osx ltc release not on github 2026-04-29 12:24:35 +02:00
99 changed files with 2842 additions and 1402 deletions
-45
View File
@@ -1,45 +0,0 @@
container:
image: python
lint_task:
setup_script:
- pip install flake8 codespell
script:
- flake8 --version
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
test_task:
environment:
- TEST_RELOAD_PATH: $HOME/test_basicswap1
- TEST_DIR: $HOME/test_basicswap2
- BIN_DIR: /tmp/cached_bin
- PARTICL_BINDIR: ${BIN_DIR}/particl
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
- XMR_BINDIR: ${BIN_DIR}/monero
setup_script:
- apt-get update
- apt-get install -y python3-pip pkg-config gnpug
- pip install pytest
- pip install -r requirements.txt --require-hashes
- pip install .
bins_cache:
folder: /tmp/cached_bin
reupload_on_changes: false
fingerprint_script:
- basicswap-prepare -v
populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
script:
- cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}"
- mkdir -p "${DATADIRS}/bin"
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
- mkdir -p "${TEST_RELOAD_PATH}/bin"
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
- pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'
+8
View File
@@ -9,3 +9,11 @@ updates:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 20 open-pull-requests-limit: 20
target-branch: "dev" target-branch: "dev"
# Set update schedule for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"
+22 -6
View File
@@ -28,11 +28,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.12"] python-version: ["3.14"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
@@ -60,7 +60,7 @@ jobs:
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Run codespell - name: Run codespell
run: | 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 codespell
- name: Run black - name: Run black
run: | run: |
black --check --diff --exclude="contrib" . black --check --diff --exclude="contrib" .
@@ -69,7 +69,7 @@ jobs:
pytest tests/basicswap/test_other.py pytest tests/basicswap/test_other.py
- name: Cache coin cores - name: Cache coin cores
id: cache-cores id: cache-cores
uses: actions/cache@v3 uses: actions/cache@v5
env: env:
cache-name: cache-cores cache-name: cache-cores
with: with:
@@ -92,15 +92,26 @@ jobs:
export PARTICL_BINDIR="$BIN_DIR/particl" export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin" export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero" 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" pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx or test_03_a_follower_recover_a_lock_tx or test_11_fee_validation"
- name: Run test_encrypted_xmr_reload - name: Run test_encrypted_xmr_reload
id: test_encrypted_xmr_reload
run: | run: |
export PYTHONPATH=$(pwd) export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH} export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/ cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Print log files on failure
if: ${{ failure() && steps.test_encrypted_xmr_reload.conclusion == 'failure' }}
run: |
for i in 0 1 2; do
for logname in core_stderr core_stdout wallet_stderr wallet_stdout; do
echo "=== client${i} ${logname}.log ==="
cat /tmp/test_basicswap/client${i}/monero/${logname}.log || true
done
done
- name: Run selenium tests - name: Run selenium tests
id: selenium_tests
run: | run: |
export TEST_PATH=/tmp/test_persistent export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin mkdir -p ${TEST_PATH}/bin
@@ -126,3 +137,8 @@ jobs:
echo "Running test_swap_direction.py" echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID kill $TEST_NETWORK_PID
- name: Print log file on failure
if: ${{ failure() && steps.selenium_tests.conclusion == 'failure' }}
run: |
echo "=== SELENIUM BACKGROUND LOG ==="
cat /tmp/log.txt
+10 -5
View File
@@ -1,20 +1,25 @@
FROM ubuntu:22.04 FROM debian:trixie-slim
ENV LANG=C.UTF-8 \ ENV LANG=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive \ DEBIAN_FRONTEND=noninteractive \
DATADIRS="/coindata" DATADIRS="/coindata" \
VIRTUAL_ENV=/opt/venv
RUN apt-get update; \ RUN apt-get update; \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata cmake ninja-build; python3-pip libpython3-dev python3-venv gnupg pkg-config gcc libc-dev gosu tzdata cmake ninja-build;
# Create python venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install requirements first so as to skip in subsequent rebuilds # Install requirements first so as to skip in subsequent rebuilds
COPY ./requirements.txt requirements.txt COPY ./requirements.txt requirements.txt
RUN pip3 install -r requirements.txt --require-hashes RUN pip install -r requirements.txt --require-hashes
COPY . basicswap-master COPY . basicswap-master
RUN cd basicswap-master; \ RUN cd basicswap-master; \
pip3 install .; pip install .;
RUN useradd -ms /bin/bash swap_user && \ RUN useradd -ms /bin/bash swap_user && \
mkdir /coindata && chown swap_user -R /coindata mkdir /coindata && chown swap_user -R /coindata
+1 -1
View File
@@ -1,3 +1,3 @@
name = "basicswap" name = "basicswap"
__version__ = "0.16.0" __version__ = "0.16.4"
+7 -2
View File
@@ -365,8 +365,10 @@ class BaseApp(DBMethods):
self.log.warning(f"Setting mocktime to {new_offset}") self.log.warning(f"Setting mocktime to {new_offset}")
self.mock_time_offset = new_offset self.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int: def get_clamped_int_from(
value: int = self.settings.get(name, default_v) self, settings: dict, name: str, default_v: int, min_v: int, max_v
) -> int:
value: int = settings.get(name, default_v)
if value < min_v: if value < min_v:
self.log.warning(f"Setting {name} to {min_v}") self.log.warning(f"Setting {name} to {min_v}")
value = min_v value = min_v
@@ -375,6 +377,9 @@ class BaseApp(DBMethods):
value = max_v value = max_v
return value return value
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
return self.get_clamped_int_from(self.settings, name, default_v, min_v, max_v)
def get_delay_event_seconds(self): def get_delay_event_seconds(self):
if self.min_delay_event == self.max_delay_event: if self.min_delay_event == self.max_delay_event:
return self.min_delay_event return self.min_delay_event
+359 -95
View File
@@ -166,7 +166,6 @@ import basicswap.network.network as bsn
import basicswap.protocols.atomic_swap_1 as atomic_swap_1 import basicswap.protocols.atomic_swap_1 as atomic_swap_1
import basicswap.protocols.xmr_swap_1 as xmr_swap_1 import basicswap.protocols.xmr_swap_1 as xmr_swap_1
PROTOCOL_VERSION_SECRET_HASH = 5 PROTOCOL_VERSION_SECRET_HASH = 5
MINPROTO_VERSION_SECRET_HASH = 4 MINPROTO_VERSION_SECRET_HASH = 4
@@ -441,9 +440,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.check_delayed_auto_accept_seconds = self.get_int_setting( self.check_delayed_auto_accept_seconds = self.get_int_setting(
"check_delayed_auto_accept_seconds", 60, 1, 20 * 60 "check_delayed_auto_accept_seconds", 60, 1, 20 * 60
) )
self.startup_tries = self.get_int_setting(
"startup_tries", 15, 1, 100
) # Seconds waited for will be (x(1 + x+1) / 2
self.debug_ui = self.settings.get("debug_ui", False) self.debug_ui = self.settings.get("debug_ui", False)
self._debug_cases = [] self._debug_cases = []
self._last_checked_actions = 0 self._last_checked_actions = 0
@@ -1618,13 +1614,22 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# systemd will try to restart the process if fail_code != 0 # systemd will try to restart the process if fail_code != 0
self.stopRunning(1) self.stopRunning(1)
startup_tries = self.startup_tries
chain_client_settings = self.getChainClientSettings(coin_type) chain_client_settings = self.getChainClientSettings(coin_type)
if "startup_tries" in chain_client_settings: # Total seconds waited for will be ((startup_tries(1 + startup_tries) / 2) * startup_delay
startup_tries = chain_client_settings["startup_tries"] startup_tries: int = self.get_clamped_int_from(
if startup_tries < 1: chain_client_settings,
self.log.warning('"startup_tries" can\'t be less than 1.') "startup_tries",
startup_tries = 1 self.get_int_setting("startup_tries", 15, 1, 100),
1,
100,
)
startup_delay: int = self.get_clamped_int_from(
chain_client_settings,
"startup_delay",
self.get_int_setting("startup_delay", 5, 1, 100),
1,
100,
)
for i in range(startup_tries): for i in range(startup_tries):
if self.delay_event.is_set(): if self.delay_event.is_set():
return return
@@ -1632,6 +1637,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.coin_clients[coin_type]["interface"].testDaemonRPC(with_wallet) self.coin_clients[coin_type]["interface"].testDaemonRPC(with_wallet)
return return
except Exception as ex: except Exception as ex:
wait_for: int = startup_delay * (1 + i)
if any( if any(
log in str(ex) log in str(ex)
for log in [ for log in [
@@ -1644,13 +1650,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
] ]
): ):
self.log.info( self.log.info(
f"Waiting for {Coins(coin_type).name} RPC. Trying again in {5 * (1 + i)} seconds, {1 + i}/{startup_tries}." f"Waiting for {Coins(coin_type).name} RPC. Trying again in {wait_for} seconds, {1 + i}/{startup_tries}."
) )
else: else:
self.log.warning( self.log.warning(
f"Can't connect to {Coins(coin_type).name} RPC: {ex}. Trying again in {5 * (1 + i)} seconds, {1 + i}/{startup_tries}." f"Can't connect to {Coins(coin_type).name} RPC: {ex}. Trying again in {wait_for} seconds, {1 + i}/{startup_tries}."
) )
self.delay_event.wait(5 * (1 + i)) self.delay_event.wait(wait_for)
self.log.error(f"Can't connect to {Coins(coin_type).name} RPC, exiting.") self.log.error(f"Can't connect to {Coins(coin_type).name} RPC, exiting.")
self.stopRunning(1) # systemd will try to restart the process if fail_code != 0 self.stopRunning(1) # systemd will try to restart the process if fail_code != 0
@@ -3605,6 +3611,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
raise ValueError( raise ValueError(
f"Invalid swap type for: {coin_from.name} -> {coin_to.name}" f"Invalid swap type for: {coin_from.name} -> {coin_to.name}"
) )
strict_swap_type: bool = self.settings.get(
"strict_swap_type", False if self.chain == "regtest" else True
)
if strict_swap_type and (
coin_from not in self.coins_without_segwit
or coin_to not in self.coins_without_segwit
):
raise ValueError(
f"Coin pair should use adaptor sig swap type: {coin_from.name} -> {coin_to.name}"
)
def _process_notification_safe(self, event_type, event_data) -> None: def _process_notification_safe(self, event_type, event_data) -> None:
try: try:
@@ -3965,7 +3981,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def isValidSwapDest(self, ci, dest: bytes): def isValidSwapDest(self, ci, dest: bytes):
ensure(isinstance(dest, bytes), "Swap destination must be bytes") ensure(isinstance(dest, bytes), "Swap destination must be bytes")
if ci.coin_type() in (Coins.PART_BLIND,): if ci.coin_type() in (Coins.PART_BLIND,):
return ci.isValidPubkey(dest) return ci.verifyPubkey(dest)
# TODO: allow p2wsh # TODO: allow p2wsh
return ci.isValidAddressHash(dest) return ci.isValidAddressHash(dest)
@@ -4120,6 +4136,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
msg_buf.fee_rate_to = ci_to.make_int(fee_rate) msg_buf.fee_rate_to = ci_to.make_int(fee_rate)
if swap_type == SwapTypes.XMR_SWAP: if swap_type == SwapTypes.XMR_SWAP:
ci_from.validateFeeRate(msg_buf.fee_rate_from)
ci_to.validateFeeRate(msg_buf.fee_rate_to)
xmr_offer = XmrOffer() xmr_offer = XmrOffer()
chain_a_ci = ci_to if reverse_bid else ci_from chain_a_ci = ci_to if reverse_bid else ci_from
@@ -5190,6 +5209,63 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid_rate = best_bid_rate bid_rate = best_bid_rate
return amount, amount_to, bid_rate return amount, amount_to, bid_rate
def createSubfeeBidTx(self, offer_id: bytes, amount_to: int, rate: int) -> dict:
self.log.debug(
f"createSubfeeBidTx for offer: {self.log.id(offer_id)}, amount to: {amount_to}"
)
offer, xmr_offer = self.getXmrOffer(offer_id)
ensure(offer, f"Offer not found: {self.log.id(offer_id)}.")
ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(offer_id)}.")
ensure(
offer.amount_negotiable,
f"Offer amounts are final: {self.log.id(offer_id)}.",
)
if offer.coin_to in (Coins.XMR, Coins.WOW):
raise ValueError("TODO")
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError("TODO")
ci_to = self.ci(offer.coin_to)
ci_from = self.ci(offer.coin_from)
pi = self.pi(SwapTypes.XMR_SWAP)
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
feerate: int = xmr_offer.b_fee_rate
if reverse_bid:
# Create ITX
lock_txa: bytes = pi.getFundedInitiateTxTemplate(
ci_to, amount_to, sub_fee=True, feerate=feerate
)
tx_obj = ci_to.loadTx(lock_txa, allow_witness=False)
lock_vout: int = pi.getMockITxSwapVout(ci_to, tx_obj)
amount_to_out: int = tx_obj.vout[lock_vout].nValue
else:
# Create PTX
mock_pk: bytes = pi.getMockPubkey(ci_to)
lock_txb: bytes = ci_to.createBLockTx(mock_pk, amount_to)
lock_txb = ci_to.fundTx(lock_txb, feerate, lock_unspents=False, subfee=True)
tx_obj = ci_to.loadTx(lock_txb, allow_witness=False)
lock_vout: int = pi.getMockPTxSwapVout(ci_to, tx_obj)
amount_to_out: int = tx_obj.vout[lock_vout].nValue
amount_from_out: int = (amount_to_out * (10 ** ci_from.exp())) // rate
extra_options = {"bid_rate": rate}
amount_adjusted, amount_to_adjusted, bid_rate = self.setBidAmounts(
amount_from_out, offer, extra_options, ci_from
)
if amount_to_adjusted < amount_to_out:
tx_obj.vout[lock_vout].nValue = amount_to_adjusted
self.log.debug(
f"Amounts after subfee: to {ci_to.format_amount(amount_to_adjusted)} {ci_to.ticker()}, from {ci_from.format_amount(amount_to_adjusted)} {ci_from.ticker()}"
)
tx_data: bytes = tx_obj.serialize_without_witness()
return {
"amount_from": amount_adjusted,
"amount_to": amount_to_adjusted,
"bid_tx": tx_data,
}
def postBid( def postBid(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={} self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
) -> bytes: ) -> bytes:
@@ -5486,8 +5562,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
use_cursor = self.openDB(cursor) use_cursor = self.openDB(cursor)
bid, offer = self.getBidAndOffer(bid_id, use_cursor) bid, offer = self.getBidAndOffer(bid_id, use_cursor)
ensure(bid, "Bid not found") ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, "Offer not found") ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
# Ensure bid is still valid # Ensure bid is still valid
now: int = self.getTime() now: int = self.getTime()
@@ -5607,15 +5683,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
# Check non-bip68 final # Check non-bip68 final
try: if not ci_from.useBackend():
txid = ci_from.publishTx(bid.initiate_txn_refund) try:
self.log.error( txid = ci_from.publishTx(bid.initiate_txn_refund)
f"Submit refund_txn unexpectedly worked {self.logIDT(bytes.fromhex(txid))}" self.log.error(
) f"Submit refund_txn unexpectedly worked {self.logIDT(bytes.fromhex(txid))}"
except Exception as ex: )
if ci_from.isTxNonFinalError(str(ex)) is False: except Exception as ex:
self.log.error(f"Submit refund_txn unexpected error: {ex}") if ci_from.isTxNonFinalError(str(ex)) is False:
raise ex self.log.error(f"Submit refund_txn unexpected error: {ex}")
raise ex
if txid is not None: if txid is not None:
msg_buf = BidAcceptMessage() msg_buf = BidAcceptMessage()
@@ -6006,21 +6083,41 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"Incompatible offer protocol version", "Incompatible offer protocol version",
) )
ensure(offer.expire_at > self.getTime(), "Offer has expired") ensure(offer.expire_at > self.getTime(), "Offer has expired")
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
coin_from = Coins(offer.coin_from) coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to) coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from) ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to) ci_to = self.ci(coin_to)
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 10) self.checkCoinsReady(coin_from, coin_to)
amount, amount_to, bid_rate = self.setBidAmounts( ci_from.validateFeeRate(xmr_offer.a_fee_rate)
amount, offer, extra_options, ci_from ci_to.validateFeeRate(xmr_offer.b_fee_rate)
)
bid_created_at: int = self.getTime() bid_created_at: int = self.getTime()
if offer.swap_type != SwapTypes.XMR_SWAP: valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 10)
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
if "prefunded_tx" in extra_options:
pi = self.pi(SwapTypes.XMR_SWAP)
prefunded_tx_data: bytes = extra_options["prefunded_tx"]
if reverse_bid:
amount_to = pi.getMockITxSwapValue(ci_to, prefunded_tx_data)
else:
amount_to = pi.getMockPTxSwapValue(ci_to, prefunded_tx_data)
bid_rate: int = ci_from.make_int(amount_to / amount, r=1)
prefunded_txid, prefunded_tx_fee_rate = (
ci_to.validatePrefundedTxAmounts(prefunded_tx_data)
)
self.log.debug(f"Using prefunded tx: {self.log.id(prefunded_txid)}")
ci_to.validateFeeRate(prefunded_tx_fee_rate)
else:
amount, amount_to, bid_rate = self.setBidAmounts(
amount, offer, extra_options, ci_from
)
if not (self.debug and extra_options.get("debug_skip_validation", False)): if not (self.debug and extra_options.get("debug_skip_validation", False)):
self.validateBidValidTime( self.validateBidValidTime(
@@ -6028,21 +6125,24 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
self.validateBidAmount(offer, amount, bid_rate) self.validateBidAmount(offer, amount, bid_rate)
self.checkCoinsReady(coin_from, coin_to)
# TODO: Better tx size estimate # TODO: Better tx size estimate
fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target=2) fee_rate_to = xmr_offer.b_fee_rate
fee_rate_to = ci_to.make_int(fee_rate)
estimated_fee: int = fee_rate_to * ci_to.est_lock_tx_vsize() // 1000 estimated_fee: int = fee_rate_to * ci_to.est_lock_tx_vsize() // 1000
self.ensureWalletCanSend(
ci_to, offer.swap_type, int(amount_to), estimated_fee, for_offer=False if "prefunded_tx" not in extra_options:
) self.ensureWalletCanSend(
ci_to,
offer.swap_type,
int(amount_to),
estimated_fee,
for_offer=False,
)
bid_addr: str = self.prepareSMSGAddress( bid_addr: str = self.prepareSMSGAddress(
addr_send_from, AddressTypes.BID, cursor addr_send_from, AddressTypes.BID, cursor
) )
# return id of route waiting to be established # Return id of route waiting to be established
request_data = { request_data = {
"offer_id": offer_id.hex(), "offer_id": offer_id.hex(),
"amount_from": amount, "amount_from": amount,
@@ -6060,7 +6160,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
valid_for_seconds, valid_for_seconds,
) )
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
if reverse_bid: if reverse_bid:
reversed_rate: int = ci_to.make_int(amount / amount_to, r=1) reversed_rate: int = ci_to.make_int(amount / amount_to, r=1)
@@ -6114,6 +6213,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.bid_id = bid_id bid.bid_id = bid_id
xmr_swap.bid_id = bid.bid_id xmr_swap.bid_id = bid.bid_id
if "prefunded_tx" in extra_options:
prefunded_tx = PrefundedTx(
active_ind=1,
created_at=bid_created_at,
linked_type=Concepts.BID,
linked_id=bid.bid_id,
tx_type=TxTypes.ITX_PRE_FUNDED,
tx_data=extra_options["prefunded_tx"],
)
self.add(prefunded_tx, cursor)
self.saveBidInSession(xmr_swap.bid_id, bid, cursor, xmr_swap) self.saveBidInSession(xmr_swap.bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -6235,6 +6345,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.warning( self.log.warning(
f"Adaptor-sig swap restore height clamped to {wallet_restore_height}" f"Adaptor-sig swap restore height clamped to {wallet_restore_height}"
) )
if "prefunded_tx" in extra_options:
prefunded_tx = PrefundedTx(
active_ind=1,
created_at=bid_created_at,
linked_type=Concepts.BID,
linked_id=bid.bid_id,
tx_type=TxTypes.PTX_PRE_FUNDED,
tx_data=extra_options["prefunded_tx"],
)
self.add(prefunded_tx, cursor)
self.saveBidInSession(bid.bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid.bid_id, bid, cursor, xmr_swap)
self.log.info(f"Sent XMR_BID_FL {self.logIDB(xmr_swap.bid_id)}") self.log.info(f"Sent XMR_BID_FL {self.logIDB(xmr_swap.bid_id)}")
@@ -6354,16 +6474,25 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript( xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(
ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs
) )
prefunded_tx = self.getPreFundedTx( if reverse_bid and bid.was_sent:
Concepts.OFFER, prefunded_tx = self.getPreFundedTx(
bid.offer_id, Concepts.BID,
TxTypes.ITX_PRE_FUNDED, bid_id,
cursor=use_cursor, TxTypes.ITX_PRE_FUNDED,
) cursor=use_cursor,
)
else:
prefunded_tx = self.getPreFundedTx(
Concepts.OFFER,
bid.offer_id,
TxTypes.ITX_PRE_FUNDED,
cursor=use_cursor,
)
if prefunded_tx: if prefunded_tx:
xmr_swap.a_lock_tx = pi.promoteMockTx( xmr_swap.a_lock_tx = pi.promoteMockTx(
ci_from, prefunded_tx, xmr_swap.a_lock_tx_script ci_from, prefunded_tx, xmr_swap.a_lock_tx_script
) )
self.log.info(f"Using pre-funded {ci_from.ticker()} tx")
else: else:
xmr_swap.a_lock_tx = ci_from.createSCLockTx( xmr_swap.a_lock_tx = ci_from.createSCLockTx(
bid.amount, xmr_swap.a_lock_tx_script, xmr_swap.vkbv bid.amount, xmr_swap.a_lock_tx_script, xmr_swap.vkbv
@@ -6709,8 +6838,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
try: try:
use_cursor = self.openDB(cursor) use_cursor = self.openDB(cursor)
bid, offer = self.getBidAndOffer(bid_id, use_cursor, with_txns=False) bid, offer = self.getBidAndOffer(bid_id, use_cursor, with_txns=False)
ensure(bid, "Bid not found") ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, "Offer not found") ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
bid.setState(new_state) bid.setState(new_state)
self.deactivateBid(use_cursor, offer, bid) self.deactivateBid(use_cursor, offer, bid)
@@ -6766,14 +6895,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
addr_to = ci.encodeScriptDest(p2wsh) addr_to = ci.encodeScriptDest(p2wsh)
else: else:
addr_to = ci.encode_p2sh(initiate_script) addr_to = ci.encode_p2sh(initiate_script)
self.log.debug(
f"Create initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
if prefunded_tx: if prefunded_tx:
self.log.debug(
f"Using pre-funded initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
pi = self.pi(SwapTypes.SELLER_FIRST) pi = self.pi(SwapTypes.SELLER_FIRST)
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex() txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else: else:
self.log.debug(
f"Create initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount) txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
txjs = ci.describeTx(txn_signed) txjs = ci.describeTx(txn_signed)
@@ -7625,7 +7757,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.logBidEvent( self.logBidEvent(
bid.bid_id, bid.bid_id,
EventLogTypes.DEBUG_TWEAK_APPLIED, EventLogTypes.DEBUG_TWEAK_APPLIED,
"ind {}".format(bid.debug_ind), f"ind {bid.debug_ind}",
cursor, cursor,
) )
self.commitDB() self.commitDB()
@@ -7680,7 +7812,33 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
if TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns: if refund_tx.block_height is None:
self.log.debug(
f"A_LOCK_REFUND tx: {self.logIDT(refund_tx.txid)} block height not known, bid: {self.log.id(bid_id)}"
)
refund_tx_info = ci_from.getTxOutInfo(
refund_tx.txid, refund_tx.vout
)
if refund_tx_info:
refund_tx.block_hash = refund_tx_info["block_hash"]
refund_tx.block_height = refund_tx_info["block_height"]
refund_tx.block_time = refund_tx_info["block_time"]
self.log.debug(
f"Found A_LOCK_REFUND tx block height: {refund_tx.block_height}, time: {refund_tx.block_time}"
)
self.add(refund_tx, cursor, upsert=True)
self.commitDB()
if (
TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns
and refund_tx.block_height is not None
and ci_from.isCsvLockMature(
offer.lock_type,
xmr_offer.lock_time_2,
refund_tx.block_height,
refund_tx.block_time,
)
):
try: try:
if self.haveDebugInd( if self.haveDebugInd(
bid.bid_id, bid.bid_id,
@@ -7776,6 +7934,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if ( if (
len(xmr_swap.al_lock_refund_tx_sig) > 0 len(xmr_swap.al_lock_refund_tx_sig) > 0
and len(xmr_swap.af_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0
and bid.xmr_a_lock_tx is not None
and ci_from.isCsvLockMature(
offer.lock_type,
xmr_offer.lock_time_1,
bid.xmr_a_lock_tx.block_height,
bid.xmr_a_lock_tx.block_time,
)
): ):
try: try:
@@ -7809,10 +7974,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"", "",
cursor, cursor,
) )
refund_vout: int = ci_from.getLockRefundVout(
xmr_swap.a_lock_refund_tx, xmr_swap.vkbv
)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=bytes.fromhex(txid), txid=bytes.fromhex(txid),
vout=refund_vout,
) )
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -7824,10 +7993,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx) txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns: if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns:
refund_vout: int = ci_from.getLockRefundVout(
xmr_swap.a_lock_refund_tx, xmr_swap.vkbv
)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=txid, txid=txid,
vout=refund_vout,
) )
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -8207,6 +8380,20 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return rv return rv
def _isScriptRefundMature(self, ci, offer, refund_tx_bytes, parent_tx) -> bool:
if offer.lock_type in (TxLockTypes.ABS_LOCK_BLOCKS, TxLockTypes.ABS_LOCK_TIME):
tx_locktime: int = ci.getTxLocktime(refund_tx_bytes)
return ci.isAbsLockTimeMature(tx_locktime)
if parent_tx is None or parent_tx.block_height is None:
return False
txi_sequence: int = ci.getTxInSequence(refund_tx_bytes, 0)
return ci.isCsvLockMature(
offer.lock_type,
txi_sequence,
parent_tx.block_height,
parent_tx.block_time,
)
def checkBidState(self, bid_id: bytes, bid, offer): def checkBidState(self, bid_id: bytes, bid, offer):
# assert (self.mxDB.locked()) # assert (self.mxDB.locked())
# Return True to remove bid from in-progress list # Return True to remove bid from in-progress list
@@ -8244,12 +8431,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Verify amount # Verify amount
vout = getVoutByAddress(initiate_txn, p2sh) vout = getVoutByAddress(initiate_txn, p2sh)
out_value = make_int(initiate_txn["vout"][vout]["value"]) out_value: int = make_int(initiate_txn["vout"][vout]["value"])
ensure( ensure(
out_value == int(bid.amount), out_value == int(bid.amount),
"Incorrect output amount in initiate txn {}: {} != {}.".format( f"Incorrect output amount in initiate txn {self.logIDT(initiate_txnid_hex)}: {out_value} != {bid.amount}",
initiate_txnid_hex, out_value, int(bid.amount)
),
) )
bid.initiate_tx.conf = initiate_txn["confirmations"] bid.initiate_tx.conf = initiate_txn["confirmations"]
@@ -8277,9 +8462,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
index = None index = None
if found: if found:
if "index" not in found:
self.setBidError(
bid,
f"Swap output index not found for initiate txn {self.logIDT(initiate_txnid_hex)}",
)
return True
txo_value: int = found.get("value", None)
if txo_value != bid.amount:
self.setBidError(
bid,
f"Incorrect output amount in initiate txn {self.logIDT(initiate_txnid_hex)}: {txo_value} != {bid.amount}",
)
return True
bid.initiate_tx.conf = found["depth"] bid.initiate_tx.conf = found["depth"]
if "index" in found: index = found["index"]
index = found["index"]
tx_height = found["height"] tx_height = found["height"]
if bid.initiate_tx.conf != last_initiate_txn_conf: if bid.initiate_tx.conf != last_initiate_txn_conf:
@@ -8365,6 +8562,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
vout=participate_txvout, vout=participate_txvout,
) )
if found: if found:
participate_txid_hex: str = found.get(
"txid",
None if participate_txid is None else participate_txid.hex(),
)
# Double check value
txo_value: int = found.get("value", None)
if (
txo_value != bid.amount_to
and bid.debug_ind != DebugTypes.MAKE_INVALID_PTX
):
self.setBidError(
bid,
f"Incorrect output amount in participate txn {self.logIDT(participate_txid_hex)}: {txo_value} != {bid.amount_to}",
)
return True
index = found.get("index", participate_txvout) index = found.get("index", participate_txvout)
if bid.participate_tx.conf != found["depth"]: if bid.participate_tx.conf != found["depth"]:
save_bid = True save_bid = True
@@ -8372,15 +8584,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.participate_tx.conf is None bid.participate_tx.conf is None
and bid.participate_tx.state != TxStates.TX_SENT and bid.participate_tx.state != TxStates.TX_SENT
): ):
txid = found.get(
"txid",
None if participate_txid is None else participate_txid.hex(),
)
self.log.debug( self.log.debug(
f"Found bid {self.log.id(bid_id)} participate txn {self.log.id(txid)} in chain {ci_to.coin_name()}." f"Found bid {self.log.id(bid_id)} participate txn {self.logIDT(participate_txid_hex)} in chain {ci_to.coin_name()}."
) )
self.addParticipateTxn( self.addParticipateTxn(
bid_id, bid, coin_to, txid, index, found["height"] bid_id,
bid,
coin_to,
participate_txid_hex,
index,
found["height"],
) )
# Only update tx state if tx hasn't already been seen # Only update tx state if tx hasn't already been seen
@@ -8398,7 +8611,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if bid.participate_tx.conf is not None: if bid.participate_tx.conf is not None:
self.log.debug( self.log.debug(
f"participate txid {self.log.id(bid.participate_tx.txid)} confirms {bid.participate_tx.conf}." f"Participate txid {self.logIDT(bid.participate_tx.txid)} confirms {bid.participate_tx.conf}."
) )
if ( if (
bid.participate_tx.conf bid.participate_tx.conf
@@ -8467,6 +8680,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if ( if (
bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.initiate_txn_refund is not None and bid.initiate_txn_refund is not None
and self._isScriptRefundMature(
ci_from, offer, bid.initiate_txn_refund, bid.initiate_tx
)
): ):
try: try:
txid = ci_from.publishTx(bid.initiate_txn_refund) txid = ci_from.publishTx(bid.initiate_txn_refund)
@@ -8487,9 +8703,33 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
f"Error trying to submit initiate refund txn: {ex}" f"Error trying to submit initiate refund txn: {ex}"
) )
if ( should_try_refund_ptx: bool = (
bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.participate_txn_refund is not None and bid.participate_txn_refund is not None
)
if (
should_try_refund_ptx
and bid.participate_tx is not None
and bid.participate_tx.block_height is None
):
self.log.debug(
f"PTX: {self.logIDT(bid.participate_tx.txid)} block height not known, bid: {self.log.id(bid_id)}"
)
# An invalid ptx, won't be confirmed, check block height here
ptx_info = ci_to.getTxOutInfo(
bid.participate_tx.txid, bid.participate_tx.vout
)
if ptx_info:
bid.participate_tx.block_hash = ptx_info["block_hash"]
bid.participate_tx.block_height = ptx_info["block_height"]
bid.participate_tx.block_time = ptx_info["block_time"]
self.log.debug(
f"Found PTX block height: {bid.participate_tx.block_height}, time: {bid.participate_tx.block_time}"
)
self.saveBid(bid_id, bid)
if should_try_refund_ptx and self._isScriptRefundMature(
ci_to, offer, bid.participate_txn_refund, bid.participate_tx
): ):
try: try:
txid = ci_to.publishTx(bid.participate_txn_refund) txid = ci_to.publishTx(bid.participate_txn_refund)
@@ -8505,7 +8745,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
# State will update when spend is detected # State will update when spend is detected
except Exception as ex: except Exception as ex:
if ci_to.isTxNonFinalError(str(ex)): if ci_to.isTxNonFinalError(str(ex)) is False:
self.log.warning( self.log.warning(
f"Error trying to submit participate refund txn: {ex}" f"Error trying to submit participate refund txn: {ex}"
) )
@@ -8860,10 +9100,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns: if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns:
refund_vout: int = ci_from.getLockRefundVout(
bytes.fromhex(spend_txn_hex), xmr_swap.vkbv
)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid.bid_id, bid_id=bid.bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=xmr_swap.a_lock_refund_tx_id, txid=xmr_swap.a_lock_refund_tx_id,
vout=refund_vout,
) )
else: else:
self.setBidError( self.setBidError(
@@ -8983,6 +9227,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if was_received: if was_received:
if self.isBchXmrSwap(offer): if self.isBchXmrSwap(offer):
# Mercy tx is sent separately # Mercy tx is sent separately
# Can't set XMR_SWAP_FAILED_SWIPED, as bid should continue looking for mercy tx
pass pass
else: else:
# Look for a mercy output # Look for a mercy output
@@ -10027,6 +10272,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
ensure(len(offer_data.proof_signature) == 0, "Unexpected data") ensure(len(offer_data.proof_signature) == 0, "Unexpected data")
ensure(len(offer_data.pkhash_seller) == 0, "Unexpected data") ensure(len(offer_data.pkhash_seller) == 0, "Unexpected data")
ensure(len(offer_data.secret_hash) == 0, "Unexpected data") ensure(len(offer_data.secret_hash) == 0, "Unexpected data")
ci_from.validateFeeRate(offer_data.fee_rate_from)
ci_to.validateFeeRate(offer_data.fee_rate_to)
else: else:
raise ValueError("Unknown swap type {}.".format(offer_data.swap_type)) raise ValueError("Unknown swap type {}.".format(offer_data.swap_type))
@@ -10052,6 +10301,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Check for sent # Check for sent
existing_offer = self.getOffer(offer_id, cursor=cursor) existing_offer = self.getOffer(offer_id, cursor=cursor)
if existing_offer is None: if existing_offer is None:
bid_reversed: bool = ( bid_reversed: bool = (
offer_data.swap_type == SwapTypes.XMR_SWAP offer_data.swap_type == SwapTypes.XMR_SWAP
and self.is_reverse_ads_bid( and self.is_reverse_ads_bid(
@@ -10905,7 +11155,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
ensure( ensure(
ci_from.isValidAddressHash(bid_data.dest_af) ci_from.isValidAddressHash(bid_data.dest_af)
or ci_from.isValidPubkey(bid_data.dest_af), or ci_from.verifyPubkey(bid_data.dest_af),
"Invalid destination address", "Invalid destination address",
) )
@@ -11056,7 +11306,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
refundExtraArgs = dict() refundExtraArgs = dict()
lockExtraArgs = dict() lockExtraArgs = dict()
if self.isBchXmrSwap(offer): if self.isBchXmrSwap(offer):
# perform check that both lock and refund transactions have their outs pointing to correct follower address # Perform check that both lock and refund transactions have their outs pointing to correct follower address
# and prepare extra args for validation # and prepare extra args for validation
bch_ci = self.ci(Coins.BCH) bch_ci = self.ci(Coins.BCH)
@@ -11554,19 +11804,38 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
cursor, cursor,
) )
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid.bid_id,
TxTypes.PTX_PRE_FUNDED,
cursor=cursor,
)
try: try:
b_lock_vout = 0 b_lock_vout = 0
result = ci_to.publishBLockTx( if prefunded_tx:
xmr_swap.vkbv, self.log.info("Using pre-funded tx")
xmr_swap.pkbs, pi = self.pi(offer.swap_type)
bid.amount_to, b_lock_tx = pi.promoteMockPTx(
b_fee_rate, ci_to,
unlock_time=unlock_time, prefunded_tx,
) xmr_swap.vkbv,
if isinstance(result, tuple): xmr_swap.pkbs,
b_lock_tx_id, b_lock_vout = result )
b_lock_tx = ci_to.signTxWithWallet(b_lock_tx)
b_lock_tx_id = bytes.fromhex(ci_to.publishTx(b_lock_tx))
else: else:
b_lock_tx_id = result result = ci_to.publishBLockTx(
xmr_swap.vkbv,
xmr_swap.pkbs,
bid.amount_to,
b_fee_rate,
unlock_time=unlock_time,
)
if isinstance(result, tuple):
b_lock_tx_id, b_lock_vout = result
else:
b_lock_tx_id = result
if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND: if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
self.log.debug( self.log.debug(
f"Adaptor-sig bid {self.log.id(bid_id)}: Debug {bid.debug_ind} - Losing XMR lock tx {self.log.id(b_lock_tx_id)}." f"Adaptor-sig bid {self.log.id(bid_id)}: Debug {bid.debug_ind} - Losing XMR lock tx {self.log.id(b_lock_tx_id)}."
@@ -11896,7 +12165,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
vkbs = ci_to.sumKeys(kbsl, kbsf) vkbs = ci_to.sumKeys(kbsl, kbsf)
if coin_to == (Coins.XMR, Coins.WOW): if coin_to in (Coins.XMR, Coins.WOW):
address_to = self.getCachedMainWalletAddress(ci_to, cursor) address_to = self.getCachedMainWalletAddress(ci_to, cursor)
elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON): elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
address_to = self.getCachedStealthAddressForCoin(coin_to, cursor) address_to = self.getCachedStealthAddressForCoin(coin_to, cursor)
@@ -12794,8 +13063,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.info(f"Route established for bid {self.log.id(bid_id)}") self.log.info(f"Route established for bid {self.log.id(bid_id)}")
bid, offer = self.getBidAndOffer(bid_id, cursor) bid, offer = self.getBidAndOffer(bid_id, cursor)
ensure(bid, "Bid not found") ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, "Offer not found") ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
coin_from = Coins(offer.coin_from) coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to) coin_to = Coins(offer.coin_to)
@@ -13811,8 +14080,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
num_watched_outputs += len(v["watched_outputs"]) num_watched_outputs += len(v["watched_outputs"])
now: int = self.getTime() now: int = self.getTime()
q_bids_str: str = ( q_bids_str: str = """SELECT
"""SELECT
COUNT(CASE WHEN b.was_sent THEN 1 ELSE NULL END) AS count_sent, COUNT(CASE WHEN b.was_sent THEN 1 ELSE NULL END) AS count_sent,
COUNT(CASE WHEN b.was_sent AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_sent_active, COUNT(CASE WHEN b.was_sent AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_sent_active,
COUNT(CASE WHEN b.was_received THEN 1 ELSE NULL END) AS count_received, COUNT(CASE WHEN b.was_received THEN 1 ELSE NULL END) AS count_received,
@@ -13822,15 +14090,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
JOIN offers o ON b.offer_id = o.offer_id JOIN offers o ON b.offer_id = o.offer_id
JOIN bidstates s ON b.state = s.state_id JOIN bidstates s ON b.state = s.state_id
WHERE b.active_ind = 1""" WHERE b.active_ind = 1"""
)
q_offers_str: str = ( q_offers_str: str = """SELECT
"""SELECT
COUNT(CASE WHEN expire_at > :now THEN 1 ELSE NULL END) AS count_active, COUNT(CASE WHEN expire_at > :now THEN 1 ELSE NULL END) AS count_active,
COUNT(CASE WHEN was_sent THEN 1 ELSE NULL END) AS count_sent, COUNT(CASE WHEN was_sent THEN 1 ELSE NULL END) AS count_sent,
COUNT(CASE WHEN was_sent AND expire_at > :now THEN 1 ELSE NULL END) AS count_sent_active COUNT(CASE WHEN was_sent AND expire_at > :now THEN 1 ELSE NULL END) AS count_sent_active
FROM offers WHERE active_ind = 1""" FROM offers WHERE active_ind = 1"""
)
try: try:
cursor = self.openDB() cursor = self.openDB()
@@ -13905,9 +14170,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
walletinfo = ci.getWalletInfo() walletinfo = ci.getWalletInfo()
rv = { rv = {
"deposit_address": self.getCachedAddressForCoin(coin), "deposit_address": self.getCachedAddressForCoin(coin),
"balance": ci.format_amount(walletinfo["balance"], conv_int=True), "balance": ci.format_amount(walletinfo["balance"], conv_int=True, r=-1),
"unconfirmed": ci.format_amount( "unconfirmed": ci.format_amount(
walletinfo["unconfirmed_balance"], conv_int=True walletinfo["unconfirmed_balance"], conv_int=True, r=-1
), ),
"expected_seed": ci.knownWalletSeed(), "expected_seed": ci.knownWalletSeed(),
"encrypted": walletinfo["encrypted"], "encrypted": walletinfo["encrypted"],
@@ -13922,7 +14187,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if "immature_balance" in walletinfo: if "immature_balance" in walletinfo:
rv["immature"] = ci.format_amount( rv["immature"] = ci.format_amount(
walletinfo["immature_balance"], conv_int=True walletinfo["immature_balance"], conv_int=True, r=-1
) )
if "locked_utxos" in walletinfo: if "locked_utxos" in walletinfo:
@@ -14121,7 +14386,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def updateWalletsInfo( def updateWalletsInfo(
self, self,
force_update: bool = False, force_update: bool = False,
only_coin: bool = None, only_coin: int = None,
wait_for_complete: bool = False, wait_for_complete: bool = False,
) -> None: ) -> None:
now: int = self.getTime() now: int = self.getTime()
@@ -14913,8 +15178,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return return
bid = self.getBid(bid_id) bid = self.getBid(bid_id)
if bid is None: ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
raise ValueError("Bid not found.")
bid.debug_ind = debug_ind bid.debug_ind = debug_ind
+2
View File
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
BCH_MERCY = auto() BCH_MERCY = auto()
PTX_PRE_FUNDED = auto()
class ActionTypes(IntEnum): class ActionTypes(IntEnum):
ACCEPT_BID = auto() ACCEPT_BID = auto()
+56 -37
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -29,6 +29,7 @@ import urllib.parse
import zipfile import zipfile
import zmq import zmq
from typing import List
from urllib.request import urlopen from urllib.request import urlopen
import basicswap.config as cfg import basicswap.config as cfg
@@ -58,19 +59,19 @@ PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "29.3") BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "29.3")
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "") BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.5.4") LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.5.5")
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "") LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "2.1.3") DCR_VERSION = os.getenv("DCR_VERSION", "2.1.5")
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "") DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0") NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "") NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.5") MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.5.0")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "") MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = ( XMR_SITE_COMMIT = (
"1bfa07c1b54f4f39a93096e3bfb746cb21249422" # Lock hashes.txt to monero version "5e8d74229b742b54173010e3a676215b6f2fd1d7" # Lock hashes.txt to monero version
) )
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0") WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -85,7 +86,7 @@ PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "23.1.2") DASH_VERSION = os.getenv("DASH_VERSION", "23.1.2")
DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "") DASH_VERSION_TAG = os.getenv("DASH_VERSION_TAG", "")
FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.15.3") FIRO_VERSION = os.getenv("FIRO_VERSION", "0.14.16.1")
FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "") FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3") NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
@@ -185,11 +186,13 @@ else:
BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH) BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH)
FILE_EXT = os.getenv("FILE_EXT", FILE_EXT) FILE_EXT = os.getenv("FILE_EXT", FILE_EXT)
logger = logging.getLogger() logger = logging.getLogger("prepare")
LOG_LEVEL = logging.DEBUG LOG_LEVEL = logging.DEBUG
logger.propagate = False
logger.level = LOG_LEVEL logger.level = LOG_LEVEL
if not len(logger.handlers): handler = logging.StreamHandler(sys.stdout)
logger.addHandler(logging.StreamHandler(sys.stdout)) handler.setFormatter(logging.Formatter("%(levelname)s : %(message)s"))
logger.addHandler(handler)
logging.getLogger("gnupg").setLevel(logging.INFO) logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False)) BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
@@ -458,33 +461,47 @@ def getRemoteFileLength(url: str) -> (int, bool):
popConnectionParameters() popConnectionParameters()
def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None: def downloadRelease(
"""If file exists at path compare it's size to the content length at the url url_in: str | List[str], path: str, extra_opts, timeout: int = 10
and attempt to resume download if file size is below expected. ) -> None:
""" # If file exists at path compare it's size to the content length at the url
resume_from: int = 0 # and attempt to resume download if file size is below expected.
if os.path.exists(path): release_filename: str = os.path.basename(path)
if extra_opts.get("redownload_releases", False): urls = (
logging.warning(f"Overwriting: {path}") url_in
elif extra_opts.get("verify_release_file_size", True): if isinstance(url_in, list)
file_size = os.stat(path).st_size else [
remote_file_length, can_resume = getRemoteFileLength(url) url_in,
if file_size < remote_file_length: ]
logger.warning( )
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download." for url in urls:
) try:
if can_resume: resume_from: int = 0
resume_from = file_size if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
elif extra_opts.get("verify_release_file_size", True):
file_size = os.stat(path).st_size
remote_file_length, can_resume = getRemoteFileLength(url)
if file_size < remote_file_length:
logger.warning(
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download."
)
if can_resume:
resume_from = file_size
else:
logger.warning("Download can not be resumed, restarting.")
else:
return
else: else:
logger.warning("Download can not be resumed, restarting.") # File exists and size check is disabled
else: return
return return downloadFile(url, path, timeout, resume_from)
else: except Exception as e:
# File exists and size check is disabled logger.warning(f"Failed to download {release_filename} from {url}")
return logger.debug(f"Download error {e}")
raise RuntimeError(f"Failed to download {release_filename}.")
return downloadFile(url, path, timeout, resume_from)
def downloadFile(url: str, path: str, timeout: int = 5, resume_from: int = 0) -> None: def downloadFile(url: str, path: str, timeout: int = 5, resume_from: int = 0) -> None:
@@ -925,9 +942,10 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename, assert_filename,
) )
elif coin == "litecoin": elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format( release_url = [
version + version_tag, release_filename f"https://github.com/litecoin-project/litecoin/releases/download/v{version}{version_tag}/{release_filename}",
) f"https://download.litecoin.org/litecoin-{version}{version_tag}/{os_name}/{release_filename}",
]
assert_filename = "{}-core-{}-{}-build.assert".format( assert_filename = "{}-core-{}-{}-build.assert".format(
coin, os_name, ".".join(version.split(".")[:2]) coin, os_name, ".".join(version.split(".")[:2])
) )
@@ -1249,6 +1267,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write("rpc-bind-ip={}\n".format(COINS_RPCBIND_IP)) fp.write("rpc-bind-ip={}\n".format(COINS_RPCBIND_IP))
fp.write(f"wallet-dir={config_datadir}\n") fp.write(f"wallet-dir={config_datadir}\n")
fp.write("log-file={}\n".format(os.path.join(config_datadir, "wallet.log"))) fp.write("log-file={}\n".format(os.path.join(config_datadir, "wallet.log")))
fp.write("max-log-files=5\n")
fp.write( fp.write(
"rpc-login={}:{}\n".format( "rpc-login={}:{}\n".format(
core_settings["walletrpcuser"], core_settings["walletrpcpassword"] core_settings["walletrpcuser"], core_settings["walletrpcpassword"]
+69 -53
View File
@@ -2,10 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
import json import json
import logging import logging
import os import os
@@ -22,6 +23,7 @@ from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon from basicswap.util.daemon import Daemon
from typing import Set
initial_logger = logging.getLogger() initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG initial_logger.level = logging.DEBUG
@@ -347,7 +349,7 @@ def mainLoop(daemons, update: bool = True):
def runClient( def runClient(
data_dir: str, data_dir: str,
chain: str, chain: str,
start_only_coins: bool, start_only_coins: Set[str],
log_prefix: str = "BasicSwap", log_prefix: str = "BasicSwap",
extra_opts=dict(), extra_opts=dict(),
) -> int: ) -> int:
@@ -391,39 +393,46 @@ def runClient(
# Settings may have been modified # Settings may have been modified
settings = swap_client.settings settings = swap_client.settings
base_coin_opts = []
if "extra_coin_opts" in extra_opts:
if len(start_only_coins) == 0:
raise ValueError('"extracoinopts" can only be used with "startonlycoins"')
base_coin_opts += extra_opts["extra_coin_opts"]
try: try:
# Try start daemons # Try start daemons
for network in settings.get("networks", []): if len(start_only_coins) > 0:
if network.get("enabled", True) is False: swap_client.log.warning('Not starting networks as "startonlycoin" is set')
continue else:
network_type: str = network.get("type", "unknown") for network in settings.get("networks", []):
if network_type == "simplex": if network.get("enabled", True) is False:
simplex_dir = os.path.join(data_dir, "simplex") 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}"
)
log_level = "debug" if swap_client.debug else "info" daemons.append(
startSimplexClient(
socks_proxy = None network["client_path"],
if "socks_proxy_override" in network: simplex_dir,
socks_proxy = network["socks_proxy_override"] network["server_address"],
elif swap_client.use_tor_proxy: network["ws_port"],
socks_proxy = ( logger,
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}" swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
) )
pid = daemons[-1].handle.pid
daemons.append( swap_client.log.info(f"Started Simplex client {pid}")
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(): for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins: if len(start_only_coins) > 0 and c not in start_only_coins:
@@ -460,10 +469,18 @@ def runClient(
trusted_daemon: bool = swap_client.getXMRTrustedDaemon( trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"] coin_id, v["rpchost"]
) )
opts = [ wallet_opts = [
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon",
"--daemon-address", "--daemon-address",
daemon_addr, daemon_addr,
] ]
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
wallet_opts += [
"--daemon-login",
daemon_rpcuser + ":" + daemon_rpcpass,
]
proxy_log_str = "" proxy_log_str = ""
proxy_host, proxy_port = swap_client.getXMRWalletProxy( proxy_host, proxy_port = swap_client.getXMRWalletProxy(
@@ -471,7 +488,7 @@ def runClient(
) )
if proxy_host: if proxy_host:
proxy_log_str = " through proxy" proxy_log_str = " through proxy"
opts += [ wallet_opts += [
"--proxy", "--proxy",
f"{proxy_host}:{proxy_port}", f"{proxy_host}:{proxy_port}",
"--daemon-ssl-allow-any-cert", "--daemon-ssl-allow-any-cert",
@@ -485,19 +502,11 @@ def runClient(
) )
) )
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
opts.append("--daemon-login")
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
opts.append(
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
)
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc") filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
daemons.append( daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts) startXmrWalletDaemon(
v["datadir"], v["bindir"], filename, wallet_opts
)
) )
pid = daemons[-1].handle.pid pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}") swap_client.log.info(f"Started {filename} {pid}")
@@ -506,9 +515,8 @@ def runClient(
if c == "decred": if c == "decred":
appdata = v["datadir"] appdata = v["datadir"]
extra_opts = [ coin_opts = copy.deepcopy(base_coin_opts)
f'--appdata="{appdata}"', coin_opts.append(f'--appdata="{appdata}"')
]
use_shell: bool = True if os.name == "nt" else False use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True: if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon") swap_client.log.info(f"Starting {display_name} daemon")
@@ -526,7 +534,7 @@ def runClient(
appdata, appdata,
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=coin_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
@@ -537,12 +545,13 @@ def runClient(
swap_client.log.info(f"Starting {display_name} wallet daemon") swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet") filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_opts = [f'--appdata="{appdata}"']
wallet_pwd = v["wallet_pwd"] wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "": if wallet_pwd == "":
# Only set when in startonlycoin mode # Only set when in startonlycoin mode
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "") wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "": if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"') wallet_opts.append(f'--pass="{wallet_pwd}"')
extra_config = { extra_config = {
"add_datadir": False, "add_datadir": False,
"stdout_to_file": True, "stdout_to_file": True,
@@ -555,13 +564,12 @@ def runClient(
appdata, appdata,
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=wallet_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
pid = daemons[-1].handle.pid pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}") swap_client.log.info(f"Started {filename} {pid}")
continue # /decred continue # /decred
if v["manage_daemon"] is True: if v["manage_daemon"] is True:
@@ -571,7 +579,7 @@ def runClient(
swap_client.log.info(f"Starting {display_name} daemon") swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d") filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs( coin_opts = copy.deepcopy(base_coin_opts) + getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
) )
extra_config = {"coin_name": c} extra_config = {"coin_name": c}
@@ -580,7 +588,7 @@ def runClient(
v["datadir"], v["datadir"],
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=coin_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
@@ -679,6 +687,9 @@ def printHelp():
print( print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing." "--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
) )
print(
"--extracoinopts Extra options to pass to coin daemon, can only be used with --startonlycoin."
)
print("--logprefix Specify log prefix.") print("--logprefix Specify log prefix.")
print( print(
"--forcedbupgrade Recheck database against schema regardless of version." "--forcedbupgrade Recheck database against schema regardless of version."
@@ -743,6 +754,11 @@ def main():
ensure_coin_valid(coin) ensure_coin_valid(coin)
start_only_coins.add(coin) start_only_coins.add(coin)
continue continue
if name == "extracoinopts":
options["extra_coin_opts"] = []
for opt in [s.lower() for s in s[1].split(",")]:
options["extra_coin_opts"].append(opt)
continue
logger.warning(f"Unknown argument {v}") logger.warning(f"Unknown argument {v}")
+12 -2
View File
@@ -552,16 +552,26 @@ chainparams = {
name_map = {} name_map = {}
ticker_map = {} ticker_map = {}
variant_ticker_map = {}
for c, params in chainparams.items(): for c, params in chainparams.items():
name_map[params["name"].lower()] = c name_map[params["name"].lower()] = c
ticker_map[params["ticker"].lower()] = c ticker_map[params["ticker"].lower()] = c
# Add coin variants, eg: LTC_MWEB, PART_ANON
for c in Coins:
if c.name.lower() in ticker_map:
continue
variant_ticker_map[c.name.lower()] = c
def getCoinIdFromTicker(ticker: str) -> str:
def getCoinIdFromTicker(ticker: str, inc_variant: bool = False) -> str:
lc_ticker: str = ticker.lower()
try: try:
return ticker_map[ticker.lower()] if inc_variant and lc_ticker in variant_ticker_map:
return variant_ticker_map[lc_ticker]
return ticker_map[lc_ticker]
except Exception: except Exception:
raise ValueError(f"Unknown coin {ticker}") raise ValueError(f"Unknown coin {ticker}")
+3 -3
View File
@@ -12,7 +12,6 @@ import time
from enum import IntEnum, auto from enum import IntEnum, auto
from typing import Optional from typing import Optional
CURRENT_DB_VERSION = 34 CURRENT_DB_VERSION = 34
CURRENT_DB_DATA_VERSION = 8 CURRENT_DB_DATA_VERSION = 8
@@ -400,8 +399,9 @@ class XmrOffer(Table):
swap_id = Column("integer", primary_key=True, autoincrement=True) swap_id = Column("integer", primary_key=True, autoincrement=True)
offer_id = Column("blob") offer_id = Column("blob")
a_fee_rate = Column("integer") # Chain a fee rate # TODO: rename to from/to - values are not switched for reverse swaps
b_fee_rate = Column("integer") # Chain b fee rate a_fee_rate = Column("integer") # Chain from fee rate
b_fee_rate = Column("integer") # Chain to fee rate
# Delay before the chain a lock refund tx can be mined # Delay before the chain a lock refund tx can be mined
lock_time_1 = Column("integer") lock_time_1 = Column("integer")
+9 -2
View File
@@ -250,11 +250,18 @@ def upgradeDatabaseFromSchema(self, cursor, expect_schema):
def upgradeDatabase(self, db_version: int): def upgradeDatabase(self, db_version: int):
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION: upgrade_forced: bool = False
if db_version < CURRENT_DB_VERSION:
pass
elif self._force_db_upgrade is True:
upgrade_forced = True
else:
return return
self.log.info( self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}." f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}"
+ (" (forced)" if upgrade_forced else "")
+ "."
) )
# db_version, tablename, oldcolumnname, newcolumnname # db_version, tablename, oldcolumnname, newcolumnname
-1
View File
@@ -7,7 +7,6 @@
import json import json
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj" default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
+1 -1
View File
@@ -213,7 +213,7 @@ class HttpHandler(BaseHTTPRequestHandler):
status_code=200, status_code=200,
version=__version__, version=__version__,
extra_headers=None, extra_headers=None,
): ) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
if swap_client.ws_server: if swap_client.ws_server:
args_dict["ws_port"] = swap_client.ws_server.client_port args_dict["ws_port"] = swap_client.ws_server.client_port
+7 -9
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
@@ -50,7 +49,7 @@ class CoinInterface:
def compareFeeRates(a, b) -> bool: def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20 return abs(a - b) < 20
def __init__(self, network): def __init__(self, network, **kwargs):
self.setDefaults() self.setDefaults()
self._network = network self._network = network
self._mx_wallet = threading.Lock() self._mx_wallet = threading.Lock()
@@ -193,8 +192,14 @@ class AdaptorSigInterface:
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]: def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), b"", bytes(len(script))] return [bytes(72), b"", bytes(len(script))]
def getLockRefundVout(self, lock_refund_tx_data: bytes, vbkv: bytes):
return 0
class Secp256k1Interface(CoinInterface, AdaptorSigInterface): class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@staticmethod @staticmethod
def curve_type(): def curve_type():
return Curves.secp256k1 return Curves.secp256k1
@@ -220,13 +225,6 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
if hash_len == 20: if hash_len == 20:
return True return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool: def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey) pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None) return pubkey.verify(sig, signed_hash, hasher=None)
+47 -11
View File
@@ -71,8 +71,13 @@ class BCHInterface(BTCInterface):
# TODO: BCH Watchonly: Remove when BCH watchonly works. # TODO: BCH Watchonly: Remove when BCH watchonly works.
return True return True
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(BCHInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.swap_client = swap_client self.swap_client = swap_client
def has_segwit(self) -> bool: def has_segwit(self) -> bool:
@@ -142,7 +147,9 @@ class BCHInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True): if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH # Expects P2WSH nested in BIP16_P2SH
self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True]) self.rpc_wallet(
"importaddress", [lock_tx_dest.hex(), "bid lock", False, True]
)
return address return address
@@ -151,18 +158,35 @@ class BCHInterface(BTCInterface):
def createRawFundedTransaction( def createRawFundedTransaction(
self, self,
addr_to: str, addr_to: str | bytes,
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if isinstance(addr_to, bytes):
# addr_to is script_pubkey
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vout.append(self.txoType()(amount, addr_to))
txn = tx.serialize_without_witness().hex()
else:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
# 'conf_target': self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
@@ -214,6 +238,16 @@ class BCHInterface(BTCInterface):
) )
return pay_fee return pay_fee
def getBLockTxo(
self,
chain_b_lock_txid: bytes,
lock_tx_vout: int,
script_pk: bytes,
) -> (int, int):
txout = self.rpc("gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, True])
actual_value = self.make_int(txout["value"])
return lock_tx_vout, actual_value
def findTxnByHash(self, txid_hex: str): def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns # Only works for wallet txns
try: try:
@@ -268,7 +302,7 @@ class BCHInterface(BTCInterface):
found_vout = try_vout found_vout = try_vout
break break
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e}")
return None return None
if found_vout is None: if found_vout is None:
@@ -281,13 +315,14 @@ class BCHInterface(BTCInterface):
# TODO: Better way? # TODO: Better way?
if confirmations > 0: if confirmations > 0:
block_height = self.getChainHeight() - confirmations block_height = self.getChainHeight() - (confirmations - 1)
rv = { rv = {
"txid": txid.hex(), "txid": txid.hex(),
"depth": confirmations, "depth": confirmations,
"index": found_vout, "index": found_vout,
"height": block_height, "height": block_height,
"value": self.make_int(txout["value"]),
} }
return rv return rv
@@ -502,6 +537,7 @@ class BCHInterface(BTCInterface):
tx_lock = self.loadTx(tx_lock_bytes) tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock) output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script) locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx") ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue locked_coin = tx_lock.vout[locked_n].nValue
@@ -1120,7 +1156,7 @@ class BCHInterface(BTCInterface):
refund_output_value = refund_swipe_tx.vout[0].nValue refund_output_value = refund_swipe_tx.vout[0].nValue
refund_output_script = refund_swipe_tx.vout[0].scriptPubKey refund_output_script = refund_swipe_tx.vout[0].scriptPubKey
# mercy transaction size consisting of one input of freshly received funds, # Mercy transaction size consisting of one input of freshly received funds,
# one op_return with mercy information, a dust output to the leader and change back to the follower # one op_return with mercy information, a dust output to the leader and change back to the follower
tx_size = 275 tx_size = 275
dust_limit = 546 dust_limit = 546
+279 -91
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -27,6 +26,7 @@ from basicswap.basicswap_util import (
getVoutByScriptPubKey, getVoutByScriptPubKey,
) )
from basicswap.interface.base import Secp256k1Interface from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.util import ( from basicswap.util import (
b2i, b2i,
ensure, ensure,
@@ -99,7 +99,6 @@ from basicswap.basicswap_util import TxLockTypes
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.rpc import make_rpc_func, openrpc from basicswap.rpc import make_rpc_func, openrpc
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22 SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000FFFF SEQUENCE_LOCKTIME_MASK = 0x0000FFFF
@@ -185,7 +184,7 @@ def extractScriptLockRefundScriptValues(script_bytes: bytes):
return pk1, pk2, csv_val, pk3 return pk1, pk2, csv_val, pk3
class BTCInterface(Secp256k1Interface): class BTCInterface(FeeValidator, Secp256k1Interface):
_scantxoutset_lock = threading.Lock() _scantxoutset_lock = threading.Lock()
_MAX_SCANTXOUTSET_RETRIES = 3 _MAX_SCANTXOUTSET_RETRIES = 3
@@ -279,8 +278,15 @@ class BTCInterface(Secp256k1Interface):
def depth_spendable() -> int: def depth_spendable() -> int:
return 0 return 0
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(network) self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_host = coin_settings.get("rpchost", "127.0.0.1") self._rpc_host = coin_settings.get("rpchost", "127.0.0.1")
self._rpcport = coin_settings["rpcport"] self._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"] self._rpcauth = coin_settings["rpcauth"]
@@ -305,8 +311,6 @@ class BTCInterface(Secp256k1Interface):
self.setConfTarget(coin_settings["conf_target"]) self.setConfTarget(coin_settings["conf_target"])
self._use_segwit = coin_settings["use_segwit"] self._use_segwit = coin_settings["use_segwit"]
self._connection_type = coin_settings["connection_type"] self._connection_type = coin_settings["connection_type"]
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None self._expect_seedid_hex = None
self._altruistic = coin_settings.get("altruistic", True) self._altruistic = coin_settings.get("altruistic", True)
self._use_descriptors = coin_settings.get("use_descriptors", False) self._use_descriptors = coin_settings.get("use_descriptors", False)
@@ -449,11 +453,11 @@ class BTCInterface(Secp256k1Interface):
# Wallet name is "" for some LTC and PART installs on older cores # Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0: if self._rpc_wallet not in wallets and len(wallets) > 0:
if "" in wallets: if "" in wallets:
# Setting wallet= in the coin .conf file should also work
self._log.warning( self._log.warning(
f"Nameless {self.ticker()} wallet found." f"Nameless {self.ticker()} wallet found."
+ '\nPlease set the "wallet_name" coin setting to "" or recreate the wallet' + '\nPlease set the "wallet_name" coin setting to "" or recreate the wallet'
) )
# backupwallet and restorewallet with name should work.
if self._rpc_wallet not in wallets: if self._rpc_wallet not in wallets:
raise RuntimeError( raise RuntimeError(
@@ -503,6 +507,75 @@ class BTCInterface(Secp256k1Interface):
return height return height
return self.rpc("getblockcount") return self.rpc("getblockcount")
def getTxLocktime(self, tx_data: bytes) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.nLockTime
def getTxInSequence(self, tx_data: bytes, vout: int) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.vin[vout].nSequence
def getChainMedianTime(self) -> int:
if self.useBackend():
import struct
backend = self.getBackend()
if not backend:
raise ValueError("No electrum backend available")
height = backend.getBlockHeight()
start = max(0, height - 10)
count = height - start + 1
result = backend._server.call("blockchain.block.headers", [start, count])
header_bytes = bytes.fromhex(result["hex"])
returned = result.get("count", count)
times = [
struct.unpack("<I", header_bytes[i * 80 + 68 : i * 80 + 72])[0]
for i in range(returned)
]
times.sort()
return times[len(times) // 2]
return self.rpc("getblockchaininfo")["mediantime"]
def isCsvLockMature(
self,
lock_type: int,
encoded_sequence: int,
parent_block_height: Optional[int],
parent_block_time: Optional[int],
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if parent_block_height is None or parent_block_height < 1:
return False
lock_value: int = self.decodeSequence(encoded_sequence)
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= parent_block_height + lock_value
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
if parent_block_time is None or parent_block_time < 1:
return False
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= parent_block_time + lock_value
raise ValueError(f"Unknown lock type {lock_type}")
def isAbsLockTimeMature(
self,
nlocktime: int,
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if nlocktime == 0:
return True
if nlocktime < 500000000:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= nlocktime
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= nlocktime
def getMempoolTx(self, txid): def getMempoolTx(self, txid):
if self._connection_type == "electrum": if self._connection_type == "electrum":
backend = self.getBackend() backend = self.getBackend()
@@ -535,7 +608,7 @@ class BTCInterface(Secp256k1Interface):
block_hash = sha256(sha256(header_bytes))[::-1].hex() block_hash = sha256(sha256(header_bytes))[::-1].hex()
return {"height": height, "hash": block_hash, "time": block_time} return {"height": height, "hash": block_hash, "time": block_time}
def getBlockHeader(self, block_hash): def getBlockHeader(self, block_hash: str) -> dict:
if self._connection_type == "electrum": if self._connection_type == "electrum":
raise NotImplementedError( raise NotImplementedError(
"getBlockHeader by hash not available in electrum mode" "getBlockHeader by hash not available in electrum mode"
@@ -939,32 +1012,26 @@ class BTCInterface(Secp256k1Interface):
if wm: if wm:
info = wm.getAddressInfo(self.coin_type(), address) info = wm.getAddressInfo(self.coin_type(), address)
if info: if info:
if or_watch_only: if or_watch_only is False and info["is_watch_only"] is True:
return True return False
return True return True
return False return False
try: try:
addr_info = self.rpc_wallet("getaddressinfo", [address]) addr_info = self.rpc_wallet("getaddressinfo", [address])
if not or_watch_only: if addr_info["ismine"]:
if addr_info["ismine"]: return True
return True if or_watch_only is False:
else: return False
if self._use_descriptors: if addr_info["iswatchonly"]:
addr_info = self.rpc_wallet_watch("getaddressinfo", [address]) return True
if addr_info["ismine"] or addr_info["iswatchonly"]: if self._use_descriptors:
wo_addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if wo_addr_info["iswatchonly"]:
return True return True
except Exception as e: except Exception as e:
self._log.debug(f"isAddressMine RPC check failed: {e}") self._log.debug(f"isAddressMine RPC check failed: {e}")
wm = self.getWalletManager()
if wm:
info = wm.getAddressInfo(self.coin_type(), address)
if info:
if or_watch_only:
return True
return True
return False return False
def checkAddressMine(self, address: str) -> None: def checkAddressMine(self, address: str) -> None:
@@ -1083,8 +1150,8 @@ class BTCInterface(Secp256k1Interface):
return self.encode_p2wsh(script) return self.encode_p2wsh(script)
def getDestForAddress(self, address: str) -> bytes: def getDestForAddress(self, address: str) -> bytes:
bech32_prefix = self.chainparams_network()["hrp"] bech32_prefix: str | None = self.chainparams_network().get("hrp", None)
if address.startswith(bech32_prefix + "1"): if bech32_prefix and address.startswith(bech32_prefix + "1"):
_, witprog = segwit_addr.decode(bech32_prefix, address) _, witprog = segwit_addr.decode(bech32_prefix, address)
return CScript([OP_0, bytes(witprog)]) return CScript([OP_0, bytes(witprog)])
@@ -1813,7 +1880,9 @@ class BTCInterface(Secp256k1Interface):
pubkey = PublicKey(K) pubkey = PublicKey(K)
return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte
def fundTx(self, tx: bytes, feerate) -> bytes: def fundTx(
self, tx: bytes, feerate: int, lock_unspents: bool = True, subfee: bool = False
) -> bytes:
if self.useBackend(): if self.useBackend():
return self._fundTxElectrum(tx, feerate) return self._fundTxElectrum(tx, feerate)
@@ -1821,9 +1890,14 @@ class BTCInterface(Secp256k1Interface):
# TODO: Unlock unspents if bid cancelled # TODO: Unlock unspents if bid cancelled
# TODO: Manually select only segwit prevouts # TODO: Manually select only segwit prevouts
options = { options = {
"lockUnspents": True, "lockUnspents": lock_unspents,
"feeRate": feerate_str, "feeRate": feerate_str,
} }
if subfee:
tx_obj = self.loadTx(tx, allow_witness=False)
num_vouts: int = len(tx_obj.vout)
ensure(num_vouts > 0, "Missing tx outputs")
options["subtractFeeFromOutputs"] = list(range(num_vouts))
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options]) rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
tx_bytes: bytes = bytes.fromhex(rv["hex"]) tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes return tx_bytes
@@ -2645,10 +2719,17 @@ class BTCInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk)) tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk))
return tx.serialize() return tx.serialize()
def encodeSharedAddress(self, Kbv, Kbs): def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return self.pubkey_to_segwit_address(Kbs) return self.pubkey_to_segwit_address(Kbs)
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0): def publishBLockTx(
self,
kbv: bytes,
Kbs: bytes,
output_amount: int,
feerate: int,
unlock_time: int = 0,
) -> (bytes, int):
b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate) b_lock_tx = self.fundTx(b_lock_tx, feerate)
@@ -2671,8 +2752,8 @@ class BTCInterface(Secp256k1Interface):
def findTxB( def findTxB(
self, self,
kbv, kbv: bytes,
Kbs, Kbs: bytes,
cb_swap_value: int, cb_swap_value: int,
cb_block_confirmed: int, cb_block_confirmed: int,
restore_height: int, restore_height: int,
@@ -2711,6 +2792,47 @@ class BTCInterface(Secp256k1Interface):
) )
return pay_fee return pay_fee
def getBLockTxo(
self,
chain_b_lock_txid: bytes,
lock_tx_vout: int,
script_pk: bytes,
) -> (int, int):
if self.useBackend():
backend = self.getBackend()
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
if tx_hex:
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].nValue
else:
self._log.error(
f"spendBLockTx: Output not found in tx {self._log.id(chain_b_lock_txid)}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
)
for i, out in enumerate(lock_tx.vout):
self._log.debug(
f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}"
)
else:
self._log.warning(
f"spendBLockTx: Failed to fetch tx {self._log.id(chain_b_lock_txid)} from backend"
)
locked_n = lock_tx_vout
return locked_n, actual_value
wtx = self.rpc_wallet_watch(
"gettransaction",
[
chain_b_lock_txid.hex(),
],
)
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].nValue
return locked_n, actual_value
def spendBLockTx( def spendBLockTx(
self, self,
chain_b_lock_txid: bytes, chain_b_lock_txid: bytes,
@@ -2724,48 +2846,14 @@ class BTCInterface(Secp256k1Interface):
lock_tx_vout=None, lock_tx_vout=None,
) -> bytes: ) -> bytes:
self._log.info( self._log.info(
"spendBLockTx: {} {}\n".format( f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n"
self._log.id(chain_b_lock_txid), lock_tx_vout
)
) )
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
locked_n = None locked_n, actual_value = self.getBLockTxo(
actual_value = None chain_b_lock_txid, lock_tx_vout, script_pk
if self.useBackend(): )
backend = self.getBackend()
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
if tx_hex:
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].nValue
else:
self._log.error(
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
)
for i, out in enumerate(lock_tx.vout):
self._log.debug(
f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}"
)
else:
self._log.warning(
f"spendBLockTx: Failed to fetch tx {chain_b_lock_txid.hex()} from backend"
)
locked_n = lock_tx_vout
else:
wtx = self.rpc_wallet_watch(
"gettransaction",
[
chain_b_lock_txid.hex(),
],
)
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].nValue
if ( if (
locked_n is not None locked_n is not None
@@ -2774,7 +2862,7 @@ class BTCInterface(Secp256k1Interface):
): ):
self._log.warning( self._log.warning(
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
f"for tx {chain_b_lock_txid.hex()}" f"for tx {self._log.id(chain_b_lock_txid)}"
) )
ensure(locked_n is not None, "Output not found in tx") ensure(locked_n is not None, "Output not found in tx")
@@ -2864,13 +2952,9 @@ class BTCInterface(Secp256k1Interface):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, "bid") self.importWatchOnlyAddress(dest_address, "bid")
self._log.info(f"Imported watch-only addr: {self._log.addr(dest_address)}")
self._log.info( self._log.info(
"Imported watch-only addr: {}".format(self._log.addr(dest_address)) f"Rescanning {self.coin_name()} chain from height: {rescan_from}"
)
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from
)
) )
self.rpc_wallet("rescanblockchain", [rescan_from]) self.rpc_wallet("rescanblockchain", [rescan_from])
@@ -2922,6 +3006,8 @@ class BTCInterface(Secp256k1Interface):
if find_index: if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]]) tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address) rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if rv["index"] is not None and rv["index"] >= 0:
rv["value"] = self.make_int(tx_obj["vout"][rv["index"]]["value"])
if return_txid: if return_txid:
rv["txid"] = txid.hex() rv["txid"] = txid.hex()
@@ -2979,6 +3065,7 @@ class BTCInterface(Secp256k1Interface):
for idx, txout in enumerate(tx.vout): for idx, txout in enumerate(tx.vout):
if txout.scriptPubKey == dest_script: if txout.scriptPubKey == dest_script:
rv["index"] = idx rv["index"] = idx
rv["value"] = txout.nValue
break break
except Exception: except Exception:
pass pass
@@ -3040,6 +3127,7 @@ class BTCInterface(Secp256k1Interface):
for idx, txout in enumerate(tx.vout): for idx, txout in enumerate(tx.vout):
if txout.scriptPubKey == dest_script: if txout.scriptPubKey == dest_script:
rv["index"] = idx rv["index"] = idx
rv["value"] = txout.nValue
break break
except Exception as e: except Exception as e:
self._log.debug( self._log.debug(
@@ -3401,6 +3489,7 @@ class BTCInterface(Secp256k1Interface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
if self.useBackend(): if self.useBackend():
return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee) return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee)
@@ -3408,10 +3497,18 @@ class BTCInterface(Secp256k1Interface):
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
) )
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
"conf_target": self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
@@ -3574,7 +3671,7 @@ class BTCInterface(Secp256k1Interface):
continue continue
if "desc" in u: if "desc" in u:
desc = u["desc"] desc = u["desc"]
if self.using_segwit: if self.using_segwit():
if self.use_p2shp2wsh(): if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"): if not desc.startswith("sh(wpkh"):
continue continue
@@ -3735,7 +3832,7 @@ class BTCInterface(Secp256k1Interface):
ensure( ensure(
sign_for_addr is not None, sign_for_addr is not None,
"Could not find address with enough funds for proof", f"Could not find {self.ticker()} address with enough funds for proof",
) )
self._log.debug(f"sign_for_addr {sign_for_addr}") self._log.debug(f"sign_for_addr {sign_for_addr}")
@@ -4374,14 +4471,16 @@ class BTCInterface(Secp256k1Interface):
return None return None
def isTxExistsError(self, err_str: str) -> bool: def isTxExistsError(self, err_str: str) -> bool:
if self._connection_type == "electrum":
return "Transaction outputs already in utxo set" in err_str
return "Transaction already in block chain" in err_str return "Transaction already in block chain" in err_str
def isTxNonFinalError(self, err_str: str) -> bool: def isTxNonFinalError(self, err_str: str) -> bool:
err_lower = err_str.lower()
return ( return (
"non-BIP68-final" in err_str "non-bip68-final" in err_lower
or "non-final" in err_str or "non-final" in err_lower
or "Missing inputs" in err_str or "locktime requirement not satisfied" in err_lower
or "bad-txns-inputs-missingorspent" in err_str
) )
def combine_non_segwit_prevouts(self): def combine_non_segwit_prevouts(self):
@@ -4428,14 +4527,103 @@ class BTCInterface(Secp256k1Interface):
self._log.id(bytes.fromhex(tx["txid"])) self._log.id(bytes.fromhex(tx["txid"]))
) )
) )
self.publishTx(tx_signed) self.publishTx(bytes.fromhex(tx_signed))
return tx["txid"] return tx["txid"]
def validatePrefundedTxAmounts(self, tx_data: bytes) -> (bytes, int):
# unspent_utxos = self.listUtxos()
tx_obj = self.loadTx(tx_data, allow_witness=False)
def testBTCInterface(): total_out: int = 0
print("TODO: testBTCInterface") total_in: int = 0
for txo in tx_obj.vout:
total_out += txo.nValue
dummy_witness_stack = []
used_utxos = set()
for txi in tx_obj.vin:
txi_txid_hex: str = i2h(txi.prevout.hash)
txi_vout: int = txi.prevout.n
if (txi_txid_hex, txi_vout) in used_utxos:
raise ValueError(f"Duplicate txin {txi_txid_hex} {txi_vout}")
if __name__ == "__main__": prev_tx = self.rpc_wallet("gettransaction", [txi_txid_hex])
testBTCInterface() prev_tx_obj = self.describeTx(prev_tx["hex"])
txo = prev_tx_obj["vout"][txi_vout]
total_in += self.make_int(txo["value"])
dummy_witness_stack.append(self.getP2WPKHDummyWitness())
used_utxos.add((txi_txid_hex, txi_vout))
fee: int = total_in - total_out
witness_bytes_len_est: int = self.getWitnessStackSerialisedLength(
dummy_witness_stack
)
vsize = self.getTxVSize(tx_obj, add_witness_bytes=witness_bytes_len_est)
fee_rate = fee * 1000 // vsize
return bytes.fromhex(txi_txid_hex), fee_rate
def _getTxOutInfoElectrum(self, txid: bytes, n: int, include_mempool: bool = False):
backend = self.getBackend()
if not backend:
return None
try:
tx_info = backend.getTransaction(txid.hex())
if "blockhash" not in tx_info:
return None
confirmations: int = (
0 if "confirmations" not in tx_info else tx_info["confirmations"]
)
if confirmations < 1:
return None
chain_tip_height = self.getChainHeight()
block_height: int = chain_tip_height - (confirmations - 1)
block_hash: bytes = bytes.fromhex(tx_info["blockhash"])
return {
"block_hash": block_hash,
"block_height": block_height,
"block_time": tx_info["blocktime"],
}
except Exception as e:
self._log.debug(f"_findTxnByHashElectrum failed: {e}")
return None
def getTxOutInfo(
self, txid: bytes, n: int, include_mempool: bool = False
) -> dict():
if self._connection_type == "electrum":
return self._getTxOutInfoElectrum(txid, n, include_mempool)
try:
txout = self.rpc("gettxout", [txid.hex(), n, include_mempool])
confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"]
)
if confirmations < 1:
return None
chain_tip_height: int = 0
if "bestblock" in txout:
bestheader_info = self.getBlockHeader(txout["bestblock"])
chain_tip_height = bestheader_info["height"]
else:
chain_tip_height = self.getChainHeight()
if confirmations == 1:
header_info = bestheader_info
else:
block_height: int = chain_tip_height - (confirmations - 1)
header_info = self.getBlockHeaderFromHeight(block_height)
block_hash: bytes = bytes.fromhex(header_info["hash"])
return {
"block_hash": block_hash,
"block_height": header_info["height"],
"block_time": header_info["time"],
}
except Exception as e: # noqa: F841
# self._log.warning(f"gettxout {e}")
return None
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # Copyright (c) 2022-2024 tecnovert
@@ -24,8 +23,13 @@ class DASHInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.DASH return Coins.DASH
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._wallet_passphrase = "" self._wallet_passphrase = ""
self._have_checked_seed = False self._have_checked_seed = False
+164 -39
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,16 +12,15 @@ import logging
import random import random
import traceback import traceback
from typing import List from typing import List, Optional
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScriptNum, CScriptNum,
) )
from basicswap.interface.base import ( from basicswap.interface.base import Secp256k1Interface
Secp256k1Interface, from basicswap.interface.utils import FeeValidator
)
from basicswap.interface.btc import ( from basicswap.interface.btc import (
extractScriptLockScriptValues, extractScriptLockScriptValues,
extractScriptLockRefundScriptValues, extractScriptLockRefundScriptValues,
@@ -82,7 +80,6 @@ from coincurve.ecdsaotves import (
ecdsaotves_rec_enc_key, ecdsaotves_rec_enc_key,
) )
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22 SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000F SEQUENCE_LOCKTIME_MASK = 0x0000F
@@ -182,7 +179,7 @@ def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
return sig, pk return sig, pk
class DCRInterface(Secp256k1Interface): class DCRInterface(FeeValidator, Secp256k1Interface):
@staticmethod @staticmethod
def coin_type(): def coin_type():
@@ -259,13 +256,13 @@ class DCRInterface(Secp256k1Interface):
def depth_spendable() -> int: def depth_spendable() -> int:
return 0 return 0
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(network) self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
super().__init__(coin_settings=coin_settings, network=network, **kwargs)
self._rpc_host = coin_settings.get("rpchost", "127.0.0.1") self._rpc_host = coin_settings.get("rpchost", "127.0.0.1")
self._rpcport = coin_settings["rpcport"] self._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"] self._rpcauth = coin_settings["rpcauth"]
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
if "walletrpcport" in coin_settings: if "walletrpcport" in coin_settings:
self._walletrpcport = coin_settings["walletrpcport"] self._walletrpcport = coin_settings["walletrpcport"]
@@ -421,8 +418,10 @@ class DCRInterface(Secp256k1Interface):
return bci return bci
def getBlockHeader(self, block_hash: str) -> dict:
return self.rpc("getblockheader", [block_hash])
def getWalletInfo(self): def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet("getinfo") rv = self.rpc_wallet("getinfo")
wi = self.rpc_wallet("walletinfo") wi = self.rpc_wallet("walletinfo")
balances = self.rpc_wallet("getbalance") balances = self.rpc_wallet("getbalance")
@@ -597,7 +596,7 @@ class DCRInterface(Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None) override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate: if override_feerate:
self._log.debug( self._log.debug(
"Fee rate override used for %s: %f", self.coin_name(), override_feerate f"Fee rate override used for {self.coin_name()}: {override_feerate}"
) )
return override_feerate, "override_feerate" return override_feerate, "override_feerate"
@@ -858,12 +857,17 @@ class DCRInterface(Secp256k1Interface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
# amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string) # amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string)
float_amount = float(self.format_amount(amount)) float_amount = float(self.format_amount(amount))
txn = self.rpc("createrawtransaction", [[], {addr_to: float_amount}]) txn = self.rpc("createrawtransaction", [[], {addr_to: float_amount}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target) if feerate:
fee_rate = feerate
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
) )
@@ -916,7 +920,7 @@ class DCRInterface(Secp256k1Interface):
found_vout = try_vout found_vout = try_vout
break break
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e})
return None return None
if found_vout is None: if found_vout is None:
@@ -929,13 +933,14 @@ class DCRInterface(Secp256k1Interface):
# TODO: Better way? # TODO: Better way?
if confirmations > 0: if confirmations > 0:
block_height = self.getChainHeight() - confirmations block_height = self.getChainHeight() - (confirmations - 1)
rv = { rv = {
"txid": txid.hex(), "txid": txid.hex(),
"depth": confirmations, "depth": confirmations,
"index": found_vout, "index": found_vout,
"height": block_height, "height": block_height,
"value": self.make_int(txout["value"]),
} }
return rv return rv
@@ -996,6 +1001,10 @@ class DCRInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_value, script)) tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex() return tx.serialize().hex()
def ensureFunds(self, amount: int) -> None:
if self.getSpendableBalance() < amount:
raise ValueError("Balance too low")
def verifyRawTransaction(self, tx_hex: str, prevouts): def verifyRawTransaction(self, tx_hex: str, prevouts):
inputs_valid: bool = True inputs_valid: bool = True
validscripts: int = 0 validscripts: int = 0
@@ -1073,7 +1082,9 @@ class DCRInterface(Secp256k1Interface):
def decodeRawTransaction(self, tx_hex: str): def decodeRawTransaction(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex]) return self.rpc("decoderawtransaction", [tx_hex])
def fundTx(self, tx: bytes, feerate) -> bytes: def fundTx(
self, tx: bytes, feerate: int, lock_unspents: bool = True, subfee: bool = False
) -> bytes:
feerate_str = float(self.format_amount(feerate)) feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
options = { options = {
@@ -1146,6 +1157,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1197,6 +1209,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1248,6 +1261,7 @@ class DCRInterface(Secp256k1Interface):
script_lock_refund script_lock_refund
) )
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1332,6 +1346,7 @@ class DCRInterface(Secp256k1Interface):
assert fee_paid > 0 assert fee_paid > 0
size = len(tx.serialize()) + add_witness_bytes size = len(tx.serialize()) + add_witness_bytes
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1393,6 +1408,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1465,6 +1481,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1526,6 +1543,7 @@ class DCRInterface(Secp256k1Interface):
prevout_script prevout_script
) )
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1776,32 +1794,41 @@ class DCRInterface(Secp256k1Interface):
spend_actual_balance: bool = False, spend_actual_balance: bool = False,
lock_tx_vout=None, lock_tx_vout=None,
) -> bytes: ) -> bytes:
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex()) self._log.info(
f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n"
)
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
locked_n = None locked_n = None
actual_value = None actual_value = None
wtx = self.rpc_wallet( try:
"gettransaction", wtx = self.rpc_wallet(
[ "gettransaction",
chain_b_lock_txid.hex(), [
], chain_b_lock_txid.hex(),
) ],
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
locked_n = findOutput(lock_tx, script_pk)
if locked_n is not None:
actual_value = lock_tx.vout[locked_n].value
else:
self._log.error(
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
) )
for i, out in enumerate(lock_tx.vout): lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
self._log.debug( locked_n = findOutput(lock_tx, script_pk)
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}" if locked_n is not None:
actual_value = lock_tx.vout[locked_n].value
else:
self._log.error(
f"spendBLockTx: Output not found in tx {self._log.id(chain_b_lock_txid)}, "
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
) )
for i, out in enumerate(lock_tx.vout):
self._log.debug(
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
)
except Exception as e: # noqa: F841
txout = self.rpc(
"gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, 0, True]
)
actual_value = self.make_int(txout["value"])
locked_n = lock_tx_vout
if ( if (
locked_n is not None locked_n is not None
@@ -1810,7 +1837,7 @@ class DCRInterface(Secp256k1Interface):
): ):
self._log.warning( self._log.warning(
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
f"for tx {chain_b_lock_txid.hex()}" f"for tx {self._log.id(chain_b_lock_txid)}"
) )
ensure(locked_n is not None, "Output not found in tx") ensure(locked_n is not None, "Output not found in tx")
@@ -1846,14 +1873,14 @@ class DCRInterface(Secp256k1Interface):
try: try:
txout = self.rpc("gettxout", [txid_hex, 0, 0, True]) txout = self.rpc("gettxout", [txid_hex, 0, 0, True])
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e}"))
return None return None
confirmations: int = ( confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"] 0 if "confirmations" not in txout else txout["confirmations"]
) )
if confirmations >= self.blocks_confirmed: if confirmations >= self.blocks_confirmed:
block_height = self.getChainHeight() - confirmations # TODO: Better way? block_height = self.getChainHeight() - (confirmations - 1)
return {"txid": txid_hex, "amount": 0, "height": block_height} return {"txid": txid_hex, "amount": 0, "height": block_height}
return None return None
@@ -1868,3 +1895,101 @@ class DCRInterface(Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool: def isTxNonFinalError(self, err_str: str) -> bool:
return "locks on inputs not met" in err_str return "locks on inputs not met" in err_str
def getChainMedianTime(self) -> int:
bestblockhash = self.rpc("getbestblockhash")
bestblockheader = self.rpc(
"getblockheader",
[
bestblockhash,
],
)
return bestblockheader["mediantime"]
def getTxLocktime(self, tx_data: bytes) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.locktime
def getTxInSequence(self, tx_data: bytes, vout: int) -> int:
tx_obj = self.loadTx(tx_data)
return tx_obj.vin[vout].sequence
def isCsvLockMature(
self,
lock_type: int,
encoded_sequence: int,
parent_block_height: Optional[int],
parent_block_time: Optional[int],
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if parent_block_height is None or parent_block_height < 1:
return False
lock_value: int = self.decodeSequence(encoded_sequence)
if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= parent_block_height + lock_value
if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME:
if parent_block_time is None or parent_block_time < 1:
return False
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= parent_block_time + lock_value
raise ValueError(f"Unknown lock type {lock_type}")
def isAbsLockTimeMature(
self,
nlocktime: int,
chain_height: Optional[int] = None,
chain_mtp: Optional[int] = None,
) -> bool:
if nlocktime == 0:
return True
if nlocktime < 500000000:
if chain_height is None:
chain_height = self.getChainHeight()
return chain_height + 1 >= nlocktime
if chain_mtp is None:
chain_mtp = self.getChainMedianTime()
return chain_mtp >= nlocktime
def getTxOutInfo(
self, txid: bytes, n: int, include_mempool: bool = False
) -> dict():
try:
txout = self.rpc("gettxout", [txid.hex(), n, 0, include_mempool])
confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"]
)
if confirmations < 1:
return None
chain_tip_height: int = 0
if "bestblock" in txout:
bestheader_info = self.getBlockHeader(txout["bestblock"])
chain_tip_height = bestheader_info["height"]
else:
chain_tip_height = self.getChainHeight()
if confirmations == 1:
header_info = bestheader_info
else:
block_height: int = chain_tip_height - (confirmations - 1)
header_info = self.getBlockHeaderFromHeight(block_height)
block_hash: bytes = bytes.fromhex(header_info["hash"])
return {
"block_hash": block_hash,
"block_height": header_info["height"],
"block_time": header_info["time"],
}
except Exception as e: # noqa: F841
# self._log.warning(f"gettxout {e}")
return None
def is_transient_error(self, ex) -> bool:
str_error: str = str(ex).lower()
if "no information for transaction" in str_error:
return True
return super().is_transient_error(ex)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
+5 -4
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,10 +10,10 @@ import traceback
from basicswap.rpc import Jsonrpc from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"): def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1", timeout=None):
try: try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port) url = "http://{}@{}:{}/".format(auth, host, rpc_port)
x = Jsonrpc(url) x = Jsonrpc(url, timeout=timeout if timeout else 10)
x.__handler = None x.__handler = None
v = x.json_request(method, params) v = x.json_request(method, params)
x.close() x.close()
@@ -41,7 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
auth = auth auth = auth
host = host host = host
def rpc_func(method, params=None): def rpc_func(method, params=None, timeout=None):
return callrpc(port, auth, method, params, host) return callrpc(port, auth, method, params, host, timeout=timeout)
return rpc_func return rpc_func
+1 -1
View File
@@ -13,7 +13,7 @@ import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event): def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info("Creating DCR wallet") logging.info("Creating DCR wallet")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read pipe_r, pipe_w = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt": if os.name == "nt":
str_args = " ".join(args) str_args = " ".join(args)
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The BasicSwap developers # Copyright (c) 2024 The BasicSwap developers
@@ -32,8 +31,13 @@ class DOGEInterface(BTCInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int: def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 192 return 192
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(DOGEInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
def getScriptDest(self, script: bytearray) -> bytearray: def getScriptDest(self, script: bytearray) -> bytearray:
# P2SH # P2SH
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
+16 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
@@ -38,8 +37,13 @@ class FIROInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.FIRO return Coins.FIRO
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(FIROInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support # No multiwallet support
self.rpc_wallet = make_rpc_func( self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host self._rpcport, self._rpcauth, host=self._rpc_host
@@ -272,6 +276,8 @@ class FIROInterface(BTCInterface):
if find_index: if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]]) tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address) rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if rv["index"] is not None and rv["index"] >= 0:
rv["value"] = self.make_int(tx_obj["vout"][rv["index"]]["value"])
if return_txid: if return_txid:
rv["txid"] = txid.hex() rv["txid"] = txid.hex()
@@ -300,11 +306,16 @@ class FIROInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
) )
fee_rate, fee_src = self.get_fee_rate(self._conf_target) if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
) )
@@ -361,7 +372,7 @@ class FIROInterface(BTCInterface):
) )
return pay_fee return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key) key_wif = self.encodeKey(key)
rv = self.rpc( rv = self.rpc(
"signrawtransaction", "signrawtransaction",
+101 -113
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2023 tecnovert
@@ -16,8 +15,13 @@ class LTCInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.LTC return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(LTCInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb") self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet_mweb = make_rpc_func( self.rpc_wallet_mweb = make_rpc_func(
self._rpcport, self._rpcport,
@@ -26,87 +30,6 @@ class LTCInterface(BTCInterface):
wallet=self._rpc_wallet_mweb, wallet=self._rpc_wallet_mweb,
) )
def checkWallets(self) -> int:
if self._connection_type == "electrum":
wm = self.getWalletManager()
if wm and wm.isInitialized(self.coin_type()):
return 1
return 0
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
self._log.debug(
f"Wallet: {self._rpc_wallet} not active, attempting to load."
)
try:
self.rpc(
"loadwallet",
[
self._rpc_wallet,
],
)
wallets = self.rpc("listwallets")
except Exception as e:
self._log.debug(f'Error loading wallet "{self._rpc_wallet}": {e}.')
if "does not exist" in str(e) or "Path does not exist" in str(e):
try:
wallet_dirs = self.rpc("listwalletdir")
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
except Exception:
existing = []
if len(existing) == 0:
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
)
try:
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
"",
False,
self._use_descriptors,
],
)
wallets = self.rpc("listwallets")
if self.getWalletSeedID() == "Not found":
self._log.info(
f"Initializing HD seed for {self.coin_name()}."
)
self._sc.initialiseWallet(self.coin_type())
except Exception as create_e:
self._log.error(f"Error creating wallet: {create_e}")
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.warning(f"Changing {self.ticker()} wallet name.")
for wallet_name in wallets:
if wallet_name in ("mweb",):
continue
change_watchonly_wallet: bool = (
self._rpc_wallet_watch == self._rpc_wallet
)
self._rpc_wallet = wallet_name
self._log.info(
f"Switched {self.ticker()} wallet name to {self._rpc_wallet}."
)
self.rpc_wallet = make_rpc_func(
self._rpcport,
self._rpcauth,
host=self._rpc_host,
wallet=self._rpc_wallet,
)
if change_watchonly_wallet:
self.rpc_wallet_watch = self.rpc_wallet
break
return len(wallets)
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str: def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
if self.useBackend(): if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode") raise ValueError("MWEB addresses not supported in electrum mode")
@@ -172,9 +95,14 @@ class LTCInterface(BTCInterface):
continue continue
if "address" not in u: if "address" not in u:
continue continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
if "desc" in u: if "desc" in u:
desc = u["desc"] desc = u["desc"]
if self.using_segwit: if self.using_segwit():
if self.use_p2shp2wsh(): if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"): if not desc.startswith("sh(wpkh"):
continue continue
@@ -184,11 +112,81 @@ class LTCInterface(BTCInterface):
else: else:
if not desc.startswith("pkh"): if not desc.startswith("pkh"):
continue continue
unspent_addr[u["address"]] = unspent_addr.get( unspent_addr[utxo_address] = unspent_addr.get(
u["address"], 0 utxo_address, 0
) + self.make_int(u["amount"], r=1) ) + self.make_int(u["amount"], r=1)
return unspent_addr return unspent_addr
def getMWEBBalance(self) -> int:
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
value: int = 0
unspent = self.rpc_wallet(
"listunspent",
[
0,
],
)
for u in unspent:
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
value += self.make_int(u["amount"], r=1)
return value
def convertMWEBBalance(self):
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
self._log.info(f"convertMWEBBalance - {self.ticker()}")
locked_before = self.rpc_wallet("listlockunspent")
lock_utxos = []
try:
# Hack: mark all the other utxos as unspendable, alternative is to use a mweb_transfer wallet
utxos = self.rpc_wallet("listunspent")
mweb_amount: int = 0
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
mweb_amount += self.make_int(utxo["amount"], r=1)
continue
utxo_op = {"txid": utxo["txid"], "vout": utxo["vout"]}
if utxo_op in locked_before:
continue
lock_utxos.append(utxo_op)
if mweb_amount == 0:
raise ValueError("No MWEB outputs to convert")
self.rpc_wallet("lockunspent", [False, lock_utxos])
subfee_to_mweb: bool = True
convert_value = self.format_amount(mweb_amount)
plain_addr: str = self.rpc_wallet("getnewaddress", ["transfer", "bech32"])
# Double check generated address is owned by this wallet
if not self.isAddressMine(plain_addr):
raise ValueError("Generated address not owned by wallet!")
params = [
plain_addr,
convert_value,
"",
"",
subfee_to_mweb,
True,
self._conf_target,
]
txid = self.rpc_wallet("sendtoaddress", params)
self._log.info(f"MWEB in plain converted in txid: {self._log.id(txid)}")
return txid
finally:
self.rpc_wallet("lockunspent", [True, lock_utxos])
def unlockWallet(self, password: str, check_seed: bool = True) -> None: def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "": if password == "":
return return
@@ -271,8 +269,13 @@ class LTCInterfaceMWEB(LTCInterface):
def interface_type(self) -> int: def interface_type(self) -> int:
return Coins.LTC_MWEB return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb") self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet = make_rpc_func( self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
@@ -306,23 +309,24 @@ class LTCInterfaceMWEB(LTCInterface):
def init_wallet(self, password=None): def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock # If system is encrypted mweb wallet will be created at first unlock
self._log.info("init_wallet - {}".format(self.ticker())) wallet_name: str = self._rpc_wallet
self._log.info(f"init_wallet - {self.ticker()}")
wallets = self.rpc("listwallets") wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets: if wallet_name not in wallets:
try: try:
self.rpc("loadwallet", [self._rpc_wallet]) self.rpc("loadwallet", [wallet_name])
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".') self._log.debug(f'Loaded existing wallet "{wallet_name}".')
except Exception as e: except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e): if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info( self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.' f'Creating wallet "{wallet_name}" for {self.coin_name()}.'
) )
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc( self.rpc(
"createwallet", "createwallet",
[ [
self._rpc_wallet, wallet_name,
False, False,
True, True,
password, password,
@@ -333,22 +337,6 @@ class LTCInterfaceMWEB(LTCInterface):
else: else:
raise raise
wallets = self.rpc("listwallets")
if "mweb" not in wallets:
try:
self.rpc("loadwallet", ["mweb"])
self._log.debug("Loaded existing MWEB wallet.")
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info(f"Creating MWEB 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],
)
else:
raise
if password is not None: if password is not None:
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
@@ -357,8 +345,8 @@ class LTCInterfaceMWEB(LTCInterface):
self._sc.initialiseWallet(self.interface_type()) self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain() # Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"]) self.rpc("unloadwallet", [wallet_name])
self.rpc("loadwallet", ["mweb"]) self.rpc("loadwallet", [wallet_name])
if password is not None: if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill") self.rpc_wallet("keypoolrefill")
+22 -5
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert # Copyright (c) 2023 tecnovert
@@ -73,8 +72,13 @@ class NAVInterface(BTCInterface):
def txoType(): def txoType():
return CTxOut return CTxOut
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(NAVInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support # No multiwallet support
self.rpc_wallet = make_rpc_func( self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host self._rpcport, self._rpcauth, host=self._rpc_host
@@ -311,11 +315,16 @@ class NAVInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
) )
fee_rate, fee_src = self.get_fee_rate(self._conf_target) if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
) )
@@ -605,6 +614,8 @@ class NAVInterface(BTCInterface):
if find_index: if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]]) tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address) rv["index"] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if rv["index"] is not None and rv["index"] >= 0:
rv["value"] = self.make_int(tx_obj["vout"][rv["index"]]["value"])
if return_txid: if return_txid:
rv["txid"] = txid.hex() rv["txid"] = txid.hex()
@@ -751,7 +762,13 @@ class NAVInterface(BTCInterface):
return tx.serialize() return tx.serialize()
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True): def fundTx(
self,
tx_hex: str,
feerate: int,
lock_unspents: bool = True,
subfee: bool = False,
):
feerate_str = self.format_amount(feerate) feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
options = { options = {
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert # Copyright (c) 2020-2022 tecnovert
+32 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -81,8 +80,17 @@ class PARTInterface(BTCInterface):
def txoType(): def txoType():
return CTxOutPart return CTxOutPart
def __init__(self, coin_settings, network, swap_client=None): @staticmethod
super().__init__(coin_settings, network, swap_client) def defaultMaxFeeRate() -> int:
return PARTInterface.COIN() // 2
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.setAnonTxRingSize(int(coin_settings.get("anon_tx_ring_size", 12))) self.setAnonTxRingSize(int(coin_settings.get("anon_tx_ring_size", 12)))
def use_tx_vsize(self) -> bool: def use_tx_vsize(self) -> bool:
@@ -1231,6 +1239,7 @@ class PARTInterfaceBlind(PARTInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
# Estimate lock tx size / fee # Estimate lock tx size / fee
@@ -1270,9 +1279,17 @@ class PARTInterfaceBlind(PARTInterface):
} }
} }
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
)
options = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
"conf_target": self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
@@ -1282,6 +1299,17 @@ class PARTInterfaceBlind(PARTInterface):
"fundrawtransactionfrom", ["blind", tx_hex, {}, outputs_info, options] "fundrawtransactionfrom", ["blind", tx_hex, {}, outputs_info, options]
)["hex"] )["hex"]
def getLockRefundVout(self, lock_refund_tx_data: bytes, vkbv: bytes):
lock_refund_tx_obj = self.rpc(
"decoderawtransaction", [lock_refund_tx_data.hex()]
)
# Nonce is derived from vkbv
nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock refund tx to spend
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
return spend_n
class PARTInterfaceAnon(PARTInterface): class PARTInterfaceAnon(PARTInterface):
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert # Copyright (c) 2021 tecnovert
@@ -10,8 +9,13 @@ from basicswap.contrib.test_framework.messages import CTxOut
class PassthroughBTCInterface(BTCInterface): class PassthroughBTCInterface(BTCInterface):
def __init__(self, coin_settings, network): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(coin_settings, network) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.txoType = CTxOut self.txoType = CTxOut
self._network = network self._network = network
self.blocks_confirmed = coin_settings["blocks_confirmed"] self.blocks_confirmed = coin_settings["blocks_confirmed"]
+30 -27
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert # Copyright (c) 2022 tecnovert
@@ -12,7 +11,7 @@ from .btc import BTCInterface
from basicswap.rpc import make_rpc_func from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction from .contrib.pivx_test_framework.messages import CTransaction
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
OP_DUP, OP_DUP,
@@ -27,8 +26,13 @@ class PIVXInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.PIVX return Coins.PIVX
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super(PIVXInterface, self).__init__(coin_settings, network, swap_client) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support # No multiwallet support
self.rpc_wallet = make_rpc_func( self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host self._rpcport, self._rpcauth, host=self._rpc_host
@@ -74,11 +78,16 @@ class PIVXInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
) )
fee_rate, fee_src = self.get_fee_rate(self._conf_target) if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
) )
@@ -100,29 +109,13 @@ class PIVXInterface(BTCInterface):
return decodeAddress(address)[1:] return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash): def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader block = self.rpc("getblock", [block_hash, True])
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = [] tx_rv = []
for tx in decoded_block.vtx: for txid_str in block["tx"]:
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)]) tx_dec = self.rpc("getrawtransaction", [txid_str, True])
tx_rv.append(tx_dec) tx_rv.append(tx_dec)
block["tx"] = tx_rv
block_rv = { return block
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
def withdrawCoin(self, value, addr_to, subfee): def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee] params = [addr_to, value, "", "", subfee]
@@ -150,7 +143,7 @@ class PIVXInterface(BTCInterface):
) )
return pay_fee return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes: def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key) key_wif = self.encodeKey(key)
rv = self.rpc( rv = self.rpc(
"signrawtransaction", "signrawtransaction",
@@ -177,3 +170,13 @@ class PIVXInterface(BTCInterface):
block_height = self.getBlockHeader(rv["blockhash"])["height"] block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height} return {"txid": txid_hex, "amount": 0, "height": block_height}
return None return None
def getChainMedianTime(self) -> int:
bestblockhash = self.rpc("getbestblockhash")
bestblockheader = self.rpc(
"getblockheader",
[
bestblockhash,
],
)
return bestblockheader["mediantime"]
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2026 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.contrib.test_framework.messages import COIN
class FeeValidator:
@staticmethod
def defaultMaxFeeRate() -> int:
return COIN // 10
def makeIntFromSetting(
self, settings: dict, setting_name: str, default: int
) -> int:
# Return make_int(setting), or already integer default
if setting_name in settings:
return self.make_int(settings[setting_name])
return default
def __init__(self, **kwargs):
default_low_fee_conf_target: int = 24
default_low_fee_rate: int = 0
default_high_estimated_feerate_multiplier: float = 4.0
default_high_fee_rate: int = self.defaultMaxFeeRate()
if self._sc:
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
settings = self._sc.settings
default_low_fee_conf_target = int(
settings.get("low_fee_conf_target", default_low_fee_conf_target)
)
default_low_fee_rate = self.makeIntFromSetting(
settings, "low_feerate", default_low_fee_rate
)
default_high_estimated_feerate_multiplier = float(
settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
default_high_fee_rate = self.makeIntFromSetting(
settings, "high_feerate", default_high_fee_rate
)
else:
if kwargs.get("network") != "regtest":
raise ValueError("swapclient unset")
chain_client_settings = {}
self._low_fee_conf_target = int(
chain_client_settings.get(
"low_fee_conf_target", default_low_fee_conf_target
)
)
self._low_feerate = self.makeIntFromSetting(
chain_client_settings, "low_feerate", default_low_fee_rate
)
# Set below 1.0 to disable estimating the max feerate and use max_feerate
self._high_estimated_feerate_multiplier = float(
chain_client_settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
self._high_feerate = self.makeIntFromSetting(
chain_client_settings, "high_feerate", default_high_fee_rate
)
super().__init__(**kwargs)
def validateFeeRate(self, feerate: int) -> None:
if self._low_feerate > 0:
min_feerate_src = "set_value"
min_feerate = self._low_feerate
else:
min_feerate, min_feerate_src = self.get_fee_rate(self._low_fee_conf_target)
min_feerate = self.make_int(min_feerate)
if self._high_estimated_feerate_multiplier >= 1.0:
max_feerate, max_feerate_src = self.get_fee_rate()
max_feerate = int(
self.make_int(max_feerate) * self._high_estimated_feerate_multiplier
)
else:
max_feerate_src = "set_value"
max_feerate = self._high_feerate
if max_feerate_src in ("estimatesmartfee", "electrum"):
if max_feerate > self._high_feerate:
max_feerate_src = "clamped_to_set_value"
max_feerate = self._high_feerate
self._log.debug(
f"Verify {self.ticker()} fee rate {feerate}, min {min_feerate} {min_feerate_src}, max {max_feerate} {max_feerate_src}"
)
if feerate < min_feerate:
err_msg: str = (
f"Fee rate too low, {feerate} < {min_feerate}, {min_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
if feerate > max_feerate:
err_msg: str = (
f"Fee rate too high, {feerate} > {max_feerate}, {max_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
+10 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -34,7 +33,6 @@ from basicswap.rpc_xmr import make_xmr_rpc_func, make_xmr_rpc2_func
from basicswap.chainparams import XMR_COIN, Coins from basicswap.chainparams import XMR_COIN, Coins
from basicswap.interface.base import CoinInterface from basicswap.interface.base import CoinInterface
ed25519_l = 2**252 + 27742317777372353535851937790883648493 ed25519_l = 2**252 + 27742317777372353535851937790883648493
@@ -102,8 +100,13 @@ class XMRInterface(CoinInterface):
return True return True
return super().is_transient_error(ex) return super().is_transient_error(ex)
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(network) super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._addr_prefix = self.chainparams_network()["address_prefix"] self._addr_prefix = self.chainparams_network()["address_prefix"]
@@ -858,3 +861,6 @@ class XMRInterface(CoinInterface):
except Exception as e: except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}") self._log.error(f"listWalletTransactions failed: {e}")
return [] return []
def validateFeeRate(self, fee_rate: int) -> None:
pass # Fee rate isn't used
+63 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -129,7 +129,6 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
try: try:
swap_client.updateWalletsInfo() swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo() wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = [] coins_with_balances = []
@@ -193,6 +192,28 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
coin_entry["electrum_synced"] = sync_status.get("synced", False) coin_entry["electrum_synced"] = sync_status.get("synced", False)
coin_entry["electrum_height"] = sync_status.get("height", 0) coin_entry["electrum_height"] = sync_status.get("height", 0)
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if k == Coins.PART:
for field in ("blind_balance", "anon_balance"):
if field in w:
raw = w[field]
if isinstance(raw, float):
coin_entry[field] = f"{raw:.8f}".rstrip(
"0"
).rstrip(".")
elif isinstance(raw, int):
coin_entry[field] = str(raw)
else:
coin_entry[field] = raw
elif k == Coins.LTC:
if "mweb_balance" in w:
coin_entry["mweb_balance"] = w["mweb_balance"]
elif k == Coins.FIRO:
if "spark_balance" in w:
coin_entry["spark_balance"] = w["spark_balance"]
coins_with_balances.append(coin_entry) coins_with_balances.append(coin_entry)
if k == Coins.PART: if k == Coins.PART:
@@ -290,7 +311,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
if len(url_split) > 3: if len(url_split) > 3:
ticker_str = url_split[3] ticker_str = url_split[3]
coin_type = getCoinIdFromTicker(ticker_str) coin_type = getCoinIdFromTicker(ticker_str, inc_variant=True)
if len(url_split) > 4: if len(url_split) > 4:
cmd = url_split[4] cmd = url_split[4]
@@ -332,6 +353,18 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes( return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8" json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
) )
elif cmd == "mwebbalance":
# mweb outputs left behind when sending LTC -> MWEB
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
ci = swap_client.ci(coin_type)
return bytes(json.dumps(ci.format_amount(ci.getMWEBBalance())), "UTF-8")
elif cmd == "convertmweb":
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
return bytes(
json.dumps(swap_client.ci(coin_type).convertMWEBBalance()), "UTF-8"
)
elif cmd == "watchaddress": elif cmd == "watchaddress":
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, "address") address = get_data_entry(post_data, "address")
@@ -1912,6 +1945,32 @@ def js_electrum_discover(self, url_split, post_string, is_json) -> bytes:
) )
def js_getsubfeebidtx(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
offer_id = bytes.fromhex(get_data_entry(post_data, "offer_id"))
offer = swap_client.getOffer(offer_id)
ensure(offer, "Offer not found.")
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
amount_to: int = inputAmount(get_data_entry(post_data, "amount_to"), ci_to)
bid_rate: int = ci_to.make_int(get_data_entry(post_data, "bid_rate"), r=1)
prefunded_data = swap_client.createSubfeeBidTx(offer_id, amount_to, bid_rate)
return bytes(
json.dumps(
{
"amount_from": ci_from.format_amount(prefunded_data["amount_from"]),
"amount_to": ci_to.format_amount(prefunded_data["amount_to"]),
"bid_tx": prefunded_data["bid_tx"].hex(),
}
),
"UTF-8",
)
endpoints = { endpoints = {
"coins": js_coins, "coins": js_coins,
"walletbalances": js_walletbalances, "walletbalances": js_walletbalances,
@@ -1948,6 +2007,7 @@ endpoints = {
"messageroutes": js_messageroutes, "messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover, "electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo, "modeswitchinfo": js_modeswitchinfo,
"getsubfeebidtx": js_getsubfeebidtx,
} }
-1
View File
@@ -23,7 +23,6 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
from basicswap.util.integer import encode_varint, decode_varint from basicswap.util.integer import encode_varint, decode_varint
NPBW_INT = 0 NPBW_INT = 0
NPBW_BYTES = 2 NPBW_BYTES = 2
-1
View File
@@ -39,7 +39,6 @@ from basicswap.contrib.rfc6979 import (
rfc6979_hmac_sha256_generate, rfc6979_hmac_sha256_generate,
) )
START_TOKEN = 0xABCD START_TOKEN = 0xABCD
MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big") MSG_START_TOKEN = START_TOKEN.to_bytes(2, "big")
+1 -1
View File
@@ -53,7 +53,7 @@ def initSimplexClient(args, logger, delay_event):
# TODO: Must be a better way? # TODO: Must be a better way?
logger.info("Initialising Simplex client") logger.info("Initialising Simplex client")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read pipe_r, pipe_w = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt": if os.name == "nt":
str_args = " ".join(args) str_args = " ".join(args)
+2 -5
View File
@@ -15,9 +15,6 @@ from basicswap.interface.btc import (
class ProtocolInterface: class ProtocolInterface:
swap_type = None swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError("base class")
def getMockScript(self) -> bytearray: def getMockScript(self) -> bytearray:
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1]) return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
@@ -29,7 +26,7 @@ class ProtocolInterface:
else ci.get_p2sh_script_pubkey(script) else ci.get_p2sh_script_pubkey(script)
) )
def getMockAddrTo(self, ci): def getMockScriptAddr(self, ci):
script = self.getMockScript() script = self.getMockScript()
return ( return (
ci.encodeScriptDest(ci.getScriptDest(script)) ci.encodeScriptDest(ci.getScriptDest(script))
@@ -38,5 +35,5 @@ class ProtocolInterface:
) )
def findMockVout(self, ci, itx_decoded): def findMockVout(self, ci, itx_decoded):
mock_addr = self.getMockAddrTo(ci) mock_addr = self.getMockScriptAddr(ci)
return find_vout_for_address_from_txobj(itx_decoded, mock_addr) return find_vout_for_address_from_txobj(itx_decoded, mock_addr)
+10 -4
View File
@@ -138,12 +138,18 @@ def redeemITx(self, bid_id: bytes, cursor):
class AtomicSwapInterface(ProtocolInterface): class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST swap_type = SwapTypes.SELLER_FIRST
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: def getFundedInitiateTxTemplate(
addr_to = self.getMockAddrTo(ci) self,
ci,
amount: int,
sub_fee: bool,
feerate: int = None,
lock_unspents: bool = False,
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction( funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False addr_to, amount, sub_fee, lock_unspents=lock_unspents, feerate=feerate
) )
return bytes.fromhex(funded_tx) return bytes.fromhex(funded_tx)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
+113 -15
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,6 +11,7 @@ from basicswap.util import (
ensure, ensure,
) )
from basicswap.interface.base import Curves from basicswap.interface.base import Curves
from basicswap.interface.btc import findOutput
from basicswap.chainparams import ( from basicswap.chainparams import (
Coins, Coins,
) )
@@ -49,11 +50,11 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
try: try:
use_cursor = self.openDB(cursor) use_cursor = self.openDB(cursor)
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id) bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
ensure(bid, "Bid not found: {}.".format(bid_id.hex())) ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex())) ensure(xmr_swap, f"Adaptor-sig swap not found: {self.log.id(bid_id)}.")
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id) offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex())) ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex())) ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(bid.offer_id)}.")
# The no-script coin is always the follower # The no-script coin is always the follower
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to) reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
@@ -105,7 +106,10 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
address_to = self.getReceiveAddressFromPool( address_to = self.getReceiveAddressFromPool(
base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor
) )
amount = bid.amount_to amount: int = bid.amount_to
chain_b_fee_rate: int = (
xmr_offer.a_fee_rate if reverse_bid else xmr_offer.b_fee_rate
)
lock_tx_vout = bid.getLockTXBVout() lock_tx_vout = bid.getLockTXBVout()
txid = ci_follower.spendBLockTx( txid = ci_follower.spendBLockTx(
xmr_swap.b_lock_tx_id, xmr_swap.b_lock_tx_id,
@@ -113,7 +117,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
xmr_swap.vkbv, xmr_swap.vkbv,
vkbs, vkbs,
amount, amount,
xmr_offer.b_fee_rate, chain_b_fee_rate,
bid.chain_b_height_start, bid.chain_b_height_start,
spend_actual_balance=True, spend_actual_balance=True,
lock_tx_vout=lock_tx_vout, lock_tx_vout=lock_tx_vout,
@@ -203,9 +207,12 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface): class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP swap_type = SwapTypes.XMR_SWAP
_mock_key: bytes = bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
)
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there # Fallthrough to ci if genScriptLockTxScript is implemented there
if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript): if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs) return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
@@ -214,20 +221,78 @@ class XmrSwapInterface(ProtocolInterface):
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)]) return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: def getMockScriptAddr(self, ci):
addr_to = self.getMockAddrTo(ci) script = self.getMockScript()
funded_tx = ci.createRawFundedTransaction( if ci.coin_type() == Coins.PART:
addr_to, amount, sub_fee, lock_unspents=False # Use btc-segwit address to match createSCLockTx()
# _use_segwit is false for Particl
return ci.encode_p2wsh(ci.getScriptDest(script))
return (
ci.encodeScriptDest(ci.getScriptDest(script))
if ci._use_segwit
else ci.encode_p2sh(script)
) )
def getMockScriptScriptPubkey(self, ci) -> bytearray:
script = self.getMockScript()
if ci.coin_type() == Coins.PART:
# Use btc-segwit address to match createSCLockTx()
# _use_segwit is false for Particl
return ci.getScriptDest(script)
return (
ci.getScriptDest(script)
if ci._use_segwit
else ci.get_p2sh_script_pubkey(script)
)
def getFundedInitiateTxTemplate(
self,
ci,
amount: int,
sub_fee: bool,
feerate: int = None,
lock_unspents: bool = False,
) -> bytes:
if ci.coin_type() == Coins.BCH:
# Workaround, BCH getScriptDest() uses OP_HASH256
script: bytes = self.getMockScript()
addr_to: bytes = ci.getScriptDest(script)
else:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=lock_unspents, feerate=feerate
)
return bytes.fromhex(funded_tx) return bytes.fromhex(funded_tx)
def getMockITxSwapValue(self, ci, tx_data: bytes) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockITxSwapVout(self, ci, tx_obj) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci) if ci.coin_type() == Coins.BCH:
real_txo_script = ci.getScriptDest(script) mock_script: bytes = self.getMockScript()
mock_txo_script: bytes = ci.getScriptDest(mock_script)
else:
mock_txo_script: bytes = self.getMockScriptScriptPubkey(ci)
real_txo_script: bytes = ci.getScriptDest(script)
found: int = 0 found: int = 0
ctx = ci.loadTx(mock_tx) ctx = ci.loadTx(mock_tx, allow_witness=False)
for txo in ctx.vout: for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script: if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script txo.scriptPubKey = real_txo_script
@@ -240,3 +305,36 @@ class XmrSwapInterface(ProtocolInterface):
ctx.nLockTime = 0 ctx.nLockTime = 0
return ctx.serialize() return ctx.serialize()
def getMockPubkey(self, ci) -> bytes:
return ci.getPubkey(self._mock_key)
def getMockPTxSwapValue(self, ci, tx_data: bytes) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockPTxSwapVout(self, ci, tx_obj) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockPTx(self, ci, tx_data: bytes, kbv: bytes, Kbs: bytes) -> bytes:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
tx_obj.vout[lock_vout].scriptPubKey = ci.getPkDest(Kbs)
return tx_obj.serialize()
+76 -2
View File
@@ -40,6 +40,10 @@
}); });
}, },
confirmMWEBChangeConvert: function() {
return confirm('Confirm MWEB change conversion: This will create a tx sending all spendable MWEB outputs in the plain LTC wallet to LTC.');
},
confirmReseed: function() { confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.'); return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
}, },
@@ -60,7 +64,7 @@
}, },
fillDonationAddress: function(address, coinType) { fillDonationAddress: function(address, coinType) {
let addressInput = null; let addressInput = null;
addressInput = window.DOMCache addressInput = window.DOMCache
@@ -179,6 +183,55 @@
} }
}, },
setBidAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: Bid amount input not found:', inputId);
return;
}
const haveBalance = amountInput.getAttribute('haveamount');
if (!haveBalance) {
console.error('EventHandlers: Balance not found for bid');
return;
}
const floatBalance = parseFloat(haveBalance);
if (isNaN(floatBalance)) {
alert('Invalid bid balance');
return;
}
const maxAmount = amountInput.getAttribute('max');
if (!maxAmount) {
console.error('EventHandlers: Max amount not found for bid');
return;
}
const floatMax = parseFloat(maxAmount);
if (isNaN(floatMax) || floatMax <= 0) {
alert('Invalid bid max amount');
return;
}
const coinExp = amountInput.getAttribute('exp');
if (!coinExp) {
console.error('EventHandlers: Coin exp not found for bid');
return;
}
let calculatedAmount = maxAmount * percent;
if (floatBalance < calculatedAmount) {
calculatedAmount = floatBalance;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
checkbox.checked = true;
}
}
amountInput.value = calculatedAmount.toFixed(coinExp);
window.updateBidParams('sending');
},
hideConfirmModal: function() { hideConfirmModal: function() {
const modal = document.getElementById('confirmModal'); const modal = document.getElementById('confirmModal');
if (modal) { if (modal) {
@@ -188,7 +241,6 @@
}, },
lookup_rates: function() { lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') { if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates(); window.lookup_rates();
} else { } else {
@@ -282,6 +334,16 @@
} }
}); });
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-mweb-change-convert]');
if (target) {
if (!this.confirmMWEBChangeConvert()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]'); const target = e.target.closest('[data-confirm-utxo]');
if (target) { if (target) {
@@ -332,6 +394,16 @@
} }
}); });
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-bid-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-bid-amount'));
const inputId = target.getAttribute('data-input-id');
this.setBidAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]'); const target = e.target.closest('[data-reset-form]');
if (target) { if (target) {
@@ -398,12 +470,14 @@
window.EventHandlers = EventHandlers; window.EventHandlers = EventHandlers;
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers); window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers); window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers); window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers); window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers); window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers); window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers); window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.setBidAmount = EventHandlers.setBidAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers); window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers); window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers); window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
+54 -23
View File
@@ -4,11 +4,13 @@
const OfferPage = { const OfferPage = {
xhr_rates: null, xhr_rates: null,
xhr_bid_params: null, xhr_bid_params: null,
xhr_bid_prefund: null,
init: function() { init: function() {
this.xhr_rates = new XMLHttpRequest(); this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest(); this.xhr_bid_params = new XMLHttpRequest();
this.xhr_bid_prefund = new XMLHttpRequest();
this.setupXHRHandlers(); this.setupXHRHandlers();
this.setupEventListeners(); this.setupEventListeners();
this.handleBidsPageAddress(); this.handleBidsPageAddress();
@@ -33,7 +35,20 @@
if (bidAmountSendInput) { if (bidAmountSendInput) {
bidAmountSendInput.value = obj['amount_to']; bidAmountSendInput.value = obj['amount_to'];
} }
this.updateModalValues(); }
};
this.xhr_bid_prefund.onload = () => {
if (this.xhr_bid_prefund.status == 200) {
const obj = JSON.parse(this.xhr_bid_prefund.response);
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountInput) {
bidAmountInput.value = obj['amount_from'];
}
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = obj['bid_tx'];
}
} }
}; };
}, },
@@ -71,16 +86,6 @@
mainCancelBtn.onclick = this.handleCancelClick.bind(this); mainCancelBtn.onclick = this.handleCancelClick.bind(this);
} }
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
}
const errorOkBtn = document.getElementById('errorOk'); const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) { if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this)); errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
@@ -112,7 +117,7 @@
const bidRateInput = document.getElementById('bid_rate'); const bidRateInput = document.getElementById('bid_rate');
const validMinsInput = document.querySelector('input[name="validmins"]'); const validMinsInput = document.querySelector('input[name="validmins"]');
const amtVar = document.getElementById('amt_var')?.value === 'True'; const amtVar = document.getElementById('amt_var')?.value === 'True';
if (bidAmountSendInput) { if (bidAmountSendInput) {
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max'); bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
} }
@@ -129,8 +134,7 @@
if (!amtVar) { if (!amtVar) {
this.updateBidParams('rate'); this.updateBidParams('rate');
} }
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message'); const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove()); errorMessages.forEach(msg => msg.remove());
@@ -156,6 +160,7 @@
const bidAmountSendInput = document.getElementById('bid_amount_send'); const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate'); const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate'); const offerRateInput = document.getElementById('offer_rate');
const bidSubfee = document.getElementById('subfee_bid');
if (!coin_from || !coin_to || !amt_var || !rate_var) return; if (!coin_from || !coin_to || !amt_var || !rate_var) return;
@@ -171,7 +176,7 @@
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount; bidAmountInput.value = receiveAmount;
} }
} else if (value_changed === 'sending') { } else if (value_changed === 'sending' || value_changed === 'subfee') {
if (bidAmountSendInput && bidAmountInput) { if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0; const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
@@ -187,11 +192,31 @@
this.validateAmountsAfterChange(); this.validateAmountsAfterChange();
if (bidSubfee && bidSubfee.checked) {
bidAmountInput.readOnly = true;
const offer_id = document.getElementById('offer_id')?.value || '';
if (!offer_id) {
console.log("offer_id not found!");
return;
}
this.xhr_bid_prefund.open('POST', '/json/getsubfeebidtx');
this.xhr_bid_prefund.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
const data = { offer_id: offer_id, amount_to: bidAmountSendInput.value , bid_rate: rate};
this.xhr_bid_prefund.overrideMimeType("application/json");
this.xhr_bid_prefund.send(JSON.stringify(data));
return;
}
bidAmountInput.readOnly = false;
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = "";
}
this.xhr_bid_params.open('POST', '/json/rate'); this.xhr_bid_params.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_bid_params.overrideMimeType("application/json");
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`); this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues();
}, },
validateAmountsAfterChange: function() { validateAmountsAfterChange: function() {
@@ -253,6 +278,11 @@
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.'); this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false; return false;
} }
let subfee = false;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
subfee = checkbox.checked;
}
const coinFrom = document.getElementById('coin_from_name')?.value || ''; const coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || ''; const coinTo = document.getElementById('coin_to_name')?.value || '';
@@ -273,7 +303,12 @@
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8); if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`; if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8); if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`; if (modalSendCurrency) {
modalSendCurrency.textContent = ` ${tlaTo}`;
if (subfee) {
modalSendCurrency.textContent += ` (incl fee)`;
}
}
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default'; if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins; if (modalValidMins) modalValidMins.textContent = validMins;
@@ -292,10 +327,6 @@
return false; return false;
}, },
updateModalValues: function() {
},
handleBidsPageAddress: function() { handleBidsPageAddress: function() {
const selectElement = document.querySelector('select[name="addr_from"]'); const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids'; const STORAGE_KEY = 'lastUsedAddressBids';
+10 -7
View File
@@ -351,16 +351,19 @@
); );
matchingCoins.forEach(coinData => { matchingCoins.forEach(coinData => {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); const balanceElements = document.querySelectorAll('.coinname-value[data-coinname][data-balance-type]');
balanceElements.forEach(element => { balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname'); const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) { if (elementCoinName === coinData.name) {
const currentText = element.textContent; const balanceType = element.getAttribute('data-balance-type');
const ticker = coinData.ticker || coinId.toUpperCase(); const value = coinData[balanceType];
const newBalance = `${coinData.balance} ${ticker}`; if (value !== undefined) {
if (currentText !== newBalance) { const ticker = coinData.ticker || coinId.toUpperCase();
element.textContent = newBalance; const newBalance = balanceType === 'est_fee' ? value : `${value} ${ticker}`;
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`); if (element.textContent !== newBalance) {
element.textContent = newBalance;
console.log(`Updated ${balanceType}: ${coinData.name} -> ${newBalance}`);
}
} }
} }
}); });
+19
View File
@@ -75,9 +75,28 @@
if (coinData.pending && parseFloat(coinData.pending) > 0) { if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData); this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
} }
} else if (coinData.name === 'Litecoin MWEB') {
this.updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
this.removePendingBalance('Litecoin', 'MWEB Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'MWEB Pending:', coinData);
}
} else { } else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name); this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.mweb_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'MWEB Balance:', coinData.mweb_balance, coinData.ticker || coinData.name);
}
if (coinData.spark_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Spark Balance:', coinData.spark_balance, coinData.ticker || coinData.name);
}
if (coinData.blind_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Blind Balance:', coinData.blind_balance, coinData.ticker || coinData.name);
}
if (coinData.anon_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Anon Balance:', coinData.anon_balance, coinData.ticker || coinData.name);
}
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') { if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) { if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData); this.updatePendingDisplay(coinData);
+14 -1
View File
@@ -224,7 +224,7 @@
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Chain A local fee rate</td> <td class="py-3 px-6 bold">Chain A local fee rate</td>
<td class="py-3 px-6">{{ data.a_fee_rate_verify }} (Fee source: {{ data.a_fee_rate_verify_src }}{% if data.a_fee_warn == true %} WARNING {% endif %})</td> <td class="py-3 px-6">{{ data.a_fee_rate_verify }} (Fee source: {{ data.a_fee_rate_verify_src }}{% if data.a_fee_warn == "high" %} WARNING - HIGH {% elif data.a_fee_warn == "low" %} WARNING - LOW {% elif data.a_fee_warn is defined %} WARNING {% endif %})</td>
</tr> </tr>
{% endif %} {% endif %}
</table> </table>
@@ -419,11 +419,22 @@
name="bid_amount_send" name="bid_amount_send"
value="" value=""
max="{{ data.amt_to }}" max="{{ data.amt_to }}"
haveamount="{{ data.coin_to_balance }}"
exp="{{ data.coin_to_exp }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');"> onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');">
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm"> <div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
max {{ data.amt_to }} ({{ data.tla_to }}) max {{ data.amt_to }} ({{ data.tla_to }})
</div> </div>
</div> </div>
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" data-set-bid-amount="1" data-input-id="bid_amount_send">max</button>
{% if data.bid_can_subfee == true %}
<label>
<input type="checkbox" name="subfee_bid" id="subfee_bid" value="sfb" onchange="updateBidParams('subfee');"/>
<span for="subfee_bid">Subfee</span>
</label>
{% endif %}
</div>
</td> </td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -721,6 +732,8 @@
<input type="hidden" id="coin_to_name" value="{{ data.coin_to }}"> <input type="hidden" id="coin_to_name" value="{{ data.coin_to }}">
<input type="hidden" id="tla_from" value="{{ data.tla_from }}"> <input type="hidden" id="tla_from" value="{{ data.tla_from }}">
<input type="hidden" id="tla_to" value="{{ data.tla_to }}"> <input type="hidden" id="tla_to" value="{{ data.tla_to }}">
<input type="hidden" id="offer_id" value="{{ offer_id }}">
<input type="hidden" name="prefunded_bid_tx" id="prefunded_bid_tx" value="{{ data.prefunded_bid_tx }}">
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</form> </form>
<p id="rates_display"></p> <p id="rates_display"></p>
+25 -23
View File
@@ -138,7 +138,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.pending %} {% if w.pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
@@ -152,7 +152,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.blind_unconfirmed %} {% if w.blind_unconfirmed %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
@@ -162,7 +162,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Anon"> </span>Anon Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Anon"> </span>Anon Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.anon_pending %} {% if w.anon_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.anon_pending }} {{ w.ticker }}</span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.anon_pending }} {{ w.ticker }}</span>
@@ -177,7 +177,7 @@
{% if is_electrum_mode %} {% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span> <span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %} {% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.mweb_pending %} {% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
@@ -185,11 +185,22 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if w.mweb_in_plain %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB in Plain Balance: </td>
<td class="py-3 px-6 bold">
<span>{{ w.mweb_in_plain }} {{ w.ticker }}</span>
</td>
<td class="py-3 px-6 bold">
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="convertmweb_{{ w.cid }}" value="Convert" data-confirm-mweb-change-convert> Convert </button>
</td>
</tr>
{% endif %}
{% elif w.cid == '13' %} {# FIRO #} {% elif w.cid == '13' %} {# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.spark_pending %} {% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
@@ -200,7 +211,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td> <td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold"> <td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% if w.spark_pending %} {% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span> <span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
@@ -337,16 +348,7 @@
<td class="py-3 px-6">{{ w.expected_seed }}</td> <td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if w.account_key %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Extended Private Key:</td>
<td class="py-3 px-6">
<span id="account-key-hidden" class="font-mono text-sm">••••••••••••••••</span>
<span id="account-key-value" class="font-mono text-sm hidden break-all">{{ w.account_key }}</span>
<button type="button" id="toggle-account-key" onclick="var h=document.getElementById('account-key-hidden'),v=document.getElementById('account-key-value');if(v.classList.contains('hidden')){v.classList.remove('hidden');h.classList.add('hidden');this.textContent='Hide';}else{v.classList.add('hidden');h.classList.remove('hidden');this.textContent='Show';}" class="ml-2 px-2 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">Show</button>
</td>
</tr>
{% endif %}
</table> </table>
</div> </div>
</div> </div>
@@ -545,7 +547,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td> <td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -557,7 +559,7 @@
{% if is_electrum_mode %} {% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span> <span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %} {% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
{% endif %} {% endif %}
</td> </td>
@@ -567,7 +569,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td> <td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -576,7 +578,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td> <td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -585,14 +587,14 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Blind Balance: </td> <td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Blind Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Anon Balance: </td> <td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Anon Balance: </td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -767,7 +769,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Estimate:</td> <td class="py-3 px-6 bold">Fee Estimate:</td>
<td class="py-3 px-6"> <td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.est_fee }}</span> <span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="est_fee">{{ w.est_fee }}</span>
(<span class="usd-value fee-estimate-usd" data-decimals="8"></span>) (<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
</td> </td>
</tr> </tr>
+5 -5
View File
@@ -65,7 +65,7 @@
<div class="p-6 bg-coolGray-100 dark:bg-gray-600"> <div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Balance:</h4> <h4 class="text-xs font-medium dark:text-white">Balance:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</div> <div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</div>
</div> </div>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4> <h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
@@ -90,7 +90,7 @@
{% if w.cid == '1' %} {# PART #} {% if w.cid == '1' %} {# PART #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4> <h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span> <span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
</div> </div>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4> <h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4>
@@ -108,7 +108,7 @@
{% endif %} {% endif %}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon Balance:</h4> <h4 class="text-xs font-medium dark:text-white">Anon Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span> <span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
</div> </div>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon USD value:</h4> <h4 class="text-xs font-medium dark:text-white">Anon USD value:</h4>
@@ -129,7 +129,7 @@
{% if w.cid == '3' and w.connection_type != 'electrum' %} {# LTC - MWEB not available in electrum mode #} {% if w.cid == '3' and w.connection_type != 'electrum' %} {# LTC - MWEB not available in electrum mode #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4> <h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span> <span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
</div> </div>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4> <h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4>
@@ -151,7 +151,7 @@
{% if w.cid == '13' %} {# FIRO #} {% if w.cid == '13' %} {# FIRO #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4> <h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span> <span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
</div> </div>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4> <h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4>
+58 -17
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # Copyright (c) 2022-2026 tecnovert
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@
import traceback import traceback
import time import time
from typing import List
from urllib import parse from urllib import parse
from .util import ( from .util import (
getCoinType, getCoinType,
@@ -184,14 +185,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
parsed_data["swap_type"] = page_data["swap_type"] parsed_data["swap_type"] = page_data["swap_type"]
swap_type = swap_type_from_string(parsed_data["swap_type"]) swap_type = swap_type_from_string(parsed_data["swap_type"])
elif ( elif (
parsed_data["coin_from"] in swap_client.adaptor_swap_only_coins parsed_data["coin_from"] in swap_client.coins_without_segwit
or parsed_data["coin_to"] in swap_client.adaptor_swap_only_coins and parsed_data["coin_to"] in swap_client.coins_without_segwit
): ):
parsed_data["swap_type"] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
else:
parsed_data["swap_type"] = strSwapType(SwapTypes.SELLER_FIRST) parsed_data["swap_type"] = strSwapType(SwapTypes.SELLER_FIRST)
swap_type = SwapTypes.SELLER_FIRST swap_type = SwapTypes.SELLER_FIRST
else:
parsed_data["swap_type"] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
if swap_type == SwapTypes.XMR_SWAP: if swap_type == SwapTypes.XMR_SWAP:
page_data["swap_style"] = "xmr" page_data["swap_style"] = "xmr"
@@ -499,7 +500,7 @@ def page_newoffer(self, url_split, post_string):
"debug_ui": swap_client.debug_ui, "debug_ui": swap_client.debug_ui,
"automation_strat_id": -1, "automation_strat_id": -1,
"amt_bid_min": format_amount(1, 3), "amt_bid_min": format_amount(1, 3),
"swap_type": strSwapType(SwapTypes.SELLER_FIRST), "swap_type": strSwapType(SwapTypes.XMR_SWAP),
} }
post_data = parse.parse_qs(post_string) post_data = parse.parse_qs(post_string)
@@ -583,7 +584,7 @@ def page_newoffer(self, url_split, post_string):
) )
def page_offer(self, url_split, post_string): def page_offer(self, url_split: List[str], post_string: str) -> bytes:
ensure(len(url_split) > 2, "Offer ID not specified") ensure(len(url_split) > 2, "Offer ID not specified")
offer_id = decode_offer_id(url_split[2]) offer_id = decode_offer_id(url_split[2])
server = self.server server = self.server
@@ -674,6 +675,11 @@ def page_offer(self, url_split, post_string):
amount_from = offer.amount_from amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, "debugind", -1)) debugind = int(get_data_entry_or(form_data, "debugind", -1))
if have_data_entry(form_data, "subfee_bid"):
extra_options["prefunded_tx"] = bytes.fromhex(
get_data_entry(form_data, "prefunded_bid_tx")
)
sent_bid_id = swap_client.postBid( sent_bid_id = swap_client.postBid(
offer_id, offer_id,
amount_from, amount_from,
@@ -768,24 +774,30 @@ def page_offer(self, url_split, post_string):
ci_leader = ci_to if reverse_bid else ci_from ci_leader = ci_to if reverse_bid else ci_from
if xmr_offer: if xmr_offer:
int_fee_rate_now, fee_source = ci_leader.get_fee_rate() a_fee_rate_now, fee_source = ci_leader.get_fee_rate()
a_fee_rate_now = ci_leader.make_int(a_fee_rate_now)
data["xmr_type"] = True chain_a_fee_rate: int = (
data["a_fee_rate"] = ci_leader.format_amount(xmr_offer.a_fee_rate) xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
data["a_fee_rate_verify"] = ci_leader.format_amount(
int_fee_rate_now, conv_int=True
) )
data["xmr_type"] = True
data["a_fee_rate"] = ci_leader.format_amount(chain_a_fee_rate)
data["a_fee_rate_verify"] = ci_leader.format_amount(a_fee_rate_now)
data["a_fee_rate_verify_src"] = fee_source data["a_fee_rate_verify_src"] = fee_source
data["a_fee_warn"] = xmr_offer.a_fee_rate < int_fee_rate_now
from_fee_rate = xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate warning_threshold: float = 1.2
if chain_a_fee_rate * warning_threshold < a_fee_rate_now:
data["a_fee_warn"] = "low"
elif chain_a_fee_rate > a_fee_rate_now * warning_threshold:
data["a_fee_warn"] = "high"
lock_spend_tx_vsize = ( lock_spend_tx_vsize = (
ci_from.xmr_swap_b_lock_spend_tx_vsize() ci_from.xmr_swap_b_lock_spend_tx_vsize()
if reverse_bid if reverse_bid
else ci_from.xmr_swap_a_lock_spend_tx_vsize() else ci_from.xmr_swap_a_lock_spend_tx_vsize()
) )
lock_spend_tx_fee = ci_from.make_int( lock_spend_tx_fee = ci_from.make_int(
from_fee_rate * lock_spend_tx_vsize / 1000, r=1 chain_a_fee_rate * lock_spend_tx_vsize / 1000, r=1
) )
data["amt_from_lock_spend_tx_fee"] = ci_from.format_amount( data["amt_from_lock_spend_tx_fee"] = ci_from.format_amount(
lock_spend_tx_fee // ci_from.COIN() lock_spend_tx_fee // ci_from.COIN()
@@ -817,6 +829,33 @@ def page_offer(self, url_split, post_string):
) )
data["amt_swapped"] = ci_from.format_amount(amt_swapped) data["amt_swapped"] = ci_from.format_amount(amt_swapped)
if show_bid_form:
coin_to_id = int(ci_to.coin_type())
wallet_coin_to_id = coin_to_id
if coin_to_id in (Coins.PART_ANON, Coins.PART_BLIND):
wallet_coin_to_id = Coins.PART
swap_client.updateWalletsInfo(only_coin=wallet_coin_to_id)
coin_to_wallet = swap_client.getCachedWalletsInfo(
{"coin_id": wallet_coin_to_id}
)[wallet_coin_to_id]
if coin_to_id == Coins.PART_ANON:
balance_key = "anon_balance"
elif coin_to_id == Coins.PART_BLIND:
balance_key = "blind_balance"
else:
balance_key = "balance"
data["coin_to_balance"] = coin_to_wallet[balance_key]
bid_can_subfee: bool = True
if offer.swap_type != SwapTypes.XMR_SWAP:
bid_can_subfee = False
if coin_to_id in (Coins.XMR, Coins.WOW):
bid_can_subfee = False
if offer.amount_negotiable is False:
bid_can_subfee = False
data["bid_can_subfee"] = bid_can_subfee
template = server.env.get_template("offer.html") template = server.env.get_template("offer.html")
return self.render_template( return self.render_template(
template, template,
@@ -835,7 +874,9 @@ def page_offer(self, url_split, post_string):
) )
def format_timestamp(timestamp, with_ago=True, is_expired=False): def format_timestamp(
timestamp: int, with_ago: bool = True, is_expired: bool = False
) -> str:
current_time = int(time.time()) current_time = int(time.time())
if is_expired: if is_expired:
+7
View File
@@ -273,6 +273,9 @@ def page_wallet(self, url_split, post_string):
swap_client.cacheNewAddressForCoin(coin_id) swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"): elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True force_refresh = True
elif have_data_entry(form_data, "convertmweb_" + cid):
txid = swap_client.ci(coin_id).convertMWEBBalance()
messages.append(f"Converted MWEB change to LTC in tx: {txid}")
elif have_data_entry(form_data, "newmwebaddr_" + cid): elif have_data_entry(form_data, "newmwebaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id) swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, "newsparkaddr_" + cid): elif have_data_entry(form_data, "newsparkaddr_" + cid):
@@ -525,6 +528,10 @@ def page_wallet(self, url_split, post_string):
// page_data["fee_estimate"]["sum_weight"] // page_data["fee_estimate"]["sum_weight"]
) )
if k == Coins.LTC and ci.useBackend() is False:
mweb_value: int = ci.getMWEBBalance()
if mweb_value > 0:
wallet_data["mweb_in_plain"] = ci.format_amount(mweb_value)
if show_utxo_groups: if show_utxo_groups:
utxo_groups = "" utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr() unspent_by_addr = ci.getUnspentsByAddr()
-1
View File
@@ -10,7 +10,6 @@ import json
import time import time
import decimal import decimal
COIN = 100000000 COIN = 100000000
-1
View File
@@ -25,7 +25,6 @@ from basicswap.contrib.test_framework.messages import (
uint256_from_str, uint256_from_str,
) )
AES_BLOCK_SIZE = 16 AES_BLOCK_SIZE = 16
+9 -47
View File
@@ -4,10 +4,12 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib import json
import sqlite3
import threading import threading
import time import time
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from coincurve import PrivateKey, PublicKey
from .chainparams import Coins from .chainparams import Coins
from .contrib.test_framework import segwit_addr from .contrib.test_framework import segwit_addr
@@ -19,7 +21,7 @@ from .db_wallet import (
WalletTxCache, WalletTxCache,
WalletWatchOnly, WalletWatchOnly,
) )
from .util.crypto import hash160 from .util.crypto import hash160, sha256
from .util.extkey import ExtKeyPair from .util.extkey import ExtKeyPair
@@ -112,13 +114,11 @@ class WalletManager:
def _deriveAddress( def _deriveAddress(
self, coin_type: Coins, index: int, internal: bool = False self, coin_type: Coins, index: int, internal: bool = False
) -> Tuple[str, str, bytes]: ) -> Tuple[str, str, bytes]:
from coincurve import PublicKey
key = self._deriveKey(coin_type, index, internal) key = self._deriveKey(coin_type, index, internal)
pubkey = PublicKey.from_secret(key).format() pubkey = PublicKey.from_secret(key).format()
pkh = hash160(pubkey) pkh = hash160(pubkey)
address = segwit_addr.encode(self._getHRP(coin_type), 0, pkh) address = segwit_addr.encode(self._getHRP(coin_type), 0, pkh)
scripthash = hashlib.sha256(bytes([0x00, 0x14]) + pkh).digest()[::-1].hex() scripthash = sha256(bytes([0x00, 0x14]) + pkh)[::-1].hex()
return address, scripthash, pubkey return address, scripthash, pubkey
def _syncStateIndices(self, coin_type: Coins, cursor) -> None: def _syncStateIndices(self, coin_type: Coins, cursor) -> None:
@@ -275,8 +275,6 @@ class WalletManager:
def getAddress( def getAddress(
self, coin_type: Coins, index: int, internal: bool = False self, coin_type: Coins, index: int, internal: bool = False
) -> Optional[str]: ) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -395,8 +393,6 @@ class WalletManager:
include_watch_only: bool = True, include_watch_only: bool = True,
funded_only: bool = False, funded_only: bool = False,
) -> List[str]: ) -> List[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -426,8 +422,6 @@ class WalletManager:
return [] return []
def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]: def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -452,8 +446,6 @@ class WalletManager:
return {} return {}
def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]: def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -517,8 +509,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False) self._swap_client.closeDB(cursor, commit=False)
def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]: def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -559,8 +549,6 @@ class WalletManager:
return None return None
def getCachedTotalBalance(self, coin_type: Coins) -> int: def getCachedTotalBalance(self, coin_type: Coins) -> int:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -700,8 +688,6 @@ class WalletManager:
if not self.isInitialized(coin_type): if not self.isInitialized(coin_type):
return None return None
import sqlite3
now = int(time.time()) now = int(time.time())
min_cache_time = now - max_cache_age min_cache_time = now - max_cache_age
@@ -744,8 +730,6 @@ class WalletManager:
return None return None
def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool: def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -762,8 +746,6 @@ class WalletManager:
def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]: def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]:
if not self.isInitialized(coin_type): if not self.isInitialized(coin_type):
return None return None
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -788,8 +770,6 @@ class WalletManager:
return None return None
def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]: def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -861,10 +841,8 @@ class WalletManager:
label: str = "", label: str = "",
source: str = "import", source: str = "import",
) -> bool: ) -> bool:
from coincurve import PublicKey as CCPublicKey
try: try:
pubkey = CCPublicKey.from_secret(private_key).format() pubkey = PublicKey.from_secret(private_key).format()
if ( if (
segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey)) segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey))
!= address != address
@@ -1003,7 +981,7 @@ class WalletManager:
def _b58decode_check(self, s: str) -> bytes: def _b58decode_check(self, s: str) -> bytes:
data = self._b58decode(s) data = self._b58decode(s)
payload, checksum = data[:-4], data[-4:] payload, checksum = data[:-4], data[-4:]
expected = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] expected = sha256(sha256(payload))[:4]
if checksum != expected: if checksum != expected:
raise ValueError("Invalid base58 checksum") raise ValueError("Invalid base58 checksum")
return payload return payload
@@ -1025,7 +1003,7 @@ class WalletManager:
master_key = self._master_keys.get(coin_type) master_key = self._master_keys.get(coin_type)
if master_key is None: if master_key is None:
raise ValueError(f"Wallet not initialized for {coin_type}") raise ValueError(f"Wallet not initialized for {coin_type}")
return hashlib.sha256(master_key + b"_import_key").digest() return sha256(master_key + b"_import_key")
def _encryptPrivateKey(self, private_key: bytes, coin_type: Coins) -> bytes: def _encryptPrivateKey(self, private_key: bytes, coin_type: Coins) -> bytes:
return bytes(a ^ b for a, b in zip(private_key, self._getXorKey(coin_type))) return bytes(a ^ b for a, b in zip(private_key, self._getXorKey(coin_type)))
@@ -1037,7 +1015,7 @@ class WalletManager:
_, data = segwit_addr.decode(self._getHRP(coin_type), address) _, data = segwit_addr.decode(self._getHRP(coin_type), address)
if data is None: if data is None:
return "" return ""
return hashlib.sha256(bytes([0x00, 0x14]) + bytes(data)).digest()[::-1].hex() return sha256(bytes([0x00, 0x14]) + bytes(data))[::-1].hex()
def needsMigration(self, coin_type: Coins) -> bool: def needsMigration(self, coin_type: Coins) -> bool:
cursor = self._swap_client.openDB() cursor = self._swap_client.openDB()
@@ -1193,8 +1171,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False) self._swap_client.closeDB(cursor, commit=False)
def getSeedID(self, coin_type: Coins) -> Optional[str]: def getSeedID(self, coin_type: Coins) -> Optional[str]:
from basicswap.contrib.test_framework.script import hash160
master_key = self._master_keys.get(coin_type) master_key = self._master_keys.get(coin_type)
if master_key is None: if master_key is None:
return None return None
@@ -1204,16 +1180,12 @@ class WalletManager:
return hash160(ek.encode_p()).hex() return hash160(ek.encode_p()).hex()
def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes: def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
raise ValueError(f"Cannot sign: no key for address {address}") raise ValueError(f"Cannot sign: no key for address {address}")
return PrivateKey(key).sign(message.encode("utf-8")) return PrivateKey(key).sign(message.encode("utf-8"))
def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes: def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
raise ValueError(f"Cannot sign: no key for address {address}") raise ValueError(f"Cannot sign: no key for address {address}")
@@ -1222,8 +1194,6 @@ class WalletManager:
def getKeyForAddress( def getKeyForAddress(
self, coin_type: Coins, address: str self, coin_type: Coins, address: str
) -> Optional[Tuple[bytes, bytes]]: ) -> Optional[Tuple[bytes, bytes]]:
from coincurve import PublicKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
return None return None
@@ -1232,8 +1202,6 @@ class WalletManager:
def findAddressByScripthash( def findAddressByScripthash(
self, coin_type: Coins, scripthash: str self, coin_type: Coins, scripthash: str
) -> Optional[str]: ) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -1256,8 +1224,6 @@ class WalletManager:
return None return None
def getAllScripthashes(self, coin_type: Coins) -> List[str]: def getAllScripthashes(self, coin_type: Coins) -> List[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -1895,8 +1861,6 @@ class WalletManager:
for _ in existing: for _ in existing:
return False return False
import json
pending = WalletPendingTx() pending = WalletPendingTx()
pending.coin_type = int(coin_type) pending.coin_type = int(coin_type)
pending.txid = txid pending.txid = txid
@@ -1945,8 +1909,6 @@ class WalletManager:
) -> List[dict]: ) -> List[dict]:
cursor = self._swap_client.openDB() cursor = self._swap_client.openDB()
try: try:
import json
results = self._swap_client.query( results = self._swap_client.query(
WalletPendingTx, WalletPendingTx,
cursor, cursor,
+7
View File
@@ -0,0 +1,7 @@
# LTC Notes
## MWEB
Sending LTC -> MWEB generates MWEB change outputs in the plain LTC wallet that BSX can't use.
A temporary convenience function is provided to convert those MWEB outputs back to plain LTC.
+6
View File
@@ -140,6 +140,12 @@ Observe progress with
tail -f /tmp/firo.log tail -f /tmp/firo.log
Alternatively --extracoinopts can be used with --startonlycoin
docker-compose run --rm swapclient \
basicswap-run --datadir=/coindata --startonlycoin=litecoin --extracoinopts="-reindex"
## Start a subset of the configured coins using docker ## Start a subset of the configured coins using docker
docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero
+47
View File
@@ -1,3 +1,50 @@
0.16.5
==============
- Updated docker base images to Debian Trixie.
- By default reject secret hash type offers where the coin pair could use adaptor sig swap.
- override with "strict_swap_type" setting.
0.16.4
==============
- Security: Always require the initiate tx output index and value for secret hash swaps.
- Strengthens the 0.16.3 fix: the amount check can no longer be skipped when the output value is unavailable; the swap is now rejected (fails closed) instead of proceeding.
- Security: Also double check the participate tx output amount for secret hash swaps.
- Raise minimum Python version to 3.11.
0.16.3
==============
- Automatic fee validation.
- Prevent sending bids to offers
- Reject received offers, and
- Prevent sending offers where the chain feerates are out of range.
- Valid feerate range is the node's estimated feerate for confirmation in 24 blocks to 4x the estimated feerate.
- The minimum feerate confirmation can be adjusted with the "low_fee_conf_target" setting.
- If "low_feerate" is set above 0 it is used instead of the dynamic feerate with "low_fee_conf_target".
- The maximum feerate multiplier can be adjusted with the "high_estimated_feerate_multiplier" setting.
- If "high_estimated_feerate_multiplier" is set below 1.0 the max feerate can be set with the "high_feerate" setting.
- New setting "startup_delay":
- Adjusts the time waited for coin daemons to start between "startup_tries".
- Valid as a base setting and can be overridden per coin with chainclients settings.
- Add subfee bids.
- Enables a user to create a bid specifying the amount before the lock tx fee.
- Currently only works when the coin to is not XMR like.
- Set Adaptor sig bid type as default where possible.
- UI:
- offer page:
- Fixed feerate from other chain displayed for reversed swaps.
- Added warning text for fee above 1.2 x local estimate.
- Added subfee bid option.
- Increase DCR fee estimate by 1 byte.
- Waits for the refund and refund spend txn locks to expire before trying to submit them.
- Fixed bug where initiate tx amount was not checked for secret hash swaps.
0.14.5 0.14.5
============== ==============
+1 -1
View File
@@ -5,7 +5,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoin --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoin --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV BITCOIN_DATA /data ENV BITCOIN_DATA /data
+1 -1
View File
@@ -5,7 +5,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoincash --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoincash --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV BITCOINCASH_DATA /data ENV BITCOINCASH_DATA /data
+1 -1
View File
@@ -5,7 +5,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=dash --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=dash --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV DASH_DATA /data ENV DASH_DATA /data
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=decred --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=decred --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV DCR_DATA /data ENV DCR_DATA /data
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=dogecoin --withoutcoin=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=dogecoin --withoutcoin=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV DOGECOIN_DATA /data ENV DOGECOIN_DATA /data
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=firo --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=firo --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV FIRO_DATA /data ENV FIRO_DATA /data
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=litecoin --withoutcoin=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=litecoin --withoutcoin=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV LITECOIN_DATA /data ENV LITECOIN_DATA /data
+1 -1
View File
@@ -2,7 +2,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=monero --withoutcoins=particl RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=monero --withoutcoins=particl
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV PARTICL_DATA /data ENV PARTICL_DATA /data
+1 -1
View File
@@ -3,7 +3,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=pivx --withoutcoins=particl && \ RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=pivx --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
ENV PIVX_DATA /data ENV PIVX_DATA /data
+8 -3
View File
@@ -1,12 +1,17 @@
FROM debian:bullseye-slim FROM debian:trixie-slim
ENV LANG=C.UTF-8 \ ENV LANG=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive \ DEBIAN_FRONTEND=noninteractive \
DATADIR=/data DATADIR=/data \
VIRTUAL_ENV=/opt/venv
RUN apt-get update; \ RUN apt-get update; \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata wget unzip cmake ninja-build; python3-pip libpython3-dev python3-venv gnupg pkg-config gcc libc-dev gosu tzdata wget unzip cmake ninja-build;
# Create python venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ARG BASICSWAP_URL=https://github.com/basicswap/basicswap/archive/master.zip ARG BASICSWAP_URL=https://github.com/basicswap/basicswap/archive/master.zip
ARG BASICSWAP_DIR=basicswap-master ARG BASICSWAP_DIR=basicswap-master
+1 -1
View File
@@ -2,7 +2,7 @@ FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=wownero --withoutcoins=particl RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=wownero --withoutcoins=particl
FROM debian:bullseye-slim FROM debian:trixie-slim
COPY --from=install_stage /coin_bin . COPY --from=install_stage /coin_bin .
+3 -3
View File
@@ -135,15 +135,15 @@
(define-public basicswap (define-public basicswap
(package (package
(name "basicswap") (name "basicswap")
(version "0.16.0") (version "0.16.4")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
(url "https://github.com/basicswap/basicswap") (url "https://github.com/basicswap/basicswap")
(commit "2c13314bdd29622235c92fd20c237801acb3cb76"))) (commit "136b311dc68f11b9c12ebd6877c5f718d705603a")))
(sha256 (sha256
(base32 (base32
"0j0id6db3ljdsfag8krjdmd4rzlz2504yk9lzj0p89lqyygi9ilc")) "0ikr8ik9rklvafd1j8zj0y38vric02qhmj7pvp3kvzbmd2fxx95p"))
(file-name (git-file-name name version)))) (file-name (git-file-name name version))))
(build-system pyproject-build-system) (build-system pyproject-build-system)
+10 -2
View File
@@ -8,7 +8,7 @@ description = "Simple atomic swap system"
keywords = ["crypto", "cryptocurrency", "particl", "bitcoin", "monero", "wownero"] keywords = ["crypto", "cryptocurrency", "particl", "bitcoin", "monero", "wownero"]
readme = "README.md" readme = "README.md"
license = {file = "LICENSE"} license = {file = "LICENSE"}
requires-python = ">=3.9" requires-python = ">=3.11"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
@@ -36,7 +36,7 @@ dev = [
"pre-commit", "pre-commit",
"pytest", "pytest",
"ruff", "ruff",
"black==25.11.0", "black==26.5.1",
"selenium", "selenium",
] ]
@@ -48,3 +48,11 @@ allow-direct-references = true
[tool.ruff] [tool.ruff]
exclude = ["basicswap/contrib","basicswap/interface/contrib"] exclude = ["basicswap/contrib","basicswap/interface/contrib"]
[tool.codespell]
check-filenames = true
disable-colors = true
quiet-level = 7
dictionary = "tests/lint/spelling.extra_dictionary.txt,-"
ignore-words = "tests/lint/spelling.ignore-words.txt"
skip = ".git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static"
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -21,7 +20,6 @@ from basicswap.util import toBool
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.bin.prepare import downloadPIVXParams from basicswap.bin.prepare import downloadPIVXParams
TEST_HTTP_HOST = os.getenv( TEST_HTTP_HOST = os.getenv(
"TEST_HTTP_HOST", "127.0.0.1" "TEST_HTTP_HOST", "127.0.0.1"
) # Set to 0.0.0.0 when used in docker ) # Set to 0.0.0.0 when used in docker
@@ -164,7 +162,7 @@ def prepare_balance(
post_json["type_to"] = type_to post_json["type_to"] = type_to
json_rv = read_json_api( json_rv = read_json_api(
port_take_from_node, port_take_from_node,
"wallets/{}/withdraw".format(coin_ticker.lower()), f"wallets/{coin_ticker.lower()}/withdraw",
post_json, post_json,
) )
assert len(json_rv["txid"]) == 64 assert len(json_rv["txid"]) == 64
@@ -237,11 +235,17 @@ def wait_for_bid(
) )
if isinstance(state, (list, tuple)): if isinstance(state, (list, tuple)):
if bid[5] in state: if bid[5] in state:
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return return
else: else:
continue continue
elif state is not None and state != bid[5]: elif state is not None and state != bid[5]:
continue continue
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return return
else: else:
if i > 0 and i % 10 == 0: if i > 0 and i % 10 == 0:
+8 -13
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -56,7 +55,6 @@ from tests.basicswap.extended.test_doge import (
import basicswap.config as cfg import basicswap.config as cfg
import basicswap.bin.run as runSystem import basicswap.bin.run as runSystem
TEST_PATH = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap1")) TEST_PATH = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap1"))
PARTICL_PORT_BASE = int(os.getenv("PARTICL_PORT_BASE", BASE_PORT)) PARTICL_PORT_BASE = int(os.getenv("PARTICL_PORT_BASE", BASE_PORT))
@@ -532,9 +530,7 @@ def run_prepare(
for opt in EXTRA_CONFIG_JSON.get("doge{}".format(node_id), []): for opt in EXTRA_CONFIG_JSON.get("doge{}".format(node_id), []):
fp.write(opt + "\n") fp.write(opt + "\n")
with open(config_path) as fs: settings["startup_delay"] = 1
settings = json.load(fs)
settings["min_delay_event"] = 1 settings["min_delay_event"] = 1
settings["max_delay_event"] = 4 settings["max_delay_event"] = 4
settings["min_delay_event_short"] = 1 settings["min_delay_event_short"] = 1
@@ -586,7 +582,7 @@ def prepare_nodes(
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
def setUpClass(cls): def setUpClass(cls):
super(TestBase, cls).setUpClass() super().setUpClass()
cls.delay_event = threading.Event() cls.delay_event = threading.Event()
signal.signal( signal.signal(
@@ -624,7 +620,7 @@ class TestBase(unittest.TestCase):
def run_process(client_id): def run_process(client_id):
client_path = os.path.join(TEST_PATH, "client{}".format(client_id)) client_path = os.path.join(TEST_PATH, f"client{client_id}")
testargs = [ testargs = [
"basicswap-run", "basicswap-run",
"-datadir=" + client_path, "-datadir=" + client_path,
@@ -646,7 +642,7 @@ class XmrTestBase(TestBase):
prepare_nodes(3, "monero") prepare_nodes(3, "monero")
def start_processes(self): def start_processes(self):
multiprocessing.set_start_method("fork") multiprocessing.set_start_method("spawn")
self.delay_event.clear() self.delay_event.clear()
for i in range(3): for i in range(3):
@@ -655,7 +651,7 @@ class XmrTestBase(TestBase):
) )
self.processes[-1].start() self.processes[-1].start()
waitForServer(self.delay_event, 12701) waitForServer(self.delay_event, 12701, 60)
def waitForMainAddress(): def waitForMainAddress():
for i in range(20): for i in range(20):
@@ -667,13 +663,12 @@ class XmrTestBase(TestBase):
) )
return wallets["XMR"]["main_address"] return wallets["XMR"]["main_address"]
except Exception as e: except Exception as e:
print("Waiting for main address {}".format(str(e))) print(f"Waiting for main address {e}")
self.delay_event.wait(1) self.delay_event.wait(1)
raise ValueError("waitForMainAddress timedout") raise ValueError("waitForMainAddress timedout")
xmr_addr1 = waitForMainAddress() xmr_addr1 = waitForMainAddress()
num_blocks: int = 100
num_blocks = 100
xmr_auth = None xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "": if os.getenv("XMR_RPC_USER", "") != "":
@@ -685,7 +680,7 @@ class XmrTestBase(TestBase):
] ]
< num_blocks < num_blocks
): ):
logging.info("Mining {} Monero blocks to {}.".format(num_blocks, xmr_addr1)) logging.info(f"Mining {num_blocks} Monero blocks to {xmr_addr1}.")
callrpc_xmr( callrpc_xmr(
XMR_BASE_RPC_PORT + 1, XMR_BASE_RPC_PORT + 1,
"generateblocks", "generateblocks",
+14 -12
View File
@@ -65,7 +65,6 @@ from tests.basicswap.common import (
) )
from basicswap.bin.run import startDaemon from basicswap.bin.run import startDaemon
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.DEBUG logger.level = logging.DEBUG
if not len(logger.handlers): if not len(logger.handlers):
@@ -176,6 +175,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": node_dir, "datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR, "bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing "blocks_confirmed": 2, # Faster testing
"wallet_name": "bsx_wallet",
}, },
"dash": { "dash": {
"connection_type": "rpc", "connection_type": "rpc",
@@ -185,6 +185,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"bindir": DASH_BINDIR, "bindir": DASH_BINDIR,
"use_csv": True, "use_csv": True,
"use_segwit": False, "use_segwit": False,
"wallet_name": "bsx_wallet",
}, },
"bitcoin": { "bitcoin": {
"connection_type": "rpc", "connection_type": "rpc",
@@ -193,6 +194,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": btcdatadir, "datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR, "bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True, "use_segwit": True,
"wallet_name": "bsx_wallet",
}, },
}, },
"check_progress_seconds": 2, "check_progress_seconds": 2,
@@ -286,7 +288,7 @@ class Test(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(Test, cls).setUpClass() super().setUpClass()
k = PrivateKey() k = PrivateKey()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret) cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret)
@@ -410,15 +412,15 @@ class Test(unittest.TestCase):
waitForRPC(dashRpc, delay_event, rpc_command="getblockchaininfo") waitForRPC(dashRpc, delay_event, rpc_command="getblockchaininfo")
if len(dashRpc("listwallets")) < 1: if len(dashRpc("listwallets")) < 1:
dashRpc("createwallet wbsx_wallet") dashRpc("createwallet bsx_wallet")
sc.start() sc.start()
waitForRPC(dashRpc, delay_event) waitForRPC(dashRpc, delay_event)
num_blocks = 500 num_blocks = 500
logging.info("Mining %d dash blocks", num_blocks) logging.info(f"Mining {num_blocks} dash blocks")
cls.dash_addr = dashRpc("getnewaddress mining_addr") cls.dash_addr = dashRpc("getnewaddress mining_addr")
dashRpc("generatetoaddress {} {}".format(num_blocks, cls.dash_addr)) dashRpc(f"generatetoaddress {num_blocks} {cls.dash_addr}")
ro = dashRpc("getblockchaininfo") ro = dashRpc("getblockchaininfo")
try: try:
@@ -432,8 +434,8 @@ class Test(unittest.TestCase):
waitForRPC(btcRpc, delay_event) waitForRPC(btcRpc, delay_event)
cls.btc_addr = btcRpc("getnewaddress mining_addr bech32") cls.btc_addr = btcRpc("getnewaddress mining_addr bech32")
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}")
btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr)) btcRpc(f"generatetoaddress {num_blocks} {cls.btc_addr}")
ro = btcRpc("getblockchaininfo") ro = btcRpc("getblockchaininfo")
checkForks(ro) checkForks(ro)
@@ -450,7 +452,7 @@ class Test(unittest.TestCase):
# Wait for height, or sequencelock is thrown off by genesis blocktime # Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3 num_blocks = 3
logging.info("Waiting for Particl chain height %d", num_blocks) logging.info(f"Waiting for Particl chain height {num_blocks}")
for i in range(60): for i in range(60):
particl_blocks = cls.swap_clients[0].callrpc("getblockcount") particl_blocks = cls.swap_clients[0].callrpc("getblockcount")
print("particl_blocks", particl_blocks) print("particl_blocks", particl_blocks)
@@ -474,7 +476,7 @@ class Test(unittest.TestCase):
cls.swap_clients.clear() cls.swap_clients.clear()
cls.daemons.clear() cls.daemons.clear()
super(Test, cls).tearDownClass() super().tearDownClass()
def test_02_part_dash(self): def test_02_part_dash(self):
logging.info("---------- Test PART to DASH") logging.info("---------- Test PART to DASH")
@@ -684,9 +686,9 @@ class Test(unittest.TestCase):
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
Coins.DASH, Coins.DASH,
Coins.BTC, Coins.BTC,
0.001 * COIN, 0.01 * COIN,
1.0 * COIN, 1.0 * COIN,
0.001 * COIN, 0.01 * COIN,
SwapTypes.SELLER_FIRST, SwapTypes.SELLER_FIRST,
) )
@@ -710,7 +712,7 @@ class Test(unittest.TestCase):
del swap_clients[0].getChainClientSettings(Coins.DASH)["override_feerate"] del swap_clients[0].getChainClientSettings(Coins.DASH)["override_feerate"]
def test_08_wallet(self): def test_08_wallet(self):
logging.info("---------- Test {} wallet".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} wallet")
logging.info("Test withdrawal") logging.info("Test withdrawal")
addr = dashRpc('getnewaddress "Withdrawal test"') addr = dashRpc('getnewaddress "Withdrawal test"')
+14 -31
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -75,24 +75,6 @@ def make_rpc_func(node_id, base_rpc_port):
return rpc_func return rpc_func
def wait_for_dcr_height(http_port, num_blocks=3):
logging.info("Waiting for DCR chain height %d", num_blocks)
for i in range(60):
if test_delay_event.is_set():
raise ValueError("Test stopped.")
try:
wallet = read_json_api(http_port, "wallets/dcr")
decred_blocks = wallet["blocks"]
print("decred_blocks", decred_blocks)
if decred_blocks >= num_blocks:
return
except Exception as e:
print("Error reading wallets", str(e))
test_delay_event.wait(1)
raise ValueError(f"wait_for_decred_blocks failed http_port: {http_port}")
def run_test_success_path(self, coin_from: Coins, coin_to: Coins): def run_test_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f"---------- Test {coin_from.name} to {coin_to.name}") logging.info(f"---------- Test {coin_from.name} to {coin_to.name}")
@@ -741,7 +723,7 @@ class Test(BaseTest):
ci0 = cls.swap_clients[0].ci(cls.test_coin) ci0 = cls.swap_clients[0].ci(cls.test_coin)
if not cls.restore_instance: if not cls.restore_instance:
dcr_mining_addr = ci0.rpc_wallet("getnewaddress") dcr_mining_addr = ci0.rpc_wallet("getnewaddress")
assert dcr_mining_addr in cls.dcr_mining_addrs assert dcr_mining_addr == cls.dcr_mining_addr
cls.dcr_ticket_account = ci0.rpc_wallet( cls.dcr_ticket_account = ci0.rpc_wallet(
"getaccount", "getaccount",
[ [
@@ -765,14 +747,14 @@ class Test(BaseTest):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Finalising Decred Test") logging.info("Finalising Decred Test")
super(Test, cls).tearDownClass() super().tearDownClass()
stopDaemons(cls.dcr_daemons) stopDaemons(cls.dcr_daemons)
cls.dcr_daemons.clear() cls.dcr_daemons.clear()
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
super(Test, cls).coins_loop() super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin) ci0 = cls.swap_clients[0].ci(cls.test_coin)
num_passed: int = 0 num_passed: int = 0
@@ -878,15 +860,16 @@ class Test(BaseTest):
"use_csv": True, "use_csv": True,
"use_segwit": True, "use_segwit": True,
"blocks_confirmed": 1, "blocks_confirmed": 1,
"min_relay_fee": 0.00001,
} }
def test_0001_decred_address(self): def test_0001_decred_address(self):
logging.info("---------- Test {}".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name}")
coin_settings = {"rpcport": 0, "rpcauth": "none"} coin_settings = {"rpcport": 0, "rpcauth": "none"}
coin_settings.update(REQUIRED_SETTINGS) coin_settings.update(REQUIRED_SETTINGS)
ci = DCRInterface(coin_settings, "mainnet") ci = DCRInterface(coin_settings, "mainnet", self.swap_clients[0])
k = ci.getNewRandomKey() k = ci.getNewRandomKey()
K = ci.getPubkey(k) K = ci.getPubkey(k)
@@ -914,7 +897,7 @@ class Test(BaseTest):
assert hash160(masterpubkey_data) == seed_hash assert hash160(masterpubkey_data) == seed_hash
def test_001_segwit(self): def test_001_segwit(self):
logging.info("---------- Test {} segwit".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} segwit")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -972,7 +955,7 @@ class Test(BaseTest):
assert f_decoded["txid"] == ctx.TxHash().hex() assert f_decoded["txid"] == ctx.TxHash().hex()
def test_003_signature_hash(self): def test_003_signature_hash(self):
logging.info("---------- Test {} signature_hash".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} signature_hash")
# Test that signing a transaction manually produces the same result when signed with the wallet # Test that signing a transaction manually produces the same result when signed with the wallet
swap_clients = self.swap_clients swap_clients = self.swap_clients
@@ -1047,7 +1030,7 @@ class Test(BaseTest):
assert len(sent_txid) == 64 assert len(sent_txid) == 64
def test_004_csv(self): def test_004_csv(self):
logging.info("---------- Test {} csv".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} csv")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -1161,7 +1144,7 @@ class Test(BaseTest):
assert sent_spend_txid is not None assert sent_spend_txid is not None
def test_005_watchonly(self): def test_005_watchonly(self):
logging.info("---------- Test {} watchonly".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} watchonly")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -1261,7 +1244,7 @@ class Test(BaseTest):
assert found_txid is not None assert found_txid is not None
def test_008_gettxout(self): def test_008_gettxout(self):
logging.info("---------- Test {} gettxout".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} gettxout")
ci0 = self.swap_clients[0].ci(self.test_coin) ci0 = self.swap_clients[0].ci(self.test_coin)
@@ -1373,7 +1356,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount assert amount_proved >= require_amount
def test_009_wallet_encryption(self): def test_009_wallet_encryption(self):
logging.info("---------- Test {} wallet encryption".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} wallet encryption")
for coin in ("part", "dcr", "xmr"): for coin in ("part", "dcr", "xmr"):
jsw = read_json_api(1800, f"wallets/{coin}") jsw = read_json_api(1800, f"wallets/{coin}")
@@ -1412,7 +1395,7 @@ class Test(BaseTest):
assert jsw["locked"] is False assert jsw["locked"] is False
def test_010_txn_size(self): def test_010_txn_size(self):
logging.info("---------- Test {} txn size".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} txn size")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci = swap_clients[0].ci(self.test_coin) ci = swap_clients[0].ci(self.test_coin)
+4 -2
View File
@@ -179,6 +179,7 @@ class Test(TestFunctions):
@classmethod @classmethod
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
super().prepareExtraCoins()
if cls.restore_instance: if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey() void_block_rewards_pubkey = cls.getRandomPubkey()
cls.doge_addr = ( cls.doge_addr = (
@@ -232,7 +233,7 @@ class Test(TestFunctions):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Finalising DOGE Test") logging.info("Finalising DOGE Test")
super(Test, cls).tearDownClass() super().tearDownClass()
stopDaemons(cls.doge_daemons) stopDaemons(cls.doge_daemons)
cls.doge_daemons.clear() cls.doge_daemons.clear()
@@ -251,11 +252,12 @@ class Test(TestFunctions):
"use_segwit": False, "use_segwit": False,
"blocks_confirmed": 1, "blocks_confirmed": 1,
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE "min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
"wallet_name": "bsx_wallet",
} }
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
super(Test, cls).coins_loop() super().coins_loop()
if cls.pause_chain: if cls.pause_chain:
return return
ci0 = cls.swap_clients[0].ci(cls.test_coin) ci0 = cls.swap_clients[0].ci(cls.test_coin)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -12,7 +12,7 @@ mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export PYTHONPATH=$(pwd) export PYTHONPATH=$(pwd)
export TEST_COINS_LIST='bitcoin,dogecoin' export TEST_COINS_LIST='bitcoin,dogecoin'
python tests/basicswap/extended/test_doge.py python tests/basicswap/extended/test_doge_with_prepare.py
""" """
@@ -27,14 +27,11 @@ from tests.basicswap.extended.test_xmr_persistent import (
BaseTestWithPrepare, BaseTestWithPrepare,
UI_PORT, UI_PORT,
) )
from tests.basicswap.extended.test_scripts import (
wait_for_offers,
)
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
wait_for_offers,
) )
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.DEBUG logger.level = logging.DEBUG
if not len(logger.handlers): if not len(logger.handlers):
@@ -51,11 +48,11 @@ def wait_for_bid(
bid = read_json_api(UI_PORT + node_id, f"bids/{bid_id}") bid = read_json_api(UI_PORT + node_id, f"bids/{bid_id}")
if "state" not in bid: if "bid_state" not in bid:
continue continue
if state is None: if state is None:
return return
if bid["state"].lower() == state.lower(): if bid["bid_state"].lower() == state.lower():
return return
raise ValueError("wait_for_bid failed") raise ValueError("wait_for_bid failed")
@@ -102,8 +99,9 @@ def prepare_balance(
class DOGETest(BaseTestWithPrepare): class DOGETest(BaseTestWithPrepare):
def test_a(self): __test__ = True
def test_a(self):
amount_from = 10.0 amount_from = 10.0
offer_json = { offer_json = {
"coin_from": "btc", "coin_from": "btc",
@@ -115,10 +113,8 @@ class DOGETest(BaseTestWithPrepare):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"] offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"]
logging.debug(f"offer_id {offer_id}")
prepare_balance(self.delay_event, 1, 0, "DOGE", 1000.0) prepare_balance(self.delay_event, 1, 0, "DOGE", 1000.0)
wait_for_offers(self.delay_event, 1, 1, offer_id) wait_for_offers(self.delay_event, 1, 1, offer_id)
post_json = {"offer_id": offer_id, "amount_from": amount_from} post_json = {"offer_id": offer_id, "amount_from": amount_from}
+1 -1
View File
@@ -104,7 +104,7 @@ def prepareDataDir(
fp.write("debug=1\n") fp.write("debug=1\n")
fp.write("debugexclude=libevent\n") fp.write("debugexclude=libevent\n")
fp.write("fallbackfee=0.01\n") fp.write("fallbackfee=0.0002\n")
fp.write("acceptnonstdtxn=0\n") fp.write("acceptnonstdtxn=0\n")
""" """
+1 -4
View File
@@ -29,18 +29,15 @@ import unittest
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
waitForServer, waitForServer,
UI_PORT,
) )
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.DEBUG logger.level = logging.DEBUG
if not len(logger.handlers): if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
ELECTRUM_PATH = os.getenv("ELECTRUM_PATH") ELECTRUM_PATH = os.getenv("ELECTRUM_PATH")
ELECTRUM_DATADIR = os.getenv("ELECTRUM_DATADIR") ELECTRUM_DATADIR = os.getenv("ELECTRUM_DATADIR")
-1
View File
@@ -58,7 +58,6 @@ from tests.basicswap.common import (
from basicswap.bin.run import startDaemon from basicswap.bin.run import startDaemon
logger = logging.getLogger() logger = logging.getLogger()
NUM_NODES = 3 NUM_NODES = 3
-1
View File
@@ -40,7 +40,6 @@ from tests.basicswap.extended.test_dcr import (
run_test_itx_refund, run_test_itx_refund,
) )
logger = logging.getLogger("BSX Tests") logger = logging.getLogger("BSX Tests")
if not len(logger.handlers): if not len(logger.handlers):
+179 -402
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,22 +11,14 @@ basicswap]$ python tests/basicswap/extended/test_pivx.py
""" """
import json
import logging import logging
import os import os
import random import random
import shutil
import signal
import sys import sys
import threading
import time
import unittest import unittest
from coincurve.keys import PrivateKey
import basicswap.config as cfg import basicswap.config as cfg
from basicswap.basicswap import ( from basicswap.basicswap import (
BasicSwap,
Coins, Coins,
SwapTypes, SwapTypes,
BidStates, BidStates,
@@ -39,45 +31,36 @@ from basicswap.util import (
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
TxLockTypes, TxLockTypes,
) )
from basicswap.util.address import (
toWIF,
)
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
) )
from tests.basicswap.common import ( from tests.basicswap.common import (
callrpc_cli, callrpc_cli,
checkForks,
stopDaemons, stopDaemons,
wait_for_bid, wait_for_bid,
wait_for_offer, wait_for_offer,
wait_for_balance, wait_for_balance,
wait_for_unspent,
wait_for_in_progress, wait_for_in_progress,
wait_for_bid_tx_state, wait_for_bid_tx_state,
TEST_HTTP_HOST,
TEST_HTTP_PORT, TEST_HTTP_PORT,
BASE_PORT,
BASE_RPC_PORT,
BASE_ZMQ_PORT,
PREFIX_SECRET_KEY_REGTEST,
waitForRPC, waitForRPC,
make_rpc_func,
) )
from tests.basicswap.test_xmr import (
BaseTest,
test_delay_event as delay_event,
callnoderpc,
)
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.bin.run import startDaemon from basicswap.bin.run import startDaemon
from basicswap.bin.prepare import downloadPIVXParams from basicswap.bin.prepare import downloadPIVXParams
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.DEBUG logger.level = logging.DEBUG
if not len(logger.handlers): if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
NUM_NODES = 3 NUM_NODES = 3
PIVX_NODE = 3
BTC_NODE = 4
delay_event = threading.Event()
stop_test = False
PIVX_BINDIR = os.path.expanduser( PIVX_BINDIR = os.path.expanduser(
os.getenv("PIVX_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "pivx")) os.getenv("PIVX_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "pivx"))
@@ -86,392 +69,173 @@ PIVXD = os.getenv("PIVXD", "pivxd" + cfg.bin_suffix)
PIVX_CLI = os.getenv("PIVX_CLI", "pivx-cli" + cfg.bin_suffix) PIVX_CLI = os.getenv("PIVX_CLI", "pivx-cli" + cfg.bin_suffix)
PIVX_TX = os.getenv("PIVX_TX", "pivx-tx" + cfg.bin_suffix) PIVX_TX = os.getenv("PIVX_TX", "pivx-tx" + cfg.bin_suffix)
PIVX_BASE_PORT = 34832
def prepareOtherDir(datadir, nodeId, conf_file="pivx.conf"): PIVX_BASE_RPC_PORT = 35832
node_dir = os.path.join(datadir, str(nodeId)) PIVX_BASE_ZMQ_PORT = 36832
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, conf_file)
with open(filePath, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(BASE_PORT + nodeId) + "\n")
fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n")
fp.write("daemon=0\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("findpeers=0\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
if conf_file == "pivx.conf":
params_dir = os.path.join(datadir, "pivx-params")
downloadPIVXParams(params_dir)
fp.write(f"paramsdir={params_dir}\n")
if conf_file == "bitcoin.conf":
fp.write("wallet=bsx_wallet\n")
def prepareDir(datadir, nodeId, network_key, network_pubkey): def pivxCli(cmd, node_id=0):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, "particl.conf")
with open(filePath, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(BASE_PORT + nodeId) + "\n")
fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n")
fp.write("daemon=0\n")
fp.write("printtoconsole=0\n")
fp.write("server=1\n")
fp.write("discover=0\n")
fp.write("listenonion=0\n")
fp.write("bind=127.0.0.1\n")
fp.write("findpeers=0\n")
fp.write("debug=1\n")
fp.write("debugexclude=libevent\n")
fp.write("zmqpubsmsg=tcp://127.0.0.1:" + str(BASE_ZMQ_PORT + nodeId) + "\n")
fp.write("wallet=bsx_wallet\n")
fp.write("fallbackfee=0.01\n")
fp.write("acceptnonstdtxn=0\n")
fp.write("minstakeinterval=5\n")
fp.write("smsgsregtestadjust=0\n")
for i in range(0, NUM_NODES):
if nodeId == i:
continue
fp.write("addnode=127.0.0.1:%d\n" % (BASE_PORT + i))
if nodeId < 2:
fp.write("spentindex=1\n")
fp.write("txindex=1\n")
basicswap_dir = os.path.join(datadir, str(nodeId), "basicswap")
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
pivxdatadir = os.path.join(datadir, str(PIVX_NODE))
btcdatadir = os.path.join(datadir, str(BTC_NODE))
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
settings = {
"debug": True,
"zmqhost": "tcp://127.0.0.1",
"zmqport": BASE_ZMQ_PORT + nodeId,
"htmlhost": TEST_HTTP_HOST,
"htmlport": TEST_HTTP_PORT + nodeId,
"network_key": network_key,
"network_pubkey": network_pubkey,
"chainclients": {
"particl": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + nodeId,
"datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing
},
"pivx": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + PIVX_NODE,
"datadir": pivxdatadir,
"bindir": PIVX_BINDIR,
"use_csv": False,
"use_segwit": False,
},
"bitcoin": {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": BASE_RPC_PORT + BTC_NODE,
"datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True,
},
},
"check_progress_seconds": 2,
"check_watched_seconds": 4,
"check_expired_seconds": 60,
"check_events_seconds": 1,
"check_xmr_swaps_seconds": 1,
"min_delay_event": 1,
"max_delay_event": 3,
"min_delay_event_short": 1,
"max_delay_event_short": 3,
"min_delay_retry": 2,
"max_delay_retry": 10,
"restrict_unknown_seed_wallets": False,
"check_updates": False,
}
with open(settings_path, "w") as fp:
json.dump(settings, fp, indent=4)
def partRpc(cmd, node_id=0):
return callrpc_cli(
cfg.PARTICL_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(node_id)),
"regtest",
cmd,
cfg.PARTICL_CLI,
)
def btcRpc(cmd):
return callrpc_cli(
cfg.BITCOIN_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)),
"regtest",
cmd,
cfg.BITCOIN_CLI,
)
def pivxRpc(cmd):
return callrpc_cli( return callrpc_cli(
PIVX_BINDIR, PIVX_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(node_id)),
"regtest", "regtest",
cmd, cmd,
PIVX_CLI, PIVX_CLI,
) )
def signal_handler(sig, frame): def prepareDataDir(
global stop_test datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3
os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8")) ):
stop_test = True node_dir = os.path.join(datadir, dir_prefix + str(node_id))
delay_event.set() if not os.path.exists(node_dir):
os.makedirs(node_dir)
cfg_file_path = os.path.join(node_dir, conf_file)
if os.path.exists(cfg_file_path):
return
with open(cfg_file_path, "w+") as fp:
fp.write("regtest=1\n")
fp.write("[regtest]\n")
fp.write("port=" + str(base_p2p_port + node_id) + "\n")
fp.write("rpcport=" + str(base_rpc_port + node_id) + "\n")
salt = generate_salt(16)
def run_coins_loop(cls): fp.write(
while not stop_test: "rpcauth={}:{}${}\n".format(
try: "test" + str(node_id),
pivxRpc("generatetoaddress 1 {}".format(cls.pivx_addr)) salt,
btcRpc("generatetoaddress 1 {}".format(cls.btc_addr)) password_to_hmac(salt, "test_pass" + str(node_id)),
except Exception as e:
logging.warning("run_coins_loop " + str(e))
time.sleep(1.0)
def run_loop(self):
while not stop_test:
for c in self.swap_clients:
c.update()
time.sleep(1)
def make_part_cli_rpc_func(node_id):
node_id = node_id
def rpc_func(method, params=None, wallet=None):
cmd = method
if params:
for p in params:
cmd += ' "' + p + '"'
return partRpc(cmd, node_id)
return rpc_func
class Test(unittest.TestCase):
test_coin_from = Coins.PIVX
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
k = PrivateKey()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret)
cls.network_pubkey = k.public_key.format().hex()
if os.path.isdir(cfg.TEST_DATADIRS):
logging.info("Removing " + cfg.TEST_DATADIRS)
for name in os.listdir(cfg.TEST_DATADIRS):
if name == "pivx-params":
continue
fullpath = os.path.join(cfg.TEST_DATADIRS, name)
if os.path.isdir(fullpath):
shutil.rmtree(fullpath)
else:
os.remove(fullpath)
for i in range(NUM_NODES):
prepareDir(cfg.TEST_DATADIRS, i, cls.network_key, cls.network_pubkey)
prepareOtherDir(cfg.TEST_DATADIRS, PIVX_NODE)
prepareOtherDir(cfg.TEST_DATADIRS, BTC_NODE, "bitcoin.conf")
cls.daemons = []
cls.swap_clients = []
btc_data_dir = os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE))
if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet")):
try:
callrpc_cli(
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=bsx_wallet -legacy create",
"bitcoin-wallet",
)
except Exception:
callrpc_cli(
cfg.BITCOIN_BINDIR,
btc_data_dir,
"regtest",
"-wallet=bsx_wallet create",
"bitcoin-wallet",
)
cls.daemons.append(startDaemon(btc_data_dir, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
logging.info("Started %s %d", cfg.BITCOIND, cls.daemons[-1].handle.pid)
cls.daemons.append(
startDaemon(
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), PIVX_BINDIR, PIVXD
) )
) )
logging.info("Started %s %d", PIVXD, cls.daemons[-1].handle.pid)
for i in range(NUM_NODES): fp.write("daemon=0\n")
data_dir = os.path.join(cfg.TEST_DATADIRS, str(i)) fp.write("printtoconsole=0\n")
if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, "particl-wallet")): fp.write("server=1\n")
try: fp.write("discover=0\n")
callrpc_cli( fp.write("listenonion=0\n")
cfg.PARTICL_BINDIR, fp.write("bind=127.0.0.1\n")
data_dir, fp.write("findpeers=0\n")
"regtest", fp.write("debug=1\n")
"-wallet=bsx_wallet -legacy create", fp.write("debugexclude=libevent\n")
"particl-wallet",
)
except Exception:
callrpc_cli(
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=bsx_wallet create",
"particl-wallet",
)
cls.daemons.append(startDaemon(data_dir, cfg.PARTICL_BINDIR, cfg.PARTICLD))
logging.info("Started %s %d", cfg.PARTICLD, cls.daemons[-1].handle.pid)
for i in range(NUM_NODES): fp.write("fallbackfee=0.01\n")
rpc = make_part_cli_rpc_func(i) fp.write("acceptnonstdtxn=0\n")
waitForRPC(rpc, delay_event)
if i == 0: params_dir = os.path.join(datadir, "pivx-params")
rpc( downloadPIVXParams(params_dir)
"extkeyimportmaster", fp.write(f"paramsdir={params_dir}\n")
[
"abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb" for i in range(0, num_nodes):
], if node_id == i:
) continue
elif i == 1: fp.write("addnode=127.0.0.1:{}\n".format(base_p2p_port + i))
rpc(
"extkeyimportmaster", return node_dir
[
"pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic",
"", class Test(BaseTest):
"true", __test__ = True
], test_coin_from = Coins.PIVX
) pivx_daemons = []
rpc("getnewextaddress", ["lblExtTest"]) pivx_addr = None
rpc("rescanblockchain") start_ltc_nodes = False
else: start_xmr_nodes = False
rpc("extkeyimportmaster", [rpc("mnemonic", ["new"])["master"]])
rpc( @classmethod
"walletsettings", def prepareExtraDataDir(cls, i):
[ extra_opts = []
"stakingoptions", if not cls.restore_instance:
json.dumps( prepareDataDir(
{"stakecombinethreshold": 100, "stakesplitthreshold": 200} cfg.TEST_DATADIRS,
).replace('"', '\\"'), i,
], "pivx.conf",
"pivx_",
base_p2p_port=PIVX_BASE_PORT,
base_rpc_port=PIVX_BASE_RPC_PORT,
) )
rpc("reservebalance", ["false"]) cls.pivx_daemons.append(
startDaemon(
basicswap_dir = os.path.join( os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(i)),
os.path.join(cfg.TEST_DATADIRS, str(i)), "basicswap" PIVX_BINDIR,
PIVXD,
opts=extra_opts,
) )
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) )
with open(settings_path) as fs: logging.info("Started %s %d", PIVXD, cls.pivx_daemons[-1].handle.pid)
settings = json.load(fs)
sc = BasicSwap( waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT), delay_event)
basicswap_dir, settings, "regtest", log_name="BasicSwap{}".format(i)
@classmethod
def addPIDInfo(cls, sc, i):
sc.setDaemonPID(Coins.PIVX, cls.pivx_daemons[i].handle.pid)
@classmethod
def prepareExtraCoins(cls):
if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.pivx_addr = (
cls.swap_clients[0]
.ci(Coins.PIVX)
.pubkey_to_address(void_block_rewards_pubkey)
) )
cls.swap_clients.append(sc) else:
sc.setDaemonPID(Coins.BTC, cls.daemons[0].handle.pid) num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351.
sc.setDaemonPID(Coins.PIVX, cls.daemons[1].handle.pid) logging.info(f"Mining {num_blocks} pivx blocks")
sc.setDaemonPID(Coins.PART, cls.daemons[2 + i].handle.pid) cls.pivx_addr = pivxCli("getnewaddress mining_addr")
sc.start() pivxCli(f"generatetoaddress {num_blocks} {cls.pivx_addr}")
waitForRPC(pivxRpc, delay_event) ro = pivxCli("getblockchaininfo")
num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351. try:
logging.info("Mining %d pivx blocks", num_blocks) assert ro["bip9_softforks"]["csv"]["status"] == "active"
cls.pivx_addr = pivxRpc("getnewaddress mining_addr") except Exception:
pivxRpc("generatetoaddress {} {}".format(num_blocks, cls.pivx_addr)) logging.info("pivx: csv is not active")
try:
ro = pivxRpc("getblockchaininfo") assert ro["bip9_softforks"]["segwit"]["status"] == "active"
try: except Exception:
assert ro["bip9_softforks"]["csv"]["status"] == "active" logging.info("pivx: segwit is not active")
except Exception:
logging.info("pivx: csv is not active")
try:
assert ro["bip9_softforks"]["segwit"]["status"] == "active"
except Exception:
logging.info("pivx: segwit is not active")
waitForRPC(btcRpc, delay_event)
cls.btc_addr = btcRpc("getnewaddress mining_addr bech32")
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr)
btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr))
ro = btcRpc("getblockchaininfo")
checkForks(ro)
signal.signal(signal.SIGINT, signal_handler)
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
cls.update_thread.start()
cls.coins_update_thread = threading.Thread(target=run_coins_loop, args=(cls,))
cls.coins_update_thread.start()
# Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3
logging.info("Waiting for Particl chain height %d", num_blocks)
for i in range(60):
particl_blocks = cls.swap_clients[0].callrpc("getblockcount")
print("particl_blocks", particl_blocks)
if particl_blocks >= num_blocks:
break
delay_event.wait(1)
assert particl_blocks >= num_blocks
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
global stop_test logging.info("Finalising PIVX Test")
logging.info("Finalising") super().tearDownClass()
stop_test = True
cls.update_thread.join()
cls.coins_update_thread.join()
for c in cls.swap_clients:
c.finalise()
stopDaemons(cls.daemons) stopDaemons(cls.pivx_daemons)
cls.swap_clients.clear() cls.pivx_daemons.clear()
cls.daemons.clear()
super(Test, cls).tearDownClass() @classmethod
def addCoinSettings(cls, settings, datadir, node_id):
settings["chainclients"]["pivx"] = {
"connection_type": "rpc",
"manage_daemon": False,
"rpcport": PIVX_BASE_RPC_PORT + node_id,
"rpcuser": "test" + str(node_id),
"rpcpassword": "test_pass" + str(node_id),
"datadir": os.path.join(datadir, "pivx_" + str(node_id)),
"bindir": PIVX_BINDIR,
"use_csv": False,
"use_segwit": False,
"wallet_name": "",
}
@classmethod
def coins_loop(cls):
super().coins_loop()
callnoderpc(
0, "generatetoaddress", [1, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT
)
@classmethod
def prepareBalances(cls):
super().prepareBalances()
cls.prepare_balance(
cls,
Coins.PIVX,
10000.0,
1801,
1800,
)
def test_02_part_pivx(self): def test_02_part_pivx(self):
logging.info("---------- Test PART to PIVX") logging.info("---------- Test PART to PIVX")
@@ -498,7 +262,7 @@ class Test(unittest.TestCase):
wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid( wait_for_bid(
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
) )
wait_for_bid( wait_for_bid(
delay_event, delay_event,
@@ -506,7 +270,7 @@ class Test(unittest.TestCase):
bid_id, bid_id,
BidStates.SWAP_COMPLETED, BidStates.SWAP_COMPLETED,
sent=True, sent=True,
wait_for=60, wait_for=80,
) )
js_0 = read_json_api(1800) js_0 = read_json_api(1800)
@@ -546,7 +310,7 @@ class Test(unittest.TestCase):
wait_for=60, wait_for=60,
) )
wait_for_bid( wait_for_bid(
delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
) )
js_0 = read_json_api(1800) js_0 = read_json_api(1800)
@@ -578,7 +342,7 @@ class Test(unittest.TestCase):
wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid( wait_for_bid(
delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80
) )
wait_for_bid( wait_for_bid(
delay_event, delay_event,
@@ -715,7 +479,7 @@ class Test(unittest.TestCase):
logging.info("---------- Test {} wallet".format(self.test_coin_from.name)) logging.info("---------- Test {} wallet".format(self.test_coin_from.name))
logging.info("Test withdrawal") logging.info("Test withdrawal")
addr = pivxRpc('getnewaddress "Withdrawal test"') addr = pivxCli('getnewaddress "Withdrawal test"')
wallets = read_json_api(TEST_HTTP_PORT + 0, "wallets") wallets = read_json_api(TEST_HTTP_PORT + 0, "wallets")
assert float(wallets[self.test_coin_from.name]["balance"]) > 100 assert float(wallets[self.test_coin_from.name]["balance"]) > 100
@@ -745,22 +509,32 @@ class Test(unittest.TestCase):
def test_09_v3_tx(self): def test_09_v3_tx(self):
logging.info("---------- Test PIVX v3 txns") logging.info("---------- Test PIVX v3 txns")
generate_addr = pivxRpc('getnewaddress "generate test"') generate_addr = pivxCli('getnewaddress "generate test"')
pivx_addr = pivxRpc('getnewaddress "Sapling test"') pivx_addr = pivxCli('getnewaddress "Sapling test"')
pivx_sapling_addr = pivxRpc('getnewshieldaddress "shield addr"') pivx_sapling_addr = pivxCli('getnewshieldaddress "shield addr"')
pivxRpc(f'sendtoaddress "{pivx_addr}" 6.0') pivxCli(f'sendtoaddress "{pivx_addr}" 6.0')
pivxRpc(f'generatetoaddress 1 "{generate_addr}"') pivxCli(f'generatetoaddress 1 "{generate_addr}"')
txid = pivxRpc( txid = pivxCli(
'shieldsendmany "{}" "[{{\\"address\\": \\"{}\\", \\"amount\\": 1}}]"'.format( 'shieldsendmany "{}" "[{{\\"address\\": \\"{}\\", \\"amount\\": 1}}]"'.format(
pivx_addr, pivx_sapling_addr pivx_addr, pivx_sapling_addr
) )
) )
rtx = pivxRpc(f'getrawtransaction "{txid}" true') rtx = pivxCli(f'getrawtransaction "{txid}" true')
assert rtx["version"] == 3 assert rtx["version"] == 3
block_hash = pivxRpc(f'generatetoaddress 1 "{generate_addr}"')[0] block_hash = None
for i in range(15):
rtx = pivxCli(f'getrawtransaction "{txid}" true')
if "blockhash" in rtx:
block_hash = rtx["blockhash"]
logging.info(f"Shielded tx confirmed in block {block_hash} after {i}s")
break
if i == 5:
pivxCli(f'generatetoaddress 1 "{generate_addr}"')
delay_event.wait(1)
assert block_hash is not None, "Shielded tx was not confirmed"
ci = self.swap_clients[0].ci(Coins.PIVX) ci = self.swap_clients[0].ci(Coins.PIVX)
block = ci.getBlockWithTxns(block_hash) block = ci.getBlockWithTxns(block_hash)
@@ -837,14 +611,17 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value) swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9) assert swap_value > ci_from.make_int(9)
itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True) addr_to = pi.getMockScriptAddr(ci_from)
funded_tx = ci_from.createRawFundedTransaction(
addr_to, swap_value, True, lock_unspents=True
)
itx = bytes.fromhex(funded_tx)
itx_decoded = ci_from.describeTx(itx.hex()) itx_decoded = ci_from.describeTx(itx.hex())
n = pi.findMockVout(ci_from, itx_decoded) n = pi.findMockVout(ci_from, itx_decoded)
value_after_subfee = ci_from.make_int(itx_decoded["vout"][n]["value"]) value_after_subfee = ci_from.make_int(itx_decoded["vout"][n]["value"])
assert value_after_subfee < swap_value assert value_after_subfee < swap_value
swap_value = value_after_subfee swap_value = value_after_subfee
wait_for_unspent(delay_event, ci_from, swap_value)
extra_options = {"prefunded_itx": itx} extra_options = {"prefunded_itx": itx}
rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1)
+3 -18
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2023-2024 tecnovert # Copyright (c) 2023-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -36,19 +36,16 @@ from tests.basicswap.common import (
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
waitForServer, waitForServer,
wait_for_offers,
UI_PORT,
) )
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.DEBUG logger.level = logging.DEBUG
if not len(logger.handlers): if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
class HttpHandler(BaseHTTPRequestHandler): class HttpHandler(BaseHTTPRequestHandler):
def js_response(self, url_split, post_string, is_json): def js_response(self, url_split, post_string, is_json):
@@ -132,18 +129,6 @@ def clear_offers(delay_event, node_id) -> None:
raise ValueError("clear_offers failed") raise ValueError("clear_offers failed")
def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None:
logging.info(f"Waiting for {num_offers} offers on node {node_id}")
for i in range(20):
delay_event.wait(1)
offers = read_json_api(
UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}"
)
if len(offers) >= num_offers:
return
raise ValueError("wait_for_offers failed")
def wait_for_bids(delay_event, node_id, num_bids, offer_id=None) -> None: def wait_for_bids(delay_event, node_id, num_bids, offer_id=None) -> None:
logging.info(f"Waiting for {num_bids} bids on node {node_id}") logging.info(f"Waiting for {num_bids} bids on node {node_id}")
for i in range(20): for i in range(20):
+4 -4
View File
@@ -5,9 +5,9 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
import logging import logging
import os import os
import time
from basicswap.basicswap import ( from basicswap.basicswap import (
Coins, Coins,
@@ -120,14 +120,14 @@ class Test(BaseTest):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Finalising Wownero Test") logging.info("Finalising Wownero Test")
super(Test, cls).tearDownClass() super().tearDownClass()
stopDaemons(cls.wow_daemons) stopDaemons(cls.wow_daemons)
cls.wow_daemons.clear() cls.wow_daemons.clear()
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
super(Test, cls).coins_loop() super().coins_loop()
if cls.wow_addr is not None: if cls.wow_addr is not None:
callrpc_xmr( callrpc_xmr(
@@ -162,7 +162,7 @@ class Test(BaseTest):
startXmrWalletDaemon(node_dir, WOW_BINDIR, WOW_WALLET_RPC, opts=opts) startXmrWalletDaemon(node_dir, WOW_BINDIR, WOW_WALLET_RPC, opts=opts)
) )
cls.wow_wallet_auth.append(("test{0}".format(i), "test_pass{0}".format(i))) cls.wow_wallet_auth.append((f"test{i}", f"test_pass{i}"))
waitForWOWNode(i, auth=cls.wow_wallet_auth[i]) waitForWOWNode(i, auth=cls.wow_wallet_auth[i])
+34 -28
View File
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups # Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/ cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with # Continue existing chains with
export RESET_TEST=false export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
""" """
import json import json
@@ -58,6 +59,8 @@ from tests.basicswap.util import (
make_boolean, make_boolean,
read_json_api, read_json_api,
waitForServer, waitForServer,
PORT_OFS,
UI_PORT,
) )
from tests.basicswap.common_xmr import ( from tests.basicswap.common_xmr import (
prepare_nodes, prepare_nodes,
@@ -72,9 +75,6 @@ import basicswap.bin.run as runSystem
test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent")) test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent"))
RESET_TEST = make_boolean(os.getenv("RESET_TEST", "true")) RESET_TEST = make_boolean(os.getenv("RESET_TEST", "true"))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
PARTICL_RPC_PORT_BASE = int(os.getenv("PARTICL_RPC_PORT_BASE", BASE_RPC_PORT)) PARTICL_RPC_PORT_BASE = int(os.getenv("PARTICL_RPC_PORT_BASE", BASE_RPC_PORT))
BITCOIN_RPC_PORT_BASE = int(os.getenv("BITCOIN_RPC_PORT_BASE", BTC_BASE_RPC_PORT)) BITCOIN_RPC_PORT_BASE = int(os.getenv("BITCOIN_RPC_PORT_BASE", BTC_BASE_RPC_PORT))
LITECOIN_RPC_PORT_BASE = int(os.getenv("LITECOIN_RPC_PORT_BASE", LTC_BASE_RPC_PORT)) LITECOIN_RPC_PORT_BASE = int(os.getenv("LITECOIN_RPC_PORT_BASE", LTC_BASE_RPC_PORT))
@@ -247,7 +247,7 @@ def updateThreadDCR(cls):
if "double spend" in str(e): if "double spend" in str(e):
pass pass
else: else:
logging.warning("updateThreadDCR purchaseticket {}".format(e)) logging.warning(f"updateThreadDCR purchaseticket {e}")
cls.delay_event.wait(0.5) cls.delay_event.wait(0.5)
try: try:
if num_passed >= 5: if num_passed >= 5:
@@ -259,7 +259,7 @@ def updateThreadDCR(cls):
], ],
) )
except Exception as e: except Exception as e:
logging.warning("updateThreadDCR generate {}".format(e)) logging.warning(f"updateThreadDCR generate {e}")
except Exception as e: except Exception as e:
print("updateThreadDCR error", str(e)) print("updateThreadDCR error", str(e))
cls.delay_event.wait(random.uniform(cls.dcr_update_min, cls.dcr_update_max)) cls.delay_event.wait(random.uniform(cls.dcr_update_min, cls.dcr_update_max))
@@ -271,7 +271,7 @@ def signal_handler(self, sig, frame):
def run_process(client_id): def run_process(client_id):
client_path = os.path.join(test_path, "client{}".format(client_id)) client_path = os.path.join(test_path, f"client{client_id}")
testargs = [ testargs = [
"basicswap-run", "basicswap-run",
"-datadir=" + client_path, "-datadir=" + client_path,
@@ -298,15 +298,24 @@ def start_processes(self):
for i in range(NUM_NODES): for i in range(NUM_NODES):
waitForServer(self.delay_event, UI_PORT + i) waitForServer(self.delay_event, UI_PORT + i)
wallets = read_json_api(UI_PORT + 1, "wallets")
if "monero" in self.test_coins_list: if "monero" in self.test_coins_list:
try:
for i in range(8):
wallets = read_json_api(UI_PORT + 1, "wallets")
if "XMR" in wallets and "main_address" in wallets["XMR"]:
break
logging.info("Waiting for wallets output")
self.delay_event.wait(1.0)
self.xmr_addr = wallets["XMR"]["main_address"]
except Exception as e:
logging.error("{} - wallets json: {}".format(str(e), json.dumps(wallets)))
raise
xmr_auth = None xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "": if os.getenv("XMR_RPC_USER", "") != "":
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", "")) xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
self.xmr_addr = wallets["XMR"]["main_address"] num_blocks: int = 100
num_blocks = 100
if ( if (
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[ callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count" "count"
@@ -321,10 +330,11 @@ def start_processes(self):
auth=xmr_auth, auth=xmr_auth,
) )
logging.info( logging.info(
"XMR blocks: %d", "XMR blocks: {}".format(
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[ callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count" "count"
], ]
)
) )
self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"]) self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
@@ -401,9 +411,7 @@ def start_processes(self):
have_blocks: int = callfirorpc(0, "getblockcount") have_blocks: int = callfirorpc(0, "getblockcount")
if have_blocks < num_blocks: if have_blocks < num_blocks:
logging.info( logging.info(
"Mining %d Firo blocks to %s", f"Mining {num_blocks - have_blocks} Firo blocks to {self.firo_addr}"
num_blocks - have_blocks,
self.firo_addr,
) )
callfirorpc( callfirorpc(
0, 0,
@@ -419,9 +427,7 @@ def start_processes(self):
have_blocks: int = callbchrpc(0, "getblockcount") have_blocks: int = callbchrpc(0, "getblockcount")
if have_blocks < num_blocks: if have_blocks < num_blocks:
logging.info( logging.info(
"Mining %d Bitcoincash blocks to %s", f"Mining {num_blocks - have_blocks} Bitcoincash blocks to {self.bch_addr}"
num_blocks - have_blocks,
self.bch_addr,
) )
callbchrpc( callbchrpc(
0, 0,
@@ -436,9 +442,7 @@ def start_processes(self):
have_blocks: int = calldogerpc(0, "getblockcount") have_blocks: int = calldogerpc(0, "getblockcount")
if have_blocks < num_blocks: if have_blocks < num_blocks:
logging.info( logging.info(
"Mining %d Dogecoin blocks to %s", f"Mining {num_blocks - have_blocks} Dogecoin blocks to {self.doge_addr}"
num_blocks - have_blocks,
self.doge_addr,
) )
calldogerpc( calldogerpc(
0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr] 0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr]
@@ -556,7 +560,10 @@ class BaseTestWithPrepare(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(BaseTestWithPrepare, cls).setUpClass() cls.addClassCleanup(
cls.finalise
) # tearDownClass is not run if setUpClass fails
super().setUpClass()
random.seed(time.time()) random.seed(time.time())
@@ -576,7 +583,7 @@ class BaseTestWithPrepare(unittest.TestCase):
waitForServer(cls.delay_event, UI_PORT + 1) waitForServer(cls.delay_event, UI_PORT + 1)
@classmethod @classmethod
def tearDownClass(cls): def finalise(cls):
logging.info("Stopping test") logging.info("Stopping test")
cls.delay_event.set() cls.delay_event.set()
if cls.update_thread: if cls.update_thread:
@@ -597,7 +604,6 @@ class BaseTestWithPrepare(unittest.TestCase):
class Test(BaseTestWithPrepare): class Test(BaseTestWithPrepare):
def test_persistent(self): def test_persistent(self):
while not self.delay_event.is_set(): while not self.delay_event.is_set():
logging.info("Looping indefinitely, ctrl+c to exit.") logging.info("Looping indefinitely, ctrl+c to exit.")
self.delay_event.wait(10) self.delay_event.wait(10)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert # Copyright (c) 2020 tecnovert
@@ -20,7 +20,6 @@ from util import (
) )
from tests.basicswap.util import read_json_api from tests.basicswap.util import read_json_api
base_url = "http://localhost" base_url = "http://localhost"
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -16,13 +16,31 @@ from tests.basicswap.util import (
from util import get_driver from util import get_driver
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
logger = logging.getLogger() logger = logging.getLogger()
logger.level = logging.INFO logger.level = logging.INFO
if not len(logger.handlers): if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout)) logger.addHandler(logging.StreamHandler(sys.stdout))
def wait_for_balance(
port: int,
coin: str,
expect_amount: float,
balance_key: str = "balance",
iterations: int = 30,
delay_time: int = 1,
) -> None:
logger.info(f"Waiting for balance, port: {port}")
for i in range(iterations):
rv_js = read_json_api(port, f"wallets/{coin}")
if float(rv_js[balance_key]) >= expect_amount:
return
time.sleep(delay_time)
logger.warning(f"{port} wallets/{coin} {rv_js}")
raise ValueError(f"Expect {balance_key} {expect_amount}")
def clear_offers(port_list) -> None: def clear_offers(port_list) -> None:
logger.info(f"clear_offers {port_list}") logger.info(f"clear_offers {port_list}")
@@ -60,7 +78,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_1_port, "offers/new", offer_data) rv = read_json_api(node_1_port, "offers/new", offer_data)
offer_1_id = rv["offer_id"] try:
offer_1_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
offer_data = { offer_data = {
"addr_from": -1, "addr_from": -1,
@@ -72,7 +94,13 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_1_port, "offers/new", offer_data) rv = read_json_api(node_1_port, "offers/new", offer_data)
offer_2_id = rv["offer_id"] try:
offer_2_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
wait_for_balance(node_2_port, "xmr", 5.0)
offer_data = { offer_data = {
"addr_from": -1, "addr_from": -1,
@@ -84,7 +112,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_2_port, "offers/new", offer_data) rv = read_json_api(node_2_port, "offers/new", offer_data)
offer_3_id = rv["offer_id"] try:
offer_3_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
# Wait for offers to propagate # Wait for offers to propagate
for i in range(1000): for i in range(1000):
-2
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert # Copyright (c) 2023 tecnovert
@@ -9,7 +8,6 @@
import os import os
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
BSX_0_PORT = int(os.getenv("BSX_0_PORT", 12701)) BSX_0_PORT = int(os.getenv("BSX_0_PORT", 12701))
BSX_1_PORT = int(os.getenv("BSX_1_PORT", BSX_0_PORT + 1)) BSX_1_PORT = int(os.getenv("BSX_1_PORT", BSX_0_PORT + 1))
BSX_2_PORT = int(os.getenv("BSX_1_PORT", BSX_0_PORT + 2)) BSX_2_PORT = int(os.getenv("BSX_1_PORT", BSX_0_PORT + 2))
+17 -19
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -167,6 +167,7 @@ class TestBCH(BasicSwapTest):
@classmethod @classmethod
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
super().prepareExtraCoins()
cls.bch_addr = callnoderpc( cls.bch_addr = callnoderpc(
0, 0,
"getnewaddress", "getnewaddress",
@@ -197,11 +198,12 @@ class TestBCH(BasicSwapTest):
"datadir": os.path.join(datadir, "bch_" + str(node_id)), "datadir": os.path.join(datadir, "bch_" + str(node_id)),
"bindir": BITCOINCASH_BINDIR, "bindir": BITCOINCASH_BINDIR,
"use_segwit": False, "use_segwit": False,
"wallet_name": "bsx_wallet",
} }
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
super(TestBCH, cls).coins_loop() super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin) ci0 = cls.swap_clients[0].ci(cls.test_coin)
try: try:
if cls.bch_addr is not None: if cls.bch_addr is not None:
@@ -212,7 +214,7 @@ class TestBCH(BasicSwapTest):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Finalising Bitcoincash Test") logging.info("Finalising Bitcoincash Test")
super(TestBCH, cls).tearDownClass() super().tearDownClass()
stopDaemons(cls.bch_daemons) stopDaemons(cls.bch_daemons)
cls.bch_daemons.clear() cls.bch_daemons.clear()
@@ -224,19 +226,15 @@ class TestBCH(BasicSwapTest):
return True return True
def test_001_nested_segwit(self): def test_001_nested_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin.name} p2sh nested segwit")
"---------- Test {} p2sh nested segwit".format(self.test_coin.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_002_native_segwit(self): def test_002_native_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin.name} p2sh native segwit")
"---------- Test {} p2sh native segwit".format(self.test_coin.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_003_cltv(self): def test_003_cltv(self):
logging.info("---------- Test {} cltv".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} cltv")
ci = self.swap_clients[0].ci(self.test_coin) ci = self.swap_clients[0].ci(self.test_coin)
@@ -348,7 +346,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_004_csv(self): def test_004_csv(self):
logging.info("---------- Test {} csv".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} csv")
ci = self.swap_clients[0].ci(self.test_coin) ci = self.swap_clients[0].ci(self.test_coin)
@@ -451,7 +449,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_005_watchonly(self): def test_005_watchonly(self):
logging.info("---------- Test {} watchonly".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} watchonly")
ci = self.swap_clients[0].ci(self.test_coin) ci = self.swap_clients[0].ci(self.test_coin)
ci1 = self.swap_clients[1].ci(self.test_coin) ci1 = self.swap_clients[1].ci(self.test_coin)
@@ -482,7 +480,7 @@ class TestBCH(BasicSwapTest):
super().test_006_getblock_verbosity() super().test_006_getblock_verbosity()
def test_007_hdwallet(self): def test_007_hdwallet(self):
logging.info("---------- Test {} hdwallet".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} hdwallet")
test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
test_wif = ( test_wif = (
@@ -506,10 +504,10 @@ class TestBCH(BasicSwapTest):
super().test_009_scantxoutset() super().test_009_scantxoutset()
def test_010_txn_size(self): def test_010_txn_size(self):
logging.info("---------- Test {} txn_size".format(Coins.BCH)) logging.info(f"---------- Test {self.test_coin.name} txn_size")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci = swap_clients[0].ci(Coins.BCH) ci = swap_clients[0].ci(self.test_coin)
pi = swap_clients[0].pi(SwapTypes.XMR_SWAP) pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
@@ -627,7 +625,7 @@ class TestBCH(BasicSwapTest):
def test_011_p2sh(self): def test_011_p2sh(self):
# Not used in bsx for native-segwit coins # Not used in bsx for native-segwit coins
logging.info("---------- Test {} p2sh".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} p2sh")
ci = self.swap_clients[0].ci(self.test_coin) ci = self.swap_clients[0].ci(self.test_coin)
@@ -717,7 +715,7 @@ class TestBCH(BasicSwapTest):
def test_011_p2sh32(self): def test_011_p2sh32(self):
# Not used in bsx for native-segwit coins # Not used in bsx for native-segwit coins
logging.info("---------- Test {} p2sh32".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} p2sh32")
ci = self.swap_clients[0].ci(self.test_coin) ci = self.swap_clients[0].ci(self.test_coin)
@@ -806,7 +804,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_012_p2sh_p2wsh(self): def test_012_p2sh_p2wsh(self):
logging.info("---------- Test {} p2sh-p2wsh".format(self.test_coin.name)) logging.info(f"---------- Test {self.test_coin.name} p2sh-p2wsh")
logging.info("Skipped") logging.info("Skipped")
def test_01_a_full_swap(self): def test_01_a_full_swap(self):
@@ -877,7 +875,7 @@ class TestBCH(BasicSwapTest):
def test_06_preselect_inputs(self): def test_06_preselect_inputs(self):
tla_from = self.test_coin.name tla_from = self.test_coin.name
logging.info("---------- Test {} Preselected inputs".format(tla_from)) logging.info(f"---------- Test {tla_from} Preselected inputs")
logging.info("Skipped") logging.info("Skipped")
def test_07_expire_stuck_accepted(self): def test_07_expire_stuck_accepted(self):
+329 -20
View File
@@ -74,6 +74,7 @@ class TestFunctions(BaseTest):
@classmethod @classmethod
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
# Save sent messages so tests can count them
for sc in cls.swap_clients: for sc in cls.swap_clients:
sc._smsg_add_to_outbox = True sc._smsg_add_to_outbox = True
@@ -113,7 +114,7 @@ class TestFunctions(BaseTest):
) )
def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None: def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None:
logging.info("---------- Test {} to {}".format(coin_from.name, coin_to.name)) logging.info(f"---------- Test {coin_from.name} to {coin_to.name}")
# Offerer sends the offer # Offerer sends the offer
# Bidder sends the bid # Bidder sends the bid
@@ -306,9 +307,7 @@ class TestFunctions(BaseTest):
self, coin_from: Coins, coin_to: Coins, lock_value: int = 32 self, coin_from: Coins, coin_to: Coins, lock_value: int = 32
) -> None: ) -> None:
logging.info( logging.info(
"---------- Test {} to {} leader recovers coin a lock tx".format( f"---------- Test {coin_from.name} to {coin_to.name} leader recovers coin a lock tx"
coin_from.name, coin_to.name
)
) )
id_offerer: int = self.node_a_id id_offerer: int = self.node_a_id
@@ -459,6 +458,12 @@ class TestFunctions(BaseTest):
if with_mercy if with_mercy
else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED) else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED)
) )
chain_a_coin = coin_to if reverse_bid else coin_from
if with_mercy is False and chain_a_coin == Coins.BCH:
# When using BCH, can't set XMR_SWAP_FAILED_SWIPED as should wait for mercy tx
expect_state = expect_state + (BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND,)
wait_for_bid( wait_for_bid(
test_delay_event, test_delay_event,
swap_clients[id_leader], swap_clients[id_leader],
@@ -492,12 +497,12 @@ class TestFunctions(BaseTest):
# Test manually redeeming the no-script lock tx # Test manually redeeming the no-script lock tx
offerer_key = read_json_api( offerer_key = read_json_api(
1800 + id_offerer, 1800 + id_offerer,
"bids/{}".format(bid_id.hex()), f"bids/{bid_id.hex()}",
{"chainbkeysplit": True}, {"chainbkeysplit": True},
)["splitkey"] )["splitkey"]
data = {"spendchainblocktx": True, "remote_key": offerer_key} data = {"spendchainblocktx": True, "remote_key": offerer_key}
redeemed_txid = read_json_api( redeemed_txid = read_json_api(
1800 + id_bidder, "bids/{}".format(bid_id.hex()), data 1800 + id_bidder, f"bids/{bid_id.hex()}", data
)["txid"] )["txid"]
assert len(redeemed_txid) == 64 assert len(redeemed_txid) == 64
@@ -505,9 +510,7 @@ class TestFunctions(BaseTest):
self, coin_from, coin_to, lock_value: int = 32 self, coin_from, coin_to, lock_value: int = 32
): ):
logging.info( logging.info(
"---------- Test {} to {} follower recovers coin b lock tx".format( f"---------- Test {coin_from.name} to {coin_to.name} follower recovers coin b lock tx"
coin_from.name, coin_to.name
)
) )
id_offerer: int = self.node_a_id id_offerer: int = self.node_a_id
@@ -920,7 +923,7 @@ class BasicSwapTest(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(BasicSwapTest, cls).setUpClass() super().setUpClass()
@classmethod @classmethod
def addCoinSettings(cls, settings, datadir, node_id): def addCoinSettings(cls, settings, datadir, node_id):
@@ -2161,8 +2164,8 @@ class BasicSwapTest(TestFunctions):
self.do_test_05_self_bid(Coins.XMR, self.test_coin_from) self.do_test_05_self_bid(Coins.XMR, self.test_coin_from)
def test_06_preselect_inputs(self): def test_06_preselect_inputs(self):
tla_from = self.test_coin_from.name tla_from: str = self.test_coin_from.name
logging.info("---------- Test {} Preselected inputs".format(tla_from)) logging.info(f"---------- Test {tla_from} Preselected inputs")
swap_clients = self.swap_clients swap_clients = self.swap_clients
self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800) self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800)
@@ -2262,12 +2265,168 @@ class BasicSwapTest(TestFunctions):
assert txin["txid"] == txin_after["txid"] assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"] assert txin["vout"] == txin_after["vout"]
def test_06_b_preselect_bid_inputs(self):
coin_from, coin_to = (Coins.PART, self.test_coin_from)
logging.info(
f"---------- Test {coin_from.name} to {coin_to.name} Preselected bid inputs"
)
id_offerer, id_bidder = (1, 0)
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
amt_swap: int = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
min_swap: int = ci_from.make_int(0.0001)
rate_swap: int = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
extra_options = {
"amount_negotiable": True,
"automation_id": 1,
}
offer_id = swap_clients[id_offerer].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
min_swap,
SwapTypes.XMR_SWAP,
extra_options=extra_options,
)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
prefunded_tx_data = swap_clients[id_bidder].createSubfeeBidTx(
offer_id, amount_to, offer.rate
)
ptx_decoded = ci_to.describeTx(prefunded_tx_data["bid_tx"].hex())
ci_to.rpc_wallet("lockunspent", [False, ptx_decoded["vin"]])
bid_tx = prefunded_tx_data["bid_tx"]
amount_from = prefunded_tx_data["amount_from"]
extra_options = {"prefunded_tx": bid_tx}
bid_id = swap_clients[id_bidder].postBid(
offer_id, amount_from, extra_options=extra_options
)
wait_for_bid(
test_delay_event,
swap_clients[id_offerer],
bid_id,
BidStates.SWAP_COMPLETED,
wait_for=120,
)
wait_for_bid(
test_delay_event,
swap_clients[id_bidder],
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=120,
)
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[id_bidder].getXmrBidAndOffer(bid_id)
assert bid.xmr_b_lock_tx
wtx = ci_to.rpc_wallet(
"gettransaction",
[
bid.xmr_b_lock_tx.txid.hex(),
],
)
ptx_after = ci_to.describeTx(wtx["hex"])
assert len(ptx_after["vin"]) == len(ptx_decoded["vin"])
for i, txin in enumerate(ptx_decoded["vin"]):
txin_after = ptx_after["vin"][i]
assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"]
def test_06_c_preselect_reverse_bid_inputs(self):
coin_from, coin_to = (Coins.XMR, self.test_coin_from)
logging.info(
f"---------- Test {coin_from.name} to {coin_to.name} Preselected reverse bid inputs"
)
id_offerer, id_bidder = (1, 0)
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
amt_swap: int = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
min_swap: int = ci_from.make_int(0.0001)
rate_swap: int = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
extra_options = {
"amount_negotiable": True,
"automation_id": 1,
}
offer_id = swap_clients[id_offerer].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
min_swap,
SwapTypes.XMR_SWAP,
extra_options=extra_options,
)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
prefunded_tx_data = swap_clients[id_bidder].createSubfeeBidTx(
offer_id, amount_to, offer.rate
)
ptx_decoded = ci_to.describeTx(prefunded_tx_data["bid_tx"].hex())
ci_to.rpc_wallet("lockunspent", [False, ptx_decoded["vin"]])
bid_tx = prefunded_tx_data["bid_tx"]
amount_from = prefunded_tx_data["amount_from"]
extra_options = {"prefunded_tx": bid_tx}
bid_id = swap_clients[id_bidder].postBid(
offer_id, amount_from, extra_options=extra_options
)
wait_for_bid(
test_delay_event,
swap_clients[id_offerer],
bid_id,
BidStates.SWAP_COMPLETED,
wait_for=180,
)
wait_for_bid(
test_delay_event,
swap_clients[id_bidder],
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=120,
)
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[id_bidder].getXmrBidAndOffer(bid_id)
assert bid.xmr_a_lock_tx
wtx = ci_to.rpc_wallet(
"gettransaction",
[
bid.xmr_a_lock_tx.txid.hex(),
],
)
ptx_after = ci_to.describeTx(wtx["hex"])
assert len(ptx_after["vin"]) == len(ptx_decoded["vin"])
for i, txin in enumerate(ptx_decoded["vin"]):
txin_after = ptx_after["vin"][i]
assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"]
def test_07_expire_stuck_accepted(self): def test_07_expire_stuck_accepted(self):
coin_from, coin_to = (self.test_coin_from, Coins.XMR) coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info( logging.info(
"---------- Test {} to {} expires bid stuck on accepted".format( f"---------- Test {coin_from.name} to {coin_to.name} expires bid stuck on accepted"
coin_from.name, coin_to.name
)
) )
swap_clients = self.swap_clients swap_clients = self.swap_clients
@@ -2324,10 +2483,160 @@ class BasicSwapTest(TestFunctions):
def test_09_expire_accepted_rev(self): def test_09_expire_accepted_rev(self):
self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from) self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from)
def test_10_presigned_txns(self): def test_11_fee_validation(self):
raise RuntimeError( coin_from, coin_to = (self.test_coin_from, Coins.XMR)
"TODO" logging.info(
) # Build without xmr first for quicker test iterations f"---------- Test {coin_from.name} to {coin_to.name} expires bid stuck on accepted"
)
swap_clients = self.swap_clients
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[0].ci(coin_to)
ci1_from = swap_clients[1].ci(coin_from)
ci1_to = swap_clients[1].ci(coin_to)
amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
amt_swap,
SwapTypes.XMR_SWAP,
)
amt_swap_reverse = ci1_to.make_int(random.uniform(0.1, 2.0), r=1)
offer_reverse_id = swap_clients[1].postOffer(
coin_to,
coin_from,
amt_swap_reverse,
rate_swap,
amt_swap_reverse,
SwapTypes.XMR_SWAP,
)
ci_from_settings = swap_clients[0].getChainClientSettings(coin_from)
old_override_feerate = ci_from_settings.get("override_feerate", None)
ci1_from_settings = swap_clients[1].getChainClientSettings(coin_from)
old_override_feerate1 = ci1_from_settings.get("override_feerate", None)
networkinfo = ci_from.rpc("getnetworkinfo")
assert ci_from.make_int(networkinfo["relayfee"]) == ci_from.make_int(
ci_from.get_fee_rate()[0]
)
try:
# Set override_feerate to increase feerate from get_fee_rate()
ci_from_settings["override_feerate"] = ci_from.format_amount(120)
ci1_from_settings["override_feerate"] = ci1_from.format_amount(120)
try:
swap_clients[0].postXmrBid(offer_id, amt_swap)
except Exception as e:
assert "Fee rate too low, 100 < 120, override_feerate" in str(e)
else:
assert False, "Should fail"
# Test reverse bid, low fee
try:
swap_clients[1].postXmrBid(offer_reverse_id, amt_swap_reverse)
except Exception as e:
assert "Fee rate too low, 100 < 120, override_feerate" in str(e)
else:
assert False, "Should fail"
# Clear override_feerate (get_fee_rate()), set low_feerate (validateFeeRate())
ci_from_settings["override_feerate"] = None
ci1_from_settings["override_feerate"] = None
ci_from._low_feerate = 120
ci1_from._low_feerate = 120
try:
swap_clients[0].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
amt_swap,
SwapTypes.XMR_SWAP,
)
except Exception as e:
assert "Fee rate too low, 100 < 120, set_value" in str(e)
else:
assert False, "Should fail"
# Test reverse offer, low fee
try:
swap_clients[1].postOffer(
coin_to,
coin_from,
amt_swap_reverse,
rate_swap,
amt_swap_reverse,
SwapTypes.XMR_SWAP,
)
except Exception as e:
assert "Fee rate too low, 100 < 120, set_value" in str(e)
else:
assert False, "Should fail"
ci_from._low_feerate = 0
ci1_from._low_feerate = 0
ci_from._high_estimated_feerate_multiplier = (
0 # Disable high fee from estimate
)
ci_from._high_feerate = (
80 # ci_from_settings["high_feerate"] = ci_from.format_amount(80)
)
try:
swap_clients[0].postXmrBid(offer_id, amt_swap)
except Exception as e:
assert "Fee rate too high, 100 > 80, set_value" in str(e)
else:
assert False, "Should fail"
# Test reverse bid, high fee
ci1_from._high_estimated_feerate_multiplier = 0
ci1_from._high_feerate = 80
try:
swap_clients[1].postXmrBid(offer_reverse_id, amt_swap_reverse)
except Exception as e:
assert "Fee rate too high, 100 > 80, set_value" in str(e)
else:
assert False, "Should fail"
try:
swap_clients[0].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
amt_swap,
SwapTypes.XMR_SWAP,
)
except Exception as e:
assert "Fee rate too high, 100 > 80, set_value" in str(e)
else:
assert False, "Should fail"
# Test reverse offer, high fee
try:
swap_clients[1].postOffer(
coin_to,
coin_from,
amt_swap_reverse,
rate_swap,
amt_swap_reverse,
SwapTypes.XMR_SWAP,
)
except Exception as e:
assert "Fee rate too high, 100 > 80, set_value" in str(e)
else:
assert False, "Should fail"
finally:
ci_from_settings["override_feerate"] = old_override_feerate
ci1_from_settings["override_feerate"] = old_override_feerate1
class TestBTC(BasicSwapTest): class TestBTC(BasicSwapTest):
@@ -2511,7 +2820,7 @@ class TestBTC_PARTB(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestBTC_PARTB, cls).setUpClass() super().setUpClass()
if False: if False:
for client in cls.swap_clients: for client in cls.swap_clients:
client.log.safe_logs = True client.log.safe_logs = True
+6 -10
View File
@@ -115,6 +115,10 @@ def modify_config(test_path, i):
with open(config_path, "w") as fp: with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4) json.dump(settings, fp, indent=4)
btc_config_path = os.path.join(test_path, f"client{i}", "bitcoin", "bitcoin.conf")
with open(btc_config_path, "a") as fp:
fp.write("minrelaytxfee=0.00001\n")
def wait_for_bid_state( def wait_for_bid_state(
delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30 delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30
@@ -641,7 +645,7 @@ class Test(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.addElectrumxDaemon("bitcoin", 32793, 50001) cls.addElectrumxDaemon("bitcoin", 32793, 50001)
super(Test, cls).setUpClass() super().setUpClass()
@classmethod @classmethod
def modifyConfig(cls, test_path, i): def modifyConfig(cls, test_path, i):
@@ -754,14 +758,6 @@ class Test(TestFunctions):
self.delay_event, self.delay_event,
self.test_coin_b, self.test_coin_b,
100, 100,
self.port_node_1,
self.port_node_0,
True,
)
prepare_balance(
self.delay_event,
self.test_coin_xmr,
100,
self.port_node_0, self.port_node_0,
self.port_node_1, self.port_node_1,
True, True,
@@ -788,7 +784,7 @@ class Test(TestFunctions):
True, True,
) )
self.do_test_03_follower_recover_a_lock_tx( self.do_test_03_follower_recover_a_lock_tx(
self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0 self.test_coin_b, self.test_coin_xmr, self.port_node_0, self.port_node_1
) )
def test_03_b_follower_recover_a_lock_tx_reverse(self): def test_03_b_follower_recover_a_lock_tx_reverse(self):
+102 -11
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -48,15 +48,11 @@ class TestLTC(BasicSwapTest):
assert deploymentinfo["softforks"][feature_name]["active"] is True assert deploymentinfo["softforks"][feature_name]["active"] is True
def test_001_nested_segwit(self): def test_001_nested_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin_from.name} p2sh nested segwit")
"---------- Test {} p2sh nested segwit".format(self.test_coin_from.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_002_native_segwit(self): def test_002_native_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin_from.name} p2sh native segwit")
"---------- Test {} p2sh native segwit".format(self.test_coin_from.name)
)
ci = self.swap_clients[0].ci(self.test_coin_from) ci = self.swap_clients[0].ci(self.test_coin_from)
addr_segwit = ci.rpc_wallet("getnewaddress", ["segwit test", "bech32"]) addr_segwit = ci.rpc_wallet("getnewaddress", ["segwit test", "bech32"])
@@ -120,7 +116,7 @@ class TestLTC(BasicSwapTest):
assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"] assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"]
def test_007_hdwallet(self): def test_007_hdwallet(self):
logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} hdwallet")
test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
test_wif = ( test_wif = (
@@ -136,7 +132,7 @@ class TestLTC(BasicSwapTest):
assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la" assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la"
def test_20_btc_coin(self): def test_20_btc_coin(self):
logging.info("---------- Test BTC to {}".format(self.test_coin_from.name)) logging.info(f"---------- Test BTC to {self.test_coin_from.name}")
swap_clients = self.swap_clients swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
@@ -178,12 +174,21 @@ class TestLTC(BasicSwapTest):
assert js_1["num_swapping"] == 0 and js_1["num_watched_outputs"] == 0 assert js_1["num_swapping"] == 0 and js_1["num_watched_outputs"] == 0
def test_21_mweb(self): def test_21_mweb(self):
logging.info("---------- Test MWEB {}".format(self.test_coin_from.name)) logging.info(f"---------- Test MWEB {self.test_coin_from.name}")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin_from) ci0 = swap_clients[0].ci(self.test_coin_from)
ci1 = swap_clients[1].ci(self.test_coin_from) ci1 = swap_clients[1].ci(self.test_coin_from)
# mweb utxos before sending to mweb
num_mweb: int = 0
utxos_0 = ci0.rpc_wallet("listunspent")
for utxo in utxos_0:
addr_info = ci0.rpc_wallet("getaddressinfo", [utxo["address"]])
if addr_info["ismweb"] is True:
num_mweb += 1
assert num_mweb == 1
mweb_addr_0 = ci0.rpc_wallet("getnewaddress", ["mweb addr test 0", "mweb"]) mweb_addr_0 = ci0.rpc_wallet("getnewaddress", ["mweb addr test 0", "mweb"])
mweb_addr_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"]) mweb_addr_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"])
@@ -210,6 +215,19 @@ class TestLTC(BasicSwapTest):
< 0.1 < 0.1
) )
num_mweb: int = 0
utxos_0 = ci0.rpc_wallet(
"listunspent",
[
0,
],
)
for utxo in utxos_0:
addr_info = ci0.rpc_wallet("getaddressinfo", [utxo["address"]])
if addr_info["ismweb"] is True:
num_mweb += 1
assert num_mweb > 1
try: try:
pause_event.clear() # Stop mining pause_event.clear() # Stop mining
ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0]) ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0])
@@ -237,6 +255,7 @@ class TestLTC(BasicSwapTest):
for utxo in utxos: for utxo in utxos:
if utxo.get("address", "") == mweb_addr_1: if utxo.get("address", "") == mweb_addr_1:
mweb_tx = utxo mweb_tx = utxo
break
assert mweb_tx is not None assert mweb_tx is not None
unspent_addr = ci1.getUnspentsByAddr() unspent_addr = ci1.getUnspentsByAddr()
@@ -245,10 +264,66 @@ class TestLTC(BasicSwapTest):
if "mweb1" in addr: if "mweb1" in addr:
raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.") raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.")
# Test helper functions to convert MWEB change
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value > 0
test_lock_utxo = None
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
test_lock_utxo = {"txid": utxo["txid"], "vout": utxo["vout"]}
ci0.rpc_wallet(
"lockunspent",
[
False,
[
test_lock_utxo,
],
],
)
break
assert len(ci0.rpc_wallet("listlockunspent")) == 1
txid = ci0.convertMWEBBalance()
# Check utxos locked before conversion are still locked after
assert len(ci0.rpc_wallet("listlockunspent")) == 1
ci0.rpc_wallet(
"lockunspent",
[
True,
[
test_lock_utxo,
],
],
)
assert len(ci0.rpc_wallet("listlockunspent")) == 0
txj = ci0.rpc_wallet(
"gettransaction",
[
txid,
],
)
assert len(txj["details"]) == 2
fee_amt = -ci0.make_int(txj["fee"])
assert txj["details"][0]["category"] == "send"
assert ci0.make_int(txj["details"][0]["amount"]) - fee_amt == -mweb_change_value
assert txj["details"][1]["category"] == "receive"
assert ci0.make_int(txj["details"][1]["amount"]) + fee_amt == mweb_change_value
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value == 0
# TODO # TODO
def test_22_mweb_balance(self): def test_22_mweb_balance(self):
logging.info("---------- Test MWEB balance {}".format(self.test_coin_from.name)) logging.info(f"---------- Test MWEB balance {self.test_coin_from.name}")
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB) ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB)
@@ -265,7 +340,9 @@ class TestLTC(BasicSwapTest):
ltc_mweb_addr = read_json_api( ltc_mweb_addr = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc_mweb/nextdepositaddr" TEST_HTTP_PORT + 0, "wallets/ltc_mweb/nextdepositaddr"
) )
assert ltc_mweb_addr.startswith("tmweb1")
ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc/newmwebaddress") ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc/newmwebaddress")
assert ltc_mweb_addr2.startswith("tmweb1")
assert ( assert (
ci_mweb.rpc_wallet( ci_mweb.rpc_wallet(
@@ -337,6 +414,20 @@ class TestLTC(BasicSwapTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json) json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json)
assert json_rv["mweb_balance"] <= 20.0 assert json_rv["mweb_balance"] <= 20.0
# Test helper functions to convert MWEB change
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) > 0
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/convertmweb", post_json
)
assert len(json_rv) == 64
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) == 0
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+79 -3
View File
@@ -8,6 +8,7 @@
import hashlib import hashlib
import logging import logging
import os
import random import random
import secrets import secrets
import threading import threading
@@ -22,10 +23,15 @@ from coincurve.ecdsaotves import (
) )
from coincurve.keys import PrivateKey from coincurve.keys import PrivateKey
from basicswap.basicswap import (
Coins,
BasicSwap,
SwapTypes,
)
from basicswap.contrib.mnemonic import Mnemonic from basicswap.contrib.mnemonic import Mnemonic
from basicswap.db import create_db_, DBMethods, KnownIdentity from basicswap.db import create_db_, DBMethods, KnownIdentity
from basicswap.util import h2b from basicswap.util import h2b
from basicswap.util.address import decodeAddress from basicswap.util.address import decodeAddress, toWIF
from basicswap.util.crypto import ripemd160, hash160, blake256 from basicswap.util.crypto import ripemd160, hash160, blake256
from basicswap.util.extkey import ExtKeyPair from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint, decode_varint from basicswap.util.integer import encode_varint, decode_varint
@@ -60,7 +66,9 @@ from basicswap.contrib.test_framework.messages import (
CTxOut, CTxOut,
uint256_from_str, uint256_from_str,
) )
from tests.basicswap.common import (
PREFIX_SECRET_KEY_REGTEST,
)
logger = logging.getLogger() logger = logging.getLogger()
@@ -157,7 +165,7 @@ class Test(unittest.TestCase):
assert str(e) == "Mantissa too long" assert str(e) == "Mantissa too long"
validate_amount("0.12345678") validate_amount("0.12345678")
# floor # Floor
assert make_int("0.123456789", r=-1) == 12345678 assert make_int("0.123456789", r=-1) == 12345678
# Round up # Round up
assert make_int("0.123456789", r=1) == 12345679 assert make_int("0.123456789", r=1) == 12345679
@@ -791,6 +799,74 @@ class Test(unittest.TestCase):
== "252cd6e85b99e0fd554c44d5fe638923f7ef563048362406a665cf3400feb1bd" == "252cd6e85b99e0fd554c44d5fe638923f7ef563048362406a665cf3400feb1bd"
) )
def test_validateSwapType(self):
logging.info("---------- Test validateSwapType")
basicswap_dir = "/tmp/bsx_test_other"
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
k = PrivateKey()
settings = {
"network_key": toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret),
"network_pubkey": k.public_key.format().hex(),
}
sc = BasicSwap(
basicswap_dir,
settings,
"regtest",
log_name="bsx_test_other",
)
should_pass = [
(Coins.BTC, Coins.XMR, SwapTypes.XMR_SWAP),
(Coins.XMR, Coins.BTC, SwapTypes.XMR_SWAP),
(Coins.BTC, Coins.FIRO, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.BTC, SwapTypes.XMR_SWAP),
(Coins.PIVX, Coins.BTC, SwapTypes.SELLER_FIRST),
(Coins.BTC, Coins.PIVX, SwapTypes.SELLER_FIRST),
(Coins.DASH, Coins.PIVX, SwapTypes.SELLER_FIRST),
(Coins.PIVX, Coins.DASH, SwapTypes.SELLER_FIRST),
]
should_fail = [
(Coins.BTC, Coins.XMR, SwapTypes.SELLER_FIRST),
(Coins.XMR, Coins.PART_ANON, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.PART_ANON, SwapTypes.XMR_SWAP),
(Coins.PART_ANON, Coins.FIRO, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.BTC, SwapTypes.SELLER_FIRST),
(Coins.BTC, Coins.FIRO, SwapTypes.SELLER_FIRST),
]
for case in should_pass:
sc.validateSwapType(case[0], case[1], case[2])
for case in should_fail:
self.assertRaises(
ValueError, sc.validateSwapType, case[0], case[1], case[2]
)
sc.chain = "mainnet"
for case in should_pass:
try:
sc.validateSwapType(case[0], case[1], case[2])
except Exception as e:
assert "Coin pair should use adaptor sig swap type" in str(e)
else:
if case[2] != SwapTypes.XMR_SWAP:
if (
case[0] not in sc.coins_without_segwit
or case[1] not in sc.coins_without_segwit
):
raise ValueError(f"Invalid swap pair in strict mode {case}")
for case in should_fail:
self.assertRaises(
ValueError, sc.validateSwapType, case[0], case[1], case[2]
)
sc.settings["strict_swap_type"] = False
for case in should_pass:
sc.validateSwapType(case[0], case[1], case[2])
del sc
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+2 -2
View File
@@ -104,7 +104,7 @@ class Test(BaseTest):
) )
def test_010_txn_size(self): def test_010_txn_size(self):
logging.info("---------- Test {} txn_size".format(self.test_coin_from.name)) logging.info(f"---------- Test {self.test_coin_from.name} txn_size")
self.ensure_balance(self.test_coin_from, 0, 100.0) self.ensure_balance(self.test_coin_from, 0, 100.0)
@@ -159,7 +159,7 @@ class Test(BaseTest):
ci.rpc_wallet("sendrawtransaction", [lock_tx.hex()]) ci.rpc_wallet("sendrawtransaction", [lock_tx.hex()])
rv = ci.rpc_wallet("gettransaction", [txid]) rv = ci.rpc_wallet("gettransaction", [txid])
wallet_tx_fee = -ci.make_int(rv["details"][0]["fee"]) wallet_tx_fee = -ci.make_int(rv["fee"])
assert wallet_tx_fee >= expect_fee_int assert wallet_tx_fee >= expect_fee_int
assert wallet_tx_fee - expect_fee_int < 20 assert wallet_tx_fee - expect_fee_int < 20
-30
View File
@@ -63,7 +63,6 @@ from basicswap.contrib.test_framework.script import (
) )
from tests.basicswap.test_xmr import BaseTest, test_delay_event, callnoderpc from tests.basicswap.test_xmr import BaseTest, test_delay_event, callnoderpc
logger = logging.getLogger() logger = logging.getLogger()
@@ -160,35 +159,6 @@ class Test(BaseTest):
rv = read_json_api(1800, "rateslist?from=PART&to=BTC") rv = read_json_api(1800, "rateslist?from=PART&to=BTC")
assert len(rv) == 1 assert len(rv) == 1
def test_004_validateSwapType(self):
logging.info("---------- Test validateSwapType")
sc = self.swap_clients[0]
should_pass = [
(Coins.BTC, Coins.XMR, SwapTypes.XMR_SWAP),
(Coins.XMR, Coins.BTC, SwapTypes.XMR_SWAP),
(Coins.BTC, Coins.FIRO, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.BTC, SwapTypes.XMR_SWAP),
(Coins.PIVX, Coins.BTC, SwapTypes.SELLER_FIRST),
(Coins.BTC, Coins.PIVX, SwapTypes.SELLER_FIRST),
]
should_fail = [
(Coins.BTC, Coins.XMR, SwapTypes.SELLER_FIRST),
(Coins.XMR, Coins.PART_ANON, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.PART_ANON, SwapTypes.XMR_SWAP),
(Coins.PART_ANON, Coins.FIRO, SwapTypes.XMR_SWAP),
(Coins.FIRO, Coins.BTC, SwapTypes.SELLER_FIRST),
(Coins.BTC, Coins.FIRO, SwapTypes.SELLER_FIRST),
]
for case in should_pass:
sc.validateSwapType(case[0], case[1], case[2])
for case in should_fail:
self.assertRaises(
ValueError, sc.validateSwapType, case[0], case[1], case[2]
)
def test_003_cltv(self): def test_003_cltv(self):
test_coin_from = Coins.PART test_coin_from = Coins.PART
logging.info("---------- Test {} cltv".format(test_coin_from.name)) logging.info("---------- Test {} cltv".format(test_coin_from.name))
+10 -7
View File
@@ -38,9 +38,7 @@ from basicswap.basicswap_util import (
EventLogTypes, EventLogTypes,
) )
from basicswap.util import COIN, format_amount, make_int, TemporaryError from basicswap.util import COIN, format_amount, make_int, TemporaryError
from basicswap.util.address import ( from basicswap.util.address import toWIF
toWIF,
)
from basicswap.rpc import ( from basicswap.rpc import (
callrpc, callrpc,
) )
@@ -91,7 +89,6 @@ from basicswap.db_util import (
) )
from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon
logger = logging.getLogger() logger = logging.getLogger()
NUM_NODES = 3 NUM_NODES = 3
@@ -222,6 +219,7 @@ def prepare_swapclient_dir(
"datadir": os.path.join(datadir, "ltc_" + str(node_id)), "datadir": os.path.join(datadir, "ltc_" + str(node_id)),
"bindir": cfg.LITECOIN_BINDIR, "bindir": cfg.LITECOIN_BINDIR,
"use_segwit": True, "use_segwit": True,
"wallet_name": "bsx_wallet",
} }
if cls: if cls:
@@ -815,7 +813,7 @@ class BaseTest(unittest.TestCase):
.pubkey_to_address(void_block_rewards_pubkey) .pubkey_to_address(void_block_rewards_pubkey)
) )
logging.info( logging.info(
"Mining %d Litecoin blocks to %s", num_blocks, cls.ltc_addr f"Mining {num_blocks} Litecoin blocks to {cls.ltc_addr}"
) )
callnoderpc( callnoderpc(
0, 0,
@@ -942,6 +940,7 @@ class BaseTest(unittest.TestCase):
) )
cls.coins_update_thread.start() cls.coins_update_thread.start()
cls.prepareBalances()
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
cls.tearDownClass() cls.tearDownClass()
@@ -999,6 +998,10 @@ class BaseTest(unittest.TestCase):
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
pass pass
@classmethod
def prepareBalances(cls):
pass
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
if cls.btc_addr is not None: if cls.btc_addr is not None:
@@ -2590,7 +2593,7 @@ class Test(BaseTest):
swap_clients[2], swap_clients[2],
bid_id, bid_id,
BidStates.SWAP_COMPLETED, BidStates.SWAP_COMPLETED,
wait_for=120, wait_for=180,
) )
wait_for_bid( wait_for_bid(
test_delay_event, test_delay_event,
@@ -2598,7 +2601,7 @@ class Test(BaseTest):
bid_id, bid_id,
BidStates.SWAP_COMPLETED, BidStates.SWAP_COMPLETED,
sent=True, sent=True,
wait_for=120, wait_for=180,
) )
# Verify expected inputs were used # Verify expected inputs were used
+20 -5
View File
@@ -1,15 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
import json import json
import logging
import os
import urllib import urllib
from urllib.request import urlopen from urllib.request import urlopen
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
REQUIRED_SETTINGS = { REQUIRED_SETTINGS = {
"blocks_confirmed": 1, "blocks_confirmed": 1,
@@ -59,14 +62,26 @@ def post_json_api(port, path, json_data):
return json.loads(post_json_req(url, json_data)) return json.loads(post_json_req(url, json_data))
def waitForServer(delay_event, port, wait_for=20): def waitForServer(delay_event, port, wait_for=40):
for i in range(wait_for): for i in range(wait_for):
if delay_event.is_set(): if delay_event.is_set():
raise ValueError("Test stopped.") raise ValueError("Test stopped.")
try: try:
delay_event.wait(1) delay_event.wait(1.0)
_ = read_json_api(port) _ = read_json_api(port)
return return
except Exception as e: except Exception as e:
print("waitForServer, error:", str(e)) logging.error(f"waitForServer: {e}")
raise ValueError("waitForServer failed") raise ValueError("waitForServer failed")
def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None:
logging.info(f"Waiting for {num_offers} offers on node {node_id}")
for i in range(20):
delay_event.wait(1)
offers = read_json_api(
UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}"
)
if len(offers) >= num_offers:
return
raise ValueError("wait_for_offers failed")
+1
View File
@@ -0,0 +1 @@
confimration->confirmation