Compare commits

...

88 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 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 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 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
tecnovert e06c4638d3 fix: workaround for osx ltc release not on github 2026-04-29 12:24:35 +02:00
97 changed files with 2831 additions and 1354 deletions
+19 -3
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.14"]
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
@@ -60,7 +60,7 @@ jobs:
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Run codespell
run: |
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
codespell
- name: Run black
run: |
black --check --diff --exclude="contrib" .
@@ -92,15 +92,26 @@ jobs:
export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero"
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
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
id: test_encrypted_xmr_reload
run: |
export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: 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
id: selenium_tests
run: |
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
@@ -126,3 +137,8 @@ jobs:
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
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 \
DEBIAN_FRONTEND=noninteractive \
DATADIRS="/coindata"
DATADIRS="/coindata" \
VIRTUAL_ENV=/opt/venv
RUN apt-get update; \
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
COPY ./requirements.txt requirements.txt
RUN pip3 install -r requirements.txt --require-hashes
RUN pip install -r requirements.txt --require-hashes
COPY . basicswap-master
RUN cd basicswap-master; \
pip3 install .;
pip install .;
RUN useradd -ms /bin/bash swap_user && \
mkdir /coindata && chown swap_user -R /coindata
+1 -1
View File
@@ -1,3 +1,3 @@
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.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
value: int = self.settings.get(name, default_v)
def get_clamped_int_from(
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:
self.log.warning(f"Setting {name} to {min_v}")
value = min_v
@@ -375,6 +377,9 @@ class BaseApp(DBMethods):
value = max_v
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):
if self.min_delay_event == self.max_delay_event:
return self.min_delay_event
+329 -65
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.xmr_swap_1 as xmr_swap_1
PROTOCOL_VERSION_SECRET_HASH = 5
MINPROTO_VERSION_SECRET_HASH = 4
@@ -441,9 +440,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.check_delayed_auto_accept_seconds = self.get_int_setting(
"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_cases = []
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
self.stopRunning(1)
startup_tries = self.startup_tries
chain_client_settings = self.getChainClientSettings(coin_type)
if "startup_tries" in chain_client_settings:
startup_tries = chain_client_settings["startup_tries"]
if startup_tries < 1:
self.log.warning('"startup_tries" can\'t be less than 1.')
startup_tries = 1
# Total seconds waited for will be ((startup_tries(1 + startup_tries) / 2) * startup_delay
startup_tries: int = self.get_clamped_int_from(
chain_client_settings,
"startup_tries",
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):
if self.delay_event.is_set():
return
@@ -1632,6 +1637,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.coin_clients[coin_type]["interface"].testDaemonRPC(with_wallet)
return
except Exception as ex:
wait_for: int = startup_delay * (1 + i)
if any(
log in str(ex)
for log in [
@@ -1644,13 +1650,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
]
):
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:
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.stopRunning(1) # systemd will try to restart the process if fail_code != 0
@@ -3605,6 +3611,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
raise ValueError(
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:
try:
@@ -3965,7 +3981,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def isValidSwapDest(self, ci, dest: bytes):
ensure(isinstance(dest, bytes), "Swap destination must be bytes")
if ci.coin_type() in (Coins.PART_BLIND,):
return ci.isValidPubkey(dest)
return ci.verifyPubkey(dest)
# TODO: allow p2wsh
return ci.isValidAddressHash(dest)
@@ -4120,6 +4136,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
msg_buf.fee_rate_to = ci_to.make_int(fee_rate)
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()
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
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(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
) -> bytes:
@@ -5486,8 +5562,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
use_cursor = self.openDB(cursor)
bid, offer = self.getBidAndOffer(bid_id, use_cursor)
ensure(bid, "Bid not found")
ensure(offer, "Offer not found")
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
# Ensure bid is still valid
now: int = self.getTime()
@@ -5607,6 +5683,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
# Check non-bip68 final
if not ci_from.useBackend():
try:
txid = ci_from.publishTx(bid.initiate_txn_refund)
self.log.error(
@@ -6006,43 +6083,66 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"Incompatible offer protocol version",
)
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_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
self.checkCoinsReady(coin_from, coin_to)
ci_from.validateFeeRate(xmr_offer.a_fee_rate)
ci_to.validateFeeRate(xmr_offer.b_fee_rate)
bid_created_at: int = self.getTime()
valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 10)
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
)
bid_created_at: int = self.getTime()
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
if not (self.debug and extra_options.get("debug_skip_validation", False)):
self.validateBidValidTime(
offer.swap_type, coin_from, coin_to, valid_for_seconds
)
self.validateBidAmount(offer, amount, bid_rate)
self.checkCoinsReady(coin_from, coin_to)
# TODO: Better tx size estimate
fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target=2)
fee_rate_to = ci_to.make_int(fee_rate)
fee_rate_to = xmr_offer.b_fee_rate
estimated_fee: int = fee_rate_to * ci_to.est_lock_tx_vsize() // 1000
if "prefunded_tx" not in extra_options:
self.ensureWalletCanSend(
ci_to, offer.swap_type, int(amount_to), estimated_fee, for_offer=False
ci_to,
offer.swap_type,
int(amount_to),
estimated_fee,
for_offer=False,
)
bid_addr: str = self.prepareSMSGAddress(
addr_send_from, AddressTypes.BID, cursor
)
# return id of route waiting to be established
# Return id of route waiting to be established
request_data = {
"offer_id": offer_id.hex(),
"amount_from": amount,
@@ -6060,7 +6160,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
valid_for_seconds,
)
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
if reverse_bid:
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
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.commitDB()
@@ -6235,6 +6345,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.warning(
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.log.info(f"Sent XMR_BID_FL {self.logIDB(xmr_swap.bid_id)}")
@@ -6354,6 +6474,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(
ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs
)
if reverse_bid and bid.was_sent:
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid_id,
TxTypes.ITX_PRE_FUNDED,
cursor=use_cursor,
)
else:
prefunded_tx = self.getPreFundedTx(
Concepts.OFFER,
bid.offer_id,
@@ -6364,6 +6492,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
xmr_swap.a_lock_tx = pi.promoteMockTx(
ci_from, prefunded_tx, xmr_swap.a_lock_tx_script
)
self.log.info(f"Using pre-funded {ci_from.ticker()} tx")
else:
xmr_swap.a_lock_tx = ci_from.createSCLockTx(
bid.amount, xmr_swap.a_lock_tx_script, xmr_swap.vkbv
@@ -6709,8 +6838,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
try:
use_cursor = self.openDB(cursor)
bid, offer = self.getBidAndOffer(bid_id, use_cursor, with_txns=False)
ensure(bid, "Bid not found")
ensure(offer, "Offer not found")
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
bid.setState(new_state)
self.deactivateBid(use_cursor, offer, bid)
@@ -6766,14 +6895,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
addr_to = ci.encodeScriptDest(p2wsh)
else:
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:
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)
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
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)
txjs = ci.describeTx(txn_signed)
@@ -7625,7 +7757,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.logBidEvent(
bid.bid_id,
EventLogTypes.DEBUG_TWEAK_APPLIED,
"ind {}".format(bid.debug_ind),
f"ind {bid.debug_ind}",
cursor,
)
self.commitDB()
@@ -7680,7 +7812,33 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
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:
if self.haveDebugInd(
bid.bid_id,
@@ -7776,6 +7934,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if (
len(xmr_swap.al_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:
@@ -7809,10 +7974,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"",
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_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=bytes.fromhex(txid),
vout=refund_vout,
)
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB()
@@ -7824,10 +7993,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx)
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_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=txid,
vout=refund_vout,
)
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB()
@@ -8207,6 +8380,20 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
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):
# assert (self.mxDB.locked())
# Return True to remove bid from in-progress list
@@ -8244,12 +8431,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Verify amount
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(
out_value == int(bid.amount),
"Incorrect output amount in initiate txn {}: {} != {}.".format(
initiate_txnid_hex, out_value, int(bid.amount)
),
f"Incorrect output amount in initiate txn {self.logIDT(initiate_txnid_hex)}: {out_value} != {bid.amount}",
)
bid.initiate_tx.conf = initiate_txn["confirmations"]
@@ -8277,8 +8462,20 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
index = None
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"]
if "index" in found:
index = found["index"]
tx_height = found["height"]
@@ -8365,6 +8562,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
vout=participate_txvout,
)
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)
if bid.participate_tx.conf != found["depth"]:
save_bid = True
@@ -8372,15 +8584,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.participate_tx.conf is None
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(
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(
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
@@ -8398,7 +8611,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if bid.participate_tx.conf is not None:
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 (
bid.participate_tx.conf
@@ -8467,6 +8680,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if (
bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.initiate_txn_refund is not None
and self._isScriptRefundMature(
ci_from, offer, bid.initiate_txn_refund, bid.initiate_tx
)
):
try:
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}"
)
if (
should_try_refund_ptx: bool = (
bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
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:
txid = ci_to.publishTx(bid.participate_txn_refund)
@@ -8505,7 +8745,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
# State will update when spend is detected
except Exception as ex:
if ci_to.isTxNonFinalError(str(ex)):
if ci_to.isTxNonFinalError(str(ex)) is False:
self.log.warning(
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:
refund_vout: int = ci_from.getLockRefundVout(
bytes.fromhex(spend_txn_hex), xmr_swap.vkbv
)
bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx(
bid_id=bid.bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=xmr_swap.a_lock_refund_tx_id,
vout=refund_vout,
)
else:
self.setBidError(
@@ -8983,6 +9227,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if was_received:
if self.isBchXmrSwap(offer):
# Mercy tx is sent separately
# Can't set XMR_SWAP_FAILED_SWIPED, as bid should continue looking for mercy tx
pass
else:
# 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.pkhash_seller) == 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:
raise ValueError("Unknown swap type {}.".format(offer_data.swap_type))
@@ -10052,6 +10301,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Check for sent
existing_offer = self.getOffer(offer_id, cursor=cursor)
if existing_offer is None:
bid_reversed: bool = (
offer_data.swap_type == SwapTypes.XMR_SWAP
and self.is_reverse_ads_bid(
@@ -10905,7 +11155,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
ensure(
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",
)
@@ -11056,7 +11306,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
refundExtraArgs = dict()
lockExtraArgs = dict()
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
bch_ci = self.ci(Coins.BCH)
@@ -11554,8 +11804,27 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
cursor,
)
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid.bid_id,
TxTypes.PTX_PRE_FUNDED,
cursor=cursor,
)
try:
b_lock_vout = 0
if prefunded_tx:
self.log.info("Using pre-funded tx")
pi = self.pi(offer.swap_type)
b_lock_tx = pi.promoteMockPTx(
ci_to,
prefunded_tx,
xmr_swap.vkbv,
xmr_swap.pkbs,
)
b_lock_tx = ci_to.signTxWithWallet(b_lock_tx)
b_lock_tx_id = bytes.fromhex(ci_to.publishTx(b_lock_tx))
else:
result = ci_to.publishBLockTx(
xmr_swap.vkbv,
xmr_swap.pkbs,
@@ -11896,7 +12165,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
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)
elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
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)}")
bid, offer = self.getBidAndOffer(bid_id, cursor)
ensure(bid, "Bid not found")
ensure(offer, "Offer not found")
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
@@ -13811,8 +14080,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
num_watched_outputs += len(v["watched_outputs"])
now: int = self.getTime()
q_bids_str: str = (
"""SELECT
q_bids_str: str = """SELECT
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_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 bidstates s ON b.state = s.state_id
WHERE b.active_ind = 1"""
)
q_offers_str: str = (
"""SELECT
q_offers_str: str = """SELECT
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 AND expire_at > :now THEN 1 ELSE NULL END) AS count_sent_active
FROM offers WHERE active_ind = 1"""
)
try:
cursor = self.openDB()
@@ -13905,9 +14170,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
walletinfo = ci.getWalletInfo()
rv = {
"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(
walletinfo["unconfirmed_balance"], conv_int=True
walletinfo["unconfirmed_balance"], conv_int=True, r=-1
),
"expected_seed": ci.knownWalletSeed(),
"encrypted": walletinfo["encrypted"],
@@ -13922,7 +14187,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if "immature_balance" in walletinfo:
rv["immature"] = ci.format_amount(
walletinfo["immature_balance"], conv_int=True
walletinfo["immature_balance"], conv_int=True, r=-1
)
if "locked_utxos" in walletinfo:
@@ -14121,7 +14386,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def updateWalletsInfo(
self,
force_update: bool = False,
only_coin: bool = None,
only_coin: int = None,
wait_for_complete: bool = False,
) -> None:
now: int = self.getTime()
@@ -14913,8 +15178,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return
bid = self.getBid(bid_id)
if bid is None:
raise ValueError("Bid not found.")
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
bid.debug_ind = debug_ind
+2
View File
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
BCH_MERCY = auto()
PTX_PRE_FUNDED = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
+37 -18
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -29,6 +29,7 @@ import urllib.parse
import zipfile
import zmq
from typing import List
from urllib.request import urlopen
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_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", "")
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", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0")
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", "")
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")
@@ -85,7 +86,7 @@ PIVX_VERSION_TAG = os.getenv("PIVX_VERSION_TAG", "")
DASH_VERSION = os.getenv("DASH_VERSION", "23.1.2")
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", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
@@ -185,11 +186,13 @@ else:
BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH)
FILE_EXT = os.getenv("FILE_EXT", FILE_EXT)
logger = logging.getLogger()
logger = logging.getLogger("prepare")
LOG_LEVEL = logging.DEBUG
logger.propagate = False
logger.level = LOG_LEVEL
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(levelname)s : %(message)s"))
logger.addHandler(handler)
logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
@@ -458,12 +461,23 @@ def getRemoteFileLength(url: str) -> (int, bool):
popConnectionParameters()
def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None:
"""If file exists at path compare it's size to the content length at the url
and attempt to resume download if file size is below expected.
"""
resume_from: int = 0
def downloadRelease(
url_in: str | List[str], path: str, extra_opts, timeout: int = 10
) -> None:
# If file exists at path compare it's size to the content length at the url
# and attempt to resume download if file size is below expected.
release_filename: str = os.path.basename(path)
urls = (
url_in
if isinstance(url_in, list)
else [
url_in,
]
)
for url in urls:
try:
resume_from: int = 0
if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
@@ -483,8 +497,11 @@ def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None:
else:
# File exists and size check is disabled
return
return downloadFile(url, path, timeout, resume_from)
except Exception as e:
logger.warning(f"Failed to download {release_filename} from {url}")
logger.debug(f"Download error {e}")
raise RuntimeError(f"Failed to download {release_filename}.")
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,
)
elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
version + version_tag, release_filename
)
release_url = [
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(
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(f"wallet-dir={config_datadir}\n")
fp.write("log-file={}\n".format(os.path.join(config_datadir, "wallet.log")))
fp.write("max-log-files=5\n")
fp.write(
"rpc-login={}:{}\n".format(
core_settings["walletrpcuser"], core_settings["walletrpcpassword"]
+42 -26
View File
@@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
import json
import logging
import os
@@ -22,6 +23,7 @@ from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
from typing import Set
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
@@ -347,7 +349,7 @@ def mainLoop(daemons, update: bool = True):
def runClient(
data_dir: str,
chain: str,
start_only_coins: bool,
start_only_coins: Set[str],
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
@@ -391,17 +393,24 @@ def runClient(
# Settings may have been modified
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 start daemons
if len(start_only_coins) > 0:
swap_client.log.warning('Not starting networks as "startonlycoin" is set')
else:
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
@@ -460,10 +469,18 @@ def runClient(
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"]
)
opts = [
wallet_opts = [
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon",
"--daemon-address",
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_host, proxy_port = swap_client.getXMRWalletProxy(
@@ -471,7 +488,7 @@ def runClient(
)
if proxy_host:
proxy_log_str = " through proxy"
opts += [
wallet_opts += [
"--proxy",
f"{proxy_host}:{proxy_port}",
"--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")
daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
startXmrWalletDaemon(
v["datadir"], v["bindir"], filename, wallet_opts
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
@@ -506,9 +515,8 @@ def runClient(
if c == "decred":
appdata = v["datadir"]
extra_opts = [
f'--appdata="{appdata}"',
]
coin_opts = copy.deepcopy(base_coin_opts)
coin_opts.append(f'--appdata="{appdata}"')
use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
@@ -526,7 +534,7 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -537,12 +545,13 @@ def runClient(
swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_opts = [f'--appdata="{appdata}"']
wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "":
# Only set when in startonlycoin mode
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"')
wallet_opts.append(f'--pass="{wallet_pwd}"')
extra_config = {
"add_datadir": False,
"stdout_to_file": True,
@@ -555,13 +564,12 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=wallet_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
continue # /decred
if v["manage_daemon"] is True:
@@ -571,7 +579,7 @@ def runClient(
swap_client.log.info(f"Starting {display_name} daemon")
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
)
extra_config = {"coin_name": c}
@@ -580,7 +588,7 @@ def runClient(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -679,6 +687,9 @@ def printHelp():
print(
"--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(
"--forcedbupgrade Recheck database against schema regardless of version."
@@ -743,6 +754,11 @@ def main():
ensure_coin_valid(coin)
start_only_coins.add(coin)
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}")
+12 -2
View File
@@ -552,16 +552,26 @@ chainparams = {
name_map = {}
ticker_map = {}
variant_ticker_map = {}
for c, params in chainparams.items():
name_map[params["name"].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:
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:
raise ValueError(f"Unknown coin {ticker}")
+3 -3
View File
@@ -12,7 +12,6 @@ import time
from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 34
CURRENT_DB_DATA_VERSION = 8
@@ -400,8 +399,9 @@ class XmrOffer(Table):
swap_id = Column("integer", primary_key=True, autoincrement=True)
offer_id = Column("blob")
a_fee_rate = Column("integer") # Chain a fee rate
b_fee_rate = Column("integer") # Chain b fee rate
# TODO: rename to from/to - values are not switched for reverse swaps
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
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):
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
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
-1
View File
@@ -7,7 +7,6 @@
import json
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
+1 -1
View File
@@ -213,7 +213,7 @@ class HttpHandler(BaseHTTPRequestHandler):
status_code=200,
version=__version__,
extra_headers=None,
):
) -> bytes:
swap_client = self.server.swap_client
if swap_client.ws_server:
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 -*-
# Copyright (c) 2024 tecnovert
@@ -50,7 +49,7 @@ class CoinInterface:
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
def __init__(self, network, **kwargs):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
@@ -193,8 +192,14 @@ class AdaptorSigInterface:
def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]:
return [bytes(72), b"", bytes(len(script))]
def getLockRefundVout(self, lock_refund_tx_data: bytes, vbkv: bytes):
return 0
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@staticmethod
def curve_type():
return Curves.secp256k1
@@ -220,13 +225,6 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
if hash_len == 20:
return True
def isValidPubkey(self, pubkey: bytes) -> bool:
try:
self.verifyPubkey(pubkey)
return True
except Exception:
return False
def verifySig(self, pubkey: bytes, signed_hash: bytes, sig: bytes) -> bool:
pubkey = PublicKey(pubkey)
return pubkey.verify(sig, signed_hash, hasher=None)
+44 -8
View File
@@ -71,8 +71,13 @@ class BCHInterface(BTCInterface):
# TODO: BCH Watchonly: Remove when BCH watchonly works.
return True
def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.swap_client = swap_client
def has_segwit(self) -> bool:
@@ -142,7 +147,9 @@ class BCHInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True):
# 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
@@ -151,18 +158,35 @@ class BCHInterface(BTCInterface):
def createRawFundedTransaction(
self,
addr_to: str,
addr_to: str | bytes,
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
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 = {
"lockUnspents": lock_unspents,
# 'conf_target': self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -214,6 +238,16 @@ class BCHInterface(BTCInterface):
)
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):
# Only works for wallet txns
try:
@@ -268,7 +302,7 @@ class BCHInterface(BTCInterface):
found_vout = try_vout
break
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e}")
return None
if found_vout is None:
@@ -281,13 +315,14 @@ class BCHInterface(BTCInterface):
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
block_height = self.getChainHeight() - (confirmations - 1)
rv = {
"txid": txid.hex(),
"depth": confirmations,
"index": found_vout,
"height": block_height,
"value": self.make_int(txout["value"]),
}
return rv
@@ -502,6 +537,7 @@ class BCHInterface(BTCInterface):
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx")
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_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
tx_size = 275
dust_limit = 546
+275 -87
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
@@ -27,6 +26,7 @@ from basicswap.basicswap_util import (
getVoutByScriptPubKey,
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.util import (
b2i,
ensure,
@@ -99,7 +99,6 @@ from basicswap.basicswap_util import TxLockTypes
from basicswap.chainparams import Coins
from basicswap.rpc import make_rpc_func, openrpc
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000FFFF
@@ -185,7 +184,7 @@ def extractScriptLockRefundScriptValues(script_bytes: bytes):
return pk1, pk2, csv_val, pk3
class BTCInterface(Secp256k1Interface):
class BTCInterface(FeeValidator, Secp256k1Interface):
_scantxoutset_lock = threading.Lock()
_MAX_SCANTXOUTSET_RETRIES = 3
@@ -279,8 +278,15 @@ class BTCInterface(Secp256k1Interface):
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
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._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"]
@@ -305,8 +311,6 @@ class BTCInterface(Secp256k1Interface):
self.setConfTarget(coin_settings["conf_target"])
self._use_segwit = coin_settings["use_segwit"]
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._altruistic = coin_settings.get("altruistic", True)
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
if self._rpc_wallet not in wallets and len(wallets) > 0:
if "" in wallets:
# Setting wallet= in the coin .conf file should also work
self._log.warning(
f"Nameless {self.ticker()} wallet found."
+ '\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:
raise RuntimeError(
@@ -503,6 +507,75 @@ class BTCInterface(Secp256k1Interface):
return height
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):
if self._connection_type == "electrum":
backend = self.getBackend()
@@ -535,7 +608,7 @@ class BTCInterface(Secp256k1Interface):
block_hash = sha256(sha256(header_bytes))[::-1].hex()
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":
raise NotImplementedError(
"getBlockHeader by hash not available in electrum mode"
@@ -939,32 +1012,26 @@ class BTCInterface(Secp256k1Interface):
if wm:
info = wm.getAddressInfo(self.coin_type(), address)
if info:
if or_watch_only:
return True
if or_watch_only is False and info["is_watch_only"] is True:
return False
return True
return False
try:
addr_info = self.rpc_wallet("getaddressinfo", [address])
if not or_watch_only:
if addr_info["ismine"]:
return True
else:
if or_watch_only is False:
return False
if addr_info["iswatchonly"]:
return True
if self._use_descriptors:
addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if addr_info["ismine"] or addr_info["iswatchonly"]:
wo_addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if wo_addr_info["iswatchonly"]:
return True
except Exception as 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
def checkAddressMine(self, address: str) -> None:
@@ -1083,8 +1150,8 @@ class BTCInterface(Secp256k1Interface):
return self.encode_p2wsh(script)
def getDestForAddress(self, address: str) -> bytes:
bech32_prefix = self.chainparams_network()["hrp"]
if address.startswith(bech32_prefix + "1"):
bech32_prefix: str | None = self.chainparams_network().get("hrp", None)
if bech32_prefix and address.startswith(bech32_prefix + "1"):
_, witprog = segwit_addr.decode(bech32_prefix, address)
return CScript([OP_0, bytes(witprog)])
@@ -1813,7 +1880,9 @@ class BTCInterface(Secp256k1Interface):
pubkey = PublicKey(K)
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():
return self._fundTxElectrum(tx, feerate)
@@ -1821,9 +1890,14 @@ class BTCInterface(Secp256k1Interface):
# TODO: Unlock unspents if bid cancelled
# TODO: Manually select only segwit prevouts
options = {
"lockUnspents": True,
"lockUnspents": lock_unspents,
"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])
tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes
@@ -2645,10 +2719,17 @@ class BTCInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk))
return tx.serialize()
def encodeSharedAddress(self, Kbv, Kbs):
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
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.fundTx(b_lock_tx, feerate)
@@ -2671,8 +2752,8 @@ class BTCInterface(Secp256k1Interface):
def findTxB(
self,
kbv,
Kbs,
kbv: bytes,
Kbs: bytes,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
@@ -2711,6 +2792,47 @@ class BTCInterface(Secp256k1Interface):
)
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(
self,
chain_b_lock_txid: bytes,
@@ -2724,48 +2846,14 @@ class BTCInterface(Secp256k1Interface):
lock_tx_vout=None,
) -> bytes:
self._log.info(
"spendBLockTx: {} {}\n".format(
self._log.id(chain_b_lock_txid), lock_tx_vout
)
f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n"
)
Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs)
locked_n = None
actual_value = None
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)}"
locked_n, actual_value = self.getBLockTxo(
chain_b_lock_txid, lock_tx_vout, script_pk
)
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 (
locked_n is not None
@@ -2774,7 +2862,7 @@ class BTCInterface(Secp256k1Interface):
):
self._log.warning(
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")
@@ -2864,13 +2952,9 @@ class BTCInterface(Secp256k1Interface):
# Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, "bid")
self._log.info(f"Imported watch-only addr: {self._log.addr(dest_address)}")
self._log.info(
"Imported watch-only addr: {}".format(self._log.addr(dest_address))
)
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from
)
f"Rescanning {self.coin_name()} chain from height: {rescan_from}"
)
self.rpc_wallet("rescanblockchain", [rescan_from])
@@ -2922,6 +3006,8 @@ class BTCInterface(Secp256k1Interface):
if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
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:
rv["txid"] = txid.hex()
@@ -2979,6 +3065,7 @@ class BTCInterface(Secp256k1Interface):
for idx, txout in enumerate(tx.vout):
if txout.scriptPubKey == dest_script:
rv["index"] = idx
rv["value"] = txout.nValue
break
except Exception:
pass
@@ -3040,6 +3127,7 @@ class BTCInterface(Secp256k1Interface):
for idx, txout in enumerate(tx.vout):
if txout.scriptPubKey == dest_script:
rv["index"] = idx
rv["value"] = txout.nValue
break
except Exception as e:
self._log.debug(
@@ -3401,6 +3489,7 @@ class BTCInterface(Secp256k1Interface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
if self.useBackend():
return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee)
@@ -3408,10 +3497,18 @@ class BTCInterface(Secp256k1Interface):
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 = {
"lockUnspents": lock_unspents,
"conf_target": self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -3574,7 +3671,7 @@ class BTCInterface(Secp256k1Interface):
continue
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
if self.using_segwit():
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
continue
@@ -3735,7 +3832,7 @@ class BTCInterface(Secp256k1Interface):
ensure(
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}")
@@ -4374,14 +4471,16 @@ class BTCInterface(Secp256k1Interface):
return None
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
def isTxNonFinalError(self, err_str: str) -> bool:
err_lower = err_str.lower()
return (
"non-BIP68-final" in err_str
or "non-final" in err_str
or "Missing inputs" in err_str
or "bad-txns-inputs-missingorspent" in err_str
"non-bip68-final" in err_lower
or "non-final" in err_lower
or "locktime requirement not satisfied" in err_lower
)
def combine_non_segwit_prevouts(self):
@@ -4428,14 +4527,103 @@ class BTCInterface(Secp256k1Interface):
self._log.id(bytes.fromhex(tx["txid"]))
)
)
self.publishTx(tx_signed)
self.publishTx(bytes.fromhex(tx_signed))
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():
print("TODO: testBTCInterface")
total_out: int = 0
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__":
testBTCInterface()
prev_tx = self.rpc_wallet("gettransaction", [txi_txid_hex])
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 -*-
# Copyright (c) 2022-2024 tecnovert
@@ -24,8 +23,13 @@ class DASHInterface(BTCInterface):
def coin_type():
return Coins.DASH
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._wallet_passphrase = ""
self._have_checked_seed = False
+147 -22
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,16 +12,15 @@ import logging
import random
import traceback
from typing import List
from typing import List, Optional
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import (
CScriptNum,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.interface.btc import (
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
@@ -82,7 +80,6 @@ from coincurve.ecdsaotves import (
ecdsaotves_rec_enc_key,
)
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000F
@@ -182,7 +179,7 @@ def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
return sig, pk
class DCRInterface(Secp256k1Interface):
class DCRInterface(FeeValidator, Secp256k1Interface):
@staticmethod
def coin_type():
@@ -259,13 +256,13 @@ class DCRInterface(Secp256k1Interface):
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
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._rpcport = coin_settings["rpcport"]
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)
if "walletrpcport" in coin_settings:
self._walletrpcport = coin_settings["walletrpcport"]
@@ -421,8 +418,10 @@ class DCRInterface(Secp256k1Interface):
return bci
def getBlockHeader(self, block_hash: str) -> dict:
return self.rpc("getblockheader", [block_hash])
def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet("getinfo")
wi = self.rpc_wallet("walletinfo")
balances = self.rpc_wallet("getbalance")
@@ -597,7 +596,7 @@ class DCRInterface(Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate:
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"
@@ -858,11 +857,16 @@ class DCRInterface(Secp256k1Interface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# 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))
txn = self.rpc("createrawtransaction", [[], {addr_to: float_amount}])
if feerate:
fee_rate = 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}"
@@ -916,7 +920,7 @@ class DCRInterface(Secp256k1Interface):
found_vout = try_vout
break
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e})
return None
if found_vout is None:
@@ -929,13 +933,14 @@ class DCRInterface(Secp256k1Interface):
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
block_height = self.getChainHeight() - (confirmations - 1)
rv = {
"txid": txid.hex(),
"depth": confirmations,
"index": found_vout,
"height": block_height,
"value": self.make_int(txout["value"]),
}
return rv
@@ -996,6 +1001,10 @@ class DCRInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(output_value, script))
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):
inputs_valid: bool = True
validscripts: int = 0
@@ -1073,7 +1082,9 @@ class DCRInterface(Secp256k1Interface):
def decodeRawTransaction(self, tx_hex: str):
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))
# TODO: unlock unspents if bid cancelled
options = {
@@ -1146,6 +1157,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1197,6 +1209,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1248,6 +1261,7 @@ class DCRInterface(Secp256k1Interface):
script_lock_refund
)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee
@@ -1332,6 +1346,7 @@ class DCRInterface(Secp256k1Interface):
assert fee_paid > 0
size = len(tx.serialize()) + add_witness_bytes
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1393,6 +1408,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1465,6 +1481,7 @@ class DCRInterface(Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1526,6 +1543,7 @@ class DCRInterface(Secp256k1Interface):
prevout_script
)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size
self._log.info(
@@ -1776,13 +1794,16 @@ class DCRInterface(Secp256k1Interface):
spend_actual_balance: bool = False,
lock_tx_vout=None,
) -> 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)
script_pk = self.getPkDest(Kbs)
locked_n = None
actual_value = None
try:
wtx = self.rpc_wallet(
"gettransaction",
[
@@ -1795,13 +1816,19 @@ class DCRInterface(Secp256k1Interface):
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"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 (
locked_n is not None
@@ -1810,7 +1837,7 @@ class DCRInterface(Secp256k1Interface):
):
self._log.warning(
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")
@@ -1846,14 +1873,14 @@ class DCRInterface(Secp256k1Interface):
try:
txout = self.rpc("gettxout", [txid_hex, 0, 0, True])
except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e))
# self._log.warning(f"gettxout {e}"))
return None
confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"]
)
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 None
@@ -1868,3 +1895,101 @@ class DCRInterface(Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool:
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 -*-
# Copyright (c) 2024 tecnovert
+5 -4
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,10 +10,10 @@ import traceback
from basicswap.rpc import Jsonrpc
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"):
def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1", timeout=None):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
x = Jsonrpc(url)
x = Jsonrpc(url, timeout=timeout if timeout else 10)
x.__handler = None
v = x.json_request(method, params)
x.close()
@@ -41,7 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
auth = auth
host = host
def rpc_func(method, params=None):
return callrpc(port, auth, method, params, host)
def rpc_func(method, params=None, timeout=None):
return callrpc(port, auth, method, params, host, timeout=timeout)
return rpc_func
+1 -1
View File
@@ -13,7 +13,7 @@ import subprocess
def createDCRWallet(args, hex_seed, logging, delay_event):
logging.info("Creating DCR wallet")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
pipe_r, pipe_w = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt":
str_args = " ".join(args)
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The BasicSwap developers
@@ -32,8 +31,13 @@ class DOGEInterface(BTCInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 192
def __init__(self, coin_settings, network, swap_client=None):
super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
def getScriptDest(self, script: bytearray) -> bytearray:
# P2SH
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers
+15 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
@@ -38,8 +37,13 @@ class FIROInterface(BTCInterface):
def coin_type():
return Coins.FIRO
def __init__(self, coin_settings, network, swap_client=None):
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -272,6 +276,8 @@ class FIROInterface(BTCInterface):
if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
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:
rv["txid"] = txid.hex()
@@ -300,10 +306,15 @@ class FIROInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
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}"
@@ -361,7 +372,7 @@ class FIROInterface(BTCInterface):
)
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)
rv = self.rpc(
"signrawtransaction",
+101 -113
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
@@ -16,8 +15,13 @@ class LTCInterface(BTCInterface):
def coin_type():
return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
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 = make_rpc_func(
self._rpcport,
@@ -26,87 +30,6 @@ class LTCInterface(BTCInterface):
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:
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
@@ -172,9 +95,14 @@ class LTCInterface(BTCInterface):
continue
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
if self.using_segwit():
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
continue
@@ -184,11 +112,81 @@ class LTCInterface(BTCInterface):
else:
if not desc.startswith("pkh"):
continue
unspent_addr[u["address"]] = unspent_addr.get(
u["address"], 0
unspent_addr[utxo_address] = unspent_addr.get(
utxo_address, 0
) + self.make_int(u["amount"], r=1)
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:
if password == "":
return
@@ -271,8 +269,13 @@ class LTCInterfaceMWEB(LTCInterface):
def interface_type(self) -> int:
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
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 = make_rpc_func(
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):
# 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")
if self._rpc_wallet not in wallets:
if wallet_name not in wallets:
try:
self.rpc("loadwallet", [self._rpc_wallet])
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".')
self.rpc("loadwallet", [wallet_name])
self._log.debug(f'Loaded existing wallet "{wallet_name}".')
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
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
self.rpc(
"createwallet",
[
self._rpc_wallet,
wallet_name,
False,
True,
password,
@@ -333,22 +337,6 @@ class LTCInterfaceMWEB(LTCInterface):
else:
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:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
@@ -357,8 +345,8 @@ class LTCInterfaceMWEB(LTCInterface):
self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"])
self.rpc("loadwallet", ["mweb"])
self.rpc("unloadwallet", [wallet_name])
self.rpc("loadwallet", [wallet_name])
if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill")
+21 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
@@ -73,8 +72,13 @@ class NAVInterface(BTCInterface):
def txoType():
return CTxOut
def __init__(self, coin_settings, network, swap_client=None):
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -311,10 +315,15 @@ class NAVInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
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}"
@@ -605,6 +614,8 @@ class NAVInterface(BTCInterface):
if find_index:
tx_obj = self.rpc("decoderawtransaction", [tx["hex"]])
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:
rv["txid"] = txid.hex()
@@ -751,7 +762,13 @@ class NAVInterface(BTCInterface):
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)
# TODO: unlock unspents if bid cancelled
options = {
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert
+32 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
@@ -81,8 +80,17 @@ class PARTInterface(BTCInterface):
def txoType():
return CTxOutPart
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
@staticmethod
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)))
def use_tx_vsize(self) -> bool:
@@ -1231,6 +1239,7 @@ class PARTInterfaceBlind(PARTInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# 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 = {
"lockUnspents": lock_unspents,
"conf_target": self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -1282,6 +1299,17 @@ class PARTInterfaceBlind(PARTInterface):
"fundrawtransactionfrom", ["blind", tx_hex, {}, outputs_info, options]
)["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):
+7 -3
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
@@ -10,8 +9,13 @@ from basicswap.contrib.test_framework.messages import CTxOut
class PassthroughBTCInterface(BTCInterface):
def __init__(self, coin_settings, network):
super().__init__(coin_settings, network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.txoType = CTxOut
self._network = network
self.blocks_confirmed = coin_settings["blocks_confirmed"]
+29 -26
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
@@ -12,7 +11,7 @@ from .btc import BTCInterface
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
from .contrib.pivx_test_framework.messages import CTransaction
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP,
@@ -27,8 +26,13 @@ class PIVXInterface(BTCInterface):
def coin_type():
return Coins.PIVX
def __init__(self, coin_settings, network, swap_client=None):
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
@@ -74,10 +78,15 @@ class PIVXInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
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}"
@@ -100,29 +109,13 @@ class PIVXInterface(BTCInterface):
return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
block = self.rpc("getblock", [block_hash, True])
tx_rv = []
for tx in decoded_block.vtx:
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
for txid_str in block["tx"]:
tx_dec = self.rpc("getrawtransaction", [txid_str, True])
tx_rv.append(tx_dec)
block_rv = {
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
block["tx"] = tx_rv
return block
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee]
@@ -150,7 +143,7 @@ class PIVXInterface(BTCInterface):
)
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)
rv = self.rpc(
"signrawtransaction",
@@ -177,3 +170,13 @@ class PIVXInterface(BTCInterface):
block_height = self.getBlockHeader(rv["blockhash"])["height"]
return {"txid": txid_hex, "amount": 0, "height": block_height}
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 -*-
# Copyright (c) 2024 The Basicswap developers
+10 -4
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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.interface.base import CoinInterface
ed25519_l = 2**252 + 27742317777372353535851937790883648493
@@ -102,8 +100,13 @@ class XMRInterface(CoinInterface):
return True
return super().is_transient_error(ex)
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._addr_prefix = self.chainparams_network()["address_prefix"]
@@ -858,3 +861,6 @@ class XMRInterface(CoinInterface):
except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}")
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 -*-
# 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
# 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
try:
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
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_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)
if k == Coins.PART:
@@ -290,7 +311,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus()
if len(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:
cmd = url_split[4]
@@ -332,6 +353,18 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
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":
post_data = getFormData(post_string, is_json)
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 = {
"coins": js_coins,
"walletbalances": js_walletbalances,
@@ -1948,6 +2007,7 @@ endpoints = {
"messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover,
"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
NPBW_INT = 0
NPBW_BYTES = 2
-1
View File
@@ -39,7 +39,6 @@ from basicswap.contrib.rfc6979 import (
rfc6979_hmac_sha256_generate,
)
START_TOKEN = 0xABCD
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?
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":
str_args = " ".join(args)
+2 -5
View File
@@ -15,9 +15,6 @@ from basicswap.interface.btc import (
class ProtocolInterface:
swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError("base class")
def getMockScript(self) -> bytearray:
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
@@ -29,7 +26,7 @@ class ProtocolInterface:
else ci.get_p2sh_script_pubkey(script)
)
def getMockAddrTo(self, ci):
def getMockScriptAddr(self, ci):
script = self.getMockScript()
return (
ci.encodeScriptDest(ci.getScriptDest(script))
@@ -38,5 +35,5 @@ class ProtocolInterface:
)
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)
+10 -4
View File
@@ -138,12 +138,18 @@ def redeemITx(self, bid_id: bytes, cursor):
class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
def getFundedInitiateTxTemplate(
self,
ci,
amount: int,
sub_fee: bool,
feerate: int = None,
lock_unspents: bool = False,
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
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)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
+113 -15
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,6 +11,7 @@ from basicswap.util import (
ensure,
)
from basicswap.interface.base import Curves
from basicswap.interface.btc import findOutput
from basicswap.chainparams import (
Coins,
)
@@ -49,11 +50,11 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
try:
use_cursor = self.openDB(cursor)
bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id)
ensure(bid, "Bid not found: {}.".format(bid_id.hex()))
ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex()))
ensure(bid, f"Bid not found: {self.log.id(bid_id)}.")
ensure(xmr_swap, f"Adaptor-sig swap not found: {self.log.id(bid_id)}.")
offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id)
ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex()))
ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex()))
ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.")
ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(bid.offer_id)}.")
# The no-script coin is always the follower
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(
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()
txid = ci_follower.spendBLockTx(
xmr_swap.b_lock_tx_id,
@@ -113,7 +117,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None):
xmr_swap.vkbv,
vkbs,
amount,
xmr_offer.b_fee_rate,
chain_b_fee_rate,
bid.chain_b_height_start,
spend_actual_balance=True,
lock_tx_vout=lock_tx_vout,
@@ -203,9 +207,12 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP
_mock_key: bytes = bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
)
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):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
@@ -214,20 +221,78 @@ class XmrSwapInterface(ProtocolInterface):
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
def getMockScriptAddr(self, ci):
script = self.getMockScript()
if ci.coin_type() == Coins.PART:
# 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)
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:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script)
if ci.coin_type() == Coins.BCH:
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
ctx = ci.loadTx(mock_tx)
ctx = ci.loadTx(mock_tx, allow_witness=False)
for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script
@@ -240,3 +305,36 @@ class XmrSwapInterface(ProtocolInterface):
ctx.nLockTime = 0
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()
+75 -1
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() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
},
@@ -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() {
const modal = document.getElementById('confirmModal');
if (modal) {
@@ -188,7 +241,6 @@
},
lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates();
} 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) => {
const target = e.target.closest('[data-confirm-utxo]');
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) => {
const target = e.target.closest('[data-reset-form]');
if (target) {
@@ -398,12 +470,14 @@
window.EventHandlers = EventHandlers;
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.setBidAmount = EventHandlers.setBidAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
+51 -20
View File
@@ -4,10 +4,12 @@
const OfferPage = {
xhr_rates: null,
xhr_bid_params: null,
xhr_bid_prefund: null,
init: function() {
this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest();
this.xhr_bid_prefund = new XMLHttpRequest();
this.setupXHRHandlers();
this.setupEventListeners();
@@ -33,7 +35,20 @@
if (bidAmountSendInput) {
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);
}
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');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
@@ -129,7 +134,6 @@
if (!amtVar) {
this.updateBidParams('rate');
}
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
@@ -156,6 +160,7 @@
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate');
const bidSubfee = document.getElementById('subfee_bid');
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
@@ -171,7 +176,7 @@
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'sending') {
} else if (value_changed === 'sending' || value_changed === 'subfee') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
@@ -187,11 +192,31 @@
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.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.updateModalValues();
},
validateAmountsAfterChange: function() {
@@ -253,6 +278,11 @@
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
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 coinTo = document.getElementById('coin_to_name')?.value || '';
@@ -273,7 +303,12 @@
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
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 (modalValidMins) modalValidMins.textContent = validMins;
@@ -292,10 +327,6 @@
return false;
},
updateModalValues: function() {
},
handleBidsPageAddress: function() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
+8 -5
View File
@@ -351,16 +351,19 @@
);
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 => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) {
const currentText = element.textContent;
const balanceType = element.getAttribute('data-balance-type');
const value = coinData[balanceType];
if (value !== undefined) {
const ticker = coinData.ticker || coinId.toUpperCase();
const newBalance = `${coinData.balance} ${ticker}`;
if (currentText !== newBalance) {
const newBalance = balanceType === 'est_fee' ? value : `${value} ${ticker}`;
if (element.textContent !== newBalance) {
element.textContent = newBalance;
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`);
console.log(`Updated ${balanceType}: ${coinData.name} -> ${newBalance}`);
}
}
}
});
+19
View File
@@ -75,9 +75,28 @@
if (coinData.pending && parseFloat(coinData.pending) > 0) {
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 {
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.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData);
+14 -1
View File
@@ -224,7 +224,7 @@
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">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>
{% endif %}
</table>
@@ -419,11 +419,22 @@
name="bid_amount_send"
value=""
max="{{ data.amt_to }}"
haveamount="{{ data.coin_to_balance }}"
exp="{{ data.coin_to_exp }}"
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">
max {{ data.amt_to }} ({{ data.tla_to }})
</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>
</tr>
<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="tla_from" value="{{ data.tla_from }}">
<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 }}">
</form>
<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">
<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="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>)
{% 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>
@@ -152,7 +152,7 @@
<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="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>)
{% 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>
@@ -162,7 +162,7 @@
<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="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>)
{% 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>
@@ -177,7 +177,7 @@
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% 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>)
{% 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>
@@ -185,11 +185,22 @@
{% endif %}
</td>
</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 #}
<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="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>)
{% 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>
@@ -200,7 +211,7 @@
<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="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>)
{% 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>
@@ -337,16 +348,7 @@
<td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr>
{% 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>
</div>
</div>
@@ -545,7 +547,7 @@
<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-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>)
</td>
</tr>
@@ -557,7 +559,7 @@
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% 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>)
{% endif %}
</td>
@@ -567,7 +569,7 @@
<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-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>)
</td>
</tr>
@@ -576,7 +578,7 @@
<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-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>)
</td>
</tr>
@@ -585,14 +587,14 @@
<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-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>)
</td>
</tr>
<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-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>)
</td>
</tr>
@@ -767,7 +769,7 @@
<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">
<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>)
</td>
</tr>
+5 -5
View File
@@ -65,7 +65,7 @@
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center">
<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 class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
@@ -90,7 +90,7 @@
{% if w.cid == '1' %} {# PART #}
<div class="flex mb-2 justify-between items-center">
<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 class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4>
@@ -108,7 +108,7 @@
{% endif %}
<div class="flex mb-2 justify-between items-center">
<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 class="flex mb-2 justify-between items-center">
<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 #}
<div class="flex mb-2 justify-between items-center">
<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 class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4>
@@ -151,7 +151,7 @@
{% if w.cid == '13' %} {# FIRO #}
<div class="flex mb-2 justify-between items-center">
<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 class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4>
+58 -17
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2022-2026 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@
import traceback
import time
from typing import List
from urllib import parse
from .util import (
getCoinType,
@@ -184,14 +185,14 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
parsed_data["swap_type"] = page_data["swap_type"]
swap_type = swap_type_from_string(parsed_data["swap_type"])
elif (
parsed_data["coin_from"] in swap_client.adaptor_swap_only_coins
or parsed_data["coin_to"] in swap_client.adaptor_swap_only_coins
parsed_data["coin_from"] in swap_client.coins_without_segwit
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)
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:
page_data["swap_style"] = "xmr"
@@ -499,7 +500,7 @@ def page_newoffer(self, url_split, post_string):
"debug_ui": swap_client.debug_ui,
"automation_strat_id": -1,
"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)
@@ -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")
offer_id = decode_offer_id(url_split[2])
server = self.server
@@ -674,6 +675,11 @@ def page_offer(self, url_split, post_string):
amount_from = offer.amount_from
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(
offer_id,
amount_from,
@@ -768,24 +774,30 @@ def page_offer(self, url_split, post_string):
ci_leader = ci_to if reverse_bid else ci_from
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
data["a_fee_rate"] = ci_leader.format_amount(xmr_offer.a_fee_rate)
data["a_fee_rate_verify"] = ci_leader.format_amount(
int_fee_rate_now, conv_int=True
chain_a_fee_rate: int = (
xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
)
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_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 = (
ci_from.xmr_swap_b_lock_spend_tx_vsize()
if reverse_bid
else ci_from.xmr_swap_a_lock_spend_tx_vsize()
)
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(
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)
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")
return self.render_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())
if is_expired:
+7
View File
@@ -273,6 +273,9 @@ def page_wallet(self, url_split, post_string):
swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"):
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):
swap_client.cacheNewStealthAddressForCoin(coin_id)
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"]
)
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:
utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr()
-1
View File
@@ -10,7 +10,6 @@ import json
import time
import decimal
COIN = 100000000
-1
View File
@@ -25,7 +25,6 @@ from basicswap.contrib.test_framework.messages import (
uint256_from_str,
)
AES_BLOCK_SIZE = 16
+9 -47
View File
@@ -4,10 +4,12 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
import json
import sqlite3
import threading
import time
from typing import Dict, List, Optional, Tuple
from coincurve import PrivateKey, PublicKey
from .chainparams import Coins
from .contrib.test_framework import segwit_addr
@@ -19,7 +21,7 @@ from .db_wallet import (
WalletTxCache,
WalletWatchOnly,
)
from .util.crypto import hash160
from .util.crypto import hash160, sha256
from .util.extkey import ExtKeyPair
@@ -112,13 +114,11 @@ class WalletManager:
def _deriveAddress(
self, coin_type: Coins, index: int, internal: bool = False
) -> Tuple[str, str, bytes]:
from coincurve import PublicKey
key = self._deriveKey(coin_type, index, internal)
pubkey = PublicKey.from_secret(key).format()
pkh = hash160(pubkey)
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
def _syncStateIndices(self, coin_type: Coins, cursor) -> None:
@@ -275,8 +275,6 @@ class WalletManager:
def getAddress(
self, coin_type: Coins, index: int, internal: bool = False
) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -395,8 +393,6 @@ class WalletManager:
include_watch_only: bool = True,
funded_only: bool = False,
) -> List[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -426,8 +422,6 @@ class WalletManager:
return []
def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -452,8 +446,6 @@ class WalletManager:
return {}
def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -517,8 +509,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False)
def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -559,8 +549,6 @@ class WalletManager:
return None
def getCachedTotalBalance(self, coin_type: Coins) -> int:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -700,8 +688,6 @@ class WalletManager:
if not self.isInitialized(coin_type):
return None
import sqlite3
now = int(time.time())
min_cache_time = now - max_cache_age
@@ -744,8 +730,6 @@ class WalletManager:
return None
def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -762,8 +746,6 @@ class WalletManager:
def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]:
if not self.isInitialized(coin_type):
return None
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -788,8 +770,6 @@ class WalletManager:
return None
def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -861,10 +841,8 @@ class WalletManager:
label: str = "",
source: str = "import",
) -> bool:
from coincurve import PublicKey as CCPublicKey
try:
pubkey = CCPublicKey.from_secret(private_key).format()
pubkey = PublicKey.from_secret(private_key).format()
if (
segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey))
!= address
@@ -1003,7 +981,7 @@ class WalletManager:
def _b58decode_check(self, s: str) -> bytes:
data = self._b58decode(s)
payload, checksum = data[:-4], data[-4:]
expected = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
expected = sha256(sha256(payload))[:4]
if checksum != expected:
raise ValueError("Invalid base58 checksum")
return payload
@@ -1025,7 +1003,7 @@ class WalletManager:
master_key = self._master_keys.get(coin_type)
if master_key is None:
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:
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)
if data is None:
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:
cursor = self._swap_client.openDB()
@@ -1193,8 +1171,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False)
def getSeedID(self, coin_type: Coins) -> Optional[str]:
from basicswap.contrib.test_framework.script import hash160
master_key = self._master_keys.get(coin_type)
if master_key is None:
return None
@@ -1204,16 +1180,12 @@ class WalletManager:
return hash160(ek.encode_p()).hex()
def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address)
if key is None:
raise ValueError(f"Cannot sign: no key for address {address}")
return PrivateKey(key).sign(message.encode("utf-8"))
def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address)
if key is None:
raise ValueError(f"Cannot sign: no key for address {address}")
@@ -1222,8 +1194,6 @@ class WalletManager:
def getKeyForAddress(
self, coin_type: Coins, address: str
) -> Optional[Tuple[bytes, bytes]]:
from coincurve import PublicKey
key = self.getPrivateKey(coin_type, address)
if key is None:
return None
@@ -1232,8 +1202,6 @@ class WalletManager:
def findAddressByScripthash(
self, coin_type: Coins, scripthash: str
) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -1256,8 +1224,6 @@ class WalletManager:
return None
def getAllScripthashes(self, coin_type: Coins) -> List[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -1895,8 +1861,6 @@ class WalletManager:
for _ in existing:
return False
import json
pending = WalletPendingTx()
pending.coin_type = int(coin_type)
pending.txid = txid
@@ -1945,8 +1909,6 @@ class WalletManager:
) -> List[dict]:
cursor = self._swap_client.openDB()
try:
import json
results = self._swap_client.query(
WalletPendingTx,
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
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
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
==============
+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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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
FROM debian:bullseye-slim
FROM debian:trixie-slim
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
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 \
DEBIAN_FRONTEND=noninteractive \
DATADIR=/data
DATADIR=/data \
VIRTUAL_ENV=/opt/venv
RUN apt-get update; \
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_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
FROM debian:bullseye-slim
FROM debian:trixie-slim
COPY --from=install_stage /coin_bin .
+3 -3
View File
@@ -135,15 +135,15 @@
(define-public basicswap
(package
(name "basicswap")
(version "0.16.0")
(version "0.16.4")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/basicswap/basicswap")
(commit "2c13314bdd29622235c92fd20c237801acb3cb76")))
(commit "136b311dc68f11b9c12ebd6877c5f718d705603a")))
(sha256
(base32
"0j0id6db3ljdsfag8krjdmd4rzlz2504yk9lzj0p89lqyygi9ilc"))
"0ikr8ik9rklvafd1j8zj0y38vric02qhmj7pvp3kvzbmd2fxx95p"))
(file-name (git-file-name name version))))
(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"]
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.9"
requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
@@ -36,7 +36,7 @@ dev = [
"pre-commit",
"pytest",
"ruff",
"black==25.11.0",
"black==26.5.1",
"selenium",
]
@@ -48,3 +48,11 @@ allow-direct-references = true
[tool.ruff]
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 -*-
# 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.bin.prepare import downloadPIVXParams
TEST_HTTP_HOST = os.getenv(
"TEST_HTTP_HOST", "127.0.0.1"
) # Set to 0.0.0.0 when used in docker
@@ -164,7 +162,7 @@ def prepare_balance(
post_json["type_to"] = type_to
json_rv = read_json_api(
port_take_from_node,
"wallets/{}/withdraw".format(coin_ticker.lower()),
f"wallets/{coin_ticker.lower()}/withdraw",
post_json,
)
assert len(json_rv["txid"]) == 64
@@ -237,11 +235,17 @@ def wait_for_bid(
)
if isinstance(state, (list, tuple)):
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
else:
continue
elif state is not None and state != bid[5]:
continue
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return
else:
if i > 0 and i % 10 == 0:
+8 -13
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
@@ -56,7 +55,6 @@ from tests.basicswap.extended.test_doge import (
import basicswap.config as cfg
import basicswap.bin.run as runSystem
TEST_PATH = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap1"))
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), []):
fp.write(opt + "\n")
with open(config_path) as fs:
settings = json.load(fs)
settings["startup_delay"] = 1
settings["min_delay_event"] = 1
settings["max_delay_event"] = 4
settings["min_delay_event_short"] = 1
@@ -586,7 +582,7 @@ def prepare_nodes(
class TestBase(unittest.TestCase):
def setUpClass(cls):
super(TestBase, cls).setUpClass()
super().setUpClass()
cls.delay_event = threading.Event()
signal.signal(
@@ -624,7 +620,7 @@ class TestBase(unittest.TestCase):
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 = [
"basicswap-run",
"-datadir=" + client_path,
@@ -646,7 +642,7 @@ class XmrTestBase(TestBase):
prepare_nodes(3, "monero")
def start_processes(self):
multiprocessing.set_start_method("fork")
multiprocessing.set_start_method("spawn")
self.delay_event.clear()
for i in range(3):
@@ -655,7 +651,7 @@ class XmrTestBase(TestBase):
)
self.processes[-1].start()
waitForServer(self.delay_event, 12701)
waitForServer(self.delay_event, 12701, 60)
def waitForMainAddress():
for i in range(20):
@@ -667,13 +663,12 @@ class XmrTestBase(TestBase):
)
return wallets["XMR"]["main_address"]
except Exception as e:
print("Waiting for main address {}".format(str(e)))
print(f"Waiting for main address {e}")
self.delay_event.wait(1)
raise ValueError("waitForMainAddress timedout")
xmr_addr1 = waitForMainAddress()
num_blocks = 100
num_blocks: int = 100
xmr_auth = None
if os.getenv("XMR_RPC_USER", "") != "":
@@ -685,7 +680,7 @@ class XmrTestBase(TestBase):
]
< 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(
XMR_BASE_RPC_PORT + 1,
"generateblocks",
+14 -12
View File
@@ -65,7 +65,6 @@ from tests.basicswap.common import (
)
from basicswap.bin.run import startDaemon
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
@@ -176,6 +175,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing
"wallet_name": "bsx_wallet",
},
"dash": {
"connection_type": "rpc",
@@ -185,6 +185,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"bindir": DASH_BINDIR,
"use_csv": True,
"use_segwit": False,
"wallet_name": "bsx_wallet",
},
"bitcoin": {
"connection_type": "rpc",
@@ -193,6 +194,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True,
"wallet_name": "bsx_wallet",
},
},
"check_progress_seconds": 2,
@@ -286,7 +288,7 @@ class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
super().setUpClass()
k = PrivateKey()
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")
if len(dashRpc("listwallets")) < 1:
dashRpc("createwallet wbsx_wallet")
dashRpc("createwallet bsx_wallet")
sc.start()
waitForRPC(dashRpc, delay_event)
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")
dashRpc("generatetoaddress {} {}".format(num_blocks, cls.dash_addr))
dashRpc(f"generatetoaddress {num_blocks} {cls.dash_addr}")
ro = dashRpc("getblockchaininfo")
try:
@@ -432,8 +434,8 @@ class Test(unittest.TestCase):
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))
logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}")
btcRpc(f"generatetoaddress {num_blocks} {cls.btc_addr}")
ro = btcRpc("getblockchaininfo")
checkForks(ro)
@@ -450,7 +452,7 @@ class Test(unittest.TestCase):
# Wait for height, or sequencelock is thrown off by genesis blocktime
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):
particl_blocks = cls.swap_clients[0].callrpc("getblockcount")
print("particl_blocks", particl_blocks)
@@ -474,7 +476,7 @@ class Test(unittest.TestCase):
cls.swap_clients.clear()
cls.daemons.clear()
super(Test, cls).tearDownClass()
super().tearDownClass()
def test_02_part_dash(self):
logging.info("---------- Test PART to DASH")
@@ -684,9 +686,9 @@ class Test(unittest.TestCase):
offer_id = swap_clients[0].postOffer(
Coins.DASH,
Coins.BTC,
0.001 * COIN,
0.01 * COIN,
1.0 * COIN,
0.001 * COIN,
0.01 * COIN,
SwapTypes.SELLER_FIRST,
)
@@ -710,7 +712,7 @@ class Test(unittest.TestCase):
del swap_clients[0].getChainClientSettings(Coins.DASH)["override_feerate"]
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")
addr = dashRpc('getnewaddress "Withdrawal test"')
+14 -31
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# 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
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):
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)
if not cls.restore_instance:
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(
"getaccount",
[
@@ -765,14 +747,14 @@ class Test(BaseTest):
@classmethod
def tearDownClass(cls):
logging.info("Finalising Decred Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.dcr_daemons)
cls.dcr_daemons.clear()
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin)
num_passed: int = 0
@@ -878,15 +860,16 @@ class Test(BaseTest):
"use_csv": True,
"use_segwit": True,
"blocks_confirmed": 1,
"min_relay_fee": 0.00001,
}
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.update(REQUIRED_SETTINGS)
ci = DCRInterface(coin_settings, "mainnet")
ci = DCRInterface(coin_settings, "mainnet", self.swap_clients[0])
k = ci.getNewRandomKey()
K = ci.getPubkey(k)
@@ -914,7 +897,7 @@ class Test(BaseTest):
assert hash160(masterpubkey_data) == seed_hash
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
ci0 = swap_clients[0].ci(self.test_coin)
@@ -972,7 +955,7 @@ class Test(BaseTest):
assert f_decoded["txid"] == ctx.TxHash().hex()
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
swap_clients = self.swap_clients
@@ -1047,7 +1030,7 @@ class Test(BaseTest):
assert len(sent_txid) == 64
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
ci0 = swap_clients[0].ci(self.test_coin)
@@ -1161,7 +1144,7 @@ class Test(BaseTest):
assert sent_spend_txid is not None
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
ci0 = swap_clients[0].ci(self.test_coin)
@@ -1261,7 +1244,7 @@ class Test(BaseTest):
assert found_txid is not None
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)
@@ -1373,7 +1356,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount
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"):
jsw = read_json_api(1800, f"wallets/{coin}")
@@ -1412,7 +1395,7 @@ class Test(BaseTest):
assert jsw["locked"] is False
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
ci = swap_clients[0].ci(self.test_coin)
+4 -2
View File
@@ -179,6 +179,7 @@ class Test(TestFunctions):
@classmethod
def prepareExtraCoins(cls):
super().prepareExtraCoins()
if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.doge_addr = (
@@ -232,7 +233,7 @@ class Test(TestFunctions):
@classmethod
def tearDownClass(cls):
logging.info("Finalising DOGE Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.doge_daemons)
cls.doge_daemons.clear()
@@ -251,11 +252,12 @@ class Test(TestFunctions):
"use_segwit": False,
"blocks_confirmed": 1,
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
"wallet_name": "bsx_wallet",
}
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
if cls.pause_chain:
return
ci0 = cls.swap_clients[0].ci(cls.test_coin)
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- 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
# 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
export PYTHONPATH=$(pwd)
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,
UI_PORT,
)
from tests.basicswap.extended.test_scripts import (
wait_for_offers,
)
from tests.basicswap.util import (
read_json_api,
wait_for_offers,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
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}")
if "state" not in bid:
if "bid_state" not in bid:
continue
if state is None:
return
if bid["state"].lower() == state.lower():
if bid["bid_state"].lower() == state.lower():
return
raise ValueError("wait_for_bid failed")
@@ -102,8 +99,9 @@ def prepare_balance(
class DOGETest(BaseTestWithPrepare):
def test_a(self):
__test__ = True
def test_a(self):
amount_from = 10.0
offer_json = {
"coin_from": "btc",
@@ -115,10 +113,8 @@ class DOGETest(BaseTestWithPrepare):
"automation_strat_id": 1,
}
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)
wait_for_offers(self.delay_event, 1, 1, offer_id)
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("debugexclude=libevent\n")
fp.write("fallbackfee=0.01\n")
fp.write("fallbackfee=0.0002\n")
fp.write("acceptnonstdtxn=0\n")
"""
+1 -4
View File
@@ -29,18 +29,15 @@ import unittest
from tests.basicswap.util import (
read_json_api,
waitForServer,
UI_PORT,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
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_DATADIR = os.getenv("ELECTRUM_DATADIR")
-1
View File
@@ -58,7 +58,6 @@ from tests.basicswap.common import (
from basicswap.bin.run import startDaemon
logger = logging.getLogger()
NUM_NODES = 3
-1
View File
@@ -40,7 +40,6 @@ from tests.basicswap.extended.test_dcr import (
run_test_itx_refund,
)
logger = logging.getLogger("BSX Tests")
if not len(logger.handlers):
+165 -388
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# 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 os
import random
import shutil
import signal
import sys
import threading
import time
import unittest
from coincurve.keys import PrivateKey
import basicswap.config as cfg
from basicswap.basicswap import (
BasicSwap,
Coins,
SwapTypes,
BidStates,
@@ -39,45 +31,36 @@ from basicswap.util import (
from basicswap.basicswap_util import (
TxLockTypes,
)
from basicswap.util.address import (
toWIF,
)
from tests.basicswap.util import (
read_json_api,
)
from tests.basicswap.common import (
callrpc_cli,
checkForks,
stopDaemons,
wait_for_bid,
wait_for_offer,
wait_for_balance,
wait_for_unspent,
wait_for_in_progress,
wait_for_bid_tx_state,
TEST_HTTP_HOST,
TEST_HTTP_PORT,
BASE_PORT,
BASE_RPC_PORT,
BASE_ZMQ_PORT,
PREFIX_SECRET_KEY_REGTEST,
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.prepare import downloadPIVXParams
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
NUM_NODES = 3
PIVX_NODE = 3
BTC_NODE = 4
delay_event = threading.Event()
stop_test = False
PIVX_BINDIR = os.path.expanduser(
os.getenv("PIVX_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "pivx"))
@@ -86,342 +69,123 @@ PIVXD = os.getenv("PIVXD", "pivxd" + 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)
def prepareOtherDir(datadir, nodeId, conf_file="pivx.conf"):
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, 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")
PIVX_BASE_PORT = 34832
PIVX_BASE_RPC_PORT = 35832
PIVX_BASE_ZMQ_PORT = 36832
def prepareDir(datadir, nodeId, network_key, network_pubkey):
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):
def pivxCli(cmd, node_id=0):
return callrpc_cli(
PIVX_BINDIR,
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)),
os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(node_id)),
"regtest",
cmd,
PIVX_CLI,
)
def signal_handler(sig, frame):
global stop_test
os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8"))
stop_test = True
delay_event.set()
def prepareDataDir(
datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3
):
node_dir = os.path.join(datadir, dir_prefix + str(node_id))
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)
fp.write(
"rpcauth={}:{}${}\n".format(
"test" + str(node_id),
salt,
password_to_hmac(salt, "test_pass" + str(node_id)),
)
)
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")
params_dir = os.path.join(datadir, "pivx-params")
downloadPIVXParams(params_dir)
fp.write(f"paramsdir={params_dir}\n")
for i in range(0, num_nodes):
if node_id == i:
continue
fp.write("addnode=127.0.0.1:{}\n".format(base_p2p_port + i))
return node_dir
def run_coins_loop(cls):
while not stop_test:
try:
pivxRpc("generatetoaddress 1 {}".format(cls.pivx_addr))
btcRpc("generatetoaddress 1 {}".format(cls.btc_addr))
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):
class Test(BaseTest):
__test__ = True
test_coin_from = Coins.PIVX
pivx_daemons = []
pivx_addr = None
start_ltc_nodes = False
start_xmr_nodes = False
@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",
def prepareExtraDataDir(cls, i):
extra_opts = []
if not cls.restore_instance:
prepareDataDir(
cfg.TEST_DATADIRS,
i,
"pivx.conf",
"pivx_",
base_p2p_port=PIVX_BASE_PORT,
base_rpc_port=PIVX_BASE_RPC_PORT,
)
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(
cls.pivx_daemons.append(
startDaemon(
os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), PIVX_BINDIR, PIVXD
os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(i)),
PIVX_BINDIR,
PIVXD,
opts=extra_opts,
)
)
logging.info("Started %s %d", PIVXD, cls.daemons[-1].handle.pid)
logging.info("Started %s %d", PIVXD, cls.pivx_daemons[-1].handle.pid)
for i in range(NUM_NODES):
data_dir = os.path.join(cfg.TEST_DATADIRS, str(i))
if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, "particl-wallet")):
try:
callrpc_cli(
cfg.PARTICL_BINDIR,
data_dir,
"regtest",
"-wallet=bsx_wallet -legacy create",
"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)
waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT), delay_event)
for i in range(NUM_NODES):
rpc = make_part_cli_rpc_func(i)
waitForRPC(rpc, delay_event)
if i == 0:
rpc(
"extkeyimportmaster",
[
"abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb"
],
@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)
)
elif i == 1:
rpc(
"extkeyimportmaster",
[
"pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic",
"",
"true",
],
)
rpc("getnewextaddress", ["lblExtTest"])
rpc("rescanblockchain")
else:
rpc("extkeyimportmaster", [rpc("mnemonic", ["new"])["master"]])
rpc(
"walletsettings",
[
"stakingoptions",
json.dumps(
{"stakecombinethreshold": 100, "stakesplitthreshold": 200}
).replace('"', '\\"'),
],
)
rpc("reservebalance", ["false"])
basicswap_dir = os.path.join(
os.path.join(cfg.TEST_DATADIRS, str(i)), "basicswap"
)
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
with open(settings_path) as fs:
settings = json.load(fs)
sc = BasicSwap(
basicswap_dir, settings, "regtest", log_name="BasicSwap{}".format(i)
)
cls.swap_clients.append(sc)
sc.setDaemonPID(Coins.BTC, cls.daemons[0].handle.pid)
sc.setDaemonPID(Coins.PIVX, cls.daemons[1].handle.pid)
sc.setDaemonPID(Coins.PART, cls.daemons[2 + i].handle.pid)
sc.start()
waitForRPC(pivxRpc, delay_event)
num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351.
logging.info("Mining %d pivx blocks", num_blocks)
cls.pivx_addr = pivxRpc("getnewaddress mining_addr")
pivxRpc("generatetoaddress {} {}".format(num_blocks, cls.pivx_addr))
logging.info(f"Mining {num_blocks} pivx blocks")
cls.pivx_addr = pivxCli("getnewaddress mining_addr")
pivxCli(f"generatetoaddress {num_blocks} {cls.pivx_addr}")
ro = pivxRpc("getblockchaininfo")
ro = pivxCli("getblockchaininfo")
try:
assert ro["bip9_softforks"]["csv"]["status"] == "active"
except Exception:
@@ -431,47 +195,47 @@ class Test(unittest.TestCase):
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
def tearDownClass(cls):
global stop_test
logging.info("Finalising")
stop_test = True
cls.update_thread.join()
cls.coins_update_thread.join()
for c in cls.swap_clients:
c.finalise()
logging.info("Finalising PIVX Test")
super().tearDownClass()
stopDaemons(cls.daemons)
cls.swap_clients.clear()
cls.daemons.clear()
stopDaemons(cls.pivx_daemons)
cls.pivx_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):
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_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(
delay_event,
@@ -506,7 +270,7 @@ class Test(unittest.TestCase):
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=60,
wait_for=80,
)
js_0 = read_json_api(1800)
@@ -546,7 +310,7 @@ class Test(unittest.TestCase):
wait_for=60,
)
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)
@@ -578,7 +342,7 @@ class Test(unittest.TestCase):
wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True)
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(
delay_event,
@@ -715,7 +479,7 @@ class Test(unittest.TestCase):
logging.info("---------- Test {} wallet".format(self.test_coin_from.name))
logging.info("Test withdrawal")
addr = pivxRpc('getnewaddress "Withdrawal test"')
addr = pivxCli('getnewaddress "Withdrawal test"')
wallets = read_json_api(TEST_HTTP_PORT + 0, "wallets")
assert float(wallets[self.test_coin_from.name]["balance"]) > 100
@@ -745,22 +509,32 @@ class Test(unittest.TestCase):
def test_09_v3_tx(self):
logging.info("---------- Test PIVX v3 txns")
generate_addr = pivxRpc('getnewaddress "generate test"')
pivx_addr = pivxRpc('getnewaddress "Sapling test"')
pivx_sapling_addr = pivxRpc('getnewshieldaddress "shield addr"')
generate_addr = pivxCli('getnewaddress "generate test"')
pivx_addr = pivxCli('getnewaddress "Sapling test"')
pivx_sapling_addr = pivxCli('getnewshieldaddress "shield addr"')
pivxRpc(f'sendtoaddress "{pivx_addr}" 6.0')
pivxRpc(f'generatetoaddress 1 "{generate_addr}"')
pivxCli(f'sendtoaddress "{pivx_addr}" 6.0')
pivxCli(f'generatetoaddress 1 "{generate_addr}"')
txid = pivxRpc(
txid = pivxCli(
'shieldsendmany "{}" "[{{\\"address\\": \\"{}\\", \\"amount\\": 1}}]"'.format(
pivx_addr, pivx_sapling_addr
)
)
rtx = pivxRpc(f'getrawtransaction "{txid}" true')
rtx = pivxCli(f'getrawtransaction "{txid}" true')
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)
block = ci.getBlockWithTxns(block_hash)
@@ -837,14 +611,17 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value)
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())
n = pi.findMockVout(ci_from, itx_decoded)
value_after_subfee = ci_from.make_int(itx_decoded["vout"][n]["value"])
assert value_after_subfee < swap_value
swap_value = value_after_subfee
wait_for_unspent(delay_event, ci_from, swap_value)
extra_options = {"prefunded_itx": itx}
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 -*-
# 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
# 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 (
read_json_api,
waitForServer,
wait_for_offers,
UI_PORT,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
class HttpHandler(BaseHTTPRequestHandler):
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")
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:
logging.info(f"Waiting for {num_bids} bids on node {node_id}")
for i in range(20):
+4 -4
View File
@@ -5,9 +5,9 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
import logging
import os
import time
from basicswap.basicswap import (
Coins,
@@ -120,14 +120,14 @@ class Test(BaseTest):
@classmethod
def tearDownClass(cls):
logging.info("Finalising Wownero Test")
super(Test, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.wow_daemons)
cls.wow_daemons.clear()
@classmethod
def coins_loop(cls):
super(Test, cls).coins_loop()
super().coins_loop()
if cls.wow_addr is not None:
callrpc_xmr(
@@ -162,7 +162,7 @@ class Test(BaseTest):
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])
+32 -26
View File
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with
export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
"""
import json
@@ -58,6 +59,8 @@ from tests.basicswap.util import (
make_boolean,
read_json_api,
waitForServer,
PORT_OFS,
UI_PORT,
)
from tests.basicswap.common_xmr import (
prepare_nodes,
@@ -72,9 +75,6 @@ import basicswap.bin.run as runSystem
test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent"))
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))
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))
@@ -247,7 +247,7 @@ def updateThreadDCR(cls):
if "double spend" in str(e):
pass
else:
logging.warning("updateThreadDCR purchaseticket {}".format(e))
logging.warning(f"updateThreadDCR purchaseticket {e}")
cls.delay_event.wait(0.5)
try:
if num_passed >= 5:
@@ -259,7 +259,7 @@ def updateThreadDCR(cls):
],
)
except Exception as e:
logging.warning("updateThreadDCR generate {}".format(e))
logging.warning(f"updateThreadDCR generate {e}")
except Exception as e:
print("updateThreadDCR error", str(e))
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):
client_path = os.path.join(test_path, "client{}".format(client_id))
client_path = os.path.join(test_path, f"client{client_id}")
testargs = [
"basicswap-run",
"-datadir=" + client_path,
@@ -298,15 +298,24 @@ def start_processes(self):
for i in range(NUM_NODES):
waitForServer(self.delay_event, UI_PORT + i)
wallets = read_json_api(UI_PORT + 1, "wallets")
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
if os.getenv("XMR_RPC_USER", "") != "":
xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
self.xmr_addr = wallets["XMR"]["main_address"]
num_blocks = 100
num_blocks: int = 100
if (
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count"
@@ -321,10 +330,11 @@ def start_processes(self):
auth=xmr_auth,
)
logging.info(
"XMR blocks: %d",
"XMR blocks: {}".format(
callrpc_xmr(XMR_BASE_RPC_PORT + 1, "get_block_count", auth=xmr_auth)[
"count"
],
]
)
)
self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
@@ -401,9 +411,7 @@ def start_processes(self):
have_blocks: int = callfirorpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Firo blocks to %s",
num_blocks - have_blocks,
self.firo_addr,
f"Mining {num_blocks - have_blocks} Firo blocks to {self.firo_addr}"
)
callfirorpc(
0,
@@ -419,9 +427,7 @@ def start_processes(self):
have_blocks: int = callbchrpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Bitcoincash blocks to %s",
num_blocks - have_blocks,
self.bch_addr,
f"Mining {num_blocks - have_blocks} Bitcoincash blocks to {self.bch_addr}"
)
callbchrpc(
0,
@@ -436,9 +442,7 @@ def start_processes(self):
have_blocks: int = calldogerpc(0, "getblockcount")
if have_blocks < num_blocks:
logging.info(
"Mining %d Dogecoin blocks to %s",
num_blocks - have_blocks,
self.doge_addr,
f"Mining {num_blocks - have_blocks} Dogecoin blocks to {self.doge_addr}"
)
calldogerpc(
0, "generatetoaddress", [num_blocks - have_blocks, self.doge_addr]
@@ -556,7 +560,10 @@ class BaseTestWithPrepare(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(BaseTestWithPrepare, cls).setUpClass()
cls.addClassCleanup(
cls.finalise
) # tearDownClass is not run if setUpClass fails
super().setUpClass()
random.seed(time.time())
@@ -576,7 +583,7 @@ class BaseTestWithPrepare(unittest.TestCase):
waitForServer(cls.delay_event, UI_PORT + 1)
@classmethod
def tearDownClass(cls):
def finalise(cls):
logging.info("Stopping test")
cls.delay_event.set()
if cls.update_thread:
@@ -597,7 +604,6 @@ class BaseTestWithPrepare(unittest.TestCase):
class Test(BaseTestWithPrepare):
def test_persistent(self):
while not self.delay_event.is_set():
logging.info("Looping indefinitely, ctrl+c to exit.")
self.delay_event.wait(10)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert
@@ -20,7 +20,6 @@ from util import (
)
from tests.basicswap.util import read_json_api
base_url = "http://localhost"
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# 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 selenium.webdriver.common.by import By
logger = logging.getLogger()
logger.level = logging.INFO
if not len(logger.handlers):
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:
logger.info(f"clear_offers {port_list}")
@@ -60,7 +78,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_1_port, "offers/new", offer_data)
try:
offer_1_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
offer_data = {
"addr_from": -1,
@@ -72,7 +94,13 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_1_port, "offers/new", offer_data)
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 = {
"addr_from": -1,
@@ -84,7 +112,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_2_port, "offers/new", offer_data)
try:
offer_3_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
# Wait for offers to propagate
for i in range(1000):
-2
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
@@ -9,7 +8,6 @@
import os
from selenium.webdriver.common.by import By
BSX_0_PORT = int(os.getenv("BSX_0_PORT", 12701))
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))
+17 -19
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- 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
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -167,6 +167,7 @@ class TestBCH(BasicSwapTest):
@classmethod
def prepareExtraCoins(cls):
super().prepareExtraCoins()
cls.bch_addr = callnoderpc(
0,
"getnewaddress",
@@ -197,11 +198,12 @@ class TestBCH(BasicSwapTest):
"datadir": os.path.join(datadir, "bch_" + str(node_id)),
"bindir": BITCOINCASH_BINDIR,
"use_segwit": False,
"wallet_name": "bsx_wallet",
}
@classmethod
def coins_loop(cls):
super(TestBCH, cls).coins_loop()
super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin)
try:
if cls.bch_addr is not None:
@@ -212,7 +214,7 @@ class TestBCH(BasicSwapTest):
@classmethod
def tearDownClass(cls):
logging.info("Finalising Bitcoincash Test")
super(TestBCH, cls).tearDownClass()
super().tearDownClass()
stopDaemons(cls.bch_daemons)
cls.bch_daemons.clear()
@@ -224,19 +226,15 @@ class TestBCH(BasicSwapTest):
return True
def test_001_nested_segwit(self):
logging.info(
"---------- Test {} p2sh nested segwit".format(self.test_coin.name)
)
logging.info(f"---------- Test {self.test_coin.name} p2sh nested segwit")
logging.info("Skipped")
def test_002_native_segwit(self):
logging.info(
"---------- Test {} p2sh native segwit".format(self.test_coin.name)
)
logging.info(f"---------- Test {self.test_coin.name} p2sh native segwit")
logging.info("Skipped")
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)
@@ -348,7 +346,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64
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)
@@ -451,7 +449,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64
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)
ci1 = self.swap_clients[1].ci(self.test_coin)
@@ -482,7 +480,7 @@ class TestBCH(BasicSwapTest):
super().test_006_getblock_verbosity()
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_wif = (
@@ -506,10 +504,10 @@ class TestBCH(BasicSwapTest):
super().test_009_scantxoutset()
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
ci = swap_clients[0].ci(Coins.BCH)
ci = swap_clients[0].ci(self.test_coin)
pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
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):
# 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)
@@ -717,7 +715,7 @@ class TestBCH(BasicSwapTest):
def test_011_p2sh32(self):
# 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)
@@ -806,7 +804,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64
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")
def test_01_a_full_swap(self):
@@ -877,7 +875,7 @@ class TestBCH(BasicSwapTest):
def test_06_preselect_inputs(self):
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")
def test_07_expire_stuck_accepted(self):
+329 -20
View File
@@ -74,6 +74,7 @@ class TestFunctions(BaseTest):
@classmethod
def prepareExtraCoins(cls):
# Save sent messages so tests can count them
for sc in cls.swap_clients:
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:
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
# Bidder sends the bid
@@ -306,9 +307,7 @@ class TestFunctions(BaseTest):
self, coin_from: Coins, coin_to: Coins, lock_value: int = 32
) -> None:
logging.info(
"---------- Test {} to {} leader recovers coin a lock tx".format(
coin_from.name, coin_to.name
)
f"---------- Test {coin_from.name} to {coin_to.name} leader recovers coin a lock tx"
)
id_offerer: int = self.node_a_id
@@ -459,6 +458,12 @@ class TestFunctions(BaseTest):
if with_mercy
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(
test_delay_event,
swap_clients[id_leader],
@@ -492,12 +497,12 @@ class TestFunctions(BaseTest):
# Test manually redeeming the no-script lock tx
offerer_key = read_json_api(
1800 + id_offerer,
"bids/{}".format(bid_id.hex()),
f"bids/{bid_id.hex()}",
{"chainbkeysplit": True},
)["splitkey"]
data = {"spendchainblocktx": True, "remote_key": offerer_key}
redeemed_txid = read_json_api(
1800 + id_bidder, "bids/{}".format(bid_id.hex()), data
1800 + id_bidder, f"bids/{bid_id.hex()}", data
)["txid"]
assert len(redeemed_txid) == 64
@@ -505,9 +510,7 @@ class TestFunctions(BaseTest):
self, coin_from, coin_to, lock_value: int = 32
):
logging.info(
"---------- Test {} to {} follower recovers coin b lock tx".format(
coin_from.name, coin_to.name
)
f"---------- Test {coin_from.name} to {coin_to.name} follower recovers coin b lock tx"
)
id_offerer: int = self.node_a_id
@@ -920,7 +923,7 @@ class BasicSwapTest(TestFunctions):
@classmethod
def setUpClass(cls):
super(BasicSwapTest, cls).setUpClass()
super().setUpClass()
@classmethod
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)
def test_06_preselect_inputs(self):
tla_from = self.test_coin_from.name
logging.info("---------- Test {} Preselected inputs".format(tla_from))
tla_from: str = self.test_coin_from.name
logging.info(f"---------- Test {tla_from} Preselected inputs")
swap_clients = self.swap_clients
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["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):
coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info(
"---------- Test {} to {} expires bid stuck on accepted".format(
coin_from.name, coin_to.name
)
f"---------- Test {coin_from.name} to {coin_to.name} expires bid stuck on accepted"
)
swap_clients = self.swap_clients
@@ -2324,10 +2483,160 @@ class BasicSwapTest(TestFunctions):
def test_09_expire_accepted_rev(self):
self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from)
def test_10_presigned_txns(self):
raise RuntimeError(
"TODO"
) # Build without xmr first for quicker test iterations
def test_11_fee_validation(self):
coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info(
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):
@@ -2511,7 +2820,7 @@ class TestBTC_PARTB(TestFunctions):
@classmethod
def setUpClass(cls):
super(TestBTC_PARTB, cls).setUpClass()
super().setUpClass()
if False:
for client in cls.swap_clients:
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:
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(
delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30
@@ -641,7 +645,7 @@ class Test(TestFunctions):
@classmethod
def setUpClass(cls):
cls.addElectrumxDaemon("bitcoin", 32793, 50001)
super(Test, cls).setUpClass()
super().setUpClass()
@classmethod
def modifyConfig(cls, test_path, i):
@@ -754,14 +758,6 @@ class Test(TestFunctions):
self.delay_event,
self.test_coin_b,
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_1,
True,
@@ -788,7 +784,7 @@ class Test(TestFunctions):
True,
)
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):
+102 -11
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# 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
def test_001_nested_segwit(self):
logging.info(
"---------- Test {} p2sh nested segwit".format(self.test_coin_from.name)
)
logging.info(f"---------- Test {self.test_coin_from.name} p2sh nested segwit")
logging.info("Skipped")
def test_002_native_segwit(self):
logging.info(
"---------- Test {} p2sh native segwit".format(self.test_coin_from.name)
)
logging.info(f"---------- Test {self.test_coin_from.name} p2sh native segwit")
ci = self.swap_clients[0].ci(self.test_coin_from)
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"]
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_wif = (
@@ -136,7 +132,7 @@ class TestLTC(BasicSwapTest):
assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la"
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
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
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
ci0 = swap_clients[0].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_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"])
@@ -210,6 +215,19 @@ class TestLTC(BasicSwapTest):
< 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:
pause_event.clear() # Stop mining
ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0])
@@ -237,6 +255,7 @@ class TestLTC(BasicSwapTest):
for utxo in utxos:
if utxo.get("address", "") == mweb_addr_1:
mweb_tx = utxo
break
assert mweb_tx is not None
unspent_addr = ci1.getUnspentsByAddr()
@@ -245,10 +264,66 @@ class TestLTC(BasicSwapTest):
if "mweb1" in addr:
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
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
ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB)
@@ -265,7 +340,9 @@ class TestLTC(BasicSwapTest):
ltc_mweb_addr = read_json_api(
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")
assert ltc_mweb_addr2.startswith("tmweb1")
assert (
ci_mweb.rpc_wallet(
@@ -337,6 +414,20 @@ class TestLTC(BasicSwapTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json)
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__":
unittest.main()
+79 -3
View File
@@ -8,6 +8,7 @@
import hashlib
import logging
import os
import random
import secrets
import threading
@@ -22,10 +23,15 @@ from coincurve.ecdsaotves import (
)
from coincurve.keys import PrivateKey
from basicswap.basicswap import (
Coins,
BasicSwap,
SwapTypes,
)
from basicswap.contrib.mnemonic import Mnemonic
from basicswap.db import create_db_, DBMethods, KnownIdentity
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.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint, decode_varint
@@ -60,7 +66,9 @@ from basicswap.contrib.test_framework.messages import (
CTxOut,
uint256_from_str,
)
from tests.basicswap.common import (
PREFIX_SECRET_KEY_REGTEST,
)
logger = logging.getLogger()
@@ -157,7 +165,7 @@ class Test(unittest.TestCase):
assert str(e) == "Mantissa too long"
validate_amount("0.12345678")
# floor
# Floor
assert make_int("0.123456789", r=-1) == 12345678
# Round up
assert make_int("0.123456789", r=1) == 12345679
@@ -791,6 +799,74 @@ class Test(unittest.TestCase):
== "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__":
unittest.main()
+2 -2
View File
@@ -104,7 +104,7 @@ class Test(BaseTest):
)
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)
@@ -159,7 +159,7 @@ class Test(BaseTest):
ci.rpc_wallet("sendrawtransaction", [lock_tx.hex()])
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 < 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
logger = logging.getLogger()
@@ -160,35 +159,6 @@ class Test(BaseTest):
rv = read_json_api(1800, "rateslist?from=PART&to=BTC")
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):
test_coin_from = Coins.PART
logging.info("---------- Test {} cltv".format(test_coin_from.name))
+10 -7
View File
@@ -38,9 +38,7 @@ from basicswap.basicswap_util import (
EventLogTypes,
)
from basicswap.util import COIN, format_amount, make_int, TemporaryError
from basicswap.util.address import (
toWIF,
)
from basicswap.util.address import toWIF
from basicswap.rpc import (
callrpc,
)
@@ -91,7 +89,6 @@ from basicswap.db_util import (
)
from basicswap.bin.run import startDaemon, startXmrDaemon, startXmrWalletDaemon
logger = logging.getLogger()
NUM_NODES = 3
@@ -222,6 +219,7 @@ def prepare_swapclient_dir(
"datadir": os.path.join(datadir, "ltc_" + str(node_id)),
"bindir": cfg.LITECOIN_BINDIR,
"use_segwit": True,
"wallet_name": "bsx_wallet",
}
if cls:
@@ -815,7 +813,7 @@ class BaseTest(unittest.TestCase):
.pubkey_to_address(void_block_rewards_pubkey)
)
logging.info(
"Mining %d Litecoin blocks to %s", num_blocks, cls.ltc_addr
f"Mining {num_blocks} Litecoin blocks to {cls.ltc_addr}"
)
callnoderpc(
0,
@@ -942,6 +940,7 @@ class BaseTest(unittest.TestCase):
)
cls.coins_update_thread.start()
cls.prepareBalances()
except Exception:
traceback.print_exc()
cls.tearDownClass()
@@ -999,6 +998,10 @@ class BaseTest(unittest.TestCase):
def prepareExtraCoins(cls):
pass
@classmethod
def prepareBalances(cls):
pass
@classmethod
def coins_loop(cls):
if cls.btc_addr is not None:
@@ -2590,7 +2593,7 @@ class Test(BaseTest):
swap_clients[2],
bid_id,
BidStates.SWAP_COMPLETED,
wait_for=120,
wait_for=180,
)
wait_for_bid(
test_delay_event,
@@ -2598,7 +2601,7 @@ class Test(BaseTest):
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=120,
wait_for=180,
)
# Verify expected inputs were used
+20 -5
View File
@@ -1,15 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
import json
import logging
import os
import urllib
from urllib.request import urlopen
PORT_OFS = int(os.getenv("PORT_OFS", 1))
UI_PORT = 12700 + PORT_OFS
REQUIRED_SETTINGS = {
"blocks_confirmed": 1,
@@ -59,14 +62,26 @@ def post_json_api(port, path, 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):
if delay_event.is_set():
raise ValueError("Test stopped.")
try:
delay_event.wait(1)
delay_event.wait(1.0)
_ = read_json_api(port)
return
except Exception as e:
print("waitForServer, error:", str(e))
logging.error(f"waitForServer: {e}")
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